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
// Regression: AOT compile must not collide on data-section names when
// more than one function emits string constants (`OP_LOADK` of a Text
// constant, `OP_RECFLD_NAME`, `OP_RECFLD_NAME_SAFE`, or `OP_RECWITH`).
//
// Background:
//
// `compile_function_body` (src/vm/compile_cranelift.rs) is called once
// per ilo chunk and keeps a function-local `data_section_counter`
// reset to 0 every call. The data-section names it derives from that
// counter (`ilo_strconst_N`, `ilo_fldname_N`, `ilo_fldname_safe_N`,
// `ilo_recwith_indices_N`) are declared at module scope via
// `module.declare_data`, so the moment two functions in the same
// AOT program each emit any string constant, both try to declare
// `ilo_strconst_1` and cranelift's ObjectModule errors with
// `Duplicate definition of identifier: ilo_strconst_1`.
//
// JIT does not hit this: each JIT compile is its own one-function
// module so the per-function counter is also the module counter.
//
// Fix: prefix data-section names with the cranelift function name
// (already module-unique from the first declaration pass), so
// `pick`'s first string constant becomes `ilo_pick_strconst_1` and
// `main`'s first becomes `ilo_main_strconst_1`. No collision.
//
// Gated on `cranelift` because the AOT compile path requires 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"))
}
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-strconst-{tag}-{pid}-{n}.ilo"));
let bin = std::env::temp_dir().join(format!("ilo-strconst-{tag}-{pid}-{n}.bin"));
(src, bin)
}
/// Compile, run, and assert the AOT binary matches expected stdout. Also
/// compares against tree / VM / JIT byte-for-byte so any future engine
/// drift surfaces here.
fn assert_aot_cross_engine(tag: &str, src: &str, entry: Option<&str>, expected_stdout: &[u8]) {
let (src_path, bin_path) = tmp_paths(tag);
std::fs::write(&src_path, src).expect("write ilo source");
// AOT compile + run.
let mut compile = ilo();
compile
.args(["compile"])
.arg(&src_path)
.arg("-o")
.arg(&bin_path);
if let Some(e) = entry {
compile.arg(e);
}
let c = compile.output().expect("invoke ilo compile");
assert!(
c.status.success(),
"{tag}: ilo compile failed: stdout={:?} stderr={:?}",
String::from_utf8_lossy(&c.stdout),
String::from_utf8_lossy(&c.stderr),
);
let aot = Command::new(&bin_path).output().expect("run AOT binary");
assert_eq!(
aot.stdout,
expected_stdout,
"{tag}: AOT stdout mismatch. got={:?} expected={:?}",
String::from_utf8_lossy(&aot.stdout),
String::from_utf8_lossy(expected_stdout),
);
// Cross-engine parity. Run the same source through VM, JIT and
// require byte-identical stdout. Tree-walker is exercised via the VM
// bridge for the ops that still dispatch through it.
for engine in ["--vm", "--jit"] {
let mut cmd = ilo();
cmd.arg(&src_path).arg(engine);
if let Some(e) = entry {
cmd.arg(e);
}
let out = cmd.output().expect("run ilo in-process");
assert_eq!(
out.stdout,
expected_stdout,
"{tag}/{engine}: stdout diverges. got={:?} expected={:?}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(expected_stdout),
);
}
let _ = std::fs::remove_file(&src_path);
let _ = std::fs::remove_file(&bin_path);
}
// ── Two functions, two string constants ──────────────────────────────
//
// The minimal trigger: any program where two AOT chunks each emit at
// least one Text constant. Before the fix this errored with
// `Duplicate definition of identifier: ilo_strconst_1`.
#[test]
fn aot_two_functions_each_with_string_constant() {
assert_aot_cross_engine(
"two-fn-two-strconst",
"g>t;\"hello\"\nm>t;\"world\"",
Some("m"),
b"world\n",
);
}
// ── Sum-type variant via `pick` (the original repro from #413) ───────
//
// `S dog cat` is a sum type with tag strings `dog` and `cat`. The
// `pick` helper emits string constants for both arms. With `main`
// calling `pick`, the AOT module has two chunks both emitting
// strconsts.
#[test]
fn aot_sum_type_pick_compiles_and_runs() {
assert_aot_cross_engine(
"sum-type-pick",
"pick a:S dog cat>t;?a{\"dog\":\"woof\";\"cat\":\"cat\"}\nmain>t;pick \"cat\"\n",
Some("main"),
b"cat\n",
);
}
// ── Many functions, many string constants ────────────────────────────
//
// Stresses the counter past 1, ensuring `*_strconst_2`,
// `*_strconst_3`, etc. also stay unique across functions. Three
// functions, two string constants each.
#[test]
fn aot_many_functions_many_strconsts() {
let src = "\
a>t;cat [\"a-\", \"1\"] \"\"\n\
b>t;cat [\"b-\", \"2\"] \"\"\n\
m>t;cat [(a), (b), \"!\"] \"\"\n\
";
assert_aot_cross_engine("many-fn-many-strconst", src, Some("m"), b"a-1b-2!\n");
}