Unary ! in a C constant expression, and the bound the idiom exists to produce
Found by pointing pxx at busybox 1.37.0, which is the top pick of [[idea-c-realworld-test-targets]] and which the user asked for directly: "testing and improving our C compiler … busybox is interesting — because it would allow a very minimal system: linux kernel, busybox, pxx compiler."
Repro
sizeof(char[3]) /* 3 ok */
sizeof(char[1 - 2*0]) /* 1 ok */
sizeof(char[!!(0)]) /* Expected: ], but got: (Kind: 136) */
sizeof(char[1 - 2*!!(0)]) /* Expected: ], but got: (Kind: 136) */
CEvalConstPrimary handled -, +, ~ (tkNot) and &, and not !
(tkLogNot). The rest of the chain was already complete — CEvalConstEquality
and CEvalConstRelational fold the cond these wrap — so exactly one unary arm
was missing. The diagnostic never mentioned !: the fold stopped, the bracket
met a token the dimension reader could not use, and it reported the bracket.
That expression is BUILD_BUG_ON, spelled identically by busybox and the Linux
kernel:
#define BUILD_BUG_ON(condition) ((void)sizeof(char[1 - 2*!!(condition)]))
The second half, which is the one that matters
With ! folded, coreutils/cat.c compiled — and so did a failing
assertion:
enum { A = 1, C = 2 };
(void)sizeof(char[1 - 2*!!(A != C)]); /* bound is -1 */
| gcc | error: size of unnamed array is negative |
| pxx | accepted, silently |
The bound is 1 when the condition is false and -1 when it holds, so the assertion IS the refusal. Accepting it does not merely differ from gcc — it inverts the program's intent, turning every compile-time assertion in the source into a no-op that reports success. busybox has hundreds. This is not the FPC-parity ceiling ("accepting a form the reference rejects is not a defect"): there the program still means what it says, here it means the opposite.
The fix
One arm in CEvalConstPrimary:
else if CurTok.Kind = tkLogNot then
begin
Next;
if CEvalConstPrimary() = 0 then Result := 1 else Result := 0;
end
and one shared CCheckArrayExtent, called from all three extent readers —
the declarator's, sizeof(type[N]), and sizeof(type[N]){...}. Three, because
I put the check at one of them first and the negative case still compiled: the
declaration parser and ParseCSizeof each fold their own extents. One rule, one
message, one place (normalise-dont-special-case.md).
Zero stays legal (char pad[0] is a GNU extension pxx already accepts, and
sizeof answers 0 for it, matching gcc), and unsized [] never reaches the
check — both callers handle tkRBrack before folding.
The c-testsuite caught my first scoping, reasoning did not
The check sat after the declarator's if/else, where it also saw the -1 that
the [] arm sets for an array sized from its initializer. C conformance went
220/0 → 217/3, all three int x[] = { 1, 0 }:
00117.c |
int x[] = { 1, 0 }; |
00216.c |
struct Wrap local_wrap[] = { … }; |
00220.c |
wchar_t s[] = L"hello…"; |
Moved inside the folded branch, which is the only one that produces a real extent. Third time today a corpus overturned an assumption a build had already "passed".
Measured
busybox 1.37.0, libbb + coreutils + editors + util-linux, 307 files,
each compiled --emit-obj with its own KBUILD_*/BB_VER defines:
| before | after | |
|---|---|---|
| compile clean | 181 | 186 |
| fail | 126 | 121 |
coreutils/cat.c, coreutils/cp.c, libbb/xfuncs.c, util-linux/renice.c,
util-linux/umount.c. Every Expected: ] failure in the sweep is gone.
| check | result |
|---|---|
run_c_conformance.sh |
220 pass / 0 fail — baseline |
run_pascal_conformance.sh |
346 pass / 0 fail — baseline |
| self-host | converged after 1 round |
gate.sh quick |
GREEN |
Gate
test/cconst_logical_not_array_bound.c (positive, gcc -O0 oracle, includes the
zero-length and unsized arrays a naive check refuses) and
test/cconst_negative_array_bound_fails.c (must NOT compile; gcc agrees), both
wired into test-core and both run green through testmgr --tier native.
What the busybox sweep says to do next
Of the 121 remaining failures, 96 are a missing crtl function, not a compiler bug — a library gap, and mostly a shallow one:
strchrnul strcasecmp strncasecmp strverscmp mempcpy dprintf
getline fseeko alarm setsid mkfifo mknod mkstemp vasprintf
clearenv putenv getopt lchown utimensat posix_fallocate …
plus a whole *_unlocked family (getc_unlocked, putchar_unlocked,
fputs_unlocked, ferror_unlocked, …) which in a crtl with no FILE locking are
aliases for the locked versions. typeof and _IOR are the odd ones out —
compiler/header features, not functions.
The remaining ~25 are real frontend gaps and are the interesting residue:
IR_UNSUPPORTED: frontend could not lower AST node (kind 1), and stray token at top level on ALIGN1 / FAST_FUNC / IP — busybox's __attribute__
wrapper macros, so likely one attribute-parsing gap wearing three names.
Two driver gaps also surfaced and are worth their own tickets: pxx has no
-include <file> and no -D name=value (only boolean -d<NAME>), so
busybox's per-file build flags had to be reproduced in a source shim; and
cat.c alone raised 21 warnings for headers resolving from /usr/include
(dirent.h, endian.h, byteswap.h, paths.h, mntent.h) — which for the
no-GNU-userland goal is the gap that actually matters, since pxx's own
lib/crtl/include has 27 headers and none of those.