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! |
…the type-id sidecar (#676) A HETEROGENEOUS funcref table (mixed signatures — falcon's fused 41-slot dispatch table) can never satisfy the closed-world type check, so every call_indirect through it loud-declined (20 falcon funcs). WASM Core §4.4.8 makes the mismatch a RUNTIME trap, so the sound lowering is the runtime check itself: the object now carries a type-id sidecar (.synth.table_type_ids — one LE u32 STRUCTURAL signature class id per slot, region order; structurally-equal types share one dense 1-based id, the meld 31-decls/25-distinct shape; id 0 reserved for null slots) which the extended R11 layout contract places at R11 + sum(all table sizes)*4, mirroring the pointer region slot for slot. The dispatch inserts, between the #642 bounds guard and the pointer load, on both Thumb-2 and A32: mov ip, idx, lsl #2 ; add ip, r11, ip ; ldr ip, [ip, #type_off] cmp ip, #expected_class_id ; beq ok ; udf The compare subsumes the #664 null trap (id 0 never equals an expected id >= 1), so heterogeneous dispatches emit null_check=false. Encoding ranges decline loudly (sidecar offset > LDR imm12, class id > 255). Homogeneous tables emit type_check=None + no sidecar section — bytes identical BY CONSTRUCTION (the #650 offset-0 / #664 null_check=false trick): whole-ELF cmp verified against origin/main on the #642/#650/#664 fixtures x cortex-m3/r5, frozen anchors 10/10, workspace green. The estimator is untouched (CallIndirect is direct-selector-only, excluded from the #511 agreement oracle). New CI-gated differential (call_indirect_676_differential.py, Thumb-2 + A32): mixed 5-slot table (two classes interleaved + structural-dup type + nulls) — matching-class calls equal wasmtime, wrong-class ("indirect call type mismatch"), null and OOB indices all stop at a UDF; wasmtime's trap REASONS are asserted per category. Non-vacuous red: a build without the check CALLS the wrong-typed function and returns a wrong value. Red at compile on origin/main (capability upgrade). Object-level contract locked in cargo CI (heterogeneous_table_676.rs: sidecar ids [1,2,1,0,0] + no-sidecar for the homogeneous fixtures). Lineage: #642 guards (#646), #650 multi-table (#653), #664 null slots (#669) — this closes the terminal layer of falcon's call_indirect story. Closes #676 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…the type-id sidecar (#676) A HETEROGENEOUS funcref table (mixed signatures — falcon's fused 41-slot dispatch table) can never satisfy the closed-world type check, so every call_indirect through it loud-declined (20 falcon funcs). WASM Core §4.4.8 makes the mismatch a RUNTIME trap, so the sound lowering is the runtime check itself: the object now carries a type-id sidecar (.synth.table_type_ids — one LE u32 STRUCTURAL signature class id per slot, region order; structurally-equal types share one dense 1-based id, the meld 31-decls/25-distinct shape; id 0 reserved for null slots) which the extended R11 layout contract places at R11 + sum(all table sizes)*4, mirroring the pointer region slot for slot. The dispatch inserts, between the #642 bounds guard and the pointer load, on both Thumb-2 and A32: mov ip, idx, lsl #2 ; add ip, r11, ip ; ldr ip, [ip, #type_off] cmp ip, #expected_class_id ; beq ok ; udf The compare subsumes the #664 null trap (id 0 never equals an expected id >= 1), so heterogeneous dispatches emit null_check=false. Encoding ranges decline loudly (sidecar offset > LDR imm12, class id > 255). Homogeneous tables emit type_check=None + no sidecar section — bytes identical BY CONSTRUCTION (the #650 offset-0 / #664 null_check=false trick): whole-ELF cmp verified against origin/main on the #642/#650/#664 fixtures x cortex-m3/r5, frozen anchors 10/10, workspace green. The estimator is untouched (CallIndirect is direct-selector-only, excluded from the #511 agreement oracle). New CI-gated differential (call_indirect_676_differential.py, Thumb-2 + A32): mixed 5-slot table (two classes interleaved + structural-dup type + nulls) — matching-class calls equal wasmtime, wrong-class ("indirect call type mismatch"), null and OOB indices all stop at a UDF; wasmtime's trap REASONS are asserted per category. Non-vacuous red: a build without the check CALLS the wrong-typed function and returns a wrong value. Red at compile on origin/main (capability upgrade). Object-level contract locked in cargo CI (heterogeneous_table_676.rs: sidecar ids [1,2,1,0,0] + no-sidecar for the homogeneous fixtures). Lineage: #642 guards (#646), #650 multi-table (#653), #664 null slots (#669) — this closes the terminal layer of falcon's call_indirect story. Closes #676 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…the type-id sidecar (#676) (#696) A HETEROGENEOUS funcref table (mixed signatures — falcon's fused 41-slot dispatch table) can never satisfy the closed-world type check, so every call_indirect through it loud-declined (20 falcon funcs). WASM Core §4.4.8 makes the mismatch a RUNTIME trap, so the sound lowering is the runtime check itself: the object now carries a type-id sidecar (.synth.table_type_ids — one LE u32 STRUCTURAL signature class id per slot, region order; structurally-equal types share one dense 1-based id, the meld 31-decls/25-distinct shape; id 0 reserved for null slots) which the extended R11 layout contract places at R11 + sum(all table sizes)*4, mirroring the pointer region slot for slot. The dispatch inserts, between the #642 bounds guard and the pointer load, on both Thumb-2 and A32: mov ip, idx, lsl #2 ; add ip, r11, ip ; ldr ip, [ip, #type_off] cmp ip, #expected_class_id ; beq ok ; udf The compare subsumes the #664 null trap (id 0 never equals an expected id >= 1), so heterogeneous dispatches emit null_check=false. Encoding ranges decline loudly (sidecar offset > LDR imm12, class id > 255). Homogeneous tables emit type_check=None + no sidecar section — bytes identical BY CONSTRUCTION (the #650 offset-0 / #664 null_check=false trick): whole-ELF cmp verified against origin/main on the #642/#650/#664 fixtures x cortex-m3/r5, frozen anchors 10/10, workspace green. The estimator is untouched (CallIndirect is direct-selector-only, excluded from the #511 agreement oracle). New CI-gated differential (call_indirect_676_differential.py, Thumb-2 + A32): mixed 5-slot table (two classes interleaved + structural-dup type + nulls) — matching-class calls equal wasmtime, wrong-class ("indirect call type mismatch"), null and OOB indices all stop at a UDF; wasmtime's trap REASONS are asserted per category. Non-vacuous red: a build without the check CALLS the wrong-typed function and returns a wrong value. Red at compile on origin/main (capability upgrade). Object-level contract locked in cargo CI (heterogeneous_table_676.rs: sidecar ids [1,2,1,0,0] + no-sidecar for the homogeneous fixtures). Lineage: #642 guards (#646), #650 multi-table (#653), #664 null slots (#669) — this closes the terminal layer of falcon's call_indirect story. Closes #676 Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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