(c ? f : g)(args)
Found compiling busybox 1.37.0, alongside [[bug-c-logical-not-is-not-folded-in-a-constant-expression]].
Repro
static int add(int a, int b) { return a + b; }
static int sub(int a, int b) { return a - b; }
int main(void) { int p = 1; return (p ? add : sub)(7, 3); }
Expected: ), but got: (Kind: 74, Line: 3)
pascal26:3: error: unexpected token
Measured which parenthesised callees pxx already accepted — the gap is narrow and precise:
| form | before |
|---|---|
add(7, 3) |
ok |
fp(7, 3) |
ok |
(*fp)(7, 3) |
ok |
(fp)(7, 3) |
ok |
(add)(7, 3) |
ok |
(p ? fp : fp)(7, 3) |
refused |
(p ? add : sub)(7, 3) |
refused |
busybox, libbb/copy_file.c:93:
if ((FLAGS_DEREF ? stat : lstat)(source, &source_stat) < 0) {
coreutils/stat.c has the same shape.
The fix, and why it is not a ninth arm
CNodeProcSig answers "what is the call signature of this node, and which node
carries the address", as a flat chain of else if ASTKind[inner] = … arms —
AN_IDENT, AN_PROCADDR, AN_CALL, AN_CALL_IND, AN_INDEX (two spellings), AN_FIELD,
AN_PTR_CAST. Eight shapes for one question, which is the smell
root-cause-over-microfix.md names.
The AN_COMMA arm at the top does something different: it recurses, because
(a, fn)(x)'s callee is itself a function-pointer expression. A ternary is the
same situation, so it gets the same treatment:
if ASTKind[inner] = AN_TERNARY then
begin
if ASTRight[inner] >= 0 then
begin
Result := CNodeProcSig(ASTLeft[ASTRight[inner]], commaCallee);
if Result < 0 then
Result := CNodeProcSig(ASTRight[ASTRight[inner]], commaCallee);
end;
Exit;
end;
C requires the two arms to have compatible types, so either supplies the
signature; ask the then-arm and fall back to the else-arm, because one may be a
null pointer constant that carries none. The callee stays the whole ternary
node, so the condition is still evaluated and the arm selected at run time —
AN_CALL_IND evaluates its callee node as a value, which is exactly what the
comma arm already relies on.
Every shape the chain knows is legal in a ternary arm, so recursion gets all of them at once and adds nothing to maintain in step.
Measured
| check | result |
|---|---|
| repro | 10, matching gcc |
coreutils/stat.c |
refused → compiles |
libbb/copy_file.c |
past the ternary; now fails later at line 351 (expected C expression), a different gap |
run_c_conformance.sh |
220 pass / 0 fail — baseline |
| self-host | converged after 1 round |
gate.sh quick |
GREEN |
coreutils/test.c, editors/ed.c and util-linux/acpid.c were filed with this
one on a shared symptom string and are not this bug: their Expected: ) is
Kind: 79, not the ternary's Kind: 74, and all three stop at busybox's
INIT_G() / INIT_S() macro. Separated rather than assumed.
Gate
test/cternary_callee.c, gcc -O0 oracle, wired into test-core: bare function
names and function-pointer variables in both arms, a ternary nested inside the
else-arm so the recursion has to go more than one level, and the condition
evaluated exactly once.
That last row is sequenced across two printfs on purpose. Written as one call
it read 4 0 under gcc and 4 1 under pxx — argument evaluation order within a
single call is unspecified in C, so the single-printf form tests the wrong
thing and both compilers are right.