1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
// Regression tests for the Cranelift JIT-helper permissive-nil sweep, batch 1.
//
// Helpers in scope: jit_lst, jit_index, jit_slc, jit_jpth, jit_listget.
//
// Before this PR these helpers silently returned TAG_NIL (or the input list)
// on failure paths where tree/VM raise runtime errors. The fix routes the
// failure paths through the same `JIT_RUNTIME_ERROR` TLS cell introduced in
// #254, so every engine now surfaces a runtime error with matching shape.
//
// Note on slc: tree and VM deliberately clamp out-of-range start/end indices
// (slc is documented to saturate), so OOB on slc is NOT an error on any
// engine. Only type errors are surfaced. The OOB-clamp tests below pin that
// the JIT continues to clamp rather than newly erroring.
//
// Note on listget: OOB-nil is the foreach loop-done sentinel and is left in
// place. Only the type-error paths are surfaced as runtime errors.
use std::process::Command;
fn ilo() -> Command {
Command::new(env!("CARGO_BIN_EXE_ilo"))
}
fn check_runtime_error(engine: &str, src: &str, kw_any: &[&str]) {
let out = ilo()
.args([src, engine, "f"])
.output()
.expect("failed to run ilo");
assert!(
!out.status.success(),
"engine={engine}: expected runtime error for `{src}`, got stdout={}",
String::from_utf8_lossy(&out.stdout)
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
kw_any.iter().any(|k| stderr.contains(k)),
"engine={engine}: expected one of {:?} in stderr, got stderr={stderr}",
kw_any
);
}
fn check_stdout(engine: &str, src: &str, expected: &str) {
let out = ilo()
.args([src, engine, "f"])
.output()
.expect("failed to run ilo");
assert!(
out.status.success(),
"engine={engine}: expected success for `{src}`, got stderr={}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(
String::from_utf8_lossy(&out.stdout).trim(),
expected,
"engine={engine}: stdout mismatch for `{src}`"
);
}
// ── lst: OOB ──────────────────────────────────────────────────────────────
#[test]
fn lst_oob_tree() {
check_runtime_error(
"--vm",
"f>L n;lst [1,2,3] 5 99",
&["lst", "out of range", "ILO-R009"],
);
}
#[test]
fn lst_oob_vm() {
check_runtime_error(
"--vm",
"f>L n;lst [1,2,3] 5 99",
&["lst", "out of range", "ILO-R004"],
);
}
#[test]
#[cfg(feature = "cranelift")]
fn lst_oob_cranelift() {
check_runtime_error(
"--jit",
"f>L n;lst [1,2,3] 5 99",
&["lst", "out of range", "ILO-R004"],
);
}
// ── lst: negative index ───────────────────────────────────────────────────
#[test]
fn lst_negative_tree() {
check_runtime_error(
"--vm",
"f>L n;lst [1,2,3] -1 99",
&["lst", "non-negative", "integer", "ILO-R009"],
);
}
#[test]
fn lst_negative_vm() {
check_runtime_error(
"--vm",
"f>L n;lst [1,2,3] -1 99",
&["lst", "non-negative", "integer", "ILO-R004"],
);
}
#[test]
#[cfg(feature = "cranelift")]
fn lst_negative_cranelift() {
check_runtime_error(
"--jit",
"f>L n;lst [1,2,3] -1 99",
&["lst", "non-negative", "integer", "ILO-R004"],
);
}
// ── lst: happy path (regression — make sure we did not break the success
// case) ───────────────────────────────────────────────────────────────────
#[test]
fn lst_ok_tree() {
check_stdout("--vm", "f>L n;lst [1,2,3] 1 99", "[1, 99, 3]");
}
#[test]
fn lst_ok_vm() {
check_stdout("--vm", "f>L n;lst [1,2,3] 1 99", "[1, 99, 3]");
}
#[test]
#[cfg(feature = "cranelift")]
fn lst_ok_cranelift() {
check_stdout("--jit", "f>L n;lst [1,2,3] 1 99", "[1, 99, 3]");
}
// ── slc: type error on non-number index ──────────────────────────────────
//
// We cannot easily express a non-number index in surface ilo (verify will
// reject it), so this case is covered by the VM-level type-mismatch path
// that we know fires when the helper is called with non-number bits. The
// happy path + OOB-clamp tests below confirm that the type-error change
// did not regress the documented saturation semantic.
// ── slc: OOB is deliberately clamped on every engine ─────────────────────
#[test]
fn slc_oob_clamps_tree() {
check_stdout("--vm", "f>L n;slc [1,2,3] 1 999", "[2, 3]");
}
#[test]
fn slc_oob_clamps_vm() {
check_stdout("--vm", "f>L n;slc [1,2,3] 1 999", "[2, 3]");
}
#[test]
#[cfg(feature = "cranelift")]
fn slc_oob_clamps_cranelift() {
check_stdout("--jit", "f>L n;slc [1,2,3] 1 999", "[2, 3]");
}
#[test]
fn slc_text_oob_clamps_tree() {
check_stdout("--vm", "f>t;slc \"hello\" 1 999", "ello");
}
#[test]
fn slc_text_oob_clamps_vm() {
check_stdout("--vm", "f>t;slc \"hello\" 1 999", "ello");
}
#[test]
#[cfg(feature = "cranelift")]
fn slc_text_oob_clamps_cranelift() {
check_stdout("--jit", "f>t;slc \"hello\" 1 999", "ello");
}
// ── jpth: path miss returns Err(...) on every engine (regression) ────────
//
// This is the existing documented contract — path miss is wrapped in a
// Result, NOT a runtime error. Pin it across engines so the type-error
// change below does not accidentally widen the error surface.
// We wrap the call in `prnt v;0` so that the returned Result reaches stdout
// without making `main` exit 1 (that would conflate Err-return with helper
// error). The stdout assertion below pins the rendered Result.
#[test]
fn jpth_path_miss_tree() {
check_stdout(
"--vm",
"f>n;v=jpth \"{\\\"a\\\":1}\" \"b\";prnt v;0",
"^key not found: b\n0",
);
}
#[test]
fn jpth_path_miss_vm() {
check_stdout(
"--vm",
"f>n;v=jpth \"{\\\"a\\\":1}\" \"b\";prnt v;0",
"^key not found: b\n0",
);
}
#[test]
#[cfg(feature = "cranelift")]
fn jpth_path_miss_cranelift() {
check_stdout(
"--jit",
"f>n;v=jpth \"{\\\"a\\\":1}\" \"b\";prnt v;0",
"^key not found: b\n0",
);
}
// ── jpth: happy path ─────────────────────────────────────────────────────
#[test]
fn jpth_ok_tree() {
check_stdout(
"--vm",
"f>n;v=jpth \"{\\\"a\\\":1}\" \"a\";prnt v;0",
"~1\n0",
);
}
#[test]
fn jpth_ok_vm() {
check_stdout(
"--vm",
"f>n;v=jpth \"{\\\"a\\\":1}\" \"a\";prnt v;0",
"~1\n0",
);
}
#[test]
#[cfg(feature = "cranelift")]
fn jpth_ok_cranelift() {
check_stdout(
"--jit",
"f>n;v=jpth \"{\\\"a\\\":1}\" \"a\";prnt v;0",
"~1\n0",
);
}
// ── index (xs.N literal-index OP_INDEX): OOB ─────────────────────────────
//
// `xs.5` on a 3-element list goes through OP_INDEX, which the Cranelift
// backend lowers to a `jit_index` call. Before this PR the JIT path
// silently returned nil; tree/VM both surface a runtime error. Pin parity
// across engines.
#[test]
fn index_oob_tree() {
check_runtime_error(
"--vm",
"f>n;xs=[10,20,30];xs.5",
&["out of bounds", "ILO-R006"],
);
}
#[test]
fn index_oob_vm() {
check_runtime_error(
"--vm",
"f>n;xs=[10,20,30];xs.5",
&["out of bounds", "ILO-R004"],
);
}
#[test]
#[cfg(feature = "cranelift")]
fn index_oob_cranelift() {
check_runtime_error(
"--jit",
"f>n;xs=[10,20,30];xs.5",
&["out of bounds", "ILO-R004"],
);
}
// ── index: happy path (regression on the new error-path edits) ───────────
#[test]
fn index_ok_tree() {
check_stdout("--vm", "f>n;xs=[10,20,30];xs.1", "20");
}
#[test]
fn index_ok_vm() {
check_stdout("--vm", "f>n;xs=[10,20,30];xs.1", "20");
}
#[test]
#[cfg(feature = "cranelift")]
fn index_ok_cranelift() {
check_stdout("--jit", "f>n;xs=[10,20,30];xs.1", "20");
}
// ── No stale-error leak after the new failure paths ──────────────────────
//
// Same shape as the #254 stale-error-leak guard, but pinned for the
// helpers added in this batch. If the JitRuntimeErrorGuard ever regressed,
// a successful call following an erroring one would inherit the stale
// error and spuriously fail.
#[test]
#[cfg(feature = "cranelift")]
fn no_stale_jit_error_leak_after_lst_oob() {
// First call: lst OOB → runtime error.
let out = ilo()
.args(["f>L n;lst [1,2,3] 5 99", "--jit", "f"])
.output()
.expect("failed to run ilo");
assert!(
!out.status.success(),
"first call: expected runtime error from lst OOB"
);
// Second call in a fresh process: must succeed cleanly.
let out = ilo()
.args(["f>L n;lst [1,2,3] 1 99", "--jit", "f"])
.output()
.expect("failed to run ilo");
assert!(
out.status.success(),
"second call: expected success, got stderr={}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(
String::from_utf8_lossy(&out.stdout).trim(),
"[1, 99, 3]",
"second call should produce the updated list"
);
}