← board

Labels-as-values is the whole of the lua regression

Two reds — regression-test-lua-compiler-srchash and regression-test-lua-cross-compiler-srchash-2 — are one construct, and it is the same root as the five test_c_gtk* reds ([[regression-test-core-test-c-gtk-2]]): 00ab464bf feat(C,B): the C frontend announces GNU C 2.7. Seven jobs, one cause.

The construct

library_candidates/lua/src/lvm.c:38:

#if !defined(LUA_USE_JUMPTABLE)
#if defined(__GNUC__)
#define LUA_USE_JUMPTABLE	1
#else
#define LUA_USE_JUMPTABLE	0
#endif
#endif

A bare defined(__GNUC__) with no version test — the exact shape 00ab464bf's own header names as the cost of the claim, one construct over from the inline asm it predicted. It selects ljumptab.h:

#define vmdispatch(x)     goto *disptab[x];
static const void *const disptab[NUM_OPCODES] = { &&L_OP_MOVE, ... };

Reproduced at binary 7ef59bc560b4b9fc, the recipe's own flags:

$ ./compiler/pascal26 -g -Ilib/crtl/include -Ilib/crtl/src -Ilibrary_candidates/lua/src \
      test/lua/runner.c /tmp/luarunner
pascal26:28: error: expected C expression
  in: library_candidates/lua/src/lvm.c
  near:       >>>  L_OP_MOVE

Byte-identical to the log tail on both tickets.

It is the ONLY blocker, proved in both directions

Not "the first error we hit" — the only one. Two independent counter-tests, each of which builds cleanly (rc=0, warnings only):

counter-test what it isolates result
-DLUA_USE_JUMPTABLE=0 the CONSTRUCT: force lua's portable switch arm builds
-U__GNUC__ the CAUSE: undo 00ab464bf's claim for this TU builds

Both proofs matter because they fail differently: the first says the jumptable is what pxx cannot compile, the second says the GNU C claim is what turns it on.

And the resulting binary is not merely a binary — it runs the corpus:

PASS closures.lua   PASS coroutines.lua   PASS files.lua
PASS numeric.lua    PASS oop.lua          PASS strings.lua       6/6

So there is nothing behind this gap. Implementing labels-as-values turns both jobs green; nothing else in lua is waiting.

What it needs

&&label as a void* value, and goto *expr as an indirect jump — an address-of-label relocation into a read-only data table, plus an indirect branch the backend and the IR's control-flow handling must both accept. That is a real C-frontend feature, not a session-side fix, which is why this is a ticket and not a commit.

What NOT to do

Do not add -DLUA_USE_JUMPTABLE=0 to the Makefile recipe. It makes the job green today and hides the gap the tier exists to find; lua's default on a GNU-announcing compiler IS the jumptable, so the flag would also stop testing what real builds do. Same for lowering the __GNUC__ claim: 00ab464bf fixed a SILENT wrong layout (__attribute__ expanding to nothing, so PACKED/ALIGNED did nothing and a gzip header union came out 12 bytes where gcc makes 8), and its trade — loud failures naming the construct, in exchange for no quiet wrong answers — is the right one. Its own words: "the answer there is inline-asm support, not a smaller version claim."

The number that commit did not have

00ab464bf measured "busybox's 265 translation units at this config: 1 fixed, 0 broken." The real cost, measured 2026-09-02 across the tier, is 7 jobs broken: five test_c_gtk* on __builtin_constant_p (fixed, see [[regression-test-core-test-c-gtk-2]]) and these two on labels-as-values. That is not an argument against the commit — it is the number nobody had, and the busybox corpus was simply the wrong place to look for it. Routed to frankD, who owns the claim and the lane.

Taken by frankD (2026-09-02), and costed before started

frankD owns 00ab464bf and the lane, and has taken this. Their scoping, kept here so the next reader does not re-derive it:

Not started immediately — a large batch lands first — and deliberately costed before being touched rather than started and stalled. Nothing here is blocked on me; the two lua tickets are blocked-by this one and will follow it.

The scoping above is WRONG, measured 2026-09-02 — it is much smaller

Both the ticket's estimate and frankD's own three bullets were reasoned, not measured. Measuring them changes the shape of the job. Three corrections, each from a probe rather than from reading:

1. Address-of-label needs NO object-writer relocation. The claim was that a &&label in a static initialiser wants a link-time relocation into .rodata. It does not, because pxx does not emit link-time relocations for address initialisers at all. A static initialiser naming an address is lowered to a runtime store: cparser.inc's PendingInit* arrays become AST assignments (AN_PROCADDR, AN_ADDR) in pasparser_prog.inc:302, executed as ordinary code. Probe: a file-scope static int (*const tab[2])(void) = { a, b }; builds and runs under pxx today and matches gcc.

2. And the store lands in the RIGHT BODY. This was the part that looked expensive: LabelPositions is reset per body (ir_codegen.inc:11626) and label ids are per body, so a fill running in the program's init code could not name a label inside luaV_execute. Measured — it does not run there. For a FUNCTION-LOCAL static, the address stores are emitted inside the declaring body:

$ PXXDBG=a.ir:main pascal26 localstatic.c        # static tab[] inside main()
0: procaddr a=806 ...
6: procaddr a=807 ...
$ PXXDBG=a.ir:PXXMAIN pascal26 localstatic.c     # the init body: none
0

lua's disptab is exactly that shape — ljumptab.h is #included inside luaV_execute's body, so its table is a function-local static and its labels are in the same body as the stores that fill it. So the existing per-body LabelPositions + LabelFixupPos/Target machinery covers the whole feature, and no module-scoped (proc, label) table is needed. CodeLen is already an absolute offset into one module-wide code buffer, so the lea is a RIP-relative displacement patched by the same fixup pass that patches a forward jmp.

3. The reachability point stands, and needs sharpening. A label whose address is taken must be marked reachable UNCONDITIONALLY, because IRMarkReachableLabels walks control-flow edges and there is no edge to see — the jump goes through a value. Reading the label set off IRVerify's label-operand kinds (the discipline that function's comment describes) keeps this honest: adding IR_LABELADDR as a label-consuming kind is what makes the walk mark it, and the verifier is what forces the arm to exist.

What it actually needs, then

Landed, 2026-09-02 — x86-64 6eea46f7c, aarch64 in the commit after it

Both halves of the construct, on both targets the lua jobs need.

x86-64: &&label -> AN_LABELADDR -> IR_LABELADDR -> lea rax,[rip+disp32] on the existing label fixup list; goto *expr -> AN_GOTO_INDIRECT -> IR_JUMP_INDIRECT -> jmp rax. LabelFixupCount had to move from a local of the body loop to unit scope: &&label is a VALUE node, emitted from IREmitNode, which is a different procedure.

aarch64: adr x0, #imm21 — PC-relative, so unlike the IR_PROCADDR arm directly above it, it needs neither a relocation nor an 8-byte literal. Forward references share the branch fixup list and the patch loop recognises the placeholder by its top byte ($10), which no other fixup shape there emits. EncodeAdrX0A64 errors above +/-1MB rather than wrapping — that guard is UNTESTED (a >1MB span is expensive to construct); it is a refusal, not a claim.

Results, both measured here:

build 6 lua programs
native x86-64 ok 6/6
cross aarch64 (qemu) ok 6/6

The control that matters. The lua suite does NOT discriminate the two interpreter paths — a -DLUA_USE_JUMPTABLE=0 build passes all six as well. What proves the computed-goto arm is the one that compiled is a binary comparison, on each target: the default build is byte-identical to -DLUA_USE_JUMPTABLE=1 and differs from -DLUA_USE_JUMPTABLE=0. Quoted without that, "6/6" would be a true sentence about the wrong claim.

test/c_labels_as_values.c is 8 rows diffed against gcc, wired into test-core and test-aarch64. Rows 2 and 3 are backward (immediate) and forward (fixup) &&label separately, because those are different lines of codegen. Rows 5/6 are the address-identity control, so an implementation returning a plausible constant fails rather than adding up to the right total by luck.

Still open here: i386, arm32 and riscv32 refuse IR_LABELADDR by name — [[feature-c-labels-as-values-on-i386-arm32-riscv32]], prio 30. Nothing measured is blocked on them: test-lua-cross's other three targets already build-fail on their variadic ABI, so implementing this there moves the failure without moving a verdict.

Log