Cranelift JIT nil-sweep batch 3: arithmetic, comparison, numeric-unary#282
Merged
Conversation
…_RUNTIME_ERROR Group A of the JIT-helper permissive-nil sweep (batches 1/2 landed in PRs #264 and #259; PR #254 wired the JIT_RUNTIME_ERROR TLS primitive and the span_bits packing). Helpers in this batch: jit_add, jit_add_inplace, jit_sub, jit_mul, jit_div (with div-by-zero), jit_mod (with mod-by-zero), jit_neg, jit_gt, jit_lt, jit_ge, jit_le, jit_abs, jit_min, jit_max, jit_flr, jit_cel, jit_rou, jit_clamp, jit_len, jit_str, jit_num. Before: every helper above silently returned TAG_NIL on the type-error path (TAG_FALSE for the ordered comparisons), diverging from the tree walker and the bytecode VM, which both raise VmError::Type with a specific message. Agents hitting `*x y` with mixed types through a slow-path register got nil where they expected an error to retry off. After: each helper signals via jit_set_runtime_error_with_span using the same wording the VM dispatcher uses ("cannot subtract non-numbers", "modulo by zero", "len requires string, list, or map", etc.), so the JIT entry point synthesises a VmRuntimeError that renders with a caret matching tree/VM diagnostics. Mechanics: - extern "C" signatures grow a `span_bits: u64` immediate (packed `(start << 32) | end`), passed through from `chunk.spans[ip]` at every cranelift call site. EQ/NE keep their 2-arg signature because NanVal-bit equality is total over all tagged values and never errors. - jit_len gains a Map arm to match tree/VM's "len requires string, list, or map" — the previous helper omitted maps entirely. - declare_helper arities updated in both src/vm/jit_cranelift.rs (in-process JIT) and src/vm/compile_cranelift.rs (AOT). The AOT dispatch sites pull pack_span_bits via the now-pub(super) helper. - Unit tests inside vm::tests::jit_helpers updated: the "*_returns_nil" / "*_returns_false" assertions now assert the runtime-error cell is set and carries the expected VmError variant.
29 tests covering arithmetic, comparison, numeric unary/binary, and len/str happy paths across tree / VM / cranelift JIT. The per-helper error-path coverage lives in vm::tests::jit_helpers (the verifier rejects surface programs that statically mix types, so the helper slow paths are unreachable from CLI ilo without dynamic dispatch). Includes a no-stale-error-leak guard: an errored cranelift invocation followed by a fresh process running clean arithmetic must succeed, pinning that JitRuntimeErrorGuard's clear-on-entry contract holds for the new error sites added in this batch.
One example with 24 entry points covering every helper touched in this batch. The examples_engines harness runs each entry through tree / VM / cranelift JIT, so the example doubles as a higher-level cross-engine regression test and as an in-context teaching example for any agent reading the examples directory.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
6 tasks
danieljohnmorris
added a commit
that referenced
this pull request
May 15, 2026
Group C of the JIT-helper permissive-nil sweep (batches 1/2/3/4 landed in PRs #264, #259, #282, #281). Helpers in this batch: jit_spl, jit_cat, jit_has, jit_range, jit_window, jit_zip, jit_chunks, jit_enumerate, jit_setunion, jit_setinter, jit_setdiff, jit_rev, jit_srt, jit_rsrt, jit_cumsum. Before: every helper above silently returned TAG_NIL on the type-error path (TAG_FALSE for the two jit_has type-mismatch arms), diverging from the tree walker and the bytecode VM, which both raise VmError::Type with a specific message. Agents hitting srt on a mixed-type list, or cat on a list of numbers, got nil where they expected an error to retry off. After: each helper signals via jit_set_runtime_error_with_span using the same wording the VM dispatcher uses ("spl requires two strings", "cat: list items must be text", "setop: elements must be text, number, or bool", "srt: list must contain all numbers or all text", etc.), so the JIT entry point synthesises a VmRuntimeError that renders with a caret matching tree/VM diagnostics. Mechanics: - extern "C" signatures grow a span_bits: u64 immediate (packed (start << 32) | end). 2-arg helpers become 3-arg; 1-arg helpers become 2-arg. - jit_srt and jit_rsrt now distinguish mixed-list (raises "...must contain all numbers or all text") from non-list/non-text (raises "...requires a list or text") instead of falling through to a single nil sentinel. - jit_setop_impl threads span_bits and surfaces both the per-arg "setop arg N requires a list" messages and the per-element "setop: elements must be text, number, or bool" message that the VM dispatcher uses. Unit tests inside vm::tests::jit_helpers updated: the *_returns_nil and *_returns_false assertions now assert the runtime-error cell is set and carries the expected VmError variant. New mixed-list test for jit_srt pins the previously-silent diverging path.
This was referenced May 15, 2026
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
Batch 3 of the Cranelift JIT-helper permissive-nil sweep. Routes the
arithmetic, comparison, and numeric unary/binary helpers through the
JIT_RUNTIME_ERRORTLS channel introduced in #254, with span_bits packedat the cranelift call site so diagnostics render with a caret matching
tree and VM.
Batches 1 and 2 (#264 and #259) covered list/record/map helpers; this
batch is the next chunk down the residual-helpers audit list. Helpers
in scope (Group A, 21 functions):
jit_add,jit_add_inplace,jit_sub,jit_mul,jit_div,jit_mod,jit_negjit_gt,jit_lt,jit_ge,jit_le(EQ/NE keep 2-arg sig — NanVal-bit equality is total)jit_abs,jit_min,jit_max,jit_flr,jit_cel,jit_rou,jit_clampjit_len,jit_str,jit_numManifesto framing: arithmetic-slow-path divergence is the highest
token-cost-saved item in the residual sweep. Every silent nil from
n - "x"on a register the verifier can't prove numeric was abewildered agent retry. Now agents see the same
cannot subtract non-numbersmessage they get on tree and VM, with the caret pointingat the offending site.
Repro before / after
Before this change, exercising the type-error path of any Group A
helper (e.g. via the in-tree unit-test API that bypasses the verifier
to drive the helpers directly):
After:
At the CLI level the verifier still rejects most surface programs
that statically mix types (ILO-T009 / ILO-T010 / ILO-T012), so the
slow-path helpers are usually unreachable from naive ilo. The error
plumbing matters for the dynamic-dispatch cases (FnRef returns, union
types via match results) and for any future codegen that emits the
slow path.
What's in the diff (per commit)
route arithmetic + comparison + numeric-unary JIT helpers through JIT_RUNTIME_ERROR
span_bits: u64(or+1for clamp's already-3-arg shape)TAG_NIL(orTAG_FALSEfor ordered comparison) withjit_set_runtime_error_with_span(VmError::Type(...), span_bits) + TAG_NIL/TAG_FALSEvm_err!messages so cross-engine diagnostics are byte-identicaljit_lengains a Map arm to match tree/VM's "len requires string, list, or map" — was missing entirelysrc/vm/jit_cranelift.rs(in-process JIT) andsrc/vm/compile_cranelift.rs(AOT)pack_span_bitsvia the now-pub(super) helperVmErrorvariantadd cross-engine regression suite for batch-3 helpers
JitRuntimeErrorGuard's clear-on-entry contractadd example pinning batch-3 helper happy paths through examples_engines
examples/jit-nil-sweep-batch3.ilowith 24 entry pointsTest plan
cargo build --release --features cranelift— greencargo clippy --release --features cranelift --all-targets— cleancargo fmt— cleancargo test --release --features cranelift --lib— 2932 passed, 0 failed (52 ignored)cargo test --release --features cranelift --test regression_jit_nil_sweep_batch3— 29/29 passedcargo test --release --features cranelift --test examples_engines— passedcargo test --release --features cranelift— every suite greenFollow-ups
This is batch 3 of 7 in the Cranelift JIT-helper permissive-nil sweep.
Remaining batches queued (one PR each, separate worktrees):
jit_fmt2,jit_trm,jit_upr,jit_lwr,jit_cap,jit_padl,jit_padr,jit_ord,jit_chr,jit_chars,jit_unq,jit_frqjit_spl,jit_cat,jit_has,jit_range,jit_window,jit_zip,jit_chunks,jit_enumerate,jit_setunion,jit_setinter,jit_setdiff,jit_rev,jit_srt,jit_rsrt,jit_cumsumjit_median,jit_quantile,jit_stdev,jit_variance,jit_fft,jit_ifft,jit_transpose,jit_matmul,jit_dot,jit_det,jit_inv,jit_solvejit_rd,jit_rdl,jit_wr,jit_wrl,jit_jpar,jit_rdjl,jit_dtfmt,jit_dtparsePer the audit, these helpers are correctly permissive-by-design and stay out of scope:
jit_pow/jit_sqrt/jit_log/jit_exp/jit_sin/jit_cos/jit_tan/jit_log10/jit_log2/jit_atan2(both VM and JIT returnf64::NANon non-numeric — already parity);jit_concat/jit_concat_inplace(only emitted for typed-string OP_ADD_SS by the type checker, defensiveunreachable!()is correct);jit_listgetOOB /jit_mgetmissing-key (legitimate-miss returns nil byO _semantics).