Skip to content

Cranelift JIT nil-sweep batch 3: arithmetic, comparison, numeric-unary#282

Merged
danieljohnmorris merged 3 commits into
mainfrom
fix/jit-nil-sweep-batch3
May 15, 2026
Merged

Cranelift JIT nil-sweep batch 3: arithmetic, comparison, numeric-unary#282
danieljohnmorris merged 3 commits into
mainfrom
fix/jit-nil-sweep-batch3

Conversation

@danieljohnmorris
Copy link
Copy Markdown
Collaborator

Summary

Batch 3 of the Cranelift JIT-helper permissive-nil sweep. Routes the
arithmetic, comparison, and numeric unary/binary helpers through the
JIT_RUNTIME_ERROR TLS channel introduced in #254, with span_bits packed
at 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):

  • arithmetic: jit_add, jit_add_inplace, jit_sub, jit_mul, jit_div, jit_mod, jit_neg
  • comparison: jit_gt, jit_lt, jit_ge, jit_le (EQ/NE keep 2-arg sig — NanVal-bit equality is total)
  • numeric unary/binary: jit_abs, jit_min, jit_max, jit_flr, jit_cel, jit_rou, jit_clamp
  • coercion / length: jit_len, jit_str, jit_num

Manifesto 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 a
bewildered agent retry. Now agents see the same cannot subtract non-numbers message they get on tree and VM, with the caret pointing
at 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):

let r = jit_sub(str_val("hello"), num(1.0));  // old 2-arg sig
assert!(is_nil(r));                            // permissive-nil
// no error surfaced

After:

let _ = jit_take_runtime_error();
let r = jit_sub(str_val("hello"), num(1.0), 0);
assert!(is_nil(r));
let err = jit_take_runtime_error().expect("expected pending error");
assert!(matches!(err.0, VmError::Type(msg) if msg.contains("subtract")));

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)

  1. route arithmetic + comparison + numeric-unary JIT helpers through JIT_RUNTIME_ERROR

    • 21 helper signatures gain span_bits: u64 (or +1 for clamp's already-3-arg shape)
    • Each error path replaces bare TAG_NIL (or TAG_FALSE for ordered comparison) with jit_set_runtime_error_with_span(VmError::Type(...), span_bits) + TAG_NIL/TAG_FALSE
    • Error wording matches the VM dispatcher's vm_err! messages so cross-engine diagnostics are byte-identical
    • jit_len gains a Map arm to match tree/VM's "len requires string, list, or map" — was missing entirely
    • declare_helper arities updated in both src/vm/jit_cranelift.rs (in-process JIT) and src/vm/compile_cranelift.rs (AOT)
    • AOT dispatch sites pull pack_span_bits via the now-pub(super) helper
    • vm::tests::jit_helpers unit tests updated to assert the runtime-error cell is set and carries the right VmError variant
  2. add cross-engine regression suite for batch-3 helpers

    • 29 tests asserting tree / VM / cranelift JIT happy-path parity for every helper touched in this batch
    • No-stale-error-leak guard pinning JitRuntimeErrorGuard's clear-on-entry contract
  3. add example pinning batch-3 helper happy paths through examples_engines

    • examples/jit-nil-sweep-batch3.ilo with 24 entry points
    • Runs through tree / VM / cranelift via the examples_engines harness; doubles as in-context teaching example for agents

Test plan

  • cargo build --release --features cranelift — green
  • cargo clippy --release --features cranelift --all-targets — clean
  • cargo fmt — clean
  • cargo test --release --features cranelift --lib — 2932 passed, 0 failed (52 ignored)
  • cargo test --release --features cranelift --test regression_jit_nil_sweep_batch3 — 29/29 passed
  • cargo test --release --features cranelift --test examples_engines — passed
  • Full cargo test --release --features cranelift — every suite green

Follow-ups

This is batch 3 of 7 in the Cranelift JIT-helper permissive-nil sweep.
Remaining batches queued (one PR each, separate worktrees):

  • Batch 4 — Group B text + len/coerce: jit_fmt2, jit_trm, jit_upr, jit_lwr, jit_cap, jit_padl, jit_padr, jit_ord, jit_chr, jit_chars, jit_unq, jit_frq
  • Batch 5 — Group C collections: 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
  • Batch 6 — Group D stats + linear algebra: jit_median, jit_quantile, jit_stdev, jit_variance, jit_fft, jit_ifft, jit_transpose, jit_matmul, jit_dot, jit_det, jit_inv, jit_solve
  • Batch 7 — Group E I/O type-error paths: jit_rd, jit_rdl, jit_wr, jit_wrl, jit_jpar, jit_rdjl, jit_dtfmt, jit_dtparse

Per 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 return f64::NAN on non-numeric — already parity); jit_concat/jit_concat_inplace (only emitted for typed-string OP_ADD_SS by the type checker, defensive unreachable!() is correct); jit_listget OOB / jit_mget missing-key (legitimate-miss returns nil by O _ semantics).

…_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
Copy link
Copy Markdown

codecov Bot commented May 15, 2026

Codecov Report

❌ Patch coverage is 92.98246% with 16 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/vm/mod.rs 87.12% 13 Missing ⚠️
src/vm/compile_cranelift.rs 94.73% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

@danieljohnmorris danieljohnmorris merged commit 71e1ca1 into main May 15, 2026
5 checks passed
@danieljohnmorris danieljohnmorris deleted the fix/jit-nil-sweep-batch3 branch May 15, 2026 15:35
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.
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