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
// Regression tests for the `rsrt fn xs` and `rsrt fn ctx xs` overloads.
//
// `rsrt` mirrors `srt`'s 1/2/3-arg pattern. The 1-arg form (descending sort
// of a list-or-text by natural order) shipped in #196. The 2-arg key-fn form
// and 3-arg closure-bind form were a 4-release standing ask from
// qa-tester / security-researcher / logs-forensics / content-mod personas:
// descending-by-key previously required `rev (srt fn xs)` or a negating
// key fn (`-0 v`) wrapped around `srt`.
//
// Routing: like `srt 2`/`srt 3`, the new `rsrt 2`/`rsrt 3` paths go through
// the tree bridge (`is_tree_bridge_eligible`) so VM and Cranelift share the
// tree interpreter's user-fn callback dispatch. These tests pin that the
// three engines agree on output for every form.
use std::process::Command;
fn ilo() -> Command {
Command::new(env!("CARGO_BIN_EXE_ilo"))
}
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}`"
);
}
fn check_all(src: &str, expected: &str) {
check_stdout("--vm", src, expected);
check_stdout("--vm", src, expected);
#[cfg(feature = "cranelift")]
check_stdout("--jit", src, expected);
}
// ── 2-arg rsrt fn xs ──────────────────────────────────────────────────────
#[test]
fn rsrt_numeric_key_desc_cross_engine() {
// sort numbers descending by themselves (identity key)
check_all("idk x:n>n;x f>L n;rsrt idk [1,3,2,5,4]", "[5, 4, 3, 2, 1]");
}
#[test]
fn rsrt_text_by_length_desc_cross_engine() {
// sort strings by length, longest first — the canonical persona case
check_all(
"ln s:t>n;len s f>L t;rsrt ln [\"a\",\"bbb\",\"cc\",\"dddd\"]",
"[dddd, bbb, cc, a]",
);
}
#[test]
fn rsrt_negated_key_inverts_to_ascending_cross_engine() {
// rsrt with a negating key fn is equivalent to ascending sort — pins
// the comparator direction is genuinely reversed vs srt's.
check_all(
"neg x:n>n;-0 x f>L n;rsrt neg [1,3,2,5,4]",
"[1, 2, 3, 4, 5]",
);
}
#[test]
fn rsrt_preserves_element_type_via_key_cross_engine() {
// The element type (text) is preserved; only the key (number) drives
// ordering. Mirrors srt's contract — return list element type = input
// list element type.
check_all(
"ln s:t>n;len s f>L t;rsrt ln [\"ab\",\"x\",\"qrst\",\"\"]",
"[qrst, ab, x, ]",
);
}
#[test]
fn rsrt_empty_list_cross_engine() {
check_all("idk x:n>n;x f>L n;rsrt idk []", "[]");
}
// ── 3-arg rsrt fn ctx xs (closure-bind) ───────────────────────────────────
#[test]
fn rsrt_closure_bind_ctx_cross_engine() {
// 3-arg form: fn receives (elem, ctx). The ctx is the same for every
// element so it doesn't change ordering, just confirms the bridge
// threads the extra arg through without dropping it.
check_all(
"addk x:n c:n>n;+x c f>L n;rsrt addk 10 [1,3,2,5,4]",
"[5, 4, 3, 2, 1]",
);
}
#[test]
fn rsrt_closure_bind_ctx_inverting_cross_engine() {
// Negate-with-ctx: the ctx supplies the scale. Identical result-shape
// to `rsrt neg xs` — exercises the closure-bind dispatch path on each
// engine.
check_all(
"scl x:n c:n>n;*x c f>L n;rsrt scl -1 [1,3,2,5,4]",
"[1, 2, 3, 4, 5]",
);
}
// ── 1-arg rsrt xs still works (regression guard) ──────────────────────────
#[test]
fn rsrt_one_arg_descending_unchanged_cross_engine() {
check_all("f>L n;rsrt [1,3,2,5,4]", "[5, 4, 3, 2, 1]");
}
#[test]
fn rsrt_one_arg_text_unchanged_cross_engine() {
check_all("f>t;rsrt \"cab\"", "cba");
}