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
// Regression: HOF callback errors must surface on every engine.
//
// `jit_call_builtin_tree` (and its OP_CALL_DYN sibling `jit_call_dyn`)
// previously swallowed every non-`Fmt` bridge error as `TAG_NIL`. For HOFs
// whose user-supplied callbacks can fail at runtime (out-of-range `at`,
// type errors, runtime `^err` propagation), this masked the failure as a
// silent nil return on Cranelift even though tree and VM raised. Filed
// during the #306 srt-cranelift-nil P0 investigation as a separate P1.
//
// The fix extends the allow-list in `jit_call_builtin_tree` to cover
// `srt`/`rsrt`/`grp`/`uniqby`/`partition`/`flatmap`/`mapr`, and promotes
// callback errors raised through `jit_call_dyn` (used by native-loop HOFs
// like `flatmap` and by general OP_CALL_DYN dispatch).
//
// Each case below:
// 1. constructs an HOF call whose callback errors at runtime,
// 2. runs it on tree, VM, and Cranelift,
// 3. asserts all three engines fail (non-zero exit, ILO-R009 surfaced).
//
// Before the fix, Cranelift would exit 0 with stdout `nil` (or `[]` for
// `flatmap`'s list accumulator); tree/VM raise.
use std::process::Command;
fn ilo() -> Command {
Command::new(env!("CARGO_BIN_EXE_ilo"))
}
#[cfg(feature = "cranelift")]
const ENGINES_ALL: &[&str] = &["--vm", "--jit"];
#[cfg(not(feature = "cranelift"))]
const ENGINES_ALL: &[&str] = &["--vm"];
fn assert_callback_error(src: &str, entry: &str, expected_code: &str) {
for engine in ENGINES_ALL {
let out = ilo()
.args([src, engine, entry])
.output()
.expect("failed to spawn ilo");
assert!(
!out.status.success(),
"engine={engine}: expected callback failure to surface as a runtime error for `{src}`, but it exited 0\nstdout={}",
String::from_utf8_lossy(&out.stdout)
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains(expected_code),
"engine={engine}: expected `{expected_code}` in stderr for `{src}`, got:\n{stderr}"
);
}
}
// `at` out-of-range inside a `srt` key function. Tree and VM both raise
// ILO-R009; pre-fix Cranelift silently returned `nil`.
#[test]
fn srt_callback_at_oob_raises_on_every_engine() {
let src = "bad x:n>n;at [10,20] (* x 100)\nmn>L n;srt bad [1,2,3]";
assert_callback_error(src, "mn", "ILO-R009");
}
// Same pattern for `rsrt` (descending sort by key). Same bridge contract
// as `srt`, must share error parity.
#[test]
fn rsrt_callback_at_oob_raises_on_every_engine() {
let src = "bad x:n>n;at [10,20] (* x 100)\nmn>L n;rsrt bad [1,2,3]";
assert_callback_error(src, "mn", "ILO-R009");
}
// `grp` group-by-key with a failing key callback.
#[test]
fn grp_callback_at_oob_raises_on_every_engine() {
let src = "bad x:n>n;at [10,20] (* x 100)\nmn>M n (L n);grp bad [1,2,3]";
assert_callback_error(src, "mn", "ILO-R009");
}
// `uniqby` deduplicate-by-key with a failing key callback.
#[test]
fn uniqby_callback_at_oob_raises_on_every_engine() {
let src = "bad x:n>n;at [10,20] (* x 100)\nmn>L n;uniqby bad [1,2,3]";
assert_callback_error(src, "mn", "ILO-R009");
}
// `partition` split-by-predicate with a failing predicate callback.
#[test]
fn partition_callback_at_oob_raises_on_every_engine() {
// Wrap `at` in an `=` so the predicate's return type is `b`.
let src = "bad x:n>b;=x (at [10,20] (* x 100))\nmn>L (L n);partition bad [1,2,3]";
assert_callback_error(src, "mn", "ILO-R009");
}
// `flatmap` uses native OP_CALL_DYN dispatch rather than the tree-bridge,
// so this exercises the `jit_call_dyn` User-fn error path specifically.
// Pre-fix, Cranelift produced `[]` while tree/VM raised.
#[test]
fn flatmap_callback_at_oob_raises_on_every_engine() {
let src = "bad x:n>L n;at [[10,20],[30,40]] (* x 100)\nmn>L n;flatmap bad [1,2,3]";
// Tree emits ILO-R009 ("at: index 100 out of range..."); VM emits ILO-R004
// ("at: index out of range") via its different runtime error class. Assert
// the shared word; both are non-success, both surface a runtime error.
for engine in ENGINES_ALL {
let out = ilo()
.args([src, engine, "mn"])
.output()
.expect("failed to spawn ilo");
assert!(
!out.status.success(),
"engine={engine}: expected flatmap callback failure to surface, got success\nstdout={}",
String::from_utf8_lossy(&out.stdout)
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("out of range"),
"engine={engine}: expected `out of range` in stderr for flatmap, got:\n{stderr}"
);
}
}
// 3-arg closure-bind `rsrt fn ctx xs` with a map context. The callback
// raises through wrong-arg-order `mget` (numeric arg first). Pre-fix,
// Cranelift returned `nil`. Handed off from the consolidated
// srt/rsrt-3-arg-with-collection-ctx P0 investigation, originally filed as
// `fix/srt-rsrt-ctx-collection-nil`; subsumed here because the root cause
// is the same dropped-error edge in `jit_call_builtin_tree` and the
// User-arm of `jit_call_dyn`.
#[test]
fn rsrt_3arg_map_ctx_mget_misuse_raises_on_every_engine() {
let src = "mn>L t;scores=mset (mset (mset mmap \"a\" 3) \"b\" 1) \"c\" 2;words=[\"a\",\"b\",\"c\"];rsrt (ctx:M t n w:t>n;v=mget ctx w;??v 0) scores words";
assert_callback_error(src, "mn", "ILO-R009");
}
// 3-arg closure-bind `srt fn ctx xs` with a list context. The named user-fn
// callback hits the User-arm of `jit_call_dyn` (not the builtin-tree arm),
// so this specifically exercises the second of the two error-dropped sites.
#[test]
fn srt_3arg_list_ctx_at_oob_user_fn_callback_raises_on_every_engine() {
let src = "keyfn i:n ctx:L n>n;at ctx i\nmn>L n;srt keyfn [0,1,2] [10,30,20]";
assert_callback_error(src, "mn", "ILO-R009");
}
// Happy-path sanity check: with a non-failing callback, every engine still
// produces the expected sorted list (no regression in the success path).
#[test]
fn srt_happy_path_unchanged_across_engines() {
let src = "absv n:n>n;?<n 0 (-0 n) n\nmn>L n;srt absv [-3,1,-2,4,-1]";
for engine in ENGINES_ALL {
let out = ilo()
.args([src, engine, "mn"])
.output()
.expect("failed to spawn ilo");
assert!(
out.status.success(),
"engine={engine}: srt happy-path failed unexpectedly\nstderr={}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.trim() == "[1, -1, -2, -3, 4]",
"engine={engine}: srt happy-path got `{}`",
stdout.trim()
);
}
}