use std::path::PathBuf;
use std::process::Command;
use object::{Object, ObjectSection, ObjectSymbol, SectionKind};
fn synth() -> &'static str {
env!("CARGO_BIN_EXE_synth")
}
fn two_mem_fixture() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../..")
.join("scripts/repro/mem406_multi_memory.wat")
}
fn wat_file(name: &str, wat: &str) -> PathBuf {
let dir = std::env::temp_dir().join("synth_mem406_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_mem406_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.\nstderr: {}",
stderr(out)
);
let err = stderr(out);
for needle in must_mention {
assert!(
err.contains(needle),
"{ctx}: refusal does not mention '{needle}'.\nstderr: {err}"
);
}
}
#[test]
fn two_memories_relocatable_green() {
let out = compile(
&two_mem_fixture(),
&["--relocatable", "--target", "cortex-m3"],
);
assert!(out.status.success(), "stderr: {}", stderr(&out));
let bytes = std::fs::read(
std::env::temp_dir()
.join("synth_mem406_tests/mem406_multi_memory_relocatabletargetcortexm3.o"),
)
.expect("read object");
let obj = object::File::parse(&*bytes).expect("parse ELF");
let mem1 = obj
.section_by_name(".synth.wasm_mem_1")
.expect("memory 1 reservation section");
assert_eq!(mem1.size(), 3 * 65536, "3 declared pages");
let data = mem1.data().expect("progbits data");
assert_eq!(
&data[16..24],
b"\xaa\xbb\xcc\xdd\x11\x22\x33\x44",
"init segment placed at its offset"
);
let sym = obj
.symbols()
.find(|s| s.name() == Ok("__synth_wasm_data_1"))
.expect("__synth_wasm_data_1 defined");
assert!(!sym.is_undefined(), "must be a defined region base");
assert_eq!(sym.address(), 0, "base of the section");
assert!(
!obj.symbols().any(|s| s.name() == Ok("__synth_wasm_data_0")),
"memory 0 keeps the R11/legacy contract — no _0 symbol"
);
}
#[test]
fn three_memories_relocatable_green() {
let wat = r#"(module
(memory $a 1 1)
(memory $b 2 2)
(memory $c 1 1)
(data (memory $c) (i32.const 8) "\01\02\03\04")
(func (export "xfer") (param $p i32) (result i32)
(i32.store $b (local.get $p) (i32.load $c offset=8 (local.get $p)))
(i32.load $b (local.get $p))))"#;
let f = wat_file("three_mem.wat", wat);
let out = compile(&f, &["--relocatable", "--target", "cortex-m3"]);
assert!(out.status.success(), "stderr: {}", stderr(&out));
let bytes = std::fs::read(
std::env::temp_dir().join("synth_mem406_tests/three_mem_relocatabletargetcortexm3.o"),
)
.expect("read object");
let obj = object::File::parse(&*bytes).expect("parse ELF");
let mem1 = obj
.section_by_name(".synth.wasm_mem_1")
.expect("mem 1 region");
assert_eq!(mem1.size(), 2 * 65536);
assert_eq!(
mem1.kind(),
SectionKind::UninitializedData,
"zero-init memory ships NOBITS (no flash cost)"
);
let mem2 = obj
.section_by_name(".synth.wasm_mem_2")
.expect("mem 2 region");
assert_eq!(mem2.size(), 65536);
assert_eq!(&mem2.data().expect("progbits")[8..12], b"\x01\x02\x03\x04");
for name in ["__synth_wasm_data_1", "__synth_wasm_data_2"] {
assert!(
obj.symbols()
.any(|s| s.name() == Ok(name) && !s.is_undefined()),
"{name} must be defined"
);
}
}
#[test]
fn multi_memory_without_relocatable_refuses() {
let out = compile(&two_mem_fixture(), &["--target", "cortex-m3"]);
assert_refused(
&out,
&["multi-memory", "#406", "--relocatable"],
"plain object path",
);
}
#[test]
fn multi_memory_self_contained_cortex_m_refuses() {
let out = compile(&two_mem_fixture(), &["--cortex-m"]);
assert_refused(&out, &["multi-memory", "#406"], "self-contained --cortex-m");
}
#[test]
fn multi_memory_native_pointer_abi_refuses() {
let out = compile(
&two_mem_fixture(),
&[
"--relocatable",
"--native-pointer-abi",
"--target",
"cortex-m3",
],
);
assert_refused(
&out,
&["multi-memory", "#406", "--native-pointer-abi"],
"native-pointer ABI",
);
}
#[test]
fn multi_memory_shadow_stack_refuses() {
let out = compile(
&two_mem_fixture(),
&[
"--relocatable",
"--native-pointer-abi",
"--shadow-stack-size",
"2048",
"--target",
"cortex-m3",
],
);
assert_refused(&out, &["multi-memory", "#406"], "--shadow-stack-size combo");
}
#[test]
fn multi_memory_safety_bounds_mpu_refuses() {
let out = compile(
&two_mem_fixture(),
&[
"--relocatable",
"--safety-bounds",
"mpu",
"--target",
"cortex-m3",
],
);
assert_refused(
&out,
&["multi-memory", "#406", "mpu"],
"per-memory MPU isolation interlock",
);
}
#[test]
fn multi_memory_riscv_refuses() {
let out = compile(&two_mem_fixture(), &["-b", "riscv", "--relocatable"]);
assert_refused(&out, &["multi-memory", "#406", "riscv"], "riscv backend");
}
#[test]
fn multi_memory_aarch64_refuses() {
let out = compile(&two_mem_fixture(), &["-b", "aarch64", "--relocatable"]);
assert_refused(
&out,
&["multi-memory", "#406", "aarch64"],
"aarch64 backend",
);
}
#[test]
fn cross_memory_copy_loud_skips() {
let wat = r#"(module
(memory $a 1 1)
(memory $b 1 1)
(func (export "xcopy") (param $d i32) (param $s i32) (param $n i32)
(memory.copy $b $a (local.get $d) (local.get $s) (local.get $n))))"#;
let f = wat_file("cross_copy.wat", wat);
let out = compile(&f, &["--relocatable", "--target", "cortex-m3"]);
assert_refused(&out, &["memory.copy", "#406"], "cross-memory memory.copy");
}
#[test]
fn non_default_memory_fill_loud_skips() {
let wat = r#"(module
(memory $a 1 1)
(memory $b 1 1)
(func (export "fill1") (param $d i32) (param $v i32) (param $n i32)
(memory.fill $b (local.get $d) (local.get $v) (local.get $n))))"#;
let f = wat_file("fill1.wat", wat);
let out = compile(&f, &["--relocatable", "--target", "cortex-m3"]);
assert_refused(&out, &["memory.fill", "#406"], "non-default memory.fill");
}
#[test]
fn wide_access_on_non_default_memory_loud_skips() {
let wat = r#"(module
(memory $a 1 1)
(memory $b 1 1)
(func (export "w") (param $p i32) (result i64)
(i64.load $b (local.get $p))))"#;
let f = wat_file("wide1.wat", wat);
let out = compile(&f, &["--relocatable", "--target", "cortex-m3"]);
assert_refused(&out, &["#406"], "i64 access on memory 1");
assert!(
stderr(&out).contains("i32 load/store family"),
"must name the phase-1 scope.\nstderr: {}",
stderr(&out)
);
}
#[test]
fn non_const_segment_offset_on_memory_k_refuses() {
let wat = r#"(module
(import "env" "off" (global $off i32))
(memory $a 1 1)
(memory $b 1 1)
(data (memory $b) (global.get $off) "\aa\bb")
(func (export "f") (param $p i32) (result i32)
(i32.load $b (local.get $p))))"#;
let f = wat_file("nonconst_seg.wat", wat);
let out = compile(&f, &["--relocatable", "--target", "cortex-m3"]);
assert_refused(
&out,
&["multi-memory", "#406", "non-constant offset"],
"non-const segment offset",
);
}
#[test]
fn segment_overflowing_memory_k_refuses() {
let wat = r#"(module
(memory $a 1 1)
(memory $b 1 1)
(data (memory $b) (i32.const 65532) "\01\02\03\04\05\06\07\08")
(func (export "f") (param $p i32) (result i32)
(i32.load $b (local.get $p))))"#;
let f = wat_file("overflow_seg.wat", wat);
let out = compile(&f, &["--relocatable", "--target", "cortex-m3"]);
assert_refused(
&out,
&["multi-memory", "#406", "overflows"],
"overflowing segment",
);
}