Skip to content

feature: HOF native closure dispatch (Phase 2 PR3)#387

Merged
danieljohnmorris merged 2 commits into
mainfrom
feature/phase2-pr3-hof-closure
May 18, 2026
Merged

feature: HOF native closure dispatch (Phase 2 PR3)#387
danieljohnmorris merged 2 commits into
mainfrom
feature/phase2-pr3-hof-closure

Conversation

@danieljohnmorris
Copy link
Copy Markdown
Collaborator

Summary

Phase 2 PR3 of 5 in the HOF native closure dispatch sweep. Migrates the first of the 2-arg HOFs off the tree-bridge so closure callbacks dispatch natively via OP_CALL_DYN per element instead of re-entering the tree interpreter on every call.

Pre-PR3 the OP_CALL_BUILTIN_TREE bridge routed every callback through the tree-walker. That cost a NanVal-to-Value round-trip per call and depended on ACTIVE_AST_PROGRAM TLS being live, which has been a source of cross-engine bugs. Post-PR3 partition does its own foreach loop and uses the closure-aware OP_CALL_DYN from PR1/PR2 (#384/#385).

What landed

  • partition 2: native foreach loop with OP_CALL_DYN per element, two accumulator lists (pass/fail) grown in place via OP_LISTAPPEND, OP_ISBOOL typecheck mirroring the flt 2 native lift, OP_LISTNEW + two appends to assemble the final [pass, fail] result. Capturing lambdas work for free via OP_MAKE_CLOSURE + closure-aware OP_CALL_DYN. Dropped from is_tree_bridge_eligible.

What's still on the bridge

The other HOFs the PR3 plan called out (srt 2, grp 2, uniqby 2, mapr 2, plus neighbours ct 2 and rsrt 2) each need a dedicated finalizer opcode for the post-callback assembly step:

  • srt 2 / rsrt 2: sort-by-key, requires Number/Text polymorphic comparison.
  • grp 2: produces a Map keyed by predicate result, needs MapKey construction.
  • uniqby 2: needs HashSet-based dedup keyed on type-tagged stringification.
  • mapr 2: Result-aware short-circuit on Err.
  • ct 2: counter only (simplest, deferable but lower-impact).

Rather than ship a half-finished foundation for those, this PR locks in the easiest migration (partition's bool-predicate shape, identical to the existing flt 2 lift) and leaves the dedicated finalizer-opcode work for follow-up PRs. Time-boxed to keep the surface area reviewable.

Bench

The Phase 2 PR3 bio canonical bench (flt all-h (window k seqs)) runs unchanged: flt 2 already had a native lift pre-PR3, so the bench numbers don't move on this partition-only migration. Bench rerun will be meaningful once srt/grp/uniqby land in a follow-up PR.

What's in the diff

Per-commit:

  1. vm: lift partition 2 off tree-bridge to OP_CALL_DYN (src/vm/mod.rs)
  2. test: cross-engine coverage for partition native dispatch (tests + examples)

Test plan

  • Build: cargo build --release --features cranelift clean
  • Full suite green: cargo test --release --features cranelift (6852 pass, 0 fail; up 9 from 6843)
  • New regression file: 9 tests, all engines (tree, VM, Cranelift), all three Phase 2 shapes plus empty input, all-pass, all-fail, text values, and non-bool predicate error
  • examples_engines harness picks up partition-closure-native.ilo across every engine
  • cargo fmt clean
  • cargo clippy --features cranelift --all-targets -D warnings clean

Follow-ups

  • PR3b/3c/3d: native lifts for srt 2 / rsrt 2 / grp 2 / uniqby 2 / mapr 2 (each needs a dedicated finalizer opcode for the post-callback assembly step).
  • PR4: tests/regression_inline_lambda.rs infra migration.
  • PR5: SPEC.md "tree-only" wording cleanup once all 2-arg HOFs are native.

Phase 2 PR3 starts the HOF native dispatch migration. partition 2
previously round-tripped every per-element callback through the
tree-bridge (OP_CALL_BUILTIN_TREE), paying a NanVal-to-Value cost
per call and depending on the ACTIVE_AST_PROGRAM TLS being live.

Now partition emits its own foreach loop using OP_CALL_DYN per
element, mirroring the shape of the existing flt 2 native lift but
threading two accumulator lists. The closure-aware OP_CALL_DYN from
the Phase 2 PR1/PR2 work means capturing lambdas already work here
without any further plumbing.

The remaining bridge HOFs (srt 2, grp 2, uniqby 2, mapr 2, ct 2,
rsrt 2) need dedicated finalizer opcodes for sort/group/dedup or
short-circuit-result handling, and are deferred to follow-up PRs.
Nine regression tests cover partition 2 across the three Phase 2
shapes (non-capturing inline lambda, capturing inline lambda, named
fn ref) plus text values, empty input, all-pass, all-fail, and the
non-bool predicate error path. All nine run against --run-tree,
--run-vm, and --run-cranelift so the engines stay in lockstep.

Updates examples/partition.ilo to drop the now-obsolete
engine-skip: jit annotation, and adds examples/partition-closure-
native.ilo which demonstrates the new native closure path with
single, double, and text-typed captures.
@codecov
Copy link
Copy Markdown

codecov Bot commented May 18, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

@danieljohnmorris danieljohnmorris merged commit 8aa58ec into main May 18, 2026
5 checks passed
@danieljohnmorris danieljohnmorris deleted the feature/phase2-pr3-hof-closure branch May 18, 2026 13:30
danieljohnmorris added a commit that referenced this pull request May 18, 2026
Phase 2 PR3c completes the HOF native dispatch migration. The 2-arg
forms of srt, grp, and uniqby previously round-tripped every per-
element callback through the tree-bridge (OP_CALL_BUILTIN_TREE),
paying a NanVal-to-Value cost per call and depending on the
ACTIVE_AST_PROGRAM TLS being live.

Each now emits its own foreach loop using OP_CALL_DYN per element to
fill two parallel scratch lists (keys + values), then hands them to a
dedicated finalizer opcode:

  OP_SRT_BY_KEY (179)  — sort vals by key; mixed-type-key fallback
                         folds to Ordering::Equal, matching the tree
                         walker exactly.
  OP_GRP_BY_KEY (180)  — bucket vals into a Map<MapKey, List<value>>;
                         numeric keys floor to i64; non-finite numbers
                         and non-t/n/b keys raise a typed runtime err.
  OP_UNIQ_BY_KEY (181) — dedup using HashSet<String> with type-prefixed
                         keys (t:/n:/b:) so values from disjoint
                         domains never alias, matching the tree
                         scheme.

Each finalizer ships a free fn that the VM dispatch arm and Cranelift
JIT/AOT helpers share, so the two paths can't drift. The closure-
aware OP_CALL_DYN from Phase 2 PR1/PR2 means capturing lambdas work
here without further plumbing.

A single emit_hof_keyed_finalize helper covers all three HOFs at the
compiler layer; the only per-HOF variation is which finalizer opcode
to emit.

mapr already shipped its native lift in PR3b (#389), so this completes
the Phase 2 HOF migration that started with partition in PR3 (#387).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant