Skip to content

feature: mapr off tree-bridge (Phase 2 PR3b, partial)#389

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

feature: mapr off tree-bridge (Phase 2 PR3b, partial)#389
danieljohnmorris merged 2 commits into
mainfrom
feature/phase2-pr3b-hof-finalizers

Conversation

@danieljohnmorris
Copy link
Copy Markdown
Collaborator

Summary

Phase 2 PR3 (#387) lifted partition 2 off the tree-bridge. This PR extends the migration to mapr 2, the smallest of the four remaining bridge HOFs and the only one that needs no new finalizer opcode (a single existing OP_WRAPOK finishes the assembly step).

The original PR3b scope was all four remaining HOFs (srt 2, grp 2, uniqby 2, mapr 2). After investigating the surface area (per-HOF new opcode 179-182 + VM dispatch arm + Cranelift JIT helper + AOT helper + tree-bridge eligibility removal + 4-5 cross-engine tests + example, multiplied by four) I time-boxed to the one that was tractable end-to-end with cross-engine parity. The other three are deferred to PR3c with a clear pattern to follow — see below.

Manifesto framing: mapr is reached for by agents doing Result-aware data validation pipelines. The bridge round-trip paid a NanVal-to-Value cost per element and pinned the run to a live ACTIVE_AST_PROGRAM TLS. The native lift removes both and aligns mapr with the other native HOFs (map, flt, fld, flatmap, partition).

Repro before/after

chk x:n>R n t;>x 5 ^\"too big\";~*x 2
f xs:L n>R (L n) t;mapr chk xs

Before this PR the VM/Cranelift paths re-entered the tree interpreter for each callback. After this PR all three engines run the same OP_FOREACHPREP/NEXT + OP_CALL_DYN + OP_ISERR/OP_ISOK + OP_UNWRAP + OP_LISTAPPEND + OP_WRAPOK bytecode shape. Output is byte-identical across tree, VM, and Cranelift JIT.

What's in the diff

  • vm: lift mapr 2 off tree-bridge with ISERR short-circuit — new arm in RegCompiler::compile_expr that emits the native bytecode shape directly, mirroring partition 2 from feature: HOF native closure dispatch (Phase 2 PR3) #387 plus the short-circuit pattern and the OP_WRAPOK finalizer. Mapr 2 removed from is_tree_bridge_eligible. The mapr! / mapr!! unwrap forms are handled by emit_result_unwrap on the produced Result, matching the bridge's behaviour. Non-Result callback returns raise through OP_PANIC_UNWRAP with a "mapr: fn must return a Result" message that contains the same substrings the tree-walker raises.

  • test: cross-engine coverage for mapr native dispatchtests/regression_phase2_hof_finalizers.rs with 7 cases covering non-capturing lambda, capturing lambda, named fn-ref, empty input, short-circuit mid-list, all-Ok run, and short-circuit on the very first element (acc-empty edge). Each case runs on tree / VM / Cranelift JIT. Plus examples/mapr-shortcircuit.ilo so the example harness exercises the lift cross-engine too.

Test plan

  • cargo test --release --features cranelift — full suite green (3180 + the rest)
  • cargo fmt --check clean
  • cargo clippy --features cranelift --release --tests -- -D warnings clean
  • New regression tests pass on tree, VM, and Cranelift (7/7)
  • Example harness picks up examples/mapr-shortcircuit.ilo and runs it across every engine

Follow-ups (PR3c)

The remaining bridge HOFs that need dedicated finalizer opcodes are deferred to PR3c:

  • srt 2 fn xs — needs OP_SRT_BY_KEY (slot 179): collect (key, value) pairs via OP_CALL_DYN per element, finalizer sorts in-place with Number/Text/mixed key handling matching the tree-walker.
  • grp 2 fn xs — needs OP_GRP_BY_KEY (slot 180): finalizer builds Map<MapKey, Vec<value>> using the existing MapKey::Int(n.floor() as i64) for finite numbers + Text fallback for Bool.
  • uniqby 2 fn xs — needs OP_UNIQ_BY_KEY (slot 181): finalizer walks a HashSet<String> with the existing type-prefixed key scheme (n:/t:/b:) and appends only first occurrences. Matches tree-walker semantics exactly; no new MapKey path.

Each needs a per-HOF extern "C" Cranelift helper mirroring the partition #387 pattern (helpers.srt_by_key etc), plus AOT wiring in compile_cranelift.rs, plus tree-bridge removal. Pattern is established by this PR.

No doc update needed

This is a behaviour-restoration lift — the mapr signature, return type, and semantics are unchanged. SPEC.md / ai.txt / SKILL.md / docs/reference/builtins.md already cover mapr fn xs : R (L T) e. Internal refactor only.

Phase 2 PR3b starts the HOF finalizer migration. mapr 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 mapr emits its own foreach loop using OP_CALL_DYN per element,
with an ISERR check that short-circuits on first Err (out_reg = res
and skip the wrap-acc tail), an ISOK typecheck that panics with a
mapr-shaped message on a non-Result callback, an OP_UNWRAP of the
Ok inner before appending to the acc list, and a final OP_WRAPOK to
assemble Ok(L T) on the natural exit. No new opcode is needed since
OP_WRAPOK already exists; that's why mapr ships in PR3b ahead of
srt/grp/uniqby which all need dedicated finalizer opcodes.

The closure-aware OP_CALL_DYN from the Phase 2 PR1/PR2 work means
capturing lambdas work here without any further plumbing. The mapr!
and mapr!! auto-unwrap forms are handled by emit_result_unwrap on
the final out_reg, matching the bridge's behaviour exactly.

The remaining HOFs needing finalizer opcodes (srt 2, grp 2, uniqby
2) are deferred to PR3c.
Seven cases across tree, VM, and Cranelift:

  - non-capturing inline lambda
  - capturing inline lambda (single capture, multiplier)
  - named fn-ref
  - empty input (vacuous Ok empty list)
  - short-circuit on first Err mid-list
  - all-Ok run with the same short-circuit-capable predicate
  - short-circuit on the very first element (acc-empty edge)

Plus an examples/mapr-shortcircuit.ilo with -- run / -- out
assertions so tests/examples_engines.rs exercises the lift across
every engine. The example also serves as in-context learning for
agents reaching for mapr in future.
@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 52ec6d9 into main May 18, 2026
5 checks passed
@danieljohnmorris danieljohnmorris deleted the feature/phase2-pr3b-hof-finalizers branch May 18, 2026 13:51
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