perf: precisely short-circuit exact forwarders (fmodf et al. libm calls) and zero-forwarders (Motoko-specific)#5961
perf: precisely short-circuit exact forwarders (fmodf et al. libm calls) and zero-forwarders (Motoko-specific)#5961
fmodf et al. libm calls) and zero-forwarders (Motoko-specific)#5961Conversation
| ingress Completed: Reply: 0x4449444c016c01b3c4b1f204680100010a00000000000000000101 | ||
| ingress Completed: Reply: 0x4449444c0000 | ||
| debug.print: (+671_088_640, 5_522_892_825) | ||
| debug.print: (+671_088_640, 5_405_452_313) |
There was a problem hiding this comment.
Instruction count reduced: 5_522_892_825 → 5_405_452_313 (−1.1%)
| ingress Completed: Reply: 0x4449444c0000 | ||
| debug.print: (50_227, +75_320_504, 1_153_792_735) | ||
| debug.print: (50_070, +86_221_288, 1_256_407_315) | ||
| debug.print: (50_227, +75_320_504, 1_153_792_714) |
There was a problem hiding this comment.
Instruction count reduced: 1_153_792_735 → 1_153_792_714 (−21 instrs)
| let rec fixpoint f x = let x' = f x in if x' = x then x else fixpoint f x' | ||
|
|
||
| let chase_forwarders (funcs : func array) (types : Wasm_exts.Types.func_type list) | ||
| (import_count : int) (exports : exports) : exports = |
| let idx = Int32.to_int fi - import_count in | ||
| if idx < 0 then None | ||
| else | ||
| let f = funcs.(idx) in |
There was a problem hiding this comment.
This is AI-written, needs rewrite to make it nicer.
|
|
||
| ### Safety | ||
|
|
||
| The replacement is safe at any call site that supplies **any `i32.const k`** |
There was a problem hiding this comment.
I think this is a lie. We need a strict guarantee that const 0 is at the exact right stack depth. See section "Precise call-site eligibility via stack-depth tracking" below!
There was a problem hiding this comment.
Agreed — the Safety paragraph is loose. "Any i32.const k before the call" isn't enough; the const has to be the value on the operand stack at depth n-1 (0-indexed from TOS, with n = callee arity) at the instant the call executes. That is exactly what the "Precise call-site eligibility via stack-depth tracking" section below describes, and what the shipped code checks at each call site via ConstTrack.lookup state (n-1) in the on_call callback.
I'll tighten this subsection so it points at the stack-depth criterion rather than implying a textual/peephole match, and remove the "any i32.const k" framing.
There was a problem hiding this comment.
Turned out there was a second, independent bug of the same spirit: the shipped on_call predicate in linkModule.ml (line 1113) matched any ConstTrack.I32 _ | I64 _ at the closure-arg depth, not zero specifically — a regression from 80ae9b7 when the flat-array heuristic (which had I32 0l | I64 0L) was replaced by the abstract interpreter.
A nonzero caller constant would have reached the terminal $k unchanged after elision, which is unsafe if $k inspects $clos (not a linker-provable invariant for an arbitrary callee). Tightened the predicate to Some (ConstTrack.I32 0l | ConstTrack.I64 0L) and rewrote the Safety subsection to match. b87813a.
Tests still pass because moc only emits i32.const 0 for top-level named calls — but the previous code was one malicious caller away from producing wrong Wasm.
| but for let-bound closure values it emits a **static closure object pointer** | ||
| (e.g. `i32.const 2097251`). The worker function ignores its received `$clos` | ||
| and synthesises its own `i32.const 0` for the callee — so the caller's | ||
| constant is irrelevant to the callee's behaviour. |
There was a problem hiding this comment.
The callee might examine the passed in closure and use it when not zero. So short circuiting is only permissible when the call-site really passes a zero.
…types Switch constTrack from Wasm.Ast to Wasm_exts.Ast types (matching linkModule's namespace). Add diagnostic pass in linkModule.ml that runs the abstract interpreter over each function body. Fix OCaml name resolution issues: open Wasm.Source for .it, extract is_zero helper, Select without wildcard. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…er rewriter The old call-site rewriter used arr[i - param_count] to check if the closure arg was Const 0 — unsound when arguments span multiple instructions. Now uses constTrack's abstract interpreter with proper stack-depth tracking via ConstTrack.lookup lru (n_params - 1). Adds on_call callback to process_block so linkModule can inspect the LRU at each call site. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Process Block bodies recursively, intersect LRU states at If join points (constants that agree in both branches survive). Loop conservatively flushes. VarBlockType resolution via optional type_section callback. Unlocks 11 new files with zero-forwarder rewrites that were previously invisible behind control flow. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Removes redundant local opens and qualified prefixes throughout. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…filter Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Propagate constants through Select: known condition picks the right operand, equal operands propagate regardless of condition. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
BrIf continues on the fall-through path (pop condition, proceed). Fix Block and If handlers: body depth tracking is authoritative — no exit shift needed (was double-counting). Evict result slots at join points conservatively since BrIf-taken paths may carry different values. Document known pessimisations and Phase 3 plan for precise joins via OCaml 5.x algebraic effects (May_leave effect + depth decrement at each Block boundary). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All three branch instructions emit May_leave. BrTable is just an iter over targets — each block collects the same LRU. fold_left intersect at the join resolves both known pessimisations. Draft OCaml 5.3 PR exists. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove constTrack eprintf and chase_forwarders eprintf - Remove unused is_zero helper - Pass type_section callback so VarBlockType blocks are resolved Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add section to call-forwarding.md explaining the stack-depth + LRU-zeros-set approach as a sound replacement for the current flat-array offset heuristic. Includes per-instruction-class delta table (const/get/set/tee/unary/binary/call), control-flow handling, and a clarification that this only applies to the 0-forwarder call-site rewriter, not chase_forwarders. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pure LRU cache keyed by stack depth tracks integral constants through straight-line Wasm code. Supports constant folding for i32/i64 arithmetic, continues past `call` with stack delta adjustment, and dumps to stderr when a known zero is in the LRU at a call site (potential forwarder elimination candidate). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fmodf et al. libm calls) and zero-forwarders (Motoko-specific)
Regression from 80ae9b7 (migration from flat-array heuristic to ConstTrack): the new `on_call` predicate matched any `I32 _ | I64 _` at the closure-arg stack depth, where the flat-array code had checked `I32 0l | I64 0L` specifically. A 0-forwarder `$foo` overwrites its received closure with `i32.const 0` before calling `$k`. Eliding `$foo` lets the caller's closure value reach `$k` unchanged. If the caller had supplied a nonzero constant and `$k` inspects `$clos` (not a locally-provable invariant for an arbitrary callee), behaviour diverges. Tightening to zero makes the rewrite an identity at `$k`'s entry. Tests still green: moc emits `i32.const 0` for top-level named calls, which is what the tightened predicate matches. Also rewrites the Safety subsection in .claude/plans/call-forwarding.md to document the correct criterion and explain why "any i32.const k" is unsound. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ycles Without cycle detection, the em1 0-forwarder rewriter could loop forever when two or more top-level Motoko functions formed a cyclic forwarder chain — moc would hang at compile time. Reproduced on classical-persistence compilation of test/run/multi-value-block.mo. Introduce `compress_forwarder_map`: one-shot Kleene iteration on an Int32Map of raw one-hop mappings, emitting a difference-map per step for termination. Bounded at [cardinal raw] iterations; on exit, any entry whose target is still a key of the converged map sits on (or leads into) a cycle and is dropped — it has no well-defined terminal, so the rewrite is unsafe. After compression the rewriter needs at most one pass and cannot loop. Applied to both sites: - `chase_forwarders` (RTS exports side) — builds raw map via `forwarding_target`, compresses, single-shot `NameMap.map`. Previously used a generic `fixpoint` helper that also had no cycle guard. - em1 0-forwarder rewriter — builds raw map via `zero_forwarder_target`, compresses, then walks each function body once. Rewriter rewritten in functional style: `collect_rewrites` returns a difference-map (instr index → new target) from `ConstTrack.process_block`; `apply_rewrites` applies it via `List.mapi`. No more `rec loop`, no `any_changed`/ `changed` refs, no in-place array mutation. Verified green: - test/run/multi-value-block.mo (was hanging): completes in 1.7s - test/run/fmodf-forward.mo (regression check): unchanged - test/run -j8 quick: full suite passes Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Regression from e98cfec: the functional rewriter collected rewrites keyed by ConstTrack's instruction index and applied them via `List.mapi` on the top-level body. But `ConstTrack.process_block_inner` recurses into `Block`/`Loop`/`If` bodies with a fresh `idx=0`, so the callback receives a block-relative index, not a body-absolute one. A rewrite fired inside a nested block would land at a top-level position with that `idx` — the wrong (and typically differently-typed) instruction — producing Wasm that fails validation at link time. Observed under `--sanity-checks --enhanced-orthogonal-persistence` in `test/run-drun/blob-dedup-upgrade`: the inserted sanity-check call at the top-level shared an index with a 0-forwarder call inside a nested `block`, so the rewriter wrote the forwarder's target over the sanity call, yielding `expected [i64, i64] but got [i64]` at load time. Fix: key rewrites on the `instr` phrase's *physical identity* (OCaml `==`), which uniquely pins the exact AST node regardless of its depth in the block tree. Apply by walking the body recursively, descending into `Block`/`Loop`/`If` bodies. Relies on the linker's Wasm IR being tree-shaped (no phrase sharing, no position aliasing) — a property held by moc's IR. Verified: - test/run-drun/blob-dedup-upgrade.drun under sanity+EOP (was failing) - test/run/multi-value-block.mo (was hanging pre-compression) - test/run/fmodf-forward.mo (no regression) - manual wasm-validate on the compiled output: clean Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 0-forwarder rewriter keys its rewrite set on OCaml (==) of the instruction phrase to sidestep ConstTrack's block-relative idx. Lift that comparison into a named predicate on the abstract-interpreter's public API so the tree-shape-IR invariant it relies on is documented in one place rather than inlined at the call site. OCaml inlines the definition, so no runtime cost. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Independent review summary (soundness pass)Dispatched an independent review agent over the three fix commits ( Sound — nothing wrong
Fragile — flagged, not errors
Alternatives considered, not taken
Bottom lineNo correctness issues found across the four commits. |
Moves the plan file here from PR #5961's branch where it sat as a leftover companion doc. Outlines the forthcoming `wasm-exts` sync work — pulling upstream instruction support forward so codegen can emit SIMD, newer ref-types, etc. This PR is the natural home. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Moved to PR #6043 (wasm-exts sync), which is the natural home for the upcoming instruction-catchup work. No content loss — file is reproduced verbatim on gabor/wasm-exts-sync. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the Phase 2 `has_br_if` eviction heuristic with a precise join that accumulates branch-target LRU states via OCaml 5.3 algebraic effects. Each `Br`/`BrIf` performs `May_leave (n, lru)` carrying the branch's label depth and outgoing LRU state. The nearest enclosing `Block`/`If` handler catches `(0, lru)` into a `branch_states` list; `(n>0, lru)` is re-raised as `(n-1, lru)` so effects propagate de-Bruijn-style through nested label-pushing constructs. `Loop` swallows `(0, _)` (back-edges; no fixed-point iteration) and re-raises higher depths. The join unifies fall-through and explicit `Br`-to-End: after body processing, any `Some lru'` return is normalised as a synthetic `May_leave (0, lru')` so it joins `branch_states` symmetrically. The final state is `List.fold_left intersect` over the list — precision now matches the Wasm semantics rather than the conservative "evict result slots whenever a BrIf was seen" of Phase 2. `If` is structurally identical to `Block` after this refactor — both legs feed the same `branch_states` via the same handler, no special- casing of then/else fall-through needed. `process_block` installs a defensive top-level handler that swallows any `May_leave` leaking past the outermost block. Eliminates the documented pessimisations: - BrIf-less blocks with constant results no longer lose them - Blocks where all branch paths agree on result values preserve them Build inside `nix develop` (OCaml 5.3 required for `Effect`). `test/run/fmodf-forward` (FileCheck) and `capture-mut` (runtime) pass unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tates The three label-pushing constructs (Block, If, Loop) had copies of the same effect-handler skeleton — catch (0, lru), catch (n>0, _) and re-raise as (n-1, _). Parametrise the depth-0 action as on_zero and the fall-through-normalisation as a boolean, so: - Block : ~on_zero:(collect into branch_states) ~normalise:true - If : (same on_zero for both legs) ~normalise:true - Loop : ~on_zero:(fun _ -> ()) ~normalise:false The fold-intersect of the collected branch states is extracted as `join_branch_states`. If's two legs become two invocations of the same runner — the structural symmetry with Block is now explicit in the code, not just in the comment. Net: -72 lines, -15 nested record literals. Behaviour unchanged; test/run/fmodf-forward and capture-mut still pass identically. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a linker-level regression test demonstrating that Phase 3 of ConstTrack (precise branch joins via algebraic effects) is what gates the zero-forwarder call-site rewrite in `collect_rewrites`. $bar is a syntactic zero-forwarder — body is `i32.const 0; local.get 1; call $quux` so `zero_forwarder_target` returns `Some $quux`. $foo calls $bar with a closure arg produced by a Block whose body contains a BrIf and whose fall-through + branch-taken paths both push `i32.const 0`. Phase 2 of ConstTrack evicted the Block's result-slot entries whenever a BrIf was seen, so the `0` at depth 1 at the `call $bar` site was lost and `collect_rewrites` wouldn't fire. Phase 3 takes the intersection of fall-through with every Br-target LRU, so the shared `i32.const 0` survives and the linker rewrites $foo's `call $bar` to `call $quux`. Verified by temporarily reverting to pre-Phase-3 ConstTrack and confirming the .ok file diff fires with `call $bar` instead of `call $quux`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the four precision-gain examples the review agent flagged:
$nested_block — reviewer ex. 1, nested Block de-Bruijn depth:
Br 1 from inner (with BrIf) + Br 1 from
fall-through, both carrying `i32.const 0` to
the outer Block's End.
$block_over_loop — reviewer ex. 2, Block { Loop { Br 1 } }:
back-edge swallowed, Br 1 decremented through
Loop handler, outer Block catches the 0.
$agreeing_brif — reviewer ex. 3, canonical agreeing-BrIf:
BrIf-taken and fall-through paths both push
0; Phase 3 intersects, keeps it.
$agreeing_if_legs — reviewer ex. 4, If with agreeing legs (one
has a BrIf that targets the function label):
shared branch_states collects both legs'
normalised fall-throughs, intersect keeps 0.
Verified each case is a strict Phase-3 regression detector by
temporarily reverting to pre-Phase-3 constTrack.ml: all four
functions' `call $bar` sites drop back to unrewritten (no call to
$quux), producing a four-way diff against the committed .ok. Under
Phase 3, every call site gets rewritten and the .ok matches.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 3 (constTrack) just landed on this branchThree commits implement the precise branch-join design sketched in
Review-agent verdictAn independent reviewer with an effects / SSA / Wasm brief walked the two constTrack commits, traced two nested cases step-by-step (
Regression detectorThe reviewer's four suggested test cases live in
Each function ends with Still unblocked from the plan docPhase 3 was gated on "OCaml 5.3 migration (draft PR exists)" — that's now |
…leanups
Small polish pass after Phase 3 landed:
- Drop redundant `Wasm_exts.Ast.` qualifier on `same_instr`'s param
types — `instr` is already in scope via `open Wasm_exts.Ast`.
- Replace intermediate-named bindings (`inner_lru`, `lru_cond`,
`entry_lru`) with shadowed `lru` where the original is not needed
after derivation. Applies to Block, If, and Loop. The handler
callbacks' argument is renamed from `lru_b` to `lru` (shadowing
the enclosing `lru` only within the callback body).
- Collapse Loop further: the inner view, the state passed to the
body, and the exit state are all equal to the flushed lru (empty
entries + empty locals make depth shifts vacuous). Drop the
`n_params`/`n_results` bindings and the trailing shift.
- Pipe `perform (May_leave ...)` as `May_leave ... |> Effect.perform`
in all four sites — reads left-to-right like a "send this effect"
rather than a function call on a constructor.
Pure syntactic / naming cleanup; no semantic change. Verified:
test/ld/zero-fwd-join (4 Phase-3 regression cases) and test/run/
fmodf-forward (FileCheck on emitted Wasm) both pass unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the placeholder `BrTable _ -> None` arm with the full semantics: pop the index, then emit one `May_leave (label_depth t, _)` per target (listed targets + default), then return `None` like any other terminator. Each target's enclosing handler catches via the same de-Bruijn decrement chain as `Br`/`BrIf`. This plugs the last structural gap in Phase 3's control-flow coverage: before this, any block whose only exit was a `br_table` would return `None` with no contributions, so the Block's branch_states stayed empty and the outer join collapsed to None — the analyser "couldn't see past" the BrTable. Fifth case `$br_table_agree` added to test/ld/zero-fwd-join.wat: a Block whose body is `i32.const 0; local.get 0; br_table 0 0 0 0` (three listed targets + default, all targeting the Block). Every target collects `[d0=I32 0]`; intersect keeps the 0; the call site gets rewritten to `$quux`. Verified as a strict regression detector by temporarily reverting only the BrTable arm — produces exactly one `call $quux` → `call $bar` diff in the .ok file (the other four cases unaffected). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up —
|
| Reviewer example | Function name |
|---|---|
| Nested-Block de-Bruijn depth | $nested_block |
Block { Loop { Br 1 } } |
$block_over_loop |
| Agreeing BrIf (canonical) | $agreeing_brif |
| If with agreeing legs | $agreeing_if_legs |
| BrTable, all targets agree | $br_table_agree |
All five call sites get rewritten under Phase 3 + BrTable; each regresses to call $bar when the corresponding feature is reverted.
Updates only the footer sections: - "Phase 3 readiness" heading renamed to note it shipped; clarifies no compose issues surfaced from structural equality on const_val because handlers exchange lru values directly. - Open Question on block/loop/if resolution updated with the landing commits (7d815c4, 14c04cd, 6f2d94c) and a one- paragraph summary of the algebraic-effect mechanism. - New "Implementation Status" section at the very bottom enumerating Phase 1/2/3 coverage, the regression tests in test/ld/zero-fwd-join.wat, and the reviewer passes. Body of the plan (design sketch, code snippets) untouched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses review comment #5961 (comment) ("Possibly a `Return`."). moc occasionally emits an explicit `return` after a tail call; the previous pattern only matched `[Call k]` and missed those bodies. Extending the final-segment pattern to `[Call k] | [Call k; Return]` picks up the variant without loosening the invariant that no other instructions may follow the call (addressing the sibling comment #5961 (comment)). The zero-fwd-join test now has a second forwarder `$bar_ret` with the trailing-Return body, and `$agreeing_brif` (case 3) is re-targeted to call it. Verified strict detection: reverting only the `[Call k; Return]` arm flips exactly `$agreeing_brif`'s call site back to `call $bar_ret` (unrewritten); the other four cases still rewrite via the non-Return arm through $bar. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses review comment #5961 (comment) The test grew beyond fmodf specifically (it covers closure-capture forwarders, Motoko-level forwarding chains, and non-forwarders like `baz`), so `forward-calls` is the more accurate name. Renames the .mo and both .ok files; no internal references to update. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Short-circuit two classes of Wasm call-indirection in the linker, before RTS imports are resolved:
Exact forwarders (
libm/RTS side).forwarding_targetinsrc/linking/linkModule.mldetects functions whose body islocal.get 0 … local.get n-1; call k— the Rust#[no_mangle]wrappers forfmod,fmodf,pow,sin,cos, ….chase_forwardersrewrites the RTS exports map so each export resolves directly to its terminal callee, bypassing the wrapper. Themoc-generated call site then hits$libm::…directly.Zero-forwarders (Motoko-specific).
zero_forwarder_targetdetects Motoko top-level functions whose body isi{32|64}.const 0; local.get 1 … local.get n-1; call k— they forward all arguments through, passing a zero as the closure slot. A new abstract interpreter (src/linking/constTrack.ml{,.mli}, ~495 lines) propagates integer constants over the Wasm operand stack: straight-line code,Block/Loop/Ifjoin points,Br/BrIf/BrTable,Select,Compare/Test,Unary/Convert, with local-tracking andFromLocalbackprop. At eachCall kwherekis a 0-forwarder and the closure arg on the stack isi{32|64}.const 0specifically, the call is rewritten to the chain's terminal. A nonzero constant is deliberately rejected: the forwarder would have overwritten the closure with zero, so eliding it would leak the caller's value to a callee that may inspect$clos.Forwarder-chain compression (termination + cycle safety)
Both forwarder maps (
chase_forwarderson RTS exports and the 0-forwarder map on em1 locals) are compressed via Kleene pointer-jumping with a difference-map output: each step expands every entry's target via the current map; iteration terminates when the diff is empty, or a fuel bound ofcardinal rawkicks in (guarding against odd-length cycles where pointer-jumping oscillates rather than converges). A post-filter drops any entry whose compressed target is still a key of the converged map — those sit on or lead into a cycle and have no well-defined terminal. With the map compressed and cycle-free, the call-site rewriter needs one pass and cannot loop.Nested-block-safe rewrite via physical identity
ConstTrack.process_block_innerrecurses intoBlock/Loop/Ifbodies with a freshidx=0, so positional keys would mis-target calls inside nested blocks. Rewrites are therefore keyed on the physical identity of theinstrphrase (ConstTrack.same_instr, backed by OCaml==). Apply walks the body recursively, descending into the three nested forms. Sound under the linker's tree-shaped IR invariant (no phrase sharing, no position aliasing); documented inconstTrack.mli.constTrack.mliis a public interface — the abstract interpreter is reusable by future linker passes.Tests
test/run/fmodf-forward.mo(FileCheck, classical + EOP + wasm64):fmodfcall site reaches$libm::…fmodf…directly.foo → bar → quuxchain collapses:foocallsquuxdirectly (bar is a 0-forwarder).baz(which adds1.0after callingquux) is not collapsed — not a pure forwarder.$fmodfwrapper remains in the binary (to be DCE'd bywasm-opt).test/run-drun/blob-dedup-upgrade.drununder--sanity-checks --enhanced-orthogonal-persistence— this was the regression that motivated the nested-block fix. The sanity-check instrumentation introduces nestedBlocks around calls; without physical-identity keying, the rewriter landed on the wrong instruction and producedexpected [i64, i64] but got [i64]at load time.test/run.sh— small portability fixes for macOS 26.4 (tr -d '\000',paste -sd' ' -).Follow-up
PR #5964 is stacked on this one and handles the case
chase_forwarderscannot:call_indirectfor capturing closures (foo_clos → bar.1 → quux.1) where the function index is statically known but the closure carries a captured value.Design notes
.claude/plans/abstract-interpreter.md.claude/plans/call-forwarding.md.claude/plans/wasm-exts-update.mdTest plan
make -C test/run fmodf-forward.only—[tc] [run] [comp] [valid] [FileCheck] [wasm-run]pass (classical, EOP, wasm64).make -C test/run-drun blob-dedup-upgrade.onlyunder--sanity-checks --enhanced-orthogonal-persistence— passes.make -C test/run -j10 quick— all tests pass.🤖 Generated with Claude Code