vm + cranelift: native map HOF dispatch (PR 2 of 4)#277
Merged
Conversation
Adds the runtime plumbing Cranelift needs to lower OP_CALL_DYN. The
JIT has no re-entrant function-pointer table for dynamic dispatch, so
the helper resolves a FnRef NanVal at runtime and:
- for a Builtin FnRef, routes through the tree-bridge (the same
path jit_call_builtin_tree uses) with program-aware arg
conversion so FnRef args render with their source names
- for a User FnRef, re-enters the bytecode VM on the same program
via VM::new(active_program).call(idx, args)
The active program is published through a new ACTIVE_PROGRAM TLS slot
set inside with_active_registry, paired with the existing
ACTIVE_FUNC_NAMES and cleared by the same drop guard. PR 1 wired the
FnRef NaN-tagging side of this; the helper lets Cranelift consume it.
Replaces the PR-1 placeholders (compile_cranelift returned an error, jit_cranelift bailed JIT compilation with None) with a real lowering. Mirrors the OP_CALL_BUILTIN_TREE pattern: spill the contiguous arg slots into a sized stack slot and pass (callee_bits, argc, regs_ptr) to jit_call_dyn. Both the JIT and AOT codegen paths get the new helper FuncId registered in their helpers struct. The lowering treats every FnRef uniformly. Per-builtin specialisation (inline numeric ops, etc.) is a perf follow-up; correctness across both kinds first.
Compiles `map fn xs` (2-arg form) to a real bytecode loop instead of
falling through to the "unsupported HOF" error path. The emitter:
- compiles `fn` to a register, where Expr::Ref of a user or builtin
name already routes to OP_LOADFN (PR 1), so the FnRef NanVal
lives in a register with no heap traffic
- compiles `xs`, allocates an acc list (OP_LISTNEW 0) and index +
item + result + arg scratch slots, with arg adjacent to result
so OP_CALL_DYN's R[A+1..=A+argc] arg layout is correct
- drives the iteration with OP_FOREACHPREP / OP_FOREACHNEXT (the
same pair `@i xs` uses), invokes the callee via OP_CALL_DYN, and
grows acc with OP_LISTAPPEND on the in-place RC=1 fast path
is_tree_bridge_eligible's doc-comment loses `map` from the not-yet
list; the rest of the HOFs are still parked for PR 3. Also drops
the `#[ignore]` from vm_map_squares and vm_map_wrong_list_arg, which
pinned the absence of this code path.
Adds six cross-engine regression tests covering the shapes that were
previously gated with `engine-skip: vm / cranelift`:
- user-function callback (`map sq xs`)
- builtin callback (`map abs xs`) — exercises the Cranelift
helper's tree-bridge dispatch arm
- empty list (early-exit before the first OP_CALL_DYN fires)
- single-element list (one trip through the loop body)
- chained map (two HOF calls in series)
- tail-position map (result is the function's return value)
Each test runs the same source against `--run-tree`, `--run-vm`, and
`--run-cranelift` and asserts identical output. examples/map-fnref.ilo
gives agents a learning sample and doubles as an engine-harness
regression via tests/examples_engines.rs.
Also drops the `#[ignore]` from `vm_map_squares` and
`vm_map_wrong_list_arg`, which pinned the absence of OP_CALL_DYN
emission. PR-3 will lift the remaining ignores for fld/flt/grp etc.
`sums>L n;map sum (window 3 [1,2,3,4,5])` now runs on every engine: `map` is natively dispatched (this PR) and `sum` is a pure builtin promoted to `F n n` by the verifier, which the Cranelift OP_CALL_DYN helper routes through the tree-bridge. The other window.ilo entries (basic/pairs/singles/toobig) only exercise the already-native `window` builtin; the engine-skip annotations existed solely to keep `sums` from breaking the cross-engine harness.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
This was referenced May 15, 2026
Merged
danieljohnmorris
added a commit
that referenced
this pull request
May 15, 2026
- slc / take / drop accept negative indices counting from end (bounds clamp), matching at xs i. Closes the quant-trader fencepost and the slc xs -np 1 np ergonomics gap (#266). - Map keys are typed: text or integer. mset m 7 v and mget m 7 work directly, no str conversion. Int(1) and Text("1") are distinct. Float keys floor to i64; jdmp stringifies numeric keys for JSON (#267). - Add map / flt / fld to the builtin reference. All HOFs (map, flt, fld, srt, grp, uniqby, partition, flatmap) now work cross-engine on tree, VM, Cranelift JIT, and AOT (#274 #277 #278 #279 #280 #283). - New Inline lambdas subsection: Phase 1 literals are cross-engine, Phase 2 closure capture is tree-only with automatic fallthrough surfacing ILO-R012 on VM and Cranelift (#265 #284). - AOT-compiled binaries from ilo compile now strip the top-level ~/^ wrapper byte-for-byte the same as in-process runners (#281).
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PR 2 of the HOF dispatch chain. PR 1 (#274) wired FnRef NaN-tagging so a function reference could survive a NanVal round-trip on VM and Cranelift. This PR puts that plumbing to work for the first HOF.
map fn xsnow runs natively on every engine. The compiler emits a real bytecode loop using the OP_CALL_DYN opcode that PR 1 reserved, and Cranelift lowers OP_CALL_DYN through a newjit_call_dynhelper that re-enters the VM for user-fn callbacks and routes builtin callbacks through the tree-bridge.Token-cost framing: the manifesto bet is that each engine should agree with the others on whatever ilo prints, so an agent never has to second-guess which engine its
-- run-vmexample ran under.engine-skip: vmlines and "tree-only" hedges in HOF examples were a tax on that promise. This PR cashes in the first slice of that formap.Repro before / after
Before:
After:
What's in the diff (per commit)
vm: thread-local active program + jit_call_dyn helper for OP_CALL_DYN — adds the runtime plumbing Cranelift needs. New
ACTIVE_PROGRAMTLS slot published bywith_active_registry(cleared by the same drop guard as the existing FUNC_NAMES / REGISTRY TLS). Newjit_call_dyn(callee_bits, argc, regs_ptr) -> u64extern "C" helper: Builtin FnRef → tree-bridge (same path asjit_call_builtin_tree), User FnRef → freshVM::new(active_program).call(idx, args). The TLS-plus-helper combo is the minimum surface that lets Cranelift dispatch a FnRef whose target id is only known at runtime.cranelift: lower OP_CALL_DYN through the jit_call_dyn helper — replaces the PR-1 placeholders in both Cranelift backends (compile_cranelift returned an error, jit_cranelift bailed JIT compilation with None). Mirrors the OP_CALL_BUILTIN_TREE pattern: spill arg slots into a sized stack slot and pass
(callee_bits, argc, regs_ptr)to the helper. Both backends register the helper FuncId.vm: native map HOF loop using OP_CALL_DYN —
(Builtin::Map, 2)arm in the bytecode compiler. Emits OP_LISTNEW for the accumulator, allocates contiguous result + arg scratch regs (so OP_CALL_DYN's R[A+1..=A+argc] layout is correct), drives the iteration with OP_FOREACHPREP / OP_FOREACHNEXT (the same pair@i xsuses), and grows the accumulator with OP_LISTAPPEND on the in-place RC=1 fast path.is_tree_bridge_eligibledoc-comment losesmapfrom the not-yet list. Also drops#[ignore]fromvm_map_squaresandvm_map_wrong_list_arg.test + example: cross-engine coverage for native map HOF — six cross-engine tests in
tests/regression_hof_map.rscovering user-fn / builtin / empty / single-element / chained / tail-position shapes.examples/map-fnref.ilodoubles as an agent-facing learning sample and runs throughtests/examples_engines.rson every engine.example: lift vm/cranelift skip from window.ilo —
sums>L n;map sum (window 3 [1,2,3,4,5])now runs on every engine and no longer needs the skip annotation.Test plan
cargo build --release --features craneliftcleancargo test --release --features cranelift— all 4803 tests pass, 0 failures across 113 suites (includes the newregression_hof_mapand the cross-engineexamples_enginesharness which exercises the updatedwindow.ilo)cargo clippy --release --features cranelift --all-targets -- -D warningscleancargo fmt --checkcleanmap sq [1,2,3,4,5]andmap abs [-3,0,4,-7]return identical output on--run-tree,--run-vm,--run-craneliftFollow-ups (PR 3, not in this diff)
flt,fld,grp,flatmap,uniqby,partition, 2-argsrt, dynamic-fmtwr— same native-loop pattern, each with its own emitter arm#[ignore] // Dynamic dispatch (OP_CALL_DYN emission) arrives in PR 2.for the other HOFsmap fn ctx xs(verifier accepts it; the emitter currently only handles 2-arg)Value::Text-as-callable dispatch (thevm_map_with_text_fn_nameshape) — needs OP_CALL_DYN to accept a Text fallback alongside FnRef