Skip to content

feat(framework): unified-closure overloads of with_handle_scope and parallelize_safe - #129

Merged
codetalcott merged 1 commit into
mainfrom
unified-closure-overloads
Oct 22, 2026
Merged

codetalcott merged 1 commit into
mainfrom
unified-closure-overloads

Conversation

@codetalcott

Copy link
Copy Markdown
Owner

Adopts the two Mojo 1.2.0 patterns from the changelog review that earn their keep here.

1. Unified-closure overloads (public API, additive)

1.2.0 kept moving stdlib and MAX APIs from compile-time capturing closure parameters to runtime closure arguments (Tuple.consume_elements, SIMD.reduce, foreach, warp reductions). Our two public closure APIs were legacy-only, which is the form that silently read a dead stack slot on 1.1.0.

with_handle_scope(b, env, body)   # body: Some[def () raises -> None]
parallelize_safe(func, n)         # func: Some[def(Int) -> None], forwarded straight to MAX's parallelize
  • The new overloads hold the implementation. The legacy with_handle_scope[body](b, env) and parallelize_safe[func](n) keep their signatures and delegate, so downstream (@qkstat/retrieve, @qkstat/embed) compiles unchanged.
  • Runtime witnesses: scopedCallUnified and parallelSquaresUnified run the same work as scopedCall and parallelSquares through the new forms. tests/host.test.js and tests/runtime.test.js run every case against both.
  • Mutation-checked: breaking either new overload failed every case for both exports of its pair, so the tests reach the shared implementation from both forms.
  • There are cover calls per overload in framework_coverage.mojo.
  • examples/vectors-addon.mojo now uses the unified form. Its results on 100,003-element vectors are bit-identical to the legacy build's.

2. match in three lookup functions (internal, no behaviour change)

napi_status_name (24 literal arms), js_type_name (case == on named constants, with null and object merged into one arm) and createTypedArrayView's string dispatch. Each was checked equivalent over every input code before the rewrite. CLAUDE.md gains the rule that makes match safe here: a named constant is case == NAME:, because a bare name binds a new variable and matches everything. --Werror catches most shapes, but not all.

Also

  • scopedCall's generated .d.ts type goes from (a: any, b: any): any to (n: number, fn: (i: number) => unknown): number. Its body now delegates, so inference cannot see it, and an explicit entry fixes that.
  • Export counts are 158 → 160 functions (README, CLAUDE.md, docs/EXPORTS.md), and the API reference and docs/MOJO-RULES.md are regenerated.

Considered and not adopted

  • not Copyable: the handle types are already non-copyable.
  • EnumLike / case .NAME: it needs the unstable __enum.
  • if case .Some(x) =: the Optional sites are N-API output slots.
  • comptime match type switches: there is no type dispatch to replace.

Verified locally (darwin-arm64, Node 24.15.0)

  • Every CI compile is warning-clean under --Werror: addon, coverage target, examples, FFI probe, kitchen sink.
  • 862 tests and 12 GC tests pass, and the full suite passes under Guard Malloc with MALLOC_STRICT_SIZE=1.
  • All static gates pass.
  • The scopedCall benchmark is unchanged (72.3 vs 71 ns).

m0-interop (non-required) will still fail its gate step until m0 is re-gated on Mojo 1.2.0.

🤖 Generated with Claude Code

…arallelize_safe

Mojo 1.2.0 kept moving stdlib and MAX APIs from compile-time closure
parameters to runtime closure arguments (Tuple.consume_elements,
SIMD.reduce, foreach, warp reductions). Both public closure APIs here
were still legacy-only, which is the form that read a dead stack slot on
1.1.0.

`with_handle_scope(b, env, body)` and `parallelize_safe(func, n)` now take
the closure as `Some[def(...)]`, an ordinary argument with an explicit
capture list, and hold the implementation. The new parallelize_safe
forwards `func` straight to MAX's parallelize. The legacy `[body]`/`[func]`
overloads are unchanged in signature and delegate, so downstream code
compiles as before and can migrate ahead of the legacy form's removal.

Each form has a runtime witness: scopedCallUnified and
parallelSquaresUnified run the same work as scopedCall and parallelSquares
through the new forms, and the host and runtime suites run every case
against both. Mutation-checked: breaking either new overload fails every
case for both exports of its pair, so the tests reach the shared
implementation from both forms. Cover calls are added per overload.
examples/vectors-addon.mojo moves to the unified form, and its results on
100,003-element vectors are bit-identical to the legacy build's.

Also uses `match` (new in 1.2.0) in napi_status_name, js_type_name and
createTypedArrayView's type dispatch. Output is unchanged; equivalence
was checked over every input code before the rewrite. CLAUDE.md gains the
rule that makes it safe: a named constant is `case == NAME:`, because a
bare name binds and matches everything.

scopedCall's generated .d.ts type goes from (a: any, b: any): any to a real
signature, because its body now delegates and inference cannot see it.

Verified on darwin-arm64 / Node 24.15.0: every CI compile (addon,
coverage target, examples, FFI probe, kitchen sink) is warning-clean under
--Werror; 862 tests, 12 GC tests; the full suite under Guard Malloc with
MALLOC_STRICT_SIZE=1; all static gates; scopedCall benchmark unchanged
(72.3 vs 71 ns).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codetalcott
codetalcott merged commit 04cdcfa into main Oct 22, 2026
10 of 11 checks passed
@codetalcott codetalcott mentioned this pull request Oct 22, 2026
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