Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions examples/jit-nil-sweep-batch1.ilo
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
-- lst / slc / jpth: cross-engine harmonisation, positive paths.
--
-- Companion to examples/at-hd-tl-oob-parity.ilo. This file exercises the
-- positive paths for the helpers covered by batch 1 of the Cranelift
-- permissive-nil sweep. The error paths are covered by the regression
-- test suite (regression_jit_nil_sweep_batch1.rs); the example exists so
-- the examples_engines harness pins the success-path behaviour across
-- tree, VM, and Cranelift on every build.
--
-- Cranelift used to silently return nil (or the input list) on these
-- helpers' failure modes. After this batch every engine surfaces a
-- runtime error with matching shape: lst OOB / negative idx / non-list,
-- index OOB / non-list, slc non-number indices / non-list-non-text,
-- jpth non-string args. slc OOB-clamp and jpth path-miss remain the
-- documented contract (saturate / Err-wrap respectively).

set-middle xs:L n>L n;lst xs 1 99
slice-clamp xs:L n>L n;slc xs 1 999
slice-text s:t>t;slc s 1 999
parse-ok>R t t;jpth "{\"a\":1}" "a"

-- run: set-middle [10,20,30]
-- out: [10, 99, 30]

-- run: slice-clamp [10,20,30]
-- out: [20, 30]

-- run: slice-text "hello"
-- out: ello

-- run: parse-ok
-- out: ~1
82 changes: 67 additions & 15 deletions src/vm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9814,18 +9814,21 @@ pub(crate) extern "C" fn jit_lst(list: u64, idx: u64, val: u64) -> u64 {
let i = NanVal(idx);
let new_val = NanVal(val);
if !i.is_number() {
return list;
jit_set_runtime_error(VmError::Type("lst: index must be a number"));
return TAG_NIL;
}
let n = i.as_number();
if n < 0.0 || n.fract() != 0.0 {
return list;
jit_set_runtime_error(VmError::Type("lst: index must be a non-negative integer"));
return TAG_NIL;
}
let pos = n as usize;
if v.is_heap()
&& let HeapObj::List(items) = unsafe { v.as_heap_ref() }
{
if pos >= items.len() {
return list;
jit_set_runtime_error(VmError::Type("lst: index out of range"));
return TAG_NIL;
}
let mut new_items: Vec<NanVal> = Vec::with_capacity(items.len());
for (i, item) in items.iter().enumerate() {
Expand All @@ -9839,6 +9842,7 @@ pub(crate) extern "C" fn jit_lst(list: u64, idx: u64, val: u64) -> u64 {
}
return NanVal::heap_list(new_items).0;
}
jit_set_runtime_error(VmError::Type("lst requires a list"));
TAG_NIL
}

Expand Down Expand Up @@ -10572,8 +10576,11 @@ pub(crate) extern "C" fn jit_slc(a: u64, start: u64, end: u64) -> u64 {
let vc = NanVal(start);
let vd = NanVal(end);
if !vc.is_number() || !vd.is_number() {
jit_set_runtime_error(VmError::Type("slc: indices must be numbers"));
return TAG_NIL;
}
// OOB is deliberately clamped on tree/VM (slc is documented to saturate),
// so do not surface a runtime error for out-of-range start/end.
let s_idx = vc.as_number() as usize;
let e_idx = vd.as_number() as usize;
if vb.is_string() {
Expand All @@ -10600,6 +10607,7 @@ pub(crate) extern "C" fn jit_slc(a: u64, start: u64, end: u64) -> u64 {
}
return NanVal::heap_list(sliced).0;
}
jit_set_runtime_error(VmError::Type("slc requires a list or text"));
TAG_NIL
}

Expand Down Expand Up @@ -10893,6 +10901,7 @@ pub(crate) extern "C" fn jit_index(a: u64, idx: u64) -> u64 {
let obj = NanVal(a);
let i = idx as usize;
if !obj.is_heap() {
jit_set_runtime_error(VmError::Type("index access on non-list"));
return TAG_NIL;
}
match unsafe { obj.as_heap_ref() } {
Expand All @@ -10901,10 +10910,14 @@ pub(crate) extern "C" fn jit_index(a: u64, idx: u64) -> u64 {
items[i].clone_rc();
items[i].0
} else {
jit_set_runtime_error(VmError::Type("list index out of bounds"));
TAG_NIL
}
}
_ => TAG_NIL,
_ => {
jit_set_runtime_error(VmError::Type("index access on non-list"));
TAG_NIL
}
}
}

Expand Down Expand Up @@ -11455,12 +11468,22 @@ pub(crate) extern "C" fn jit_listnew(regs: *const u64, n: u64) -> u64 {
}

/// LISTGET for foreach loops: returns Ok(item) if found, TAG_NIL if out of bounds.
///
/// OOB returning TAG_NIL is intentional: it is the loop-done sentinel that
/// matches VM OP_LISTGET's fall-through-to-JMP semantic. Type errors
/// (non-list collection, non-number index) are surfaced as runtime errors
/// to match tree/VM behaviour.
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_listget(list: u64, idx: u64) -> u64 {
let lv = NanVal(list);
let iv = NanVal(idx);
if !lv.is_heap() || !iv.is_number() {
if !lv.is_heap() {
jit_set_runtime_error(VmError::Type("foreach requires a list"));
return TAG_NIL;
}
if !iv.is_number() {
jit_set_runtime_error(VmError::Type("list index must be a number"));
return TAG_NIL;
}
let i = iv.as_number() as usize;
Expand All @@ -11470,10 +11493,14 @@ pub(crate) extern "C" fn jit_listget(list: u64, idx: u64) -> u64 {
items[i].clone_rc();
NanVal::heap_ok(items[i]).0
} else {
// OOB: loop-done sentinel, not an error.
TAG_NIL
}
}
_ => TAG_NIL,
_ => {
jit_set_runtime_error(VmError::Type("foreach requires a list"));
TAG_NIL
}
}
}

Expand All @@ -11483,6 +11510,7 @@ pub(crate) extern "C" fn jit_jpth(a: u64, b: u64) -> u64 {
let av = NanVal(a);
let bv = NanVal(b);
if !av.is_string() || !bv.is_string() {
jit_set_runtime_error(VmError::Type("jpth requires two strings"));
return TAG_NIL;
}
let json_str = unsafe {
Expand Down Expand Up @@ -25805,44 +25833,68 @@ f>n;r=mk 10 20;+r.x r.y";
assert_eq!(items[2].as_number(), 30.0);
}

// NOTE: these tests used to assert that jit_lst silently returned the
// input list (or TAG_NIL) on failure modes. The cross-engine permissive-
// nil sweep aligned the JIT helper with the tree/VM behaviour: every
// failure path now sets the JIT_RUNTIME_ERROR TLS cell and returns
// TAG_NIL, which `jit_cranelift::call` then surfaces as a runtime error.
// Each helper test therefore takes the error cell after the call and
// asserts (a) the return value is TAG_NIL and (b) the error message
// matches the tree/VM diagnostic.

#[cfg(feature = "cranelift")]
#[test]
fn jit_lst_out_of_range_returns_original() {
fn jit_lst_out_of_range_errors() {
let _g = JitRuntimeErrorGuard::new();
let list = NanVal::heap_list(vec![NanVal::number(1.0), NanVal::number(2.0)]);
let bits = jit_lst(list.0, NanVal::number(5.0).0, NanVal::number(99.0).0);
assert_eq!(bits, list.0);
assert_eq!(bits, TAG_NIL);
let err = jit_take_runtime_error().expect("expected runtime error");
assert!(format!("{err:?}").contains("out of range"), "got: {err:?}");
}

#[cfg(feature = "cranelift")]
#[test]
fn jit_lst_negative_index_returns_original() {
fn jit_lst_negative_index_errors() {
let _g = JitRuntimeErrorGuard::new();
let list = NanVal::heap_list(vec![NanVal::number(1.0), NanVal::number(2.0)]);
let bits = jit_lst(list.0, NanVal::number(-1.0).0, NanVal::number(99.0).0);
assert_eq!(bits, list.0);
assert_eq!(bits, TAG_NIL);
let err = jit_take_runtime_error().expect("expected runtime error");
assert!(format!("{err:?}").contains("non-negative"), "got: {err:?}");
}

#[cfg(feature = "cranelift")]
#[test]
fn jit_lst_fractional_index_returns_original() {
fn jit_lst_fractional_index_errors() {
let _g = JitRuntimeErrorGuard::new();
let list = NanVal::heap_list(vec![NanVal::number(1.0), NanVal::number(2.0)]);
let bits = jit_lst(list.0, NanVal::number(0.5).0, NanVal::number(99.0).0);
assert_eq!(bits, list.0);
assert_eq!(bits, TAG_NIL);
let err = jit_take_runtime_error().expect("expected runtime error");
assert!(format!("{err:?}").contains("integer"), "got: {err:?}");
}

#[cfg(feature = "cranelift")]
#[test]
fn jit_lst_non_number_index_returns_original() {
fn jit_lst_non_number_index_errors() {
let _g = JitRuntimeErrorGuard::new();
let list = NanVal::heap_list(vec![NanVal::number(1.0)]);
let bits = jit_lst(list.0, NanVal::boolean(true).0, NanVal::number(99.0).0);
assert_eq!(bits, list.0);
assert_eq!(bits, TAG_NIL);
let err = jit_take_runtime_error().expect("expected runtime error");
assert!(format!("{err:?}").contains("number"), "got: {err:?}");
}

#[cfg(feature = "cranelift")]
#[test]
fn jit_lst_non_list_returns_nil() {
fn jit_lst_non_list_errors() {
let _g = JitRuntimeErrorGuard::new();
let s = NanVal::heap_string("abc".to_string());
let bits = jit_lst(s.0, NanVal::number(0.0).0, NanVal::number(99.0).0);
assert_eq!(bits, TAG_NIL);
let err = jit_take_runtime_error().expect("expected runtime error");
assert!(format!("{err:?}").contains("list"), "got: {err:?}");
}

// ---- VM compile coverage for 3-arg `wr` overload ----
Expand Down
18 changes: 10 additions & 8 deletions tests/eval_inline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1368,21 +1368,23 @@ fn run_cranelift_not_eligible() {
// L441-443: run_default interpreter fallback error
#[test]
fn run_default_interpreter_error() {
// f xs:L n>n;xs.0 with empty list [] — JIT now handles this,
// returns nil for out-of-bounds index
// `f xs:L n>n;xs.0` with empty list [] — OP_INDEX on an out-of-bounds
// literal is a runtime error on every engine after the JIT permissive-
// nil sweep (batch 1). Tree/VM already surfaced this; the JIT helper
// now matches via JIT_RUNTIME_ERROR.
let out = ilo()
.args(["f xs:L n>n;xs.0", "f", "[]"])
.output()
.expect("failed to run ilo");
assert!(
out.status.success(),
"JIT handles empty list index, stderr: {}",
String::from_utf8_lossy(&out.stderr)
!out.status.success(),
"expected runtime error for OOB list index, got stdout: {}",
String::from_utf8_lossy(&out.stdout)
);
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stdout.trim() == "nil",
"expected nil for empty list index, got: {stdout}"
stderr.contains("out of bounds") || stderr.contains("ILO-R004"),
"expected list-index-out-of-bounds diagnostic, got stderr: {stderr}"
);
}

Expand Down
13 changes: 10 additions & 3 deletions tests/regression_dot_list_index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,22 @@ fn dot_index_out_of_range_tree_vm() {
#[test]
#[cfg(feature = "cranelift")]
fn dot_index_out_of_range_cranelift() {
// After the JIT permissive-nil sweep (batch 1), Cranelift surfaces
// a runtime error for OOB literal-index OP_INDEX, matching tree/VM.
let src = "f>n;xs=[10,20,30];xs.5";
let out = ilo()
.args([src, "--run-cranelift", "f"])
.output()
.expect("failed to run ilo");
assert!(
out.status.success(),
"cranelift: expected success returning nil for xs.5, got stderr={}",
String::from_utf8_lossy(&out.stderr)
!out.status.success(),
"cranelift: expected runtime error for xs.5, got stdout={}",
String::from_utf8_lossy(&out.stdout)
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("out of bounds") || stderr.contains("ILO-R004"),
"cranelift: expected out-of-bounds diagnostic, got stderr={stderr}"
);
}

Expand Down
Loading
Loading