Skip to content

route I/O JIT helpers through JIT_RUNTIME_ERROR (batch 7)#288

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

route I/O JIT helpers through JIT_RUNTIME_ERROR (batch 7)#288
danieljohnmorris merged 3 commits into
mainfrom
fix/jit-nil-sweep-batch7

Conversation

@danieljohnmorris
Copy link
Copy Markdown
Collaborator

Summary

Group E of the JIT-helper permissive-nil sweep (batches 1-6 in #264, #259, #282, #285). Routes the type-error paths in jit_rd, jit_rdl, jit_wr, jit_wrl, jit_jpar, jit_rdjl, jit_dtfmt, jit_dtparse through JIT_RUNTIME_ERROR so cranelift surfaces a VmError::Type diagnostic on the same inputs where tree and VM raise. jit_rdjl additionally routes its fs::read_to_string failure through the same channel to match OP_RDJL in the VM. The Result-typed heap_err returns in jit_rd/jit_rdl/jit_wr/jit_wrl for actual filesystem failures are preserved unchanged because the VM produces the same heap_err there too.

Manifesto-aligned: agents calling rd path with a value they expected to be a string (but wasn't, through dynamic dispatch) previously got nil on Cranelift and a typed error on tree/VM. They now get the same diagnostic everywhere, with no token cost in the source program.

Repro

Before, on Cranelift:

jit_rd(non-string) -> TAG_NIL
jit_dtfmt(text, fmt) -> TAG_NIL
jit_wrl(path, non-list) -> TAG_NIL
jit_rdjl("missing-file") -> TAG_NIL

After:

jit_rd(non-string) -> sets VmError::Type("rd requires a string path"), TAG_NIL
jit_dtfmt(text, fmt) -> sets VmError::Type("dtfmt requires a number (epoch)"), TAG_NIL
jit_wrl(path, non-list) -> sets VmError::Type("wrl arg 2 must be a list"), TAG_NIL
jit_rdjl("missing-file") -> sets VmError::Type("rdjl failed to read file"), TAG_NIL

JIT_RUNTIME_ERROR is then picked up by the entry-point post-call check (PR #254) and surfaced as VmRuntimeError with a caret matching tree/VM.

What's in the diff (per commit)

  • 12a92e7 route I/O JIT helpers through JIT_RUNTIME_ERROR. All eight helpers grow a span_bits: u64 immediate; call sites in src/vm/jit_cranelift.rs (in-process JIT) and src/vm/compile_cranelift.rs (AOT) pack chunk.spans[ip] and pass it through. declare_helper arities updated in both modules. The existing jit_jpar unit tests are updated for the new signature, and 11 new unit tests cover every type-error path for the eight helpers.
  • ea6295c add cross-engine regression suite for batch-7 helpers. 7 CLI tests pin happy-path parity for wr/rd, wrl/rdl, jpar, rdjl, dtfmt, dtparse across tree, VM, Cranelift, plus a stale-error-leak guard.
  • 181eff5 add example pinning batch-7 helper happy paths through examples_engines. Five functions in examples/jit-io-roundtrip.ilo run on every engine via the tests/examples_engines.rs harness.

Test plan

  • cargo build --release --features cranelift clean
  • cargo fmt --check clean
  • cargo clippy --release --features cranelift --all-targets clean
  • cargo test --release --features cranelift 4992 passing across 123 binaries (zero failures)
  • New regression_jit_nil_sweep_batch7 suite passes on all engines (7/7)
  • New examples/jit-io-roundtrip.ilo runs clean on tree, VM, Cranelift
  • No em dashes in new code; commit messages follow the project's human-voice style

Follow-ups

Groups F+ (network helpers jit_get/jit_post/jit_geth/jit_posth/jit_getmany, jit_env, the remaining map/record nil-returners) are not in this batch. The Cranelift CLI div-by-zero behaviour for the always-num inline path noted in batch 3 is still tracked separately.

Group E of the JIT-helper permissive-nil sweep (batches 1-6 landed in
PRs #264, #259, #282, #285).

Helpers in this batch: jit_rd, jit_rdl, jit_wr, jit_wrl, jit_jpar,
jit_rdjl, jit_dtfmt, jit_dtparse.

Before: every helper above silently returned TAG_NIL on the type-error
path (non-string path, non-number epoch, non-string format), diverging
from the tree walker and bytecode VM, which both raise VmError::Type
with a specific message. Agents calling rd/wr/dtfmt/... with a value
they expected to be a string (but wasn't) through a dynamic-dispatch
slow path got nil where they expected an error to retry off.

After: each type-error path signals via jit_set_runtime_error_with_span
using the same wording the VM dispatcher uses ("rd requires a string
path", "dtfmt requires a number (epoch)", "wrl list elements must be
strings", etc.), so the JIT entry point synthesises a VmRuntimeError
that renders with a caret matching tree/VM diagnostics.

jit_rdjl additionally routes its fs::read_to_string failure through
the same channel with VmError::Type("rdjl failed to read file") to
match OP_RDJL in the VM. The Err-returning paths in rd/rdl/wr/wrl for
actual I/O failures (heap_err with the io::Error message) stay
unchanged because the VM produces the same heap_err there too: those
are Result-typed values flowing back to the caller, not runtime
errors.

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 in both src/vm/jit_cranelift.rs (in-process
    JIT) and src/vm/compile_cranelift.rs (AOT).
  - declare_helper arities updated in both modules.
  - jit_jpar's existing unit tests updated; 11 new unit tests added
    inside vm::tests::jit_helpers covering every type-error path for
    every helper in this batch.

Full test suite green locally (4992 tests across 123 binaries).
7 tests covering jit_rd/jit_rdl/jit_wr/jit_wrl round-trips, jit_jpar
JSON parse, jit_rdjl JSONL count, and jit_dtfmt/jit_dtparse format
round-trip across tree, VM, and Cranelift JIT. The per-helper
type-error coverage lives in vm::tests::jit_helpers (the verifier
rejects surface programs that statically pass non-strings to these
builtins, so the helper slow paths are unreachable from CLI ilo
without dynamic dispatch).

Includes a no-stale-error-leak guard: an errored cranelift invocation
(hd []) followed by a fresh process running a wr/rd happy-path must
succeed, pinning that JitRuntimeErrorGuard's clear-on-entry contract
holds for the new error sites added in this batch.
Five functions covering text round-trip, lines round-trip, JSONL count,
dtfmt, and dtparse. The examples_engines harness runs every function
across tree, VM, and Cranelift JIT, so this acts as a higher-level
regression test in addition to the dedicated suite. Also gives agents
a worked example of the now-consistent I/O helper behaviour: a type
error in one of these surfaces a VmError::Type diagnostic on every
engine, not nil on Cranelift and an error on tree/VM.
@codecov
Copy link
Copy Markdown

codecov Bot commented May 15, 2026

Codecov Report

❌ Patch coverage is 83.87097% with 30 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.30% 16 Missing ⚠️
src/vm/compile_cranelift.rs 76.66% 7 Missing ⚠️
src/vm/jit_cranelift.rs 76.66% 7 Missing ⚠️

📢 Thoughts on this report? Let us know!

@danieljohnmorris danieljohnmorris merged commit 00b8c22 into main May 15, 2026
4 of 5 checks passed
@danieljohnmorris danieljohnmorris deleted the fix/jit-nil-sweep-batch7 branch May 15, 2026 16:44
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