use std::path::PathBuf;
use std::process::Command;
fn synth() -> &'static str {
env!("CARGO_BIN_EXE_synth")
}
fn wat_file(name: &str, wat: &str) -> PathBuf {
let dir = std::env::temp_dir().join("synth_start_1046_tests");
std::fs::create_dir_all(&dir).expect("mkdir");
let p = dir.join(name);
std::fs::write(&p, wat).expect("write wat");
p
}
fn compile(input: &std::path::Path, extra: &[&str]) -> std::process::Output {
let out = std::env::temp_dir()
.join("synth_start_1046_tests")
.join(format!(
"{}_{}.o",
input.file_stem().unwrap().to_str().unwrap(),
extra.join("").replace(['-', '/', ' '], "")
));
let mut args = vec![
"compile",
input.to_str().unwrap(),
"--all-exports",
"-o",
out.to_str().unwrap(),
];
args.extend_from_slice(extra);
Command::new(synth())
.args(&args)
.output()
.expect("run synth")
}
fn stderr(out: &std::process::Output) -> String {
String::from_utf8_lossy(&out.stderr).into_owned()
}
fn assert_refused(out: &std::process::Output, must_mention: &[&str], ctx: &str) {
assert!(
!out.status.success(),
"{ctx}: expected a loud refusal, got success (exit 0 is the #1046 \
silent-drop bug — the (start ...) section was discarded and the \
module compiled as if its instantiation-time init did not exist).\n\
stderr: {}",
stderr(out)
);
let err = stderr(out);
for needle in must_mention {
assert!(
err.contains(needle),
"{ctx}: refusal does not mention '{needle}'.\nstderr: {err}"
);
}
}
const START_WAT: &str = r#"(module
(memory 1)
(func $init (i32.const 0) (i32.const 42) i32.store)
(start $init)
(func (export "get") (result i32) i32.const 0 i32.load))"#;
#[test]
fn start_on_arm_relocatable_refuses_loudly() {
let f = wat_file("start_arm_reloc.wat", START_WAT);
let out = compile(&f, &["--relocatable", "--target", "cortex-m4"]);
assert_refused(
&out,
&["start function", "#1046"],
"(start) on ARM --relocatable",
);
}
#[test]
fn start_on_arm_selfcontained_is_invoked_from_reset_handler() {
let f = wat_file("start_arm_sc.wat", START_WAT);
let out = compile(&f, &["--target", "cortex-m4"]);
assert!(
out.status.success(),
"(start) on ARM self-contained must compile (#1017):\n{}",
stderr(&out)
);
let elf = std::env::temp_dir()
.join("synth_start_1046_tests")
.join("start_arm_sc_targetcortexm4.o");
let compile_log = String::from_utf8_lossy(&out.stdout).to_string() + &stderr(&out);
assert!(
compile_log.contains("Reset_Handler invokes it before the entry call"),
"the compile must announce that the self-contained Reset_Handler invokes \
the start function (#1017); got:\n{compile_log}"
);
assert!(
elf.is_file() && elf.metadata().map(|m| m.len() > 0).unwrap_or(false),
"the compile reported success but produced no object at {}\n(dir listing: {:?})",
elf.display(),
std::fs::read_dir(elf.parent().unwrap())
.map(|d| d
.filter_map(|e| e.ok().map(|e| e.file_name()))
.collect::<Vec<_>>())
.unwrap_or_default()
);
let dis = Command::new(synth())
.args(["disasm", elf.to_str().unwrap()])
.output()
.expect("run synth disasm");
let text =
String::from_utf8_lossy(&dis.stdout).to_string() + &String::from_utf8_lossy(&dis.stderr);
let Some(reset) = text.find("<Reset_Handler>:") else {
eprintln!(
"NOTE: skipping the byte-shape half — `synth disasm` could not \
disassemble ARM on this host (exit {:?}). The acceptance assertion \
above still ran; the execution proof is the selector-parity oracle.\n\
---- disasm ----\n{}",
dis.status.code(),
text.chars().take(600).collect::<String>()
);
return;
};
let after = text[reset..]
.find("<Default_Handler>:")
.map(|i| reset + i)
.unwrap_or(text.len());
let startup = &text[reset..after];
let bl = startup
.find("bl\t")
.or_else(|| startup.find("bl "))
.unwrap_or_else(|| panic!("Reset_Handler carries no BL to the start function:\n{startup}"));
let blx = startup
.find("blx\tr0")
.or_else(|| startup.find("blx r0"))
.unwrap_or_else(|| panic!("Reset_Handler carries no entry BLX r0:\n{startup}"));
assert!(
bl < blx,
"the start BL must precede the entry BLX r0 (instantiation before any export):\n{startup}"
);
assert!(
startup[bl..blx].contains("func_0"),
"the start BL must target func_0 (the (start $init) body):\n{startup}"
);
assert!(
text.contains("<func_0>:"),
"the start function body must be in the image (closure seeded with it):\n{text}"
);
}
#[test]
fn imported_start_on_arm_selfcontained_still_refuses() {
let f = wat_file(
"start_arm_sc_imported.wat",
r#"(module
(import "env" "init" (func $init))
(memory 1)
(start $init)
(func (export "get") (result i32) i32.const 0 i32.load))"#,
);
let out = compile(&f, &["--target", "cortex-m4"]);
assert_refused(
&out,
&["start function", "#1046"],
"(start $imported) on ARM self-contained",
);
}
#[test]
fn start_with_imports_degrading_to_relocatable_still_refuses() {
let f = wat_file(
"start_arm_sc_degraded.wat",
r#"(module
(import "env" "host" (func $host (param i32)))
(memory 1)
(func $init (i32.const 0) (i32.const 42) i32.store)
(start $init)
(func (export "get") (result i32) (call $host (i32.const 1)) i32.const 0 i32.load))"#,
);
let out = compile(&f, &["--target", "cortex-m4"]);
assert_refused(
&out,
&["start function", "#1046"],
"(start) with host imports on --target cortex-m4 (ET_REL degradation)",
);
}
#[test]
fn start_on_a32_relocatable_refuses_loudly() {
let f = wat_file("start_a32.wat", START_WAT);
let out = compile(&f, &["--relocatable", "--target", "cortex-r5"]);
assert_refused(
&out,
&["start function", "#1046"],
"(start) on cortex-r5 --relocatable",
);
}
#[test]
fn start_on_riscv_refuses_loudly() {
let f = wat_file("start_rv32.wat", START_WAT);
let out = compile(&f, &["-b", "riscv", "--target", "rv32imac"]);
assert_refused(
&out,
&["start function", "#1046"],
"(start) on RISC-V rv32imac",
);
}
#[test]
fn start_on_aarch64_refuses_loudly() {
let f = wat_file("start_a64.wat", START_WAT);
let out = compile(&f, &["-b", "aarch64"]);
assert_refused(&out, &["start function", "#1046"], "(start) on aarch64");
}
#[test]
fn exported_start_function_still_refuses() {
let f = wat_file(
"start_exported.wat",
r#"(module
(memory 1)
(func $init (export "init") (i32.const 0) (i32.const 42) i32.store)
(start $init)
(func (export "get") (result i32) i32.const 0 i32.load))"#,
);
let out = compile(&f, &["--relocatable", "--target", "cortex-m4"]);
assert_refused(
&out,
&["start function", "#1046"],
"(start) naming an exported function",
);
}
#[test]
fn start_on_single_function_path_refuses_loudly() {
let f = wat_file("start_single.wat", START_WAT);
let out_path = std::env::temp_dir()
.join("synth_start_1046_tests")
.join("start_single_fn.o");
let out = Command::new(synth())
.args([
"compile",
f.to_str().unwrap(),
"--func-name",
"get",
"--relocatable",
"--target",
"cortex-m4",
"-o",
out_path.to_str().unwrap(),
])
.output()
.expect("run synth");
assert_refused(
&out,
&["start function", "#1046"],
"(start) on the single-function path",
);
}
#[test]
fn start_free_module_still_compiles_everywhere() {
let f = wat_file(
"no_start.wat",
r#"(module
(memory 1)
(func $init (i32.const 0) (i32.const 42) i32.store)
(func (export "get") (result i32) i32.const 0 i32.load))"#,
);
for (ctx, extra) in [
(
"ARM --relocatable",
&["--relocatable", "--target", "cortex-m4"][..],
),
("ARM self-contained", &["--target", "cortex-m4"][..]),
("RISC-V", &["-b", "riscv", "--target", "rv32imac"][..]),
("aarch64", &["-b", "aarch64"][..]),
] {
let out = compile(&f, extra);
assert!(
out.status.success(),
"{ctx}: start-free control module must compile (exit 0), got \
failure.\nstderr: {}",
stderr(&out)
);
}
}