← board

x ?: y

Found compiling busybox 1.37.0, one of a four-file expected C expression cluster. The other three are the _IOW/_IOR ioctl macro family — a different cause; the shared symptom string is not a shared bug. Sibling findings from the same sweep: [[bug-c-a-ternary-cannot-be-the-callee-of-a-call]], [[bug-c-logical-not-is-not-folded-in-a-constant-expression]], [[feature-c-gcc-extended-inline-asm]].

Repro

editors/vi.c:791:

return col - ((col % tabstop) ?: tabstop);

Reduced:

int printf(const char *, ...);
static int calls;
static int side(int v) { calls++; return v; }
int main(void) {
  int a = 0, b = 7;
  printf("%d %d\n", a ?: b, b ?: a);
  printf("%d\n", side(3) ?: 5);
  printf("%d\n", calls);
  return 0;
}
pascal26:6: error: expected C expression

gcc -O0 prints 7 7 / 3 / 1.

Cause

ParseCExpr (compiler/cparser.inc) handled ? by unconditionally parsing a middle operand:

if CurTok.Kind = tkQuestion then
begin
  Next;
  thenE := ParseCCommaExpr();   { middle }
  Expect(tkColon, ':');

With the middle omitted, CurTok is already : and ParseCCommaExpr has nothing to parse.

Why the obvious fix is wrong

The tempting one-liner is thenE := left — reuse the condition node as the then-arm. That is wrong on evaluation count: gcc evaluates the left operand exactly once, and calls above proves it (0 -> 1 per side() call, not 2). Duplicating the node makes the AST evaluate it twice, so side(3) ?: 5 would call side twice and any f() ?: d with a side effect would silently run it again.

Fix

Bind the condition to a hidden temp and let the assignment be the condition — an AN_ASSIGN yields the stored value, so the truth test sees it without a second evaluation — then read the temp back as the then-arm. Everything downstream sees an ordinary AN_TERNARY, so CTernaryResultTk, the CNodeDecaysToPointer pointer-result path and the ir.inc short-circuit lowering all apply unchanged. Syms[tmp].PtrElemRec is carried over from the condition so (np ?: pp)->field still resolves.

Outcome

Landed in ParseCExpr (compiler/cparser.inc). test/cternary_elvis.c, wired into test-core, oracled byte-for-byte against gcc -O0: int arms, the once-only evaluation count, pointer arms with ->field through the result, char promotion, float arms, nesting, the busybox vi.c shape, and the else arm's side effect.

One test row had to be split across two printfs: reading the side-effect counter in the same call as the expression that bumps it tests the compiler's argument evaluation order, which C leaves unspecified — the same trap that bit [[bug-c-a-ternary-cannot-be-the-callee-of-a-call]].

Gates: c-conformance 220/0 (unchanged), gate.sh quick GREEN.

busybox: editors/vi.c clears line 791 and now reaches line 2446, where it stops on strncasecmp — a crtl gap, not a compiler one. The four-file expected C expression cluster is down to three, all of them the _IOW/_IOR family, which measurement shows is also a crtl header gap (pxx's <sys/ioctl.h> never defines the _IO* macros; busybox spells FDGETPRM itself as _IOR(2, 0x04, struct floppy_struct) and expects the header to supply the macro). Filed separately — not a compiler defect.

Log