use std::path::PathBuf;
use std::process::{Command, Output};
fn synth() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_synth"))
}
fn workdir(tag: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("synth-1102-elem-{tag}"));
std::fs::create_dir_all(&d).expect("temp dir");
d
}
const ELEM_DECLINED_DYN: &str = r#"(module
(type $t (func (result i32)))
(table 2 funcref)
(func $good (type $t) i32.const 7)
(func $bad (type $t)
v128.const i64x2 0 0
i64x2.extract_lane 0
i32.wrap_i64)
(func (export "run") (param i32) (result i32)
local.get 0
call_indirect (type $t))
(elem (i32.const 0) $good $bad))
"#;
const ELEM_ALL_GOOD: &str = r#"(module
(type $t (func (result i32)))
(table 2 funcref)
(func $good (type $t) i32.const 7)
(func $also_good (type $t) i32.const 9)
(func (export "run") (param i32) (result i32)
local.get 0
call_indirect (type $t))
(elem (i32.const 0) $good $also_good))
"#;
const DECLINED_NOT_IN_TABLE: &str = r#"(module
(type $t (func (result i32)))
(table 1 funcref)
(func $good (type $t) i32.const 7)
(func (export "bad") (type $t)
v128.const i64x2 0 0
i64x2.extract_lane 0
i32.wrap_i64)
(func (export "run") (param i32) (result i32)
local.get 0
call_indirect (type $t))
(elem (i32.const 0) $good))
"#;
fn compile(dir: &std::path::Path, wat: &str, out_name: &str, args: &[&str]) -> Output {
let src = dir.join("m.wat");
std::fs::write(&src, wat).expect("write wat");
let obj = dir.join(out_name);
let _ = std::fs::remove_file(&obj);
let mut c = Command::new(synth());
c.arg("compile").arg(src.to_str().unwrap());
c.args(args);
c.args(["-o", obj.to_str().unwrap()]);
c.output().expect("run synth compile")
}
fn stderr(o: &Output) -> String {
String::from_utf8_lossy(&o.stderr).into_owned()
}
fn assert_table_refusal(out: &Output, dir: &std::path::Path, out_name: &str) {
let err = stderr(out);
assert!(
err.contains("skipping function 'func_1'") && err.contains("#680"),
"fixture no longer trips the v128 decline this test depends on — \
premise gone, revisit rather than pass on some other error.\nstderr:\n{err}"
);
assert_eq!(
out.status.code(),
Some(1),
"expected the clean refusal (exit 1); 0 means an object with an \
unpopulatable dispatch table was shipped, 101 means a panic.\nstderr:\n{err}"
);
assert!(
!err.contains("panicked at") && !err.contains("RUST_BACKTRACE"),
"refusal was delivered via panic, not a clean error.\nstderr:\n{err}"
);
assert!(
err.contains("RQ-62-TABLEDANGLE") && err.contains("slot 1 -> function 1"),
"refusal must name the table class and the dead slot.\nstderr:\n{err}"
);
assert!(
!dir.join(out_name).exists(),
"refused compile still wrote an output object"
);
}
#[test]
fn arm_thumb2_relocatable_refuses_table_dangle() {
let dir = workdir("arm-rel");
let out = compile(
&dir,
ELEM_DECLINED_DYN,
"a.o",
&["--target", "cortex-m3", "--relocatable"],
);
assert_table_refusal(&out, &dir, "a.o");
}
#[test]
fn a32_cortex_r5_relocatable_refuses_table_dangle() {
let dir = workdir("a32-rel");
let out = compile(
&dir,
ELEM_DECLINED_DYN,
"r.o",
&["--target", "cortex-r5", "--relocatable"],
);
assert_table_refusal(&out, &dir, "r.o");
}
#[test]
fn allow_skipped_exports_does_not_waive_table_refusal() {
let dir = workdir("arm-rel-waive");
let out = compile(
&dir,
ELEM_DECLINED_DYN,
"w.o",
&[
"--target",
"cortex-m3",
"--relocatable",
"--allow-skipped-exports",
],
);
assert_table_refusal(&out, &dir, "w.o");
}
#[test]
fn aarch64_refuses_table_dangle_at_driver() {
let dir = workdir("a64");
let out = compile(&dir, ELEM_DECLINED_DYN, "a64.o", &["-b", "aarch64"]);
assert_table_refusal(&out, &dir, "a64.o");
}
#[test]
fn rv32_upstream_call_indirect_decline_guards_table_path() {
let dir = workdir("rv32");
let out = compile(
&dir,
ELEM_DECLINED_DYN,
"rv.o",
&["-b", "riscv", "--target", "esp32c3", "--relocatable"],
);
let err = stderr(&out);
assert!(
err.contains("skipping function 'run'") && err.contains("CallIndirect"),
"RV32 no longer declines call_indirect — the table-dangle gate's \
RV32 coverage rested on this upstream decline; re-verify the table \
path red-first before trusting it.\nstderr:\n{err}"
);
assert_eq!(
out.status.code(),
Some(1),
"expected #952 to refuse the skipped export.\nstderr:\n{err}"
);
assert!(
err.contains("#952"),
"expected the #952 gate.\nstderr:\n{err}"
);
assert!(
!dir.join("rv.o").exists(),
"refused compile still wrote an output object"
);
}
#[test]
fn arm_selfcontained_keeps_275_bail() {
let dir = workdir("arm-sc");
let out = compile(
&dir,
ELEM_DECLINED_DYN,
"sc.elf",
&["--cortex-m", "--target", "cortex-m3"],
);
let err = stderr(&out);
assert_eq!(out.status.code(), Some(1), "stderr:\n{err}");
assert!(
err.contains("refusing to link a broken dispatch table"),
"the self-contained path must keep its #275 bail.\nstderr:\n{err}"
);
assert!(!dir.join("sc.elf").exists());
}
#[test]
fn all_good_table_still_compiles() {
for (tag, args) in [
(
"good-arm-rel",
&["--target", "cortex-m3", "--relocatable"][..],
),
("good-a64", &["-b", "aarch64"][..]),
("good-arm-sc", &["--cortex-m", "--target", "cortex-m3"][..]),
] {
let dir = workdir(tag);
let out = compile(&dir, ELEM_ALL_GOOD, "g.elf", args);
let err = stderr(&out);
assert_eq!(
out.status.code(),
Some(0),
"control module must still compile ({tag}).\nstderr:\n{err}"
);
assert!(
dir.join("g.elf").exists(),
"control object was not emitted ({tag})"
);
}
}
#[test]
fn declined_function_outside_table_does_not_trip_gate() {
let dir = workdir("outside");
let out = compile(
&dir,
DECLINED_NOT_IN_TABLE,
"o.o",
&[
"--target",
"cortex-m3",
"--relocatable",
"--allow-skipped-exports",
],
);
let err = stderr(&out);
assert!(
err.contains("skipping function 'bad'"),
"control's decline premise gone.\nstderr:\n{err}"
);
assert_eq!(
out.status.code(),
Some(0),
"a declined function outside the table must stay a routine \
partial-object skip.\nstderr:\n{err}"
);
assert!(dir.join("o.o").exists(), "control object was not emitted");
}