← board

feature: value-bearing expression nodes for the C frontend (ternary + side-effecting exprs)

Why

Pascal is not a C superset at the AST level for ONE reason: C makes value- bearing expressions out of constructs Pascal models as statements. Any of these nested inside a larger expression has no pxx AST node and no lowering:

C yields pxx today
a ? b : c (ternary) a value no node (Pascal has no if-expression)
x = y, a = b = c, if ((n=f())>0) the assigned value AN_ASSIGN is statement-only (lowering returns a store, not the value)
++x / x++ / a[i++] new / old value Inc/Dec are statements
(a, b) comma operator b AN_SEQ is a statement chain (Left=stmt, Right=next), not value-bearing

NOT gaps (already map cleanly): union→variant record, function pointers→ AN_PROCADDR/AN_CALL_IND, goto/labels→AN_GOTO/AN_LABEL, casts/bitops/ enums→AN_PTR_CAST/AN_BINOP, do/whileAN_REPEAT, x+=y→desugar x=x+y (only when the lvalue has no side effects — a[i++]+=1 falls back to slice 2). switch fallthrough is a separate case-model gap, not an expression node.

The IR layer needs no new ops: store-then-yield and IR_IF→temp already suffice. The whole delta is at AST→IR: perform the side effect AND leave the value available, in C's evaluation order (sequence points). Pascal never had to sequence side effects mid-expression, so that machinery is absent.

Slices (do slice 1 first; slices 2/3 are separable and may be parked)

Slice 1 — AN_TERNARY (SHALLOW, the only un-fakeable node) — do this

The only construct that can't be honestly desugared into existing nodes. Cheap:

Slice 2 — value-bearing assignment + pre/post ++/-- (RABBIT HOLE risk)

The deep part. Needs a hidden-temp side-effect-hoisting lowering that:

Slice 3 — comma operator (a, b)

Falls out of slice 2's value-bearing sequence node: evaluate a for effect, yield b. Trivial once slice 2's mechanism exists; otherwise a tiny standalone AN_SEQ-value.

Acceptance

Notes

Slice 1 landed (2026-06-26, Track A — commit 01a92173, pin v76)

AN_TERNARY added (defs.inc) + parser if c then a else b expression form (ParseFactor tkIf) + IR lowering (mirrors AN_IF, each arm stored into one hidden temp via the AN_ASSIGN path, yields a load; short-circuit preserved). Frozen string-literal arms carried as managed AnsiString. Verified int/bool/char/ pointer/string arms, short-circuit, nesting, ternary in writeln / call arg / RHS; valgrind-clean; make test green incl cross; self-host byte-identical (Pascal compiler source emits no ternary, so the node is a pure addition with a built-in safety net). C frontend (feat/cfront) can now lower c ? a : b onto AN_TERNARY after rebasing on pin v76.

Slices 2 (value-bearing assignment + pre/post ++/--) and 3 (comma operator) remain open — they need the side-effect-hoisting pass and are the real scope question. Not started.

Slice 3 landed (2026-06-26, Track A — commit pending, pin v77)

AN_COMMA added — (a, b, ...) evaluates each operand for side effects, yields the last. Done standalone (no slice 2 needed): same append-side-effect-then-yield trick as AN_TERNARY (lower Left as a discarded statement, yield Right). Parser splices it in ParseFactor's tkLParen path; a parenthesised comma list was a syntax error before, so conflict-free and identical to C. Guarded out of Python mode (tuples). Verified value/side-effects/grouping/nesting/regress (calls, sets, index, casts); make test green incl cross; self-host byte-identical.

Only slice 2 (value-bearing assignment x=y, chained a=b=c, pre/post ++/--) remains — the side-effect-hoisting / evaluate-lvalue-once pass, the real scope question. Not started.

Slice 2 landed — ticket COMPLETE (2026-06-26, Track A — commit 0c8595e0, pin v78)

AN_INCDEC (++/--) + AN_COMPOUND_ASSIGN (+= -= *= /=, and plain value-bearing =/chained a=b=c for the C frontend). Core primitive: evaluate the lvalue ADDRESS exactly ONCE into a hidden pointer temp, then load/modify/store through it — so a[i++] += 1 bumps i once and postfix yields the old value. No new IR ops. New lexer tokens ++ -- += -= *= /= (Pascal lexer only; inert for existing source → byte-identical). Prefix/postfix in ParseFactor, compound tail in ParseExpr; lvalue check at IR lowering (rejects a++ += x). Pointer ++ scales by element size. Verified incl lvalue-once, chaining, illegal-lvalue rejection, pointer arithmetic; valgrind-clean; make test green incl cross; self-host byte-identical.

Statement-level forms (x++;, x += 5;) are intentionally NOT added to the Pascal statement parser (delicate := dispatch, byte-identical risk) — the C frontend wraps these as expression-statements on its own side, and Pascal tests use expression position. If a Pascal statement surface is wanted later, it is a separate small ticket.

All three value-bearing gaps (ternary, comma, assignment/inc-dec) are now closed. Pascal is a C superset at the expression/statement-model level for these. The C frontend (feat/cfront) lowers ?:, ,, =/+=/++ onto AN_TERNARY / AN_COMMA / AN_COMPOUND_ASSIGN / AN_INCDEC after rebasing on pin v78.