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
// Regression test: AOT-compiled binaries must strip the top-level `~`/`^`
// Result wrapper on stdout/stderr the same way the in-process runners do.
//
// Background:
//
// PR #275 split top-level Result handling for the in-process runners
// (tree, VM, Cranelift JIT) so that `~v` returned from main prints `v`
// bare on stdout (exit 0) and `^e` prints `^e` on stderr (exit 1). The
// AOT path (`ilo compile main.ilo -o ./main && ./main`) was left calling
// the `jit_prt` helper directly from `generate_main`, so AOT binaries
// kept printing the visible wrapper and always exited 0 — even for `^e`.
//
// The fix routes `generate_main`'s final result through a new helper,
// `jit_prt_main_result`, that mirrors `print_value`'s top-level treatment
// and returns the desired process exit code. The in-program `prnt`
// builtin still uses `jit_prt` (which always shows the wrapper) — that
// path is *inside* a program and genuinely wants `~v` / `^e` visible.
//
// Each case here compares the AOT binary's stdout, stderr, and exit code
// against the three in-process runners byte-for-byte, so any future
// divergence between AOT and the others shows up immediately in CI.
//
// Gated on the `cranelift` feature because both AOT compile and the
// `--jit` baseline require it.
#![cfg(feature = "cranelift")]
use std::path::PathBuf;
use std::process::Command;
use std::sync::atomic::{AtomicU32, Ordering};
fn ilo() -> Command {
Command::new(env!("CARGO_BIN_EXE_ilo"))
}
/// Per-process counter so concurrent test threads don't stomp each
/// other's source / binary paths in /tmp.
static COUNTER: AtomicU32 = AtomicU32::new(0);
fn tmp_paths(tag: &str) -> (PathBuf, PathBuf) {
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
let src = std::env::temp_dir().join(format!("ilo-aot-{tag}-{pid}-{n}.ilo"));
let bin = std::env::temp_dir().join(format!("ilo-aot-{tag}-{pid}-{n}.bin"));
(src, bin)
}
/// Run the in-process Cranelift runner and capture (stdout, stderr, exit).
fn run_in_process(src_path: &PathBuf, engine: &str) -> (Vec<u8>, Vec<u8>, i32) {
let out = ilo()
.arg(src_path)
.arg(engine)
.output()
.expect("failed to run ilo in-process");
(out.stdout, out.stderr, out.status.code().unwrap_or(-1))
}
/// Compile the source to an AOT binary, run it, capture (stdout, stderr, exit).
fn run_aot(src_path: &PathBuf, bin_path: &PathBuf) -> (Vec<u8>, Vec<u8>, i32) {
let compile = ilo()
.args(["compile"])
.arg(src_path)
.arg("-o")
.arg(bin_path)
.output()
.expect("failed to invoke ilo compile");
assert!(
compile.status.success(),
"ilo compile failed: stdout={:?} stderr={:?}",
String::from_utf8_lossy(&compile.stdout),
String::from_utf8_lossy(&compile.stderr),
);
let out = Command::new(bin_path)
.output()
.expect("failed to run AOT binary");
(out.stdout, out.stderr, out.status.code().unwrap_or(-1))
}
/// Assert AOT output matches all three in-process runners (tree, VM,
/// Cranelift) byte-for-byte, and matches the explicit expected values.
fn assert_aot_matches_in_process(
tag: &str,
src: &str,
expected_stdout: &[u8],
expected_stderr: &[u8],
expected_exit: i32,
) {
let (src_path, bin_path) = tmp_paths(tag);
std::fs::write(&src_path, src).expect("write ilo source");
let (aot_stdout, aot_stderr, aot_exit) = run_aot(&src_path, &bin_path);
assert_eq!(
aot_stdout,
expected_stdout,
"{tag}: AOT stdout mismatch. got={:?} expected={:?}",
String::from_utf8_lossy(&aot_stdout),
String::from_utf8_lossy(expected_stdout),
);
assert_eq!(
aot_stderr,
expected_stderr,
"{tag}: AOT stderr mismatch. got={:?} expected={:?}",
String::from_utf8_lossy(&aot_stderr),
String::from_utf8_lossy(expected_stderr),
);
assert_eq!(
aot_exit, expected_exit,
"{tag}: AOT exit mismatch. got={aot_exit} expected={expected_exit}",
);
// Cross-engine parity: AOT must match all three in-process runners
// byte-for-byte. This is the contract PR #275 set for in-process and
// this PR extends to AOT.
for engine in ["--vm", "--jit"] {
let (s, e, c) = run_in_process(&src_path, engine);
assert_eq!(
s,
aot_stdout,
"{tag}/{engine}: stdout diverges from AOT. in-proc={:?} aot={:?}",
String::from_utf8_lossy(&s),
String::from_utf8_lossy(&aot_stdout),
);
assert_eq!(
e,
aot_stderr,
"{tag}/{engine}: stderr diverges from AOT. in-proc={:?} aot={:?}",
String::from_utf8_lossy(&e),
String::from_utf8_lossy(&aot_stderr),
);
assert_eq!(
c, aot_exit,
"{tag}/{engine}: exit diverges from AOT. in-proc={c} aot={aot_exit}",
);
}
// Best-effort cleanup; ignore failures.
let _ = std::fs::remove_file(&src_path);
let _ = std::fs::remove_file(&bin_path);
}
// ── ~"hello" → stdout `hello\n`, exit 0 ───────────────────────────────────
#[test]
fn aot_ok_text_strips_wrapper_to_stdout() {
assert_aot_matches_in_process("ok-text", "m>R t t;~\"hello\"", b"hello\n", b"", 0);
}
// ── ^"err" → stderr `^err\n`, exit 1 ──────────────────────────────────────
#[test]
fn aot_err_routes_to_stderr_exit_1() {
assert_aot_matches_in_process("err-text", "m>R t t;^\"err\"", b"", b"^err\n", 1);
}
// ── bare 42 → stdout `42\n`, exit 0 ───────────────────────────────────────
#[test]
fn aot_bare_value_unchanged() {
assert_aot_matches_in_process("bare-num", "m>n;42", b"42\n", b"", 0);
}
// ── ~7 (number-typed Result) → stdout `7\n`, exit 0 ───────────────────────
//
// Pins the wrapper-strip for a number-typed Result variant in addition to
// the text variant above. A regression that strips only `~"text"` and not
// `~num` would otherwise slip through.
#[test]
fn aot_ok_num_strips_wrapper_to_stdout() {
assert_aot_matches_in_process("ok-num", "m>R n t;~7", b"7\n", b"", 0);
}