feat(codegen): multi-table call_indirect — contiguous R11 table region (#650)#653
Conversation
#650) Tables become ONE contiguous region of raw 4-byte code pointers at R11, in declaration order: table 0 at R11+0 (unchanged), table N at R11 + sum(size(0..N))*4 — a compile-time constant, since tables are provably fixed-size (#642: table.grow/table.set loud-skip at decode). - decoder: per-table sizes (table_sizes), per-segment table attribution (ElemSegmentInfo.table_index), CallIndirectGuards restructured to per-table TableGuards { table_size, base_byte_offset, type_reject } — the #646 closed-world type verification now runs per (table, type), and an unverifiable segment poisons only the table it targets (passive/declared/non-const-offset segments still poison all). - selector: shared resolve_call_indirect_guards() feeds both arms; the bounds guard compares against the DISPATCHED table's own size; a table past the linked region, an unknown size, an unknown base (preceding growable import), or a base past LDR imm12 (4095) each loud-decline with a named reason. - encoder (Thumb-2 + A32): non-zero base folds into the pointer load (add ip, r11, ip; ldr ip, [ip, #off]); offset 0 emits the exact pre-#650 bytes, so single-table modules are byte-identical BY CONSTRUCTION (verified: whole-ELF identity vs origin/main on the #642/#594/#597 fixtures, cortex-m3/-r5/-m7dp, ±relocatable; frozen anchors 10/10). - oracle: call_indirect_650_differential.py (CI-gated) — two tables, overlapping indices, aliasing canary (table0[1] != table1[1]), OOB traps per-table, both ISAs; 26/26 green here, red on <= v0.33.1 (compile-time decline — this is a capability upgrade). Unblocks falcon's multi-table fused components (20 of 146 functions dispatched through table 1). Fixes #650. Builds on #646 (#642 guards) and the #275 R11 arc. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Automated review for PR #653pulseengine/synth: Verdict: 💬 Comment Summary: The changes proposed in this pull request are well-thought-out and align with the project's goals. They address a critical issue related to the layout contract for multi-table call_indirect operations, ensuring that tables are linked back-to-back in declaration order and providing compile-time size information for bounds checking. The addition of a frozen-fixture job and oracles helps maintain the Findings: 0 mechanical (rivet) · 1 from local AI model. Findings (1):
Generated by a local AI model and post-validated against a strict JSON contract. Each finding includes the verbatim line being criticised — verify by reading the file at the cited location. Reviewed at |
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Fixes #650. Builds on #646 (the #642 guards) and the #275 R11 arc.
The gap
The ARM backend linked only table 0 at R11: any
call_indirectthrough table index ≥ 1 loud-declined, and #646's closed-world verifier additionally rejected element segments targeting a non-zero table. falcon's fused v1.112 component carries two funcref tables —(table 0 7 7)+(table 1 41 41)— with 20 of 146 functions dispatching through table 1, making this the second-largest skip class after #369.Layout contract (#650)
Tables become one contiguous region of raw 4-byte code pointers based at R11, in declaration order (imported tables first):
The offsets are constants because tables are provably fixed-size (#642:
table.grow/table.setare unsupported ops whose functions loud-skip at decode). The contract is documented onCallIndirectGuardsand in the CI oracle job.What changed
synth-core):table_sizes: Vec<Option<u32>>per table (exact-limit rule for imports unchanged),ElemSegmentInfo.table_indexattributes active segments to their table, andCallIndirectGuardsis restructured to per-tableTableGuards { table_size, base_byte_offset, type_reject }. The fix(codegen): call_indirect bounds guard + compile-time type check — OOB/wrong-type traps per WASM §4.4.8 (#642) #646 closed-world type verification now runs per (table, expected-type); an unverifiable-contents segment poisons only the table it targets (passive/declared/non-const-offset segments still poison all —table.initcould land anywhere).synth-synthesis): both arms (select_with_stack+select_default) shareresolve_call_indirect_guards(). The bounds guard compares against the dispatched table's own size; named loud declines for: table index past the linked region, unknown table size (growable import), unknown base offset (a preceding growable import), base offset > 4095 (LDR imm12 range), and the per-table type verdicts.CallIndirectnever reaches the optimized path (it declines atoptimizer_bridge, thumb-2: call_indirect emits no bounds-check and no type-check — OOB/wrong-type index does an uncontrolled indirect branch instead of trapping (WASM 4.4.8) #642), so it stays on the test(vcr-oracle): estimator↔encoder agreement oracle for the optimized path (#498, #242) #511 oracle's exclusion list.Single-table byte-identity — BY CONSTRUCTION, then verified
Offset 0 emits the exact pre-#650 byte sequence (a literal branch in the encoder, not a re-derivation). Verified against an origin/main-built binary: whole-ELF
cmpidentity on the #642, #594, and #597 fixtures across cortex-m3 / cortex-r5 / cortex-m7dp, both--relocatableand self-contained. Frozen anchors 10/10.Oracle (CI-gated:
call-indirect-650-multitable-oracle)scripts/repro/call_indirect_650_differential.py+call_indirect_650_multitable.wat: two 3-entry tables, overlapping indices, distinct functions — the aliasing canary (table0[1]→ x+200,table1[1]→ 1000−x) catches a backend that drops the table index. Per case, unicorn-executed synth code (region linked at R11 per the contract) vs wasmtime:SYMBOL MISSING), since fix(codegen): call_indirect bounds guard + compile-time type check — OOB/wrong-type traps per WASM §4.4.8 (#642) #646's all-table poison rejected even table-0 dispatch when a segment targeted table 1. This is a capability upgrade: red = "declines today", green = correct dispatch.The issue's exact
via_t1repro now compiles oncortex-m7dp(3/3 functions) — including with table 0 left uninitialized, since poison is per-table.Honest residuals (named declines, unchanged behavior class)
call_indirectwas not lowered there before and still isn't (unchanged).Workspace tests green (re-run after rebasing onto #648, which also touched the decoder), fmt clean, clippy
-D warningsclean workspace-wide.🤖 Generated with Claude Code