use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use synth_backend::{
ArmBackend, ArmRelocationType, ElfBuilder, ElfSectionType, ElfType, ProgramFlags,
ProgramHeader, Relocation, Section, SectionFlags, Symbol, SymbolBinding, SymbolType,
VectorTable, W2C2Backend,
};
use synth_core::HardwareCapabilities;
use synth_core::SafetyManifest;
use synth_core::backend::{Backend, BackendRegistry, CompileConfig, SafetyBounds, VolatileRange};
use synth_core::target::TargetSpec;
use synth_core::wasm_decoder::ImportEntry;
use synth_core::wsc_facts::WscFact;
use synth_synthesis::{
FunctionOps, GlobalInit, WasmGlobal, WasmMemory, WasmOp, decode_wasm_functions,
decode_wasm_module,
};
use tracing::{Level, info, warn};
use wast::parser::{self, ParseBuffer};
use wast::{Wast, WastDirective};
#[allow(dead_code)]
mod shadow_budget;
mod sign;
const SBOM_DEFAULT_SENTINEL: &str = "\u{0}sbom-default\u{0}";
fn fact_spec_enabled() -> bool {
std::env::var("SYNTH_FACT_SPEC").is_ok_and(|v| v != "0")
}
struct SpecializedFn {
ops: Vec<WasmOp>,
block_arity: Vec<(u8, u8)>,
#[cfg_attr(not(feature = "verify"), allow(dead_code))]
kept: Vec<usize>,
elide_div_zero: Vec<usize>,
elide_div_ovf: Vec<usize>,
elide_mem_bounds: Vec<usize>,
}
fn maybe_fact_spec(
func_name: &str,
ops: &[WasmOp],
block_arity: &[(u8, u8)],
facts: &[WscFact],
params_i64: &[bool],
linear_memory_bytes: u32,
) -> Option<SpecializedFn> {
if !fact_spec_enabled() || facts.is_empty() {
return None;
}
#[cfg(feature = "verify")]
{
let r = synth_verify::fact_spec::specialize_function(
func_name,
ops,
block_arity,
facts,
params_i64,
linear_memory_bytes,
);
for line in &r.declined {
eprintln!("fact-spec: DECLINE {line}");
}
for line in &r.admitted {
eprintln!("fact-spec: ADMIT {line}");
}
if r.changed()
|| !r.elide_div_zero.is_empty()
|| !r.elide_div_ovf.is_empty()
|| !r.elide_mem_bounds.is_empty()
{
eprintln!(
"fact-spec: '{func_name}' specialized — {} elision(s) admitted, \
{} declined ({} → {} ops, {} zero-guard + {} overflow-guard + \
{} bounds-guard marks)",
r.admitted.len(),
r.declined.len(),
ops.len(),
r.ops.len(),
r.elide_div_zero.len(),
r.elide_div_ovf.len(),
r.elide_mem_bounds.len(),
);
return Some(SpecializedFn {
ops: r.ops,
block_arity: r.block_arity,
kept: r.kept,
elide_div_zero: r.elide_div_zero,
elide_div_ovf: r.elide_div_ovf,
elide_mem_bounds: r.elide_mem_bounds,
});
}
None
}
#[cfg(not(feature = "verify"))]
{
let _ = (
func_name,
ops,
block_arity,
facts,
params_i64,
linear_memory_bytes,
);
eprintln!(
"warning: SYNTH_FACT_SPEC is set but this synth was built without the \
'verify' feature — the per-elision proof obligation (#494) cannot be \
discharged, so every elision is DECLINED and the general lowering is \
emitted. Rebuild with `--features verify` to enable fact-based \
specialization."
);
None
}
}
#[derive(Parser)]
#[command(name = "synth")]
#[command(about = "WebAssembly-to-ARM Cortex-M AOT compiler")]
#[command(
long_about = "Synth compiles WebAssembly (WASM/WAT) to native ARM Cortex-M machine code,\n\
producing bare-metal ELF binaries for embedded targets.\n\n\
Examples:\n \
synth compile input.wat -o output.elf\n \
synth compile input.wat --cortex-m -o firmware.elf\n \
synth compile input.wat --cortex-m --link -o firmware.elf\n \
synth disasm firmware.elf\n \
synth verify input.wat firmware.elf"
)]
#[command(version)]
struct Cli {
#[command(subcommand)]
command: Commands,
#[arg(short, long)]
verbose: bool,
}
#[allow(clippy::large_enum_variant)]
#[derive(Subcommand)]
enum Commands {
Parse {
#[arg(value_name = "INPUT")]
input: PathBuf,
#[arg(short, long, value_name = "OUTPUT")]
output: Option<PathBuf>,
},
Synthesize {
#[arg(value_name = "INPUT")]
input: PathBuf,
#[arg(short, long, value_name = "OUTPUT")]
output: PathBuf,
#[arg(
short,
long,
value_name = "TARGET",
default_value = "thumbv7em-none-eabihf"
)]
target: String,
#[arg(long, value_name = "HARDWARE", default_value = "nrf52840")]
hardware: String,
#[arg(short = 'O', long, value_name = "LEVEL", default_value = "2")]
opt_level: String,
#[arg(long)]
xip: bool,
#[arg(long)]
verify: bool,
},
TargetInfo {
#[arg(value_name = "TARGET")]
target: String,
},
Compile {
#[arg(value_name = "INPUT")]
input: Option<PathBuf>,
#[arg(short, long, value_name = "OUTPUT", default_value = "output.elf")]
output: PathBuf,
#[arg(short, long, value_name = "DEMO")]
demo: Option<String>,
#[arg(short, long, value_name = "INDEX")]
func_index: Option<u32>,
#[arg(short = 'n', long, value_name = "NAME")]
func_name: Option<String>,
#[arg(long)]
all_exports: bool,
#[arg(long)]
cortex_m: bool,
#[arg(short, long, value_name = "TARGET")]
target: Option<String>,
#[arg(long)]
no_optimize: bool,
#[arg(long)]
loom_compat: bool,
#[arg(long)]
loom: bool,
#[arg(long)]
bounds_check: bool,
#[arg(long, value_name = "MODE")]
safety_bounds: Option<String>,
#[arg(short, long)]
backend: Option<String>,
#[arg(long)]
verify: bool,
#[arg(long)]
link: bool,
#[arg(long, value_name = "BUILTINS")]
builtins: Option<PathBuf>,
#[arg(long)]
relocatable: bool,
#[arg(long)]
native_pointer_abi: bool,
#[arg(long)]
no_bind_cabi_arena: bool,
#[arg(
long,
value_name = "PATH",
num_args = 0..=1,
default_missing_value = SBOM_DEFAULT_SENTINEL
)]
sbom: Option<PathBuf>,
#[arg(long)]
sign_output: bool,
#[arg(long, value_name = "BYTES")]
shadow_stack_size: Option<u32>,
#[arg(long)]
debug_line: bool,
#[arg(long)]
emit_provenance: bool,
#[arg(long)]
emit_wcet: bool,
#[arg(long, value_name = "FILE")]
wcet_hints: Option<PathBuf>,
#[arg(long, value_name = "BASE:LEN")]
volatile_segment: Vec<String>,
#[arg(long, value_enum, default_value_t = StackLayoutArg::High)]
stack_layout: StackLayoutArg,
#[arg(long, value_name = "PATH")]
proven_safe: Option<PathBuf>,
#[arg(long, value_name = "BYTES")]
stack_size: Option<u32>,
},
Disasm {
#[arg(value_name = "INPUT")]
input: PathBuf,
},
Backends,
Verify {
#[arg(value_name = "WASM")]
wasm_input: PathBuf,
#[arg(value_name = "ELF")]
elf_input: PathBuf,
#[arg(short, long, default_value = "arm")]
backend: String,
},
RiscvRuntime {
#[arg(short = 'o', long, value_name = "DIR", default_value = ".")]
outdir: PathBuf,
#[arg(short, long, default_value = "rv32imac")]
target: String,
#[arg(long, default_value = "0x0")]
flash_origin: String,
#[arg(long, default_value = "0x80000000")]
ram_origin: String,
#[arg(long, default_value = "65536")]
linear_memory_size: u64,
#[arg(long, default_value = "4096")]
stack_size: u64,
#[arg(long)]
enable_fpu: bool,
},
}
fn main() -> Result<()> {
let cli = Cli::parse();
let level = if cli.verbose {
Level::DEBUG
} else {
Level::INFO
};
tracing_subscriber::fmt()
.with_max_level(level)
.with_target(false)
.init();
match cli.command {
Commands::Parse { input, output } => {
parse_command(input, output)?;
}
Commands::Synthesize {
input,
output,
target,
hardware,
opt_level,
xip,
verify,
} => {
synthesize_command(input, output, target, hardware, opt_level, xip, verify)?;
}
Commands::TargetInfo { target } => {
target_info_command(target)?;
}
Commands::Compile {
input,
output,
demo,
func_index,
func_name,
all_exports,
cortex_m,
target,
no_optimize,
loom_compat,
loom,
bounds_check,
safety_bounds,
backend,
verify,
link,
builtins,
relocatable,
native_pointer_abi,
no_bind_cabi_arena,
sbom,
sign_output,
shadow_stack_size,
debug_line,
emit_provenance,
emit_wcet,
wcet_hints,
volatile_segment,
stack_layout,
stack_size,
proven_safe,
} => {
let backend_explicit = backend.is_some();
let backend = backend.unwrap_or_else(|| "arm".to_string());
let target_spec =
resolve_target_spec(target.as_deref(), cortex_m, &backend, backend_explicit)?;
let wcet_hints = wcet_hints
.map(|p| -> Result<synth_core::wcet::WcetHints> {
anyhow::ensure!(
emit_wcet,
"--wcet-hints requires --emit-wcet (hints only affect the WCET sidecar)"
);
let text = std::fs::read_to_string(&p)
.context(format!("failed to read --wcet-hints file: {}", p.display()))?;
let hints: synth_core::wcet::WcetHints = serde_json::from_str(&text)
.context(format!("--wcet-hints {} is not valid JSON", p.display()))?;
anyhow::ensure!(
hints.schema == synth_core::wcet::HINTS_SCHEMA,
"--wcet-hints {}: schema '{}' != required '{}'",
p.display(),
hints.schema,
synth_core::wcet::HINTS_SCHEMA
);
Ok(hints)
})
.transpose()?;
let is_cortex_m =
cortex_m || target_spec.family == synth_core::target::ArchFamily::ArmCortexM;
let stack_layout =
resolve_stack_layout(stack_layout, stack_size, relocatable, is_cortex_m, &backend)?;
let loom_compat = loom_compat || loom;
let resolved_safety_bounds =
resolve_safety_bounds(safety_bounds.as_deref(), bounds_check, &backend)?;
let sbom_path = resolve_sbom_path(sbom, &output);
let volatile_segments = parse_volatile_segments(&volatile_segment)?;
compile_command(
input,
output.clone(),
demo,
func_index,
func_name,
all_exports,
is_cortex_m,
no_optimize,
loom_compat,
loom,
resolved_safety_bounds,
&backend,
verify,
&target_spec,
relocatable,
native_pointer_abi,
no_bind_cabi_arena,
sbom_path,
sign_output,
shadow_stack_size,
debug_line,
emit_provenance,
emit_wcet,
wcet_hints,
volatile_segments,
stack_layout,
proven_safe,
)?;
if link {
link_firmware(&output, builtins.as_deref(), &target_spec)?;
}
}
Commands::Disasm { input } => {
disasm_command(input)?;
}
Commands::Backends => {
backends_command()?;
}
Commands::Verify {
wasm_input,
elf_input,
backend,
} => {
verify_command(wasm_input, elf_input, &backend)?;
}
Commands::RiscvRuntime {
outdir,
target,
flash_origin,
ram_origin,
linear_memory_size,
stack_size,
enable_fpu,
} => {
riscv_runtime_command(
outdir,
target,
flash_origin,
ram_origin,
linear_memory_size,
stack_size,
enable_fpu,
)?;
}
}
Ok(())
}
#[cfg(feature = "riscv")]
#[allow(clippy::too_many_arguments)]
fn riscv_runtime_command(
outdir: PathBuf,
target: String,
flash_origin: String,
ram_origin: String,
linear_memory_size: u64,
stack_size: u64,
enable_fpu: bool,
) -> Result<()> {
use synth_backend_riscv::{
LinkerScriptConfig, RiscVLinkerScriptGenerator, RiscVStartupGenerator, StartupConfig,
};
use synth_core::{HardwareCapabilities, RISCVVariant, TargetArch};
let variant = match target.as_str() {
"rv32i" => RISCVVariant::RV32I,
"rv32imac" | "rv32imc" => RISCVVariant::RV32IMAC,
"rv32gc" => RISCVVariant::RV32GC,
"rv64i" => RISCVVariant::RV64I,
"rv64imac" => RISCVVariant::RV64IMAC,
"rv64gc" => RISCVVariant::RV64GC,
_ => anyhow::bail!(
"unknown RISC-V target: {}. Supported: rv32i, rv32imac, rv32gc, rv64i, rv64imac, rv64gc",
target
),
};
let parse_addr = |s: &str| -> Result<u64> {
let s = s.trim_start_matches("0x").trim_start_matches("0X");
u64::from_str_radix(s, 16).context(format!("invalid hex address: {}", s))
};
let flash_origin_v = parse_addr(&flash_origin)?;
let ram_origin_v = parse_addr(&ram_origin)?;
let hw_caps = HardwareCapabilities {
arch: TargetArch::RISCV(variant),
has_mpu: false,
mpu_regions: 0,
has_pmp: true,
pmp_entries: 16,
has_fpu: enable_fpu,
fpu_precision: None,
has_simd: false,
simd_level: None,
xip_capable: true,
flash_size: 64 * 1024,
ram_size: 64 * 1024,
};
std::fs::create_dir_all(&outdir).context("failed to create output directory")?;
let startup = RiscVStartupGenerator::new(hw_caps.clone()).with_config(StartupConfig {
enable_fpu,
..Default::default()
});
let startup_path = outdir.join("startup.c");
std::fs::write(&startup_path, startup.generate())
.context(format!("failed to write {}", startup_path.display()))?;
let linker = RiscVLinkerScriptGenerator::new(hw_caps).with_config(LinkerScriptConfig {
flash_origin: flash_origin_v,
ram_origin: ram_origin_v,
linear_memory_size,
stack_size,
});
let linker_path = outdir.join("linker.ld");
std::fs::write(&linker_path, linker.generate())
.context(format!("failed to write {}", linker_path.display()))?;
println!("Wrote {}", startup_path.display());
println!("Wrote {}", linker_path.display());
println!();
let march = if matches!(target.as_str(), "rv32imac" | "rv32imc") {
"rv32imac"
} else {
target.as_str()
};
println!("Link your synth-compiled .o with:");
println!(
" riscv64-unknown-elf-gcc -nostartfiles -nostdlib -mabi=ilp32 -march={} \\",
march
);
println!(
" -T {} -o firmware.elf {} <synth.o>",
linker_path.display(),
startup_path.display()
);
Ok(())
}
#[cfg(not(feature = "riscv"))]
#[allow(clippy::too_many_arguments)]
fn riscv_runtime_command(
_outdir: PathBuf,
_target: String,
_flash_origin: String,
_ram_origin: String,
_linear_memory_size: u64,
_stack_size: u64,
_enable_fpu: bool,
) -> Result<()> {
anyhow::bail!("RISC-V backend was not compiled in (rebuild with --features riscv)")
}
fn parse_command(input: PathBuf, output: Option<PathBuf>) -> Result<()> {
info!("Parsing WebAssembly component: {}", input.display());
let component =
synth_frontend::parse_component_file(&input).context("Failed to parse component")?;
synth_frontend::validate_component(&component).context("Component validation failed")?;
info!("Component parsed successfully");
info!(" Name: {}", component.name);
info!(" Modules: {}", component.modules.len());
info!(" Total memories: {}", component.total_memories());
info!(
" Total memory size: {} bytes",
component.total_memory_size()
);
if let Some(output_path) = output {
let json =
serde_json::to_string_pretty(&component).context("Failed to serialize component")?;
std::fs::write(&output_path, json).context(format!(
"Failed to write output to {}",
output_path.display()
))?;
info!("Component JSON written to: {}", output_path.display());
}
Ok(())
}
fn synthesize_command(
input: PathBuf,
output: PathBuf,
target: String,
hardware: String,
opt_level: String,
xip: bool,
verify: bool,
) -> Result<()> {
info!("Synthesizing WebAssembly component: {}", input.display());
info!(" Target: {}", target);
info!(" Hardware: {}", hardware);
info!(" Optimization level: {}", opt_level);
info!(" XIP: {}", xip);
info!(" Verification: {}", verify);
let component =
synth_frontend::parse_component_file(&input).context("Failed to parse component")?;
synth_frontend::validate_component(&component).context("Component validation failed")?;
let hw_caps = match hardware.as_str() {
"nrf52840" => HardwareCapabilities::nrf52840(),
"stm32f407" => HardwareCapabilities::stm32f407(),
"stm32h743" => HardwareCapabilities::stm32h743(),
"imxrt1062" => HardwareCapabilities::imxrt1062(),
_ => {
anyhow::bail!(
"Unsupported hardware: {}. Use nrf52840, stm32f407, stm32h743, imxrt1062",
hardware
);
}
};
info!("Hardware capabilities:");
info!(" MPU regions: {}", hw_caps.mpu_regions);
info!(" FPU: {}", hw_caps.has_fpu);
info!(" Flash: {} KB", hw_caps.flash_size / 1024);
info!(" RAM: {} KB", hw_caps.ram_size / 1024);
info!("Synthesis pipeline (PoC - not yet fully implemented):");
info!(" 1. Component parsing: ✓");
info!(" 2. Memory layout analysis: TODO");
info!(" 3. MPU region allocation: TODO");
info!(" 4. Optimization: TODO");
info!(" 5. Code generation: TODO");
info!(" 6. Binary emission: TODO");
info!("Output would be written to: {}", output.display());
Ok(())
}
fn target_info_command(target: String) -> Result<()> {
info!("Target information for: {}", target);
match target.as_str() {
"nrf52840" => {
let caps = HardwareCapabilities::nrf52840();
print_hardware_info(&caps);
}
"stm32f407" => {
let caps = HardwareCapabilities::stm32f407();
print_hardware_info(&caps);
}
"stm32h743" => {
let caps = HardwareCapabilities::stm32h743();
print_hardware_info(&caps);
}
"imxrt1062" => {
let caps = HardwareCapabilities::imxrt1062();
print_hardware_info(&caps);
}
_ => {
anyhow::bail!(
"Unknown target: {}. Supported: nrf52840, stm32f407, stm32h743, imxrt1062",
target
);
}
}
Ok(())
}
fn print_hardware_info(caps: &HardwareCapabilities) {
println!("Hardware Capabilities:");
println!(" Architecture: {:?}", caps.arch);
println!(" MPU: {} (regions: {})", caps.has_mpu, caps.mpu_regions);
println!(" FPU: {}", caps.has_fpu);
if let Some(precision) = caps.fpu_precision {
println!(" Precision: {:?}", precision);
}
println!(" SIMD: {}", caps.has_simd);
if let Some(level) = caps.simd_level {
println!(" Level: {:?}", level);
}
println!(" XIP capable: {}", caps.xip_capable);
println!(
" Flash: {} KB ({} MB)",
caps.flash_size / 1024,
caps.flash_size / (1024 * 1024)
);
println!(" RAM: {} KB", caps.ram_size / 1024);
}
struct ElfFunction {
name: String,
debug_name: Option<String>,
wasm_index: u32,
code: Vec<u8>,
relocations: Vec<synth_core::backend::CodeRelocation>,
op_offsets: Vec<u32>,
line_map: synth_core::backend::LineMap,
}
fn backend_accepted_targets(backend: &str) -> Option<&'static str> {
match backend {
"arm" => {
Some("cortex-m3, cortex-m4, cortex-m4f, cortex-m7, cortex-m7dp, cortex-m55, cortex-r5")
}
"riscv" => Some("rv32imac, rv32imc, rv32im, rv32i, rv32gc, esp32c3, riscv32"),
"aarch64" => Some("cortex-a53"),
_ => None,
}
}
fn family_name(family: &synth_core::target::ArchFamily) -> &'static str {
use synth_core::target::ArchFamily;
match family {
ArchFamily::ArmCortexM => "ARM Cortex-M",
ArchFamily::ArmCortexR => "ARM Cortex-R",
ArchFamily::ArmCortexA => "AArch64",
ArchFamily::RiscV => "RISC-V",
}
}
fn resolve_target_spec(
target: Option<&str>,
cortex_m: bool,
backend: &str,
backend_explicit: bool,
) -> Result<TargetSpec> {
use synth_core::target::ArchFamily;
let (name, spec) = match target {
Some(name) => {
let spec =
TargetSpec::from_triple(name).map_err(|e| {
match backend_accepted_targets(backend) {
Some(list) if backend_explicit => {
anyhow::anyhow!("{e}\ntargets accepted by backend '{backend}': {list}")
}
_ => anyhow::anyhow!("{e}"),
}
})?;
(name, spec)
}
None if backend == "riscv" => return Ok(TargetSpec::riscv32("imac")),
None if backend == "aarch64" => return Ok(TargetSpec::cortex_a53()),
None if cortex_m => return Ok(TargetSpec::cortex_m3()),
None => {
return Ok(TargetSpec {
isa: synth_core::target::IsaVariant::Arm32,
..TargetSpec::cortex_m4()
});
}
};
let required_backend = match spec.family {
ArchFamily::ArmCortexM | ArchFamily::ArmCortexR => "arm",
ArchFamily::ArmCortexA => "aarch64",
ArchFamily::RiscV => "riscv",
};
let backend_is_isa = matches!(backend, "arm" | "riscv" | "aarch64");
if backend_is_isa && backend != required_backend {
let family = family_name(&spec.family);
if backend_explicit {
let accepted = backend_accepted_targets(backend).unwrap_or("(none)");
anyhow::bail!(
"backend '{backend}' does not accept --target {name} ({family}).\n\
targets accepted by backend '{backend}': {accepted}\n\
for {name}, use `-b {required_backend}`"
);
}
let article = match spec.family {
ArchFamily::ArmCortexA => "an",
_ => "a",
};
anyhow::bail!(
"--target {name} is {article} {family} target, but no backend was selected, so the \
default 'arm' backend would silently produce a wrong-ISA object — refusing.\n\
pass `-b {required_backend}` for this target, or pick an ARM target: {arm}",
arm = backend_accepted_targets("arm").unwrap_or("(none)"),
);
}
Ok(spec)
}
fn build_backend_registry() -> BackendRegistry {
let mut registry = BackendRegistry::new();
registry.register(Box::new(ArmBackend::new()));
registry.register(Box::new(synth_backend_aarch64::AArch64Backend::new()));
registry.register(Box::new(W2C2Backend::new()));
#[cfg(feature = "awsm")]
registry.register(Box::new(synth_backend_awsm::AwsmBackend::new()));
#[cfg(feature = "wasker")]
registry.register(Box::new(synth_backend_wasker::WaskerBackend::new()));
#[cfg(feature = "riscv")]
registry.register(Box::new(synth_backend_riscv::RiscVBackend::new()));
registry
}
fn maybe_run_loom(enabled: bool, wasm_bytes: Vec<u8>) -> Result<Vec<u8>> {
if !enabled {
return Ok(wasm_bytes);
}
anyhow::bail!(
"--loom is not yet available. The loom WASM optimizer integration is pending.\n\
See https://github.com/pulseengine/loom for status.\n\n\
In the meantime, use --loom-compat to skip synth passes that overlap\n\
with loom's optimizations (constant folding, strength reduction)."
);
}
fn resolve_safety_bounds(
safety_bounds: Option<&str>,
legacy_bounds_check: bool,
backend: &str,
) -> Result<SafetyBounds> {
let resolved = if let Some(v) = safety_bounds {
let parsed = SafetyBounds::parse(v).map_err(|e| anyhow::anyhow!(e))?;
if legacy_bounds_check {
eprintln!(
"warning: --bounds-check is deprecated; --safety-bounds={} takes precedence",
parsed.as_str()
);
}
parsed
} else if legacy_bounds_check {
eprintln!("warning: --bounds-check is deprecated; use --safety-bounds=software instead");
SafetyBounds::Software
} else if backend == "aarch64" {
SafetyBounds::Software
} else {
SafetyBounds::None
};
if backend == "aarch64" && matches!(resolved, SafetyBounds::Mask | SafetyBounds::Mpu) {
anyhow::bail!(
"--safety-bounds {} is not implemented on the aarch64 backend — \
refusing to silently emit UNCHECKED memory accesses (#865). Use \
--safety-bounds software (the default: per-access bounds checks \
that trap out-of-bounds) or --safety-bounds none (explicit \
unchecked opt-out).",
resolved.as_str()
);
}
Ok(resolved)
}
fn maybe_emit_safety_manifest(
elf_path: &std::path::Path,
target_spec: &TargetSpec,
safety_bounds: SafetyBounds,
linear_memory_bytes: u32,
) -> Result<()> {
if safety_bounds == SafetyBounds::None {
return Ok(());
}
let manifest = SafetyManifest {
synth_version: env!("CARGO_PKG_VERSION").to_string(),
target_triple: target_spec.triple.clone(),
safety_bounds,
safety_div_zero: true,
safety_div_overflow: true,
linear_memory_bytes,
};
let sidecar = SafetyManifest::sidecar_path(elf_path);
let json = manifest.to_json();
std::fs::write(&sidecar, json)
.with_context(|| format!("Failed to write safety manifest: {}", sidecar.display()))?;
info!("Wrote safety manifest: {}", sidecar.display());
Ok(())
}
fn resolve_sbom_path(sbom: Option<PathBuf>, output: &std::path::Path) -> Option<PathBuf> {
match sbom {
None => None,
Some(p) if p.as_os_str() == SBOM_DEFAULT_SENTINEL => {
Some(synth_core::CycloneDxSbom::sidecar_path(output))
}
Some(p) => Some(p),
}
}
fn parse_volatile_segments(raw: &[String]) -> Result<Vec<VolatileRange>> {
fn parse_u32(field: &str, whole: &str) -> Result<u32> {
let t = field.trim();
let parsed = if let Some(hex) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
u32::from_str_radix(hex, 16)
} else {
t.parse::<u32>()
};
parsed.map_err(|_| {
anyhow::anyhow!(
"invalid --volatile-segment '{whole}': '{field}' is not a u32 \
(expected hex like 0x20001000 or decimal)"
)
})
}
let mut ranges = Vec::with_capacity(raw.len());
for spec in raw {
let (base_s, len_s) = spec.split_once(':').ok_or_else(|| {
anyhow::anyhow!(
"invalid --volatile-segment '{spec}': expected '<base>:<len>' \
(e.g. 0x20001000:4096)"
)
})?;
let base = parse_u32(base_s, spec)?;
let len = parse_u32(len_s, spec)?;
if len == 0 {
anyhow::bail!("invalid --volatile-segment '{spec}': length must be non-zero");
}
if base.checked_add(len).is_none() {
anyhow::bail!(
"invalid --volatile-segment '{spec}': base + len overflows the 32-bit \
linear-memory address space"
);
}
ranges.push(VolatileRange { base, len });
}
Ok(ranges)
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, clap::ValueEnum)]
enum StackLayoutArg {
High,
Low,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum StackLayout {
High,
Low { stack_size: u32 },
}
const DEFAULT_LOW_STACK_SIZE: u32 = 4096;
impl StackLayout {
fn stack_reserve(self) -> u32 {
match self {
StackLayout::High => 0,
StackLayout::Low { stack_size } => stack_size,
}
}
fn startup_linmem_base(self, ram_base: u32) -> u32 {
ram_base + self.stack_reserve()
}
fn optimized_linmem_base(self) -> u32 {
synth_core::backend::OPTIMIZED_LINMEM_BASE + self.stack_reserve()
}
}
fn resolve_stack_layout(
arg: StackLayoutArg,
stack_size: Option<u32>,
relocatable: bool,
is_cortex_m: bool,
backend_name: &str,
) -> Result<StackLayout> {
if arg == StackLayoutArg::High {
if let Some(sz) = stack_size {
warn!(
"--stack-size {sz} has no effect under --stack-layout=high: the stack \
grows down from the top of SRAM (pass --stack-layout=low to reserve \
a fixed region at the SRAM bottom)"
);
}
return Ok(StackLayout::High);
}
if relocatable {
anyhow::bail!(
"--stack-layout=low applies only to self-contained Cortex-M images: \
--relocatable produces an ET_REL object whose stack/linmem layout is \
owned by the host linker script, so the flag would silently do \
nothing — refusing (#687)"
);
}
if !is_cortex_m || backend_name == "aarch64" {
anyhow::bail!(
"--stack-layout=low applies only to self-contained Cortex-M images \
(synth emits the vector table and startup there); backend '{backend_name}' \
/ this target does not produce one — refusing rather than silently \
ignoring the flag (#687)"
);
}
let stack_size = stack_size.unwrap_or(DEFAULT_LOW_STACK_SIZE);
if stack_size < 256 {
anyhow::bail!("--stack-size {stack_size} is below the 256-byte minimum (#687)");
}
if !stack_size.is_multiple_of(8) {
anyhow::bail!(
"--stack-size {stack_size} must be a multiple of 8 (AAPCS SP alignment, #687)"
);
}
Ok(StackLayout::Low { stack_size })
}
fn emit_sbom(
sbom_path: &std::path::Path,
input_path: &std::path::Path,
input_wasm_bytes: &[u8],
output_path: &std::path::Path,
output_elf_bytes: &[u8],
target_spec: &TargetSpec,
backend_name: &str,
imports: &[ImportEntry],
) -> Result<()> {
let inputs = synth_core::SbomInputs {
synth_version: env!("CARGO_PKG_VERSION"),
input_path,
input_bytes: input_wasm_bytes,
output_path,
output_bytes: output_elf_bytes,
target_triple: &target_spec.triple,
backend: backend_name,
imports,
};
let sbom = synth_core::CycloneDxSbom::new(&inputs, synth_core::sbom::now_rfc3339());
std::fs::write(sbom_path, sbom.to_json())
.with_context(|| format!("Failed to write SBOM: {}", sbom_path.display()))?;
info!(
"Wrote CycloneDX SBOM ({} components): {}",
sbom.components.len(),
sbom_path.display()
);
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn compile_command(
input: Option<PathBuf>,
output: PathBuf,
demo: Option<String>,
func_index: Option<u32>,
func_name_arg: Option<String>,
all_exports: bool,
cortex_m: bool,
no_optimize: bool,
loom_compat: bool,
loom: bool,
safety_bounds: SafetyBounds,
backend_name: &str,
verify: bool,
target_spec: &TargetSpec,
relocatable: bool,
native_pointer_abi: bool,
no_bind_cabi_arena: bool,
sbom_path: Option<PathBuf>,
sign_output: bool,
shadow_stack_size: Option<u32>,
debug_line: bool,
emit_provenance: bool,
emit_wcet: bool,
wcet_hints: Option<synth_core::wcet::WcetHints>,
volatile_segments: Vec<VolatileRange>,
stack_layout: StackLayout,
proven_safe: Option<PathBuf>,
) -> Result<()> {
let registry = build_backend_registry();
let backend = registry.get(backend_name).ok_or_else(|| {
let available: Vec<_> = registry
.list()
.iter()
.map(|b| b.name().to_string())
.collect();
anyhow::anyhow!(
"Unknown backend '{}'. Available: {}",
backend_name,
available.join(", ")
)
})?;
if !backend.is_available() {
anyhow::bail!(
"Backend '{}' is not available (external tool not installed)",
backend_name
);
}
info!("Using backend: {}", backend.name());
let use_all_exports =
all_exports || (input.is_some() && func_index.is_none() && func_name_arg.is_none());
if !use_all_exports && proven_safe.is_some() {
anyhow::bail!(
"--proven-safe is not consumed on the single-function path \
(--func-index / --func-name): that path builds no elision marks and \
writes no attestation, so the flag would silently do nothing. Compile \
the whole module instead (drop --func-index/--func-name, or pass \
--all-exports)."
);
}
if use_all_exports {
return compile_all_exports(
input,
output,
cortex_m,
no_optimize,
loom_compat,
loom,
safety_bounds,
backend,
verify,
target_spec,
relocatable,
native_pointer_abi,
no_bind_cabi_arena,
sbom_path,
sign_output,
shadow_stack_size,
debug_line,
emit_provenance,
emit_wcet,
wcet_hints,
volatile_segments,
stack_layout,
proven_safe,
);
}
let func_index = func_index.unwrap_or(0);
let mut sbom_wasm_bytes: Option<Vec<u8>> = None;
let mut sbom_imports: Vec<ImportEntry> = Vec::new();
let mut current_func_params_i64: Vec<bool> = Vec::new(); let mut current_func_params_f32: Vec<bool> = Vec::new();
let mut func_params_f32_all: Vec<Vec<bool>> = Vec::new();
let mut current_func_params_f64: Vec<bool> = Vec::new();
let mut func_params_f64_all: Vec<Vec<bool>> = Vec::new();
let mut current_func_ret_f32 = false;
let mut current_func_ret_f64 = false;
let mut func_ret_f32_all: Vec<bool> = Vec::new();
let mut func_ret_f64_all: Vec<bool> = Vec::new();
let mut type_ret_f32_all: Vec<bool> = Vec::new();
let mut type_ret_f64_all: Vec<bool> = Vec::new();
let mut current_func_param_count: Option<u32> = None;
let mut func_ret_i64: Vec<bool> = Vec::new(); let mut type_ret_i64: Vec<bool> = Vec::new(); let mut global_widths: Vec<u32> = Vec::new();
let mut startup_globals_words: Vec<u32> = Vec::new();
let mut single_func_has_data_segments = false;
let mut single_func_nonconst_data: Option<String> = None;
let mut single_func_linear_memory_bytes: u32 = 0;
let mut current_func_block_arity: Vec<(u8, u8)> = Vec::new(); let mut wsc_facts: Vec<WscFact> = Vec::new();
let mut current_func_facts: Vec<WscFact> = Vec::new();
let mut call_indirect_guards = synth_core::CallIndirectGuards::default();
let (wasm_ops, func_name): (Vec<WasmOp>, String) = match (&input, &demo) {
(Some(path), _) => {
info!("Compiling WASM file: {}", path.display());
let file_bytes = std::fs::read(path)
.context(format!("Failed to read input file: {}", path.display()))?;
let wasm_bytes = if path.extension().is_some_and(|ext| ext == "wast") {
info!("Parsing WAST to WASM (extracting module)...");
let contents =
String::from_utf8(file_bytes).context("WAST file is not valid UTF-8")?;
extract_module_from_wast(&contents)?
} else if path.extension().is_some_and(|ext| ext == "wat") {
info!("Parsing WAT to WASM...");
wat::parse_bytes(&file_bytes)
.context("Failed to parse WAT file")?
.into_owned()
} else {
file_bytes
};
let wasm_bytes = maybe_run_loom(loom, wasm_bytes)?;
let module = decode_wasm_module(&wasm_bytes)
.context("Failed to decode WASM module (signature tables)")?;
call_indirect_guards = module.call_indirect_guards();
func_ret_i64 = module.func_ret_i64;
type_ret_i64 = module.type_ret_i64;
for g in &module.globals {
let i = g.index as usize;
if global_widths.len() <= i {
global_widths.resize(i + 1, 4);
}
global_widths[i] = g.slot_bytes;
}
startup_globals_words = globals_table_words(&module.globals);
single_func_has_data_segments = !module.data_segments.is_empty();
single_func_nonconst_data = module.default_memory_nonconst_data.clone();
single_func_linear_memory_bytes = module
.memories
.first()
.map(|m| m.initial_bytes())
.unwrap_or(0);
let module_func_params_i64 = module.func_params_i64;
let module_func_arg_counts = module.func_arg_counts;
func_params_f32_all = module.func_params_f32.clone();
func_params_f64_all = module.func_params_f64.clone();
func_ret_f32_all = module.func_ret_f32.clone();
func_ret_f64_all = module.func_ret_f64.clone();
type_ret_f32_all = module.type_ret_f32.clone();
type_ret_f64_all = module.type_ret_f64.clone();
wsc_facts = module.wsc_facts;
if sbom_path.is_some() {
sbom_imports = module.imports;
sbom_wasm_bytes = Some(wasm_bytes.clone());
}
let functions =
decode_wasm_functions(&wasm_bytes).context("Failed to decode WASM functions")?;
info!("Found {} functions in module", functions.len());
for f in &functions {
if let Some(ref name) = f.export_name {
info!(" Export '{}' -> function index {}", name, f.index);
}
}
let func = if let Some(ref name) = func_name_arg {
functions
.into_iter()
.find(|f| f.export_name.as_deref() == Some(name.as_str()))
.context(format!("Function '{}' not found", name))?
} else {
functions
.into_iter()
.find(|f| f.index == func_index)
.context(format!("Function index {} not found", func_index))?
};
let name = func
.export_name
.clone()
.unwrap_or_else(|| format!("func_{}", func.index));
info!("Compiling function {} ({} ops)", name, func.ops.len());
if let Some(p) = module_func_params_i64.get(func.index as usize) {
current_func_params_i64 = p.clone();
}
if let Some(p) = func_params_f32_all.get(func.index as usize) {
current_func_params_f32 = p.clone();
}
if let Some(p) = func_params_f64_all.get(func.index as usize) {
current_func_params_f64 = p.clone();
}
current_func_ret_f32 = func_ret_f32_all
.get(func.index as usize)
.copied()
.unwrap_or(false);
current_func_ret_f64 = func_ret_f64_all
.get(func.index as usize)
.copied()
.unwrap_or(false);
current_func_param_count = module_func_arg_counts.get(func.index as usize).copied();
current_func_block_arity = func.block_arity.clone();
current_func_facts = wsc_facts
.iter()
.filter(|f| f.func_index == func.index)
.cloned()
.collect();
if let Some(reason) = &func.unsupported {
anyhow::bail!(
"function '{}' contains an unsupported operator ({}) the '{}' \
backend cannot lower — it was dropped at decode, so refusing \
to emit a silent miscompile (GI-FPU-001; #369, #554). Implement \
the op or compile a function the backend supports.",
name,
reason,
backend.name()
);
}
(func.ops, name)
}
(None, Some(demo_name)) => {
info!("Compiling demo function: {}", demo_name);
match demo_name.as_str() {
"add" => (
vec![WasmOp::LocalGet(0), WasmOp::LocalGet(1), WasmOp::I32Add],
"add".to_string(),
),
"mul" => (
vec![WasmOp::LocalGet(0), WasmOp::LocalGet(1), WasmOp::I32Mul],
"mul".to_string(),
),
"calc" => (
vec![
WasmOp::I32Const(5),
WasmOp::I32Const(3),
WasmOp::I32Mul,
WasmOp::I32Const(2),
WasmOp::I32Add,
],
"calc".to_string(),
),
_ => anyhow::bail!("Unknown demo: {}. Available: add, mul, calc", demo_name),
}
}
(None, None) => {
info!("No input specified, using 'add' demo");
(
vec![WasmOp::LocalGet(0), WasmOp::LocalGet(1), WasmOp::I32Add],
"add".to_string(),
)
}
};
let mut wasm_ops = wasm_ops;
let mut fact_div_zero_elide = Vec::new();
let mut fact_div_ovf_elide = Vec::new();
let mut fact_mem_bounds_elide = Vec::new();
if let Some(spec) = maybe_fact_spec(
&func_name,
&wasm_ops,
¤t_func_block_arity,
¤t_func_facts,
¤t_func_params_i64,
0,
) {
wasm_ops = spec.ops;
current_func_block_arity = spec.block_arity;
fact_div_zero_elide = spec.elide_div_zero;
fact_div_ovf_elide = spec.elide_div_ovf;
fact_mem_bounds_elide = spec.elide_mem_bounds;
}
info!("WASM operations: {:?}", wasm_ops);
let config = CompileConfig {
no_optimize,
loom_compat,
safety_bounds,
target: target_spec.clone(),
volatile_segments,
current_func_params_i64,
current_func_params_f32,
func_params_f32: func_params_f32_all,
current_func_params_f64,
func_params_f64: func_params_f64_all,
current_func_ret_f32,
current_func_ret_f64,
func_ret_f32: func_ret_f32_all,
func_ret_f64: func_ret_f64_all,
type_ret_f32: type_ret_f32_all,
type_ret_f64: type_ret_f64_all,
current_func_param_count,
func_ret_i64,
type_ret_i64,
global_widths,
current_func_block_arity,
wsc_facts,
current_func_facts,
fact_div_zero_elide,
fact_div_ovf_elide,
fact_mem_bounds_elide,
call_indirect_guards,
linmem_base: stack_layout.optimized_linmem_base(),
linear_memory_bytes: if backend.name() == "aarch64" {
single_func_linear_memory_bytes
} else {
0
},
..CompileConfig::default()
};
let compiled = backend
.compile_function(&func_name, &wasm_ops, &config)
.map_err(|e| anyhow::anyhow!("Backend '{}' compilation failed: {}", backend.name(), e))?;
let code = compiled.code;
info!("Encoded {} bytes of machine code", code.len());
let elf_data = if backend.name() == "aarch64" {
if single_func_has_data_segments {
anyhow::bail!(
"module carries active data segment(s), but the aarch64 \
backend does not materialize data segments — a load from the \
initialized region would silently read zeros; refusing \
(#851). Data-segment init is a documented follow-on."
);
}
if let Some(reason) = &single_func_nonconst_data {
anyhow::bail!("aarch64: {reason} — refusing to ship the region uninitialized (#851)");
}
build_aarch64_elf(&code, &func_name)?
} else if matches!(target_spec.family, synth_core::target::ArchFamily::RiscV) {
if !compiled.relocations.is_empty() {
anyhow::bail!(
"function '{}' contains {} external call site(s), but the \
single-function RISC-V path emits no relocation table — the \
calls would silently target themselves. Compile the module \
with --all-exports (which emits .rela.text, #871).",
func_name,
compiled.relocations.len()
);
}
build_riscv_elf(&code, &func_name)?
} else if cortex_m {
if single_func_has_data_segments {
anyhow::bail!(
"module carries active data segment(s), but the single-function \
self-contained Cortex-M path cannot ship them (they would \
silently read as zero). Compile the whole module (drop \
--func-index/--func-name so the multi-function `--all-exports` \
path runs, which materializes data at reset) or use \
--relocatable (#758)."
);
}
build_cortex_m_elf(
&code,
&func_name,
target_spec,
&startup_globals_words,
stack_layout,
)?
} else {
build_simple_elf(&code, &func_name)?
};
info!("Generated {} byte ELF file", elf_data.len());
let mut file = File::create(&output).context(format!(
"Failed to create output file: {}",
output.display()
))?;
file.write_all(&elf_data)
.context("Failed to write ELF data")?;
maybe_emit_safety_manifest(
&output,
target_spec,
safety_bounds,
single_func_linear_memory_bytes,
)?;
if let Some(ref sbom_dest) = sbom_path {
match (sbom_wasm_bytes.as_deref(), input.as_deref()) {
(Some(wasm), Some(in_path)) => {
emit_sbom(
sbom_dest,
in_path,
wasm,
&output,
&elf_data,
target_spec,
backend_name,
&sbom_imports,
)?;
}
_ => {
eprintln!(
"warning: --sbom requires a WASM/WAT input file; \
skipping SBOM for demo compilation"
);
}
}
}
if sign_output {
sign::sign_elf(&output)?;
}
println!("Compiled {} to {}", func_name, output.display());
println!(" Code size: {} bytes", code.len());
println!(" ELF size: {} bytes", elf_data.len());
println!("\nInspect with: synth disasm {}", output.display());
if verify {
let caps = backend.capabilities();
if caps.supports_rule_verification {
#[cfg(feature = "verify")]
{
run_verification(&wasm_ops, &func_name)?;
}
#[cfg(not(feature = "verify"))]
{
println!("\nVerification requested but not compiled into this binary.");
println!("Rebuild with: cargo build --features verify");
}
} else {
println!(
"\nBackend '{}' does not support rule verification.",
backend.name()
);
if caps.supports_binary_verification {
println!("Binary-level translation validation is planned but not yet implemented.");
}
}
}
Ok(())
}
#[cfg(feature = "verify")]
fn run_verification(wasm_ops: &[WasmOp], func_name: &str) -> Result<()> {
use std::collections::HashSet;
use synth_synthesis::{ArmOp, Condition, Operand2, Pattern, Reg, Replacement, SynthesisRule};
println!("\nRunning translation validation for '{}'...", func_name);
let mut rules = Vec::new();
let mut seen = HashSet::new();
for op in wasm_ops {
let disc = std::mem::discriminant(op);
if !seen.insert(disc) {
continue; }
let rule = match op {
WasmOp::I32Add => Some(SynthesisRule {
name: "i32.add → ADD".into(),
priority: 0,
pattern: Pattern::WasmInstr(WasmOp::I32Add),
replacement: Replacement::ArmInstr(ArmOp::Add {
rd: Reg::R0,
rn: Reg::R0,
op2: Operand2::Reg(Reg::R1),
}),
cost: synth_synthesis::Cost {
cycles: 1,
code_size: 2,
registers: 2,
},
}),
WasmOp::I32Sub => Some(SynthesisRule {
name: "i32.sub → SUB".into(),
priority: 0,
pattern: Pattern::WasmInstr(WasmOp::I32Sub),
replacement: Replacement::ArmInstr(ArmOp::Sub {
rd: Reg::R0,
rn: Reg::R0,
op2: Operand2::Reg(Reg::R1),
}),
cost: synth_synthesis::Cost {
cycles: 1,
code_size: 2,
registers: 2,
},
}),
WasmOp::I32Mul => Some(SynthesisRule {
name: "i32.mul → MUL".into(),
priority: 0,
pattern: Pattern::WasmInstr(WasmOp::I32Mul),
replacement: Replacement::ArmInstr(ArmOp::Mul {
rd: Reg::R0,
rn: Reg::R0,
rm: Reg::R1,
}),
cost: synth_synthesis::Cost {
cycles: 1,
code_size: 2,
registers: 2,
},
}),
WasmOp::I32And => Some(SynthesisRule {
name: "i32.and → AND".into(),
priority: 0,
pattern: Pattern::WasmInstr(WasmOp::I32And),
replacement: Replacement::ArmInstr(ArmOp::And {
rd: Reg::R0,
rn: Reg::R0,
op2: Operand2::Reg(Reg::R1),
}),
cost: synth_synthesis::Cost {
cycles: 1,
code_size: 2,
registers: 2,
},
}),
WasmOp::I32Or => Some(SynthesisRule {
name: "i32.or → ORR".into(),
priority: 0,
pattern: Pattern::WasmInstr(WasmOp::I32Or),
replacement: Replacement::ArmInstr(ArmOp::Orr {
rd: Reg::R0,
rn: Reg::R0,
op2: Operand2::Reg(Reg::R1),
}),
cost: synth_synthesis::Cost {
cycles: 1,
code_size: 2,
registers: 2,
},
}),
WasmOp::I32Xor => Some(SynthesisRule {
name: "i32.xor → EOR".into(),
priority: 0,
pattern: Pattern::WasmInstr(WasmOp::I32Xor),
replacement: Replacement::ArmInstr(ArmOp::Eor {
rd: Reg::R0,
rn: Reg::R0,
op2: Operand2::Reg(Reg::R1),
}),
cost: synth_synthesis::Cost {
cycles: 1,
code_size: 2,
registers: 2,
},
}),
WasmOp::I32Eq => Some(SynthesisRule {
name: "i32.eq → CMP + SetCond(EQ)".into(),
priority: 0,
pattern: Pattern::WasmInstr(WasmOp::I32Eq),
replacement: Replacement::ArmSequence(vec![
ArmOp::Cmp {
rn: Reg::R0,
op2: Operand2::Reg(Reg::R1),
},
ArmOp::SetCond {
rd: Reg::R0,
cond: Condition::EQ,
},
]),
cost: synth_synthesis::Cost {
cycles: 2,
code_size: 4,
registers: 2,
},
}),
WasmOp::I32Ne => Some(SynthesisRule {
name: "i32.ne → CMP + SetCond(NE)".into(),
priority: 0,
pattern: Pattern::WasmInstr(WasmOp::I32Ne),
replacement: Replacement::ArmSequence(vec![
ArmOp::Cmp {
rn: Reg::R0,
op2: Operand2::Reg(Reg::R1),
},
ArmOp::SetCond {
rd: Reg::R0,
cond: Condition::NE,
},
]),
cost: synth_synthesis::Cost {
cycles: 2,
code_size: 4,
registers: 2,
},
}),
WasmOp::I32LtS => Some(SynthesisRule {
name: "i32.lt_s → CMP + SetCond(LT)".into(),
priority: 0,
pattern: Pattern::WasmInstr(WasmOp::I32LtS),
replacement: Replacement::ArmSequence(vec![
ArmOp::Cmp {
rn: Reg::R0,
op2: Operand2::Reg(Reg::R1),
},
ArmOp::SetCond {
rd: Reg::R0,
cond: Condition::LT,
},
]),
cost: synth_synthesis::Cost {
cycles: 2,
code_size: 4,
registers: 2,
},
}),
WasmOp::I32LeS => Some(SynthesisRule {
name: "i32.le_s → CMP + SetCond(LE)".into(),
priority: 0,
pattern: Pattern::WasmInstr(WasmOp::I32LeS),
replacement: Replacement::ArmSequence(vec![
ArmOp::Cmp {
rn: Reg::R0,
op2: Operand2::Reg(Reg::R1),
},
ArmOp::SetCond {
rd: Reg::R0,
cond: Condition::LE,
},
]),
cost: synth_synthesis::Cost {
cycles: 2,
code_size: 4,
registers: 2,
},
}),
WasmOp::I32GtS => Some(SynthesisRule {
name: "i32.gt_s → CMP + SetCond(GT)".into(),
priority: 0,
pattern: Pattern::WasmInstr(WasmOp::I32GtS),
replacement: Replacement::ArmSequence(vec![
ArmOp::Cmp {
rn: Reg::R0,
op2: Operand2::Reg(Reg::R1),
},
ArmOp::SetCond {
rd: Reg::R0,
cond: Condition::GT,
},
]),
cost: synth_synthesis::Cost {
cycles: 2,
code_size: 4,
registers: 2,
},
}),
WasmOp::I32GeS => Some(SynthesisRule {
name: "i32.ge_s → CMP + SetCond(GE)".into(),
priority: 0,
pattern: Pattern::WasmInstr(WasmOp::I32GeS),
replacement: Replacement::ArmSequence(vec![
ArmOp::Cmp {
rn: Reg::R0,
op2: Operand2::Reg(Reg::R1),
},
ArmOp::SetCond {
rd: Reg::R0,
cond: Condition::GE,
},
]),
cost: synth_synthesis::Cost {
cycles: 2,
code_size: 4,
registers: 2,
},
}),
WasmOp::I32LtU => Some(SynthesisRule {
name: "i32.lt_u → CMP + SetCond(LO)".into(),
priority: 0,
pattern: Pattern::WasmInstr(WasmOp::I32LtU),
replacement: Replacement::ArmSequence(vec![
ArmOp::Cmp {
rn: Reg::R0,
op2: Operand2::Reg(Reg::R1),
},
ArmOp::SetCond {
rd: Reg::R0,
cond: Condition::LO,
},
]),
cost: synth_synthesis::Cost {
cycles: 2,
code_size: 4,
registers: 2,
},
}),
WasmOp::I32LeU => Some(SynthesisRule {
name: "i32.le_u → CMP + SetCond(LS)".into(),
priority: 0,
pattern: Pattern::WasmInstr(WasmOp::I32LeU),
replacement: Replacement::ArmSequence(vec![
ArmOp::Cmp {
rn: Reg::R0,
op2: Operand2::Reg(Reg::R1),
},
ArmOp::SetCond {
rd: Reg::R0,
cond: Condition::LS,
},
]),
cost: synth_synthesis::Cost {
cycles: 2,
code_size: 4,
registers: 2,
},
}),
WasmOp::I32GtU => Some(SynthesisRule {
name: "i32.gt_u → CMP + SetCond(HI)".into(),
priority: 0,
pattern: Pattern::WasmInstr(WasmOp::I32GtU),
replacement: Replacement::ArmSequence(vec![
ArmOp::Cmp {
rn: Reg::R0,
op2: Operand2::Reg(Reg::R1),
},
ArmOp::SetCond {
rd: Reg::R0,
cond: Condition::HI,
},
]),
cost: synth_synthesis::Cost {
cycles: 2,
code_size: 4,
registers: 2,
},
}),
WasmOp::I32GeU => Some(SynthesisRule {
name: "i32.ge_u → CMP + SetCond(HS)".into(),
priority: 0,
pattern: Pattern::WasmInstr(WasmOp::I32GeU),
replacement: Replacement::ArmSequence(vec![
ArmOp::Cmp {
rn: Reg::R0,
op2: Operand2::Reg(Reg::R1),
},
ArmOp::SetCond {
rd: Reg::R0,
cond: Condition::HS,
},
]),
cost: synth_synthesis::Cost {
cycles: 2,
code_size: 4,
registers: 2,
},
}),
WasmOp::I32Eqz => Some(SynthesisRule {
name: "i32.eqz → CMP #0 + SetCond(EQ)".into(),
priority: 0,
pattern: Pattern::WasmInstr(WasmOp::I32Eqz),
replacement: Replacement::ArmSequence(vec![
ArmOp::Cmp {
rn: Reg::R0,
op2: Operand2::Imm(0),
},
ArmOp::SetCond {
rd: Reg::R0,
cond: Condition::EQ,
},
]),
cost: synth_synthesis::Cost {
cycles: 2,
code_size: 4,
registers: 1,
},
}),
_ => None,
};
if let Some(r) = rule {
rules.push(r);
}
}
if rules.is_empty() {
println!(" No verifiable computational rules for this function.");
println!(" (LocalGet/Set/Const are register operations, not verified by SMT)");
return Ok(());
}
println!(" Verifying {} instruction selection rules...", rules.len());
let (verified, failed, unknown) = synth_verify::with_verification_context(|| {
let validator = synth_verify::TranslationValidator::new();
let mut verified = 0u32;
let mut failed = 0u32;
let mut unknown = 0u32;
for rule in &rules {
match validator.verify_rule(rule) {
Ok(synth_verify::ValidationResult::Verified) => {
println!(" ✓ {} verified", rule.name);
verified += 1;
}
Ok(synth_verify::ValidationResult::Invalid { counterexample }) => {
println!(" ✗ {} INVALID: {:?}", rule.name, counterexample);
failed += 1;
}
Ok(synth_verify::ValidationResult::Unknown { reason }) => {
println!(" ? {} unknown: {}", rule.name, reason);
unknown += 1;
}
Err(e) => {
println!(" ! {} error: {}", rule.name, e);
unknown += 1;
}
}
}
(verified, failed, unknown)
});
println!(
"\nVerification summary: {} verified, {} failed, {} unknown",
verified, failed, unknown
);
if failed > 0 {
anyhow::bail!(
"Translation validation failed: {} rules produced counterexamples",
failed
);
}
Ok(())
}
fn extract_all_modules_from_wast(contents: &str) -> Result<Vec<Vec<u8>>> {
let buf = ParseBuffer::new(contents)
.map_err(|e| anyhow::anyhow!("Failed to create parse buffer: {}", e))?;
let wast: Wast =
parser::parse(&buf).map_err(|e| anyhow::anyhow!("Failed to parse WAST: {}", e))?;
let mut modules = Vec::new();
for directive in wast.directives {
if let WastDirective::Module(mut quote_wat) = directive {
match quote_wat.encode() {
Ok(binary) => modules.push(binary),
Err(e) => {
info!("Skipping unencoded module: {}", e);
}
}
}
}
if modules.is_empty() {
anyhow::bail!("No module found in WAST file");
}
Ok(modules)
}
fn extract_module_from_wast(contents: &str) -> Result<Vec<u8>> {
let modules = extract_all_modules_from_wast(contents)?;
for module_bytes in &modules {
if let Ok(decoded) = decode_wasm_module(module_bytes)
&& decoded.functions.iter().any(|f| f.export_name.is_some())
{
return Ok(module_bytes.clone());
}
}
modules
.into_iter()
.next()
.ok_or_else(|| anyhow::anyhow!("no modules found in WAST file"))
}
fn identify_stack_pointer_global(globals: &[WasmGlobal], linmem_bytes: u32) -> Option<(u32, i32)> {
globals
.iter()
.filter(|g| g.mutable)
.filter_map(|g| match g.init {
Some(GlobalInit::I32(v)) => Some((g.index, v)),
_ => None,
})
.filter(|&(_, v)| v > 0 && (v as u32) <= linmem_bytes)
.max_by_key(|&(_, v)| v)
}
fn globals_table_words(globals: &[WasmGlobal]) -> Vec<u32> {
let mut words: Vec<u32> = Vec::new();
for g in globals {
let n = (g.slot_bytes.max(4) / 4) as usize;
let base = words.len();
words.resize(base + n, 0);
match g.init {
Some(GlobalInit::I32(v)) => words[base] = v as u32,
Some(GlobalInit::I64(v)) => {
words[base] = v as u32;
if n > 1 {
words[base + 1] = ((v as u64) >> 32) as u32;
}
}
None => {}
}
}
words
}
fn reachable_from_exports(
funcs: &[FunctionOps],
num_imports: u32,
elem_func_indices: &[u32],
) -> std::collections::BTreeSet<u32> {
let pos_by_index: std::collections::HashMap<u32, usize> = funcs
.iter()
.enumerate()
.map(|(i, f)| (f.index, i))
.collect();
let mut reachable: std::collections::BTreeSet<u32> = std::collections::BTreeSet::new();
let mut work: Vec<u32> = Vec::new();
let mut table_included = false;
for f in funcs {
if f.export_name.is_some() && reachable.insert(f.index) {
work.push(f.index);
}
}
while let Some(idx) = work.pop() {
if let Some(&p) = pos_by_index.get(&idx) {
for op in &funcs[p].ops {
match op {
WasmOp::Call(target) if *target >= num_imports && reachable.insert(*target) => {
work.push(*target);
}
WasmOp::CallIndirect { .. } if !table_included => {
table_included = true;
for &t in elem_func_indices {
if t >= num_imports && reachable.insert(t) {
work.push(t);
}
}
}
_ => {}
}
}
}
}
reachable
}
#[allow(clippy::too_many_arguments)]
fn compile_all_exports(
input: Option<PathBuf>,
output: PathBuf,
cortex_m: bool,
no_optimize: bool,
loom_compat: bool,
loom: bool,
safety_bounds: SafetyBounds,
backend: &dyn Backend,
verify: bool,
target_spec: &TargetSpec,
relocatable: bool,
native_pointer_abi: bool,
no_bind_cabi_arena: bool,
sbom_path: Option<PathBuf>,
sign_output: bool,
shadow_stack_size: Option<u32>,
debug_line: bool,
emit_provenance: bool,
emit_wcet: bool,
wcet_hints: Option<synth_core::wcet::WcetHints>,
volatile_segments: Vec<VolatileRange>,
stack_layout: StackLayout,
proven_safe: Option<PathBuf>,
) -> Result<()> {
let path = input.context("--all-exports requires an input file")?;
info!("Compiling all exports from: {}", path.display());
let module_name = path
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "module".to_string());
let mut provenance_map =
emit_provenance.then(|| synth_core::provenance::ProvenanceMap::new(module_name.clone()));
let wcet_core_class = synth_backend::wcet::sound_core_class(&target_spec.triple)
.map(str::to_string)
.unwrap_or_else(|| target_spec.triple.clone());
let mut wcet_report =
emit_wcet.then(|| synth_core::wcet::WcetReport::new(module_name, wcet_core_class));
let file_bytes =
std::fs::read(&path).context(format!("Failed to read input file: {}", path.display()))?;
let mut sbom_wasm_bytes: Option<Vec<u8>> = None;
let (
all_exports,
all_memories,
all_imports,
max_num_imported_funcs,
func_arg_counts,
func_result_counts, type_arg_counts,
all_data_segments, stack_pointer_global_opt, all_globals, all_global_widths, all_func_ret_i64, all_type_ret_i64, all_func_params_i64, all_func_params_f32, all_func_params_f64, all_func_ret_f32, all_func_ret_f64, all_type_ret_f32, all_type_ret_f64, all_wsc_facts, all_extra_memory_segments, multi_memory_decline, default_memory_nonconst_data, all_call_indirect_guards, all_funcref_slots, a64_plan_inputs, ) = if path.extension().is_some_and(|ext| ext == "wast") {
info!("Parsing WAST (extracting all modules)...");
let contents = String::from_utf8(file_bytes).context("WAST file is not valid UTF-8")?;
let module_binaries = extract_all_modules_from_wast(&contents)?;
info!("Found {} modules in WAST file", module_binaries.len());
let mut export_map: std::collections::HashMap<String, FunctionOps> =
std::collections::HashMap::new();
let mut merged_memories: Vec<WasmMemory> = Vec::new();
let mut merged_imports: Vec<ImportEntry> = Vec::new();
let mut max_imports: u32 = 0;
let mut merged_func_arg_counts: Vec<u32> = Vec::new();
let mut merged_func_result_counts: Vec<u32> = Vec::new(); let mut merged_type_arg_counts: Vec<u32> = Vec::new();
for (idx, wasm_bytes) in module_binaries.iter().enumerate() {
let wasm_bytes = maybe_run_loom(loom, wasm_bytes.clone())?;
if sbom_wasm_bytes.is_none() {
sbom_wasm_bytes = Some(wasm_bytes.clone());
}
match decode_wasm_module(&wasm_bytes) {
Ok(module) => {
let export_count = module
.functions
.iter()
.filter(|f| f.export_name.is_some())
.count();
info!(
" Module {}: {} functions ({} exports), {} memories",
idx,
module.functions.len(),
export_count,
module.memories.len()
);
for func in module.functions {
if let Some(name) = func.export_name.clone() {
export_map.insert(name, func);
}
}
for mem in &module.memories {
if merged_memories.is_empty()
|| mem.initial_pages
> merged_memories
.first()
.map(|m| m.initial_pages)
.unwrap_or(0)
{
merged_memories = vec![mem.clone()];
}
}
if module.num_imported_funcs > max_imports {
max_imports = module.num_imported_funcs;
merged_imports = module.imports.clone();
merged_func_arg_counts = module.func_arg_counts.clone();
merged_func_result_counts = module.func_result_counts.clone();
merged_type_arg_counts = module.type_arg_counts.clone();
sbom_wasm_bytes = Some(wasm_bytes.clone());
} else if merged_func_arg_counts.is_empty() {
merged_func_arg_counts = module.func_arg_counts.clone();
merged_func_result_counts = module.func_result_counts.clone();
merged_type_arg_counts = module.type_arg_counts.clone();
}
}
Err(e) => {
info!(" Module {}: decode failed ({}), skipping", idx, e);
}
}
}
let exports: Vec<_> = export_map.into_values().collect();
(
exports,
merged_memories,
merged_imports,
max_imports,
merged_func_arg_counts,
merged_func_result_counts, merged_type_arg_counts,
Vec::new(), None, Vec::new(), Vec::new(), Vec::new(), Vec::new(),
Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new(), None, None, synth_core::CallIndirectGuards::default(),
Vec::new(),
synth_backend_aarch64::substrate::PlanInputs::default(),
)
} else {
let wasm_bytes = if path.extension().is_some_and(|ext| ext == "wat") {
info!("Parsing WAT to WASM...");
wat::parse_bytes(&file_bytes)
.context("Failed to parse WAT file")?
.into_owned()
} else {
file_bytes
};
let wasm_bytes = maybe_run_loom(loom, wasm_bytes)?;
let wasm_bytes = if cortex_m
&& !relocatable
&& !native_pointer_abi
&& backend.name() == "arm"
&& !no_bind_cabi_arena
{
match synth_core::arena_bind::bind_cabi_arena_realloc(&wasm_bytes)? {
synth_core::arena_bind::ArenaBind::Bound(b) => {
info!(
"#418: bound env::__cabi_arena_realloc to a synthesized in-image \
arena allocator: wasm [0x{:x}, 0x{:x}) ({} bytes, traps on \
exhaustion; opt-out --no-bind-cabi-arena)",
b.arena_base,
b.arena_end,
b.arena_end - b.arena_base
);
b.bytes
}
synth_core::arena_bind::ArenaBind::KeptHostSeam(reason) => {
info!("#418: env::__cabi_arena_realloc NOT bound: {reason}");
wasm_bytes
}
synth_core::arena_bind::ArenaBind::NoArenaImport => wasm_bytes,
}
} else {
wasm_bytes
};
let module = decode_wasm_module(&wasm_bytes).context("Failed to decode WASM module")?;
sbom_wasm_bytes = Some(wasm_bytes);
let guards = module.call_indirect_guards();
let funcref_slots = module.funcref_region_slots();
let a64_inputs = synth_backend_aarch64::substrate::PlanInputs::from_module(&module);
let func_arg_counts = module.func_arg_counts;
let func_result_counts = module.func_result_counts; let type_arg_counts = module.type_arg_counts;
let memories = module.memories;
let imports = module.imports;
let num_imports = module.num_imported_funcs;
for imp in &imports {
if !matches!(imp.kind, synth_core::wasm_decoder::ImportKind::Function(_)) {
continue;
}
if let synth_core::async_intrinsics::AsyncClassification::Declined(d) =
synth_core::async_intrinsics::classify(&imp.module, &imp.name)
{
anyhow::bail!("{d}");
}
}
let data_segs = module.data_segments; let elem_func_indices = module.elem_func_indices; let linmem_bytes = memories.first().map(|m| m.initial_bytes()).unwrap_or(0);
let sp_global = identify_stack_pointer_global(&module.globals, linmem_bytes);
let global_widths: Vec<u32> = {
let mut widths = Vec::new();
for g in &module.globals {
let i = g.index as usize;
if widths.len() <= i {
widths.resize(i + 1, 4);
}
widths[i] = g.slot_bytes;
}
widths
};
if native_pointer_abi && global_widths.iter().any(|&w| w > 4) {
anyhow::bail!(
"--native-pointer-abi does not support i64/f64/v128 globals \
(the `__synth_globals` slot region is 4-byte i32 slots) — \
refusing to truncate them to 32 bits (#643)"
);
}
#[cfg_attr(not(feature = "exports_only_275_probe"), allow(unused_mut))]
let mut reachable =
reachable_from_exports(&module.functions, num_imports, &elem_func_indices);
#[cfg(feature = "exports_only_275_probe")]
if std::env::var_os("EXPORTS_ONLY_275").is_some() {
reachable.retain(|idx| {
module
.functions
.iter()
.any(|f| f.index == *idx && f.export_name.is_some())
});
}
let exports: Vec<_> = module
.functions
.into_iter()
.filter(|f| reachable.contains(&f.index))
.collect();
(
exports,
memories,
imports,
num_imports,
func_arg_counts,
func_result_counts, type_arg_counts,
data_segs,
sp_global,
module.globals,
global_widths,
module.func_ret_i64,
module.type_ret_i64,
module.func_params_i64,
module.func_params_f32,
module.func_params_f64,
module.func_ret_f32,
module.func_ret_f64,
module.type_ret_f32,
module.type_ret_f64,
module.wsc_facts,
module.extra_memory_data_segments,
module.multi_memory_decline,
module.default_memory_nonconst_data, guards, funcref_slots, a64_inputs, )
};
if !all_memories.is_empty() {
info!("Memories ({} total):", all_memories.len());
for mem in &all_memories {
let max_str = mem
.max_pages
.map(|m| format!("{}", m))
.unwrap_or_else(|| "unlimited".to_string());
info!(
" memory[{}]: {} initial pages, {} max pages ({}KB initial)",
mem.index,
mem.initial_pages,
max_str,
mem.initial_pages * 64
);
}
}
if let Some(reason) = &multi_memory_decline {
anyhow::bail!("multi-memory (#406): {reason}");
}
if all_memories.len() > 1 {
if backend.name() != "arm" {
anyhow::bail!(
"multi-memory (#406): the '{}' backend has no per-memory base \
lowering — a module with {} linear memories compiles only on \
the ARM --relocatable path (VCR-MEM-002 phase 1)",
backend.name(),
all_memories.len()
);
}
if !relocatable {
anyhow::bail!(
"multi-memory (#406): a module with {} linear memories cannot \
be compiled into a self-contained image — there is only ONE \
runtime linear-memory base (R11), so every memory would alias \
it (a store to memory 1 silently clobbering memory 0). Compile \
with --relocatable: memory k > 0 is addressed via its own \
`__synth_wasm_data_<k>` region symbol, which the host \
linker/runtime places (VCR-MEM-002 phase 1)",
all_memories.len()
);
}
if native_pointer_abi {
anyhow::bail!(
"multi-memory (#406): --native-pointer-abi is memory-0-only in \
phase 1 — the static-data region classification \
(#345/#354/#678/#739) has no per-memory layering. Drop \
--native-pointer-abi or keep the module single-memory"
);
}
if shadow_stack_size.is_some() {
anyhow::bail!(
"multi-memory (#406): --shadow-stack-size shrinks MEMORY 0's \
reservation geometry and has no defined meaning for a module \
with {} linear memories in phase 1 — refusing rather than \
shrinking the wrong region",
all_memories.len()
);
}
if safety_bounds == SafetyBounds::Mpu {
anyhow::bail!(
"multi-memory (#406): per-memory MPU isolation (--safety-bounds \
mpu) is not yet realizable for a module with {} linear \
memories. Programming one MPU region per memory requires synth \
to emit the startup that writes MPU_RBAR/RASR — the \
self-contained reset handler — but multi-memory compiles ONLY \
on --relocatable, where the host owns startup and synth emits \
no MPU programming; the self-contained path in turn declines \
multi-memory (one R11 base). Refusing rather than accepting a \
silent MPU no-op. Blocked on self-contained multi-memory \
(#406 phase 2)",
all_memories.len()
);
}
}
if max_num_imported_funcs > 0 {
info!(
"Module imports {} functions (Meld dispatch enabled):",
max_num_imported_funcs
);
for imp in &all_imports {
if matches!(imp.kind, synth_core::ImportKind::Function(_)) {
info!(" import[{}]: {}::{}", imp.index, imp.module, imp.name);
}
}
}
if all_exports.is_empty() {
anyhow::bail!("No exported functions found in module");
}
info!("Found {} exported functions:", all_exports.len());
for f in &all_exports {
let display_name = f
.export_name
.as_deref()
.map_or_else(|| format!("func_{}", f.index), String::from);
info!(" '{}' (index {})", display_name, f.index);
}
let a64_plan_inputs = a64_plan_inputs.with_usage(
all_exports.iter().any(|f| {
f.ops
.iter()
.any(|op| matches!(op, WasmOp::GlobalGet(_) | WasmOp::GlobalSet(_)))
}),
all_exports.iter().any(|f| {
f.ops
.iter()
.any(|op| matches!(op, WasmOp::CallIndirect { .. }))
}),
);
let a64_substrate = synth_backend_aarch64::substrate::plan(&a64_plan_inputs);
if backend.name() == "aarch64"
&& let Err(reason) = &a64_substrate
{
anyhow::bail!("aarch64: {reason}");
}
let a64_substrate_emitted = a64_substrate.as_ref().is_ok_and(|s| s.emitted);
let proven_safe_module_min_bytes: Option<u32> = all_memories.first().map(|m| m.initial_bytes());
let proven_safe_ingest: Option<synth_core::proven_safe::ProvenSafeIngest> =
proven_safe.as_ref().map(|path| {
let module_bytes: &[u8] = sbom_wasm_bytes.as_deref().unwrap_or(&[]);
if module_bytes.is_empty() {
eprintln!(
"warning: --proven-safe {}: this input has no single module to hash (a multi-module .wast merge); NO bounds guard is elided (fail closed)",
path.display()
);
}
let Some(min_bytes) = proven_safe_module_min_bytes else {
eprintln!(
"warning: --proven-safe {}: this module defines no linear memory \
(an imported memory declares its minimum elsewhere, which synth \
does not yet carry), so NO memory floor can be established; NO \
bounds guard is elided (fail closed). This compile's module \
hashes to {}.",
path.display(),
synth_core::proven_safe::hex_sha256(module_bytes)
);
return synth_core::proven_safe::ProvenSafeIngest::refused(
"no linear memory is DEFINED by this module, so no memory floor \
can be established; refusing rather than validating verdicts \
against a 0 B floor (#932)",
);
};
let r = synth_core::proven_safe::ingest(path, module_bytes, min_bytes);
for d in &r.diagnostics {
eprintln!("warning: proven-safe: {d}");
}
if r.accepted {
eprintln!(
"proven-safe: ACCEPTED {} — scry {} proved {} access site(s) in-bounds against the {} B floor; module_sha256 {} verified",
path.display(),
if r.scry_version.is_empty() {
"<unversioned>"
} else {
&r.scry_version
},
r.offered.len(),
r.declared_memory_min_bytes,
&r.actual_module_sha256[..16.min(r.actual_module_sha256.len())],
);
}
r
});
let proven_safe_backend_supported = backend.name() == "arm";
let mut proven_safe_elisions: Vec<synth_core::proven_safe::AttestedElision> = Vec::new();
let mut proven_safe_diagnostics: Vec<String> = proven_safe_ingest
.as_ref()
.map(|r| r.diagnostics.clone())
.unwrap_or_default();
let config = CompileConfig {
no_optimize,
loom_compat,
safety_bounds,
num_imports: max_num_imported_funcs,
func_arg_counts,
func_result_counts, type_arg_counts,
target: target_spec.clone(),
relocatable,
native_pointer_abi,
linear_memory_bytes: all_memories.first().map(|m| m.initial_bytes()).unwrap_or(0),
memory_pages: {
let slots = all_memories
.iter()
.map(|m| m.index as usize + 1)
.max()
.unwrap_or(0);
let mut pages = vec![0u32; slots];
for m in &all_memories {
pages[m.index as usize] = m.initial_pages;
}
pages
},
stack_pointer_global: stack_pointer_global_opt,
func_ret_i64: all_func_ret_i64.clone(),
type_ret_i64: all_type_ret_i64.clone(),
global_widths: all_global_widths.clone(),
func_params_i64: all_func_params_i64.clone(),
func_params_f32: all_func_params_f32.clone(),
current_func_params_f32: Vec::new(),
func_params_f64: all_func_params_f64.clone(),
current_func_params_f64: Vec::new(),
current_func_ret_f32: false,
current_func_ret_f64: false,
func_ret_f32: all_func_ret_f32.clone(),
func_ret_f64: all_func_ret_f64.clone(),
type_ret_f32: all_type_ret_f32.clone(),
type_ret_f64: all_type_ret_f64.clone(),
volatile_segments,
wsc_facts: all_wsc_facts.clone(),
call_indirect_guards: all_call_indirect_guards,
type_result_counts: a64_plan_inputs.type_result_counts.clone(),
type_class_ids: a64_plan_inputs.type_class_ids.clone(),
a64_substrate_emitted,
self_contained_funcref_table: cortex_m && !relocatable && max_num_imported_funcs == 0,
linmem_base: stack_layout.optimized_linmem_base(),
wcet_hints,
..CompileConfig::default()
};
let mut compiled_funcs = Vec::new();
let mut skipped_funcs: Vec<(String, String)> = Vec::new();
let mut wcet_intermediates: Vec<synth_core::wcet::WcetIntermediate> = Vec::new();
let mut wcet_label_index: std::collections::HashMap<String, usize> =
std::collections::HashMap::new();
for func in &all_exports {
let name = func
.export_name
.clone()
.unwrap_or_else(|| format!("func_{}", func.index));
info!(
"Compiling function '{}' via backend '{}'...",
name,
backend.name()
);
let mut func_config = {
let mut fc = config.clone();
if let Some(p) = all_func_params_i64.get(func.index as usize)
&& !p.is_empty()
{
fc.current_func_params_i64 = p.clone();
}
if let Some(p) = all_func_params_f32.get(func.index as usize)
&& !p.is_empty()
{
fc.current_func_params_f32 = p.clone();
}
if let Some(p) = all_func_params_f64
.get(func.index as usize)
.filter(|p| !p.is_empty())
{
fc.current_func_params_f64 = p.clone();
}
fc.current_func_ret_f32 = all_func_ret_f32
.get(func.index as usize)
.copied()
.unwrap_or(false);
fc.current_func_ret_f64 = all_func_ret_f64
.get(func.index as usize)
.copied()
.unwrap_or(false);
fc.current_func_block_arity = func.block_arity.clone();
fc.current_func_param_count = config.func_arg_counts.get(func.index as usize).copied();
fc.current_func_index = Some(func.index);
fc.current_func_facts = all_wsc_facts
.iter()
.filter(|f| f.func_index == func.index)
.cloned()
.collect();
fc
};
if let Some(reason) = &func.unsupported {
eprintln!(
"warning: skipping function '{name}': contains an unsupported \
operator ({reason}) the {} backend cannot lower — emitting no \
code for it rather than a silent miscompile (GI-FPU-001, #369)",
backend.name()
);
skipped_funcs.push((name.clone(), format!("unsupported operator: {reason}")));
continue;
}
let spec = maybe_fact_spec(
&name,
&func.ops,
&func_config.current_func_block_arity,
&func_config.current_func_facts,
&func_config.current_func_params_i64,
func_config.linear_memory_bytes,
);
if let Some(ing) = &proven_safe_ingest
&& ing.accepted
&& safety_bounds == SafetyBounds::Software
&& proven_safe_backend_supported
{
let mut notes = Vec::new();
let marks = ing.validate_function(func.index, &func.ops, &mut notes);
for n in ¬es {
eprintln!("proven-safe: DROP {n}");
}
proven_safe_diagnostics.extend(notes);
if spec.is_some() {
if !marks.is_empty() {
let msg = format!(
"func {} ('{name}') — REFUSED: SYNTH_FACT_SPEC specialized this function, renumbering the operator index space the scry verdicts are keyed in. {} externally-proven mark(s) dropped; every guard is retained (VCR-MEM-004, #901)",
func.index,
marks.len()
);
eprintln!("proven-safe: {msg}");
proven_safe_diagnostics.push(msg);
}
} else if !marks.is_empty() {
for &pc in &marks {
if let Some(site) = ing
.offered_for_func(func.index)
.into_iter()
.find(|s| s.pc as usize == pc)
{
proven_safe_elisions.push(synth_core::proven_safe::AttestedElision {
func: func.index,
pc: site.pc,
op: site.op.clone(),
width: site.width,
authority: synth_core::proven_safe::SAFE_ACCESSES_SCHEMA.to_string(),
});
}
}
eprintln!(
"proven-safe: ELIDE func {} ('{name}') — {} bounds guard(s) elided on scry's proof (op indices {marks:?})",
func.index,
marks.len()
);
func_config.proven_safe_mem_elide = marks;
}
}
let (ops_for_compile, op_offsets_for_elf): (&[WasmOp], Vec<u32>) = match &spec {
Some(s) => {
func_config.current_func_block_arity = s.block_arity.clone();
func_config.fact_div_zero_elide = s.elide_div_zero.clone();
func_config.fact_div_ovf_elide = s.elide_div_ovf.clone();
func_config.fact_mem_bounds_elide = s.elide_mem_bounds.clone();
(
&s.ops,
s.kept
.iter()
.filter_map(|&i| func.op_offsets.get(i).copied())
.collect(),
)
}
None => (&func.ops, func.op_offsets.clone()),
};
let compiled = match backend.compile_function(&name, ops_for_compile, &func_config) {
Ok(c) => c,
Err(e) => {
eprintln!(
"warning: skipping function '{}': backend '{}' failed: {}",
name,
backend.name(),
e
);
skipped_funcs.push((name.clone(), e.to_string()));
continue;
}
};
info!(" {} bytes of machine code", compiled.code.len());
if let Some(pm) = provenance_map.as_mut() {
let has_branch = compiled
.branch_map
.iter()
.any(|(_, c)| *c == synth_core::backend::BranchClass::CondBranch);
let all_none = !compiled.line_map.is_empty()
&& compiled.line_map.iter().all(|(_, oi)| oi.is_none());
if has_branch && all_none {
eprintln!(
"warning: provenance: skipping '{name}' — its lowering path carries no \
source map (line_map all-None), so object branches cannot be reconciled \
to source conditions (VCR-DEC-003 v1 covers the ARM direct/Thumb selector \
path; #396 follow-up: optimized ir_to_arm source_line)"
);
} else {
let eliminated: Vec<(usize, String, u32)> = match &spec {
Some(s) => {
let kept: std::collections::HashSet<usize> =
s.kept.iter().copied().collect();
func.ops
.iter()
.enumerate()
.filter(|(i, _)| !kept.contains(i))
.filter_map(|(i, op)| {
synth_core::provenance::covered_source_op_name(op).map(|n| {
(
i,
n.to_string(),
func.op_offsets.get(i).copied().unwrap_or(0),
)
})
})
.collect()
}
None => Vec::new(),
};
pm.functions
.push(synth_core::provenance::derive_function_provenance(
func.index,
&name,
ops_for_compile,
&op_offsets_for_elf,
&compiled.line_map,
&compiled.branch_map,
&eliminated,
));
}
}
if wcet_report.is_some() {
let inter = compiled.wcet_intermediate.clone().unwrap_or_else(|| {
synth_core::wcet::WcetIntermediate::Declined {
name: name.clone(),
reason: synth_core::wcet::WcetDecline::UnsupportedCore,
site: None,
hint_rejections: Vec::new(),
}
});
wcet_label_index.insert(format!("func_{}", func.index), wcet_intermediates.len());
wcet_intermediates.push(inter);
}
if !compiled.relocations.is_empty() {
info!(
" {} relocations (external symbol references)",
compiled.relocations.len()
);
}
compiled_funcs.push(ElfFunction {
name: name.clone(),
debug_name: func.debug_name.clone(),
wasm_index: func.index,
code: compiled.code,
relocations: compiled.relocations,
op_offsets: op_offsets_for_elf,
line_map: compiled.line_map,
});
if verify {
#[cfg(feature = "verify")]
run_verification(&func.ops, &name)?;
#[cfg(not(feature = "verify"))]
{
eprintln!("Warning: --verify requires the 'verify' feature.");
eprintln!(" Rebuild with: cargo build --features verify");
}
}
}
if let Some(wr) = wcet_report.as_mut() {
wr.functions = synth_backend::wcet_compose::compose(&wcet_intermediates, &wcet_label_index);
}
if !skipped_funcs.is_empty() {
eprintln!(
"warning: {} of {} functions were skipped (not in output): {}",
skipped_funcs.len(),
all_exports.len(),
skipped_funcs
.iter()
.map(|(n, _)| n.as_str())
.collect::<Vec<_>>()
.join(", ")
);
}
if compiled_funcs.is_empty() {
anyhow::bail!(
"no functions compiled successfully ({} skipped) — nothing to emit",
skipped_funcs.len()
);
}
let has_relocations = compiled_funcs.iter().any(|f| !f.relocations.is_empty());
let mut internal_labels: std::collections::HashSet<&str> = std::collections::HashSet::new();
for f in &compiled_funcs {
internal_labels.insert(f.name.as_str());
}
let func_index_labels: Vec<String> = compiled_funcs
.iter()
.map(|f| format!("func_{}", f.wasm_index))
.collect();
for label in &func_index_labels {
internal_labels.insert(label.as_str());
}
if config.self_contained_funcref_table {
internal_labels.insert(synth_core::backend::FUNC_TABLE_SYMBOL);
}
let has_external_relocations = compiled_funcs
.iter()
.flat_map(|f| &f.relocations)
.any(|r| !internal_labels.contains(r.symbol.as_str()));
let is_riscv = matches!(target_spec.family, synth_core::target::ArchFamily::RiscV);
let is_aarch64 = backend.name() == "aarch64";
let produced_relocatable = is_riscv || is_aarch64 || has_external_relocations || relocatable;
if stack_layout != StackLayout::High && produced_relocatable {
anyhow::bail!(
"--stack-layout=low applies only to self-contained Cortex-M images: this \
module produces a relocatable object (imported functions require host \
linking), whose layout is owned by the linker script — refusing (#687)"
);
}
let input_dwarf = if debug_line {
sbom_wasm_bytes
.as_deref()
.map(synth_core::dwarf_line::read_input_dwarf_line)
} else {
None
};
let dwarf_effective = !is_riscv && !is_aarch64 && (has_external_relocations || relocatable);
if debug_line && !dwarf_effective {
warn!(
"--debug-line has no effect on this output: DWARF line tables are emitted only on \
the ARM relocatable-object path (link via --relocatable, then `ld`). \
{} produces no .debug_* sections.",
if is_riscv {
"The RISC-V backend"
} else if is_aarch64 {
"The AArch64 backend"
} else {
"A self-contained executable image"
}
);
}
let elf_data = if is_aarch64 {
if !all_data_segments.is_empty() {
anyhow::bail!(
"module carries {} active data segment(s), but the aarch64 \
backend does not materialize data segments (no data section \
or startup) — a load from the initialized region would \
silently read zeros; refusing (#851). Data-segment init is a \
documented follow-on.",
all_data_segments.len()
);
}
if let Some(reason) = &default_memory_nonconst_data {
anyhow::bail!("aarch64: {reason} — refusing to ship the region uninitialized (#851)");
}
info!("Building AArch64 multi-function relocatable object (EM_AARCH64)");
let substrate = a64_substrate
.as_ref()
.map_err(|e| anyhow::anyhow!("aarch64: {e}"))?;
build_multi_func_aarch64_elf(&compiled_funcs, substrate)?
} else if is_riscv {
let rv_segments: Vec<synth_core::static_data_addr::DataSegment> = all_data_segments
.iter()
.map(|(off, d)| synth_core::static_data_addr::DataSegment {
linmem_off: *off,
bytes: d.clone(),
})
.collect();
let rv_mem_size = all_memories.first().map(|m| m.initial_bytes()).unwrap_or(0) as u64;
let rv_extent = synth_core::static_data_addr::image_extent(&rv_segments);
if rv_extent > rv_mem_size {
anyhow::bail!(
"active data segment extends to {} bytes but linear memory is only \
{} bytes — instantiation would trap; refusing to truncate the \
initializer (#798)",
rv_extent,
rv_mem_size
);
}
let rv_wasm_data = synth_core::static_data_addr::pack_segment_records(&rv_segments);
let rv_served = synth_core::static_data_addr::served_image_from_records(&rv_wasm_data)
.ok_or_else(|| {
anyhow::anyhow!(
"VCR-VER-003 (#798): emitted .wasm_data records failed to parse \
back — this is a compiler bug in the record packing"
)
})?;
if let synth_core::static_data_addr::ImageVerdict::Mismatch(mismatches) =
synth_core::static_data_addr::validate_served_image(&rv_segments, &rv_served)
{
let detail = mismatches
.iter()
.take(8)
.map(|m| format!(" {}", m.describe()))
.collect::<Vec<_>>()
.join("\n");
anyhow::bail!(
"VCR-VER-003 (#798): RV32 .wasm_data record validation FAILED — {} \
byte(s) the shipped records serve disagree with the runtime \
linear-memory image (segments applied in declaration order, \
later-wins). This is a compiler bug in the record packing:\n{detail}",
mismatches.len()
);
}
if !rv_segments.is_empty() {
info!(
"Shipping {} wasm data segment(s) ({} initializer byte(s)) as \
.wasm_data records (#798) — the generated startup copies them to \
__linear_memory_base + off at reset; regenerate startup.c/linker.ld \
via `synth riscv-runtime` if yours predate v0.48",
rv_segments.len(),
rv_segments.iter().map(|s| s.bytes.len()).sum::<usize>()
);
}
info!("Building RISC-V multi-function relocatable object (EM_RISCV)");
build_multi_func_riscv_elf(&compiled_funcs, &all_imports, &rv_wasm_data)?
} else if has_external_relocations || relocatable {
let total_relocs: usize = compiled_funcs.iter().map(|f| f.relocations.len()).sum();
if has_relocations {
info!(
"Producing relocatable object (ET_REL): {} import call relocations",
total_relocs
);
} else {
info!("Producing relocatable object (ET_REL): forced by --relocatable");
}
build_relocatable_elf(
&compiled_funcs,
&all_imports,
&all_data_segments,
all_memories.first().map(|m| m.initial_bytes()).unwrap_or(0),
if native_pointer_abi {
Some(NativeGlobalsLayout {
globals: all_globals
.iter()
.map(|g| {
(
g.index,
match g.init {
Some(GlobalInit::I32(v)) => v,
_ => 0,
},
)
})
.collect(),
sp_init: stack_pointer_global_opt.map(|(_, v)| v).unwrap_or(0),
sp_alias_indices: match stack_pointer_global_opt {
Some((_, sp_v)) => all_globals
.iter()
.filter(|g| {
g.mutable && matches!(g.init, Some(GlobalInit::I32(v)) if v == sp_v)
})
.map(|g| g.index)
.collect(),
None => Vec::new(),
},
shadow_stack_size,
})
} else {
None
},
input_dwarf.as_ref(),
target_spec,
&config.call_indirect_guards.type_ids_image,
&all_memories
.iter()
.filter(|m| m.index > 0)
.map(|m| (m.index, m.initial_bytes()))
.collect::<Vec<_>>(),
&all_extra_memory_segments,
)?
} else if cortex_m {
build_multi_func_cortex_m_elf(
&compiled_funcs,
&all_memories,
target_spec,
&globals_table_words(&all_globals),
stack_layout,
&all_data_segments,
&all_funcref_slots,
&config.call_indirect_guards.type_ids_image,
config.call_indirect_guards.type_ids_byte_offset,
)?
} else {
build_multi_func_simple_elf(&compiled_funcs)?
};
info!("Generated {} byte ELF file", elf_data.len());
let mut file = File::create(&output).context(format!(
"Failed to create output file: {}",
output.display()
))?;
file.write_all(&elf_data)
.context("Failed to write ELF data")?;
let linear_mem_bytes = all_memories.first().map(|m| m.initial_bytes()).unwrap_or(0);
maybe_emit_safety_manifest(&output, target_spec, safety_bounds, linear_mem_bytes)?;
if let Some(ref sbom_dest) = sbom_path {
let wasm = sbom_wasm_bytes.as_deref().unwrap_or(&[]);
emit_sbom(
sbom_dest,
&path,
wasm,
&output,
&elf_data,
target_spec,
backend.name(),
&all_imports,
)?;
}
if sign_output {
sign::sign_elf(&output)?;
}
let total_code: usize = compiled_funcs.iter().map(|f| f.code.len()).sum();
let total_relocs: usize = compiled_funcs.iter().map(|f| f.relocations.len()).sum();
println!(
"Compiled {} functions to {}",
compiled_funcs.len(),
output.display()
);
println!(" Total code size: {} bytes", total_code);
println!(" ELF size: {} bytes", elf_data.len());
if produced_relocatable {
println!(
" Relocations: {} (requires linking with Kiln bridge)",
total_relocs
);
println!(" ELF type: relocatable object (ET_REL)");
println!(
"\n Link with: arm-none-eabi-ld -o firmware.elf {} kiln_bridge.o",
output.display()
);
} else if has_relocations {
println!(
" Internal calls: {} resolved in place (standalone executable)",
total_relocs
);
}
println!("\nFunction addresses:");
println!(
" Use 'synth disasm {}' or 'objdump -t {}' to see symbols",
output.display(),
output.display()
);
if let Some(ing) = &proven_safe_ingest {
if ing.accepted && proven_safe_elisions.is_empty() {
let why = if ing.offered.is_empty() {
"the document proves zero access sites".to_string()
} else if !proven_safe_backend_supported {
format!(
"the `{}` backend does not consume proven-safe marks — only the ARM \
direct selector strips the inline guard today. Every guard is \
retained (sound), and this attestation records ZERO elisions rather \
than claiming an elision that never happened",
backend.name()
)
} else if safety_bounds != SafetyBounds::Software {
format!(
"--safety-bounds is `{}`, not `software` — there is no inline guard to elide (the verdicts are mode-independent, the strip is not)",
safety_bounds.as_str()
)
} else {
format!(
"none of the {} offered site(s) reached the selector — see the \
REFUSED/DROP lines above. If they were DROPPED on key validation \
and the producer emitted wasm BYTE OFFSETS instead of 0-based \
OPERATOR indices, that is the cause",
ing.offered.len()
)
};
let msg = format!(
"--proven-safe was given and the document was ACCEPTED, but NOTHING was elided: {why}"
);
eprintln!("warning: proven-safe: {msg}");
proven_safe_diagnostics.push(msg);
}
let attestation = synth_core::proven_safe::ElisionAttestation {
schema: synth_core::proven_safe::ELISION_ATTESTATION_SCHEMA.to_string(),
synth_version: env!("CARGO_PKG_VERSION").to_string(),
scry_version: ing.scry_version.clone(),
module_sha256: ing.actual_module_sha256.clone(),
declared_module_sha256: ing.declared_module_sha256.clone(),
memory_min_bytes: proven_safe_module_min_bytes.unwrap_or(0),
declared_memory_min_bytes: ing.declared_memory_min_bytes,
safety_bounds: safety_bounds.as_str().to_string(),
accepted: ing.accepted,
refusal: ing.refusal.clone(),
sites_offered: ing.offered.len(),
sites_elided: proven_safe_elisions.len(),
sites_not_elided: ing.offered.len().saturating_sub(proven_safe_elisions.len()),
elisions: proven_safe_elisions.clone(),
diagnostics: proven_safe_diagnostics.clone(),
};
let sidecar = synth_core::proven_safe::ElisionAttestation::sidecar_path(&output);
std::fs::write(&sidecar, attestation.to_json()).with_context(|| {
format!(
"Failed to write proven-safe attestation: {}",
sidecar.display()
)
})?;
println!(
" Proven-safe: wrote {} ({}, {} of {} site(s) elided) — {}",
sidecar.display(),
if attestation.accepted {
"accepted"
} else {
"REFUSED — no guard elided"
},
attestation.sites_elided,
attestation.sites_offered,
synth_core::proven_safe::ELISION_ATTESTATION_SCHEMA,
);
}
if let Some(pm) = provenance_map {
let sidecar = provenance_sidecar_path(&output);
std::fs::write(&sidecar, pm.to_json())
.with_context(|| format!("Failed to write provenance map: {}", sidecar.display()))?;
println!(
" Provenance: wrote {} ({} functions) — synth-provenance-v1",
sidecar.display(),
pm.functions.len()
);
}
if let Some(wr) = wcet_report {
if let Some(hints) = &config.wcet_hints {
let compiled: std::collections::BTreeSet<&str> = wr
.functions
.iter()
.map(|f| match f {
synth_core::wcet::WcetFunction::Bounded { name, .. }
| synth_core::wcet::WcetFunction::Declined { name, .. } => name.as_str(),
})
.collect();
for unknown in hints
.functions
.keys()
.filter(|k| !compiled.contains(k.as_str()))
{
eprintln!(
"warning: --wcet-hints names function '{unknown}' which is not in this \
module — the hint was not consumed"
);
}
}
let sidecar = synth_core::wcet::WcetReport::sidecar_path(&output);
let json = wr
.to_json()
.with_context(|| "Failed to serialize WCET report".to_string())?;
std::fs::write(&sidecar, json)
.with_context(|| format!("Failed to write WCET report: {}", sidecar.display()))?;
let bounded = wr
.functions
.iter()
.filter(|f| matches!(f, synth_core::wcet::WcetFunction::Bounded { .. }))
.count();
let declined = wr.functions.len() - bounded;
println!(
" WCET: wrote {} ({} bounded, {} declined, core {}) — synth-wcet-v1",
sidecar.display(),
bounded,
declined,
wr.core_class
);
}
Ok(())
}
fn provenance_sidecar_path(output: &std::path::Path) -> PathBuf {
let mut s = output.as_os_str().to_os_string();
s.push(".provenance.json");
PathBuf::from(s)
}
fn arm_build_attributes(target: &TargetSpec) -> Section {
use synth_backend::{aeabi, arm_attributes_section};
use synth_core::target::IsaVariant;
match target.isa {
IsaVariant::Arm32 => {
arm_attributes_section(aeabi::CPU_ARCH_V7, aeabi::PROFILE_R, 1, 0, 0, 0)
}
IsaVariant::Thumb => {
arm_attributes_section(aeabi::CPU_ARCH_V6M, aeabi::PROFILE_M, 0, 1, 0, 0)
}
_ => {
let cpu_arch = if target.triple.starts_with("thumbv8.1m") {
aeabi::CPU_ARCH_V8_1M_MAIN
} else if target.triple.starts_with("thumbv7em") {
aeabi::CPU_ARCH_V7EM
} else {
aeabi::CPU_ARCH_V7
};
let (fp_arch, vfp_args) = if target.has_fpu() {
(aeabi::FP_ARCH_VFPV4_D16, aeabi::VFP_ARGS_VFP_REGS)
} else {
(0, 0)
};
arm_attributes_section(cpu_arch, aeabi::PROFILE_M, 0, 2, fp_arch, vfp_args)
}
}
}
fn build_multi_func_simple_elf(funcs: &[ElfFunction]) -> Result<Vec<u8>> {
let base_addr: u32 = 0x8000;
let mut elf_builder = ElfBuilder::new_arm32().with_entry(base_addr);
let mut all_code = Vec::new();
let mut func_offsets = Vec::new();
for func in funcs {
while all_code.len() % 4 != 0 {
all_code.push(0);
}
func_offsets.push(all_code.len() as u32);
all_code.extend_from_slice(&func.code);
}
let text_section = Section::new(".text", ElfSectionType::ProgBits)
.with_flags(SectionFlags::ALLOC | SectionFlags::EXEC)
.with_addr(base_addr)
.with_align(4)
.with_data(all_code);
elf_builder.add_section(text_section);
for (i, func) in funcs.iter().enumerate() {
let func_sym = Symbol::new(&func.name)
.with_value(base_addr + func_offsets[i])
.with_size(func.code.len() as u32)
.with_binding(SymbolBinding::Global)
.with_type(SymbolType::Func)
.with_section(4);
elf_builder.add_symbol(func_sym);
}
elf_builder.build().context("ELF generation failed")
}
struct NativeGlobalsLayout {
globals: Vec<(u32, i32)>,
sp_init: i32,
sp_alias_indices: Vec<u32>,
shadow_stack_size: Option<u32>,
}
fn find_baked_static_movw_movt(
code: &[u8],
reloc_offsets: &[u32],
lo: u32,
hi: u32,
) -> Option<(usize, u32)> {
fn imm16(code: &[u8], off: usize) -> u32 {
let hw1 = u16::from_le_bytes([code[off], code[off + 1]]) as u32;
let hw2 = u16::from_le_bytes([code[off + 2], code[off + 3]]) as u32;
((hw1 & 0xF) << 12) | (((hw1 >> 10) & 1) << 11) | (((hw2 >> 12) & 0x7) << 8) | (hw2 & 0xFF)
}
let mut off = 0usize;
while off + 8 <= code.len() {
let hw1_w = u16::from_le_bytes([code[off], code[off + 1]]);
let hw1_t = u16::from_le_bytes([code[off + 4], code[off + 5]]);
let rd_w = (u16::from_le_bytes([code[off + 2], code[off + 3]]) >> 8) & 0xF;
let rd_t = (u16::from_le_bytes([code[off + 6], code[off + 7]]) >> 8) & 0xF;
if hw1_w & 0xFBF0 == 0xF240 && hw1_t & 0xFBF0 == 0xF2C0 && rd_w == rd_t {
let k = (imm16(code, off + 4) << 16) | imm16(code, off);
let has_reloc = reloc_offsets
.iter()
.any(|&r| (r as usize) < off + 8 && off < (r + 4) as usize);
if !has_reloc && k >= lo && k < hi {
return Some((off, k));
}
}
off += 2;
}
None
}
fn build_relocatable_elf(
funcs: &[ElfFunction],
imports: &[ImportEntry],
data_segments: &[(u32, Vec<u8>)],
linear_memory_bytes: u32,
native_globals: Option<NativeGlobalsLayout>,
dwarf_line: Option<&synth_core::dwarf_line::InputDwarfLine>,
target_spec: &TargetSpec,
table_type_ids: &[u32],
extra_memories: &[(u32, u32)],
extra_memory_data_segments: &[(u32, u32, Vec<u8>)],
) -> Result<Vec<u8>> {
use std::collections::HashMap;
let thumb_funcs = !matches!(target_spec.isa, synth_core::target::IsaVariant::Arm32);
let mut elf_builder = ElfBuilder::new_arm32()
.with_thumb_funcs(thumb_funcs) .with_entry(0)
.with_type(ElfType::Rel);
let mut all_code = Vec::new();
let mut func_offsets = Vec::new();
for func in funcs {
while all_code.len() % 4 != 0 {
all_code.push(0);
}
func_offsets.push(all_code.len() as u32);
all_code.extend_from_slice(&func.code);
}
let needs_wasm_data = funcs
.iter()
.flat_map(|f| &f.relocations)
.any(|r| r.symbol == "__synth_wasm_data" || r.symbol == "__synth_globals");
let emit_wasm_data = needs_wasm_data && linear_memory_bytes > 0;
if let Some(ng) = &native_globals
&& ng.shadow_stack_size.is_some()
&& !emit_wasm_data
{
anyhow::bail!(
"--shadow-stack-size: no __synth_wasm_data/__synth_globals relocation \
reaches the native-pointer region (linear memory {linear_memory_bytes} B), \
so there is no reservation to shrink — refusing rather than silently \
ignoring the flag. VCR-MEM-001/#739."
);
}
let native_layout = native_globals.filter(|_| emit_wasm_data);
fn thm_imm16(code: &[u8], off: usize) -> u32 {
let hw1 = u16::from_le_bytes([code[off], code[off + 1]]) as u32;
let hw2 = u16::from_le_bytes([code[off + 2], code[off + 3]]) as u32;
((hw1 & 0xF) << 12) | (((hw1 >> 10) & 1) << 11) | (((hw2 >> 12) & 0x7) << 8) | (hw2 & 0xFF)
}
let used_extent: u32 = native_layout
.as_ref()
.map(|ng| {
let data_end = data_segments
.iter()
.map(|(off, d)| off + d.len() as u32)
.max()
.unwrap_or(0);
let sp_top = ng.sp_init.max(0) as u32;
let global_top = ng
.globals
.iter()
.map(|&(_, v)| v.max(0) as u32)
.filter(|&v| v <= linear_memory_bytes)
.max()
.unwrap_or(0);
let static_top = funcs
.iter()
.flat_map(|f| {
f.relocations
.iter()
.filter(|&r| {
r.symbol == "__synth_wasm_data"
&& matches!(r.kind, synth_core::RelocKind::MovwAbs)
})
.map(|r| {
let lo = thm_imm16(&f.code, r.offset as usize);
let hi = f
.relocations
.iter()
.find(|m| {
m.offset == r.offset + 4
&& matches!(m.kind, synth_core::RelocKind::MovtAbs)
})
.map(|m| thm_imm16(&f.code, m.offset as usize))
.unwrap_or(0);
(hi << 16) | lo
})
})
.map(|a: u32| a.saturating_add(8))
.max()
.unwrap_or(0);
let static_top_abs32 = funcs
.iter()
.flat_map(|f| {
f.relocations.iter().filter_map(move |r| {
if r.symbol != "__synth_wasm_data"
|| !matches!(r.kind, synth_core::RelocKind::Abs32)
{
return None;
}
let pos = r.offset as usize;
if pos + 4 > f.code.len() {
return None;
}
Some(u32::from_le_bytes([
f.code[pos],
f.code[pos + 1],
f.code[pos + 2],
f.code[pos + 3],
]))
})
})
.map(|a: u32| a.saturating_add(8))
.max()
.unwrap_or(0);
data_end
.max(sp_top)
.max(global_top)
.max(static_top)
.max(static_top_abs32)
.max(4)
.min(linear_memory_bytes)
.next_multiple_of(4)
})
.unwrap_or(linear_memory_bytes);
let globals_bytes: u32 = native_layout
.as_ref()
.and_then(|ng| ng.globals.iter().map(|(i, _)| (i + 1) * 4).max())
.unwrap_or(0);
let split_linmem_bss = native_layout.is_some() && data_segments.is_empty();
let wasm_data_base: u32 = native_layout
.as_ref()
.map(|ng| ng.sp_init.max(0) as u32)
.unwrap_or(0);
let all_static_data_abs32 = funcs.iter().flat_map(|f| &f.relocations).all(|r| {
r.symbol != "__synth_wasm_data" || matches!(r.kind, synth_core::backend::RelocKind::Abs32)
});
let mixed_separable = native_layout.is_some()
&& !data_segments.is_empty()
&& all_static_data_abs32
&& data_segments.iter().all(|(off, _)| *off >= wasm_data_base);
let mixed_layout: Option<(Vec<u32>, u32, u32)> = if mixed_separable {
let mut packed = Vec::with_capacity(data_segments.len());
let mut cur = 0u32;
for (_off, d) in data_segments {
cur = cur.next_multiple_of(4);
packed.push(cur);
cur += d.len() as u32;
}
let globals_off = cur.next_multiple_of(4);
Some((packed, globals_off, globals_off + globals_bytes))
} else {
None
};
let do_mixed_split = mixed_layout.is_some();
if native_layout.is_some() && !data_segments.is_empty() && !do_mixed_split {
info!(
"Native-pointer linmem: init (data) segment not separable (below base \
{wasm_data_base} or non-Abs32 static reloc); keeping one PROGBITS \
.data (correct, not per-region split) — #354 fallback"
);
}
let mut retarget: HashMap<(usize, u32), (String, i32)> = HashMap::new();
let mut addr_resolutions: Vec<synth_core::static_data_addr::RelocResolution> = Vec::new();
let mut mixed_init_blob: Option<Vec<u8>> = None;
if do_mixed_split {
for (i, func) in funcs.iter().enumerate() {
for reloc in &func.relocations {
if reloc.symbol != "__synth_wasm_data"
|| !matches!(reloc.kind, synth_core::backend::RelocKind::Abs32)
{
continue;
}
let pos = (func_offsets[i] + reloc.offset) as usize;
if pos + 4 > all_code.len() {
continue;
}
let c = u32::from_le_bytes([
all_code[pos],
all_code[pos + 1],
all_code[pos + 2],
all_code[pos + 3],
]);
if let Some(k) = data_segments
.iter()
.rposition(|(off, d)| c >= *off && c < *off + d.len() as u32)
{
let new_addend = (c - data_segments[k].0) as i32;
retarget.insert(
(i, reloc.offset),
(format!("__synth_wasm_seg_{k}"), new_addend),
);
all_code[pos..pos + 4].copy_from_slice(&new_addend.to_le_bytes());
addr_resolutions.push(synth_core::static_data_addr::RelocResolution {
seg_index: k,
addend: new_addend as u32,
label: format!("func {i} reloc @ 0x{:x} (linmem 0x{c:x})", reloc.offset),
});
}
}
}
let val_segments: Vec<synth_core::static_data_addr::DataSegment> = data_segments
.iter()
.map(|(off, d)| synth_core::static_data_addr::DataSegment {
linmem_off: *off,
bytes: d.clone(),
})
.collect();
let (packed, globals_off, _data_size) = mixed_layout.as_ref().unwrap();
let mut init_blob = vec![0u8; *globals_off as usize];
for ((_off, d), &poff) in data_segments.iter().zip(packed.iter()) {
init_blob[poff as usize..poff as usize + d.len()].copy_from_slice(d);
}
if let synth_core::static_data_addr::Verdict::Mismatch(mismatches) =
synth_core::static_data_addr::validate_reloc_resolutions_spanned(
&val_segments,
&addr_resolutions,
&synth_core::static_data_addr::PackedInit {
seg_packed_off: packed,
bytes: &init_blob,
},
)
{
let detail = mismatches
.iter()
.map(|m| format!(" {}", m.describe()))
.collect::<Vec<_>>()
.join("\n");
anyhow::bail!(
"VCR-VER-003: static-data addressing validation FAILED — {} \
relocation byte(s) disagree with the runtime linear-memory \
image (this is the #757 silent-miscompile class; segments must \
apply in declaration order, later-wins — a span mismatch means \
an access starting in one packed segment would read stale or \
non-adjacent bytes the runtime image does not hold):\n{detail}",
mismatches.len()
);
}
mixed_init_blob = Some(init_blob);
}
let (reserved_extent, rebased_globals): (u32, Option<Vec<(u32, i32)>>) = match native_layout
.as_ref()
.and_then(|ng| ng.shadow_stack_size.map(|b| (ng, b)))
{
None => (used_extent, None),
Some((ng, budget)) => {
let sp = ng.sp_init.max(0) as u32;
if !(split_linmem_bss || do_mixed_split) {
anyhow::bail!(
"--shadow-stack-size: this module's native-pointer layout keeps static \
data inline in the reservation (one-PROGBITS fallback); the shadow-stack \
shrink only supports the per-region-split geometry. Tracked VCR-MEM-001/#383."
);
}
if budget > sp {
anyhow::bail!(
"--shadow-stack-size {budget} exceeds the declared shadow-stack top \
sp_init={sp}; refusing (would enlarge, not shrink)."
);
}
let down = sp.saturating_sub(budget);
for (i, func) in funcs.iter().enumerate() {
for reloc in &func.relocations {
if reloc.symbol != "__synth_wasm_data"
|| retarget.contains_key(&(i, reloc.offset))
{
continue;
}
let pos = (func_offsets[i] + reloc.offset) as usize;
let c = match reloc.kind {
synth_core::backend::RelocKind::Abs32 if pos + 4 <= all_code.len() => {
u32::from_le_bytes([
all_code[pos],
all_code[pos + 1],
all_code[pos + 2],
all_code[pos + 3],
])
}
other => anyhow::bail!(
"--shadow-stack-size: unhandled native-pointer static reloc {other:?} \
into the reservation; refusing. VCR-MEM-001/#383."
),
};
if c == 0 {
continue;
}
if c < sp {
anyhow::bail!(
"--shadow-stack-size: a native-pointer static access addends {c}, below \
sp_init={sp}; down-shifting it would collide with the live stack \
reservation. Refusing rather than mis-addressing. VCR-MEM-001/#678."
);
}
const STRADDLE_W: u32 = 8;
if data_segments
.iter()
.any(|(off, d)| c < off + d.len() as u32 && *off < c + STRADDLE_W)
{
anyhow::bail!(
"--shadow-stack-size: a native-pointer static at {c} straddles an \
initialized (data) segment boundary; part of its access window lives \
in .data and part in the .bss tail, so it cannot be down-shifted \
soundly. Refusing rather than mis-addressing. VCR-MEM-001/#678."
);
}
let new_c = c - down;
all_code[pos..pos + 4].copy_from_slice(&new_c.to_le_bytes());
}
}
for func in funcs.iter() {
let reloc_offsets: Vec<u32> = func.relocations.iter().map(|r| r.offset).collect();
if let Some((off, k)) =
find_baked_static_movw_movt(&func.code, &reloc_offsets, sp, linear_memory_bytes)
{
anyhow::bail!(
"--shadow-stack-size: the encoded text bakes the static-region \
address {k:#x} (>= sp_init {sp:#x}, < linear memory \
{linear_memory_bytes:#x}) as an un-relocated MOVW/MOVT \
immediate at text offset {off:#x} — the rebase walks \
relocations and CANNOT re-base it, so the shrunk layout \
would access out of range (silent miscompile, #739). \
Refusing. VCR-MEM-001/#739."
);
}
}
if ng.sp_alias_indices.is_empty() {
anyhow::bail!(
"--shadow-stack-size: no mutable global carries init == sp_init {}; cannot \
identify the shadow-stack global to re-base; refusing. VCR-MEM-001/#383.",
ng.sp_init
);
}
if ng.sp_alias_indices.len() > 1 {
info!(
"Native-pointer shadow-stack shrink (#707): re-basing {} aliased \
__stack_pointer globals (all init == sp_init {}) to budget {budget} — a \
multi-provider shared-memory fused node shares one reservation.",
ng.sp_alias_indices.len(),
ng.sp_init
);
}
let mut rebased = ng.globals.clone();
for slot in rebased.iter_mut() {
if ng.sp_alias_indices.contains(&slot.0) {
slot.1 = budget as i32;
}
}
let new_extent = budget
.saturating_add(used_extent.saturating_sub(sp))
.next_multiple_of(4);
info!(
"Native-pointer shadow-stack shrink (#383): sp_init {sp} -> {budget}, \
reservation {used_extent} -> {new_extent} B (post-link oracle: stack/static \
disjoint, all reservation accesses in-range)"
);
(new_extent, Some(rebased))
}
};
let text_section = Section::new(".text", ElfSectionType::ProgBits)
.with_flags(SectionFlags::ALLOC | SectionFlags::EXEC)
.with_addr(0)
.with_align(4)
.with_data(all_code);
elf_builder.add_section(text_section);
if emit_wasm_data {
if do_mixed_split {
let (_packed, globals_off, data_size) = mixed_layout.as_ref().unwrap();
let bss = Section::new(".bss", ElfSectionType::NoBits)
.with_flags(SectionFlags::ALLOC | SectionFlags::WRITE)
.with_addr(0)
.with_align(4)
.with_size(reserved_extent);
elf_builder.add_section(bss);
let mut blob = mixed_init_blob
.take()
.expect("mixed_init_blob is built whenever do_mixed_split");
blob.resize(*data_size as usize, 0);
if let Some(ng) = &native_layout {
let globals = rebased_globals.as_ref().unwrap_or(&ng.globals);
for (idx, init) in globals {
let at = (*globals_off + idx * 4) as usize;
blob[at..at + 4].copy_from_slice(&init.to_le_bytes());
}
}
let data = Section::new(".data", ElfSectionType::ProgBits)
.with_flags(SectionFlags::ALLOC | SectionFlags::WRITE)
.with_addr(0)
.with_align(4)
.with_data(blob);
elf_builder.add_section(data);
} else if split_linmem_bss {
let bss = Section::new(".bss", ElfSectionType::NoBits)
.with_flags(SectionFlags::ALLOC | SectionFlags::WRITE)
.with_addr(0)
.with_align(4)
.with_size(reserved_extent);
elf_builder.add_section(bss);
if let Some(ng) = &native_layout {
let mut blob = vec![0u8; globals_bytes as usize];
let globals = rebased_globals.as_ref().unwrap_or(&ng.globals);
for (idx, init) in globals {
let at = (idx * 4) as usize;
blob[at..at + 4].copy_from_slice(&init.to_le_bytes());
}
let globals = Section::new(".data", ElfSectionType::ProgBits)
.with_flags(SectionFlags::ALLOC | SectionFlags::WRITE)
.with_addr(0)
.with_align(4)
.with_data(blob);
elf_builder.add_section(globals);
}
} else {
let size = (used_extent + globals_bytes) as usize;
let section = if let Some(ng) = &native_layout {
let mut blob = vec![0u8; size];
for (off, d) in data_segments {
blob[*off as usize..*off as usize + d.len()].copy_from_slice(d);
}
for (idx, init) in &ng.globals {
let at = (used_extent + idx * 4) as usize;
blob[at..at + 4].copy_from_slice(&init.to_le_bytes());
}
Section::new(".data", ElfSectionType::ProgBits)
.with_flags(SectionFlags::ALLOC | SectionFlags::WRITE)
.with_addr(0)
.with_align(4)
.with_data(blob)
} else if data_segments.is_empty() {
Section::new(".bss", ElfSectionType::NoBits)
.with_flags(SectionFlags::ALLOC | SectionFlags::WRITE)
.with_addr(0)
.with_align(4)
.with_size(linear_memory_bytes)
} else {
let mut blob = vec![0u8; linear_memory_bytes as usize];
for (off, d) in data_segments {
blob[*off as usize..*off as usize + d.len()].copy_from_slice(d);
}
Section::new(".data", ElfSectionType::ProgBits)
.with_flags(SectionFlags::ALLOC | SectionFlags::WRITE)
.with_addr(0)
.with_align(4)
.with_data(blob)
};
elf_builder.add_section(section);
}
}
let mut next_section_index: u16 = 5;
if emit_wasm_data {
next_section_index += if do_mixed_split || split_linmem_bss {
2
} else {
1
};
}
let mut extra_memory_syms: Vec<(String, u16)> = Vec::new();
for &(mem_idx, mem_bytes) in extra_memories {
assert!(mem_idx > 0, "#406: memory 0 is the legacy wasm-data region");
let segs: Vec<&(u32, u32, Vec<u8>)> = extra_memory_data_segments
.iter()
.filter(|(k, _, _)| *k == mem_idx)
.collect();
for (_, off, d) in &segs {
if (*off as u64) + (d.len() as u64) > mem_bytes as u64 {
anyhow::bail!(
"multi-memory (#406): active data segment [{off:#x}, \
{:#x}) overflows memory {mem_idx}'s declared initial size \
({mem_bytes} B) — instantiation would trap; refusing to \
truncate",
(*off as u64) + (d.len() as u64)
);
}
}
let name = format!(".synth.wasm_mem_{mem_idx}");
let section = if segs.is_empty() {
Section::new(&name, ElfSectionType::NoBits)
.with_flags(SectionFlags::ALLOC | SectionFlags::WRITE)
.with_addr(0)
.with_align(4)
.with_size(mem_bytes)
} else {
let mut blob = vec![0u8; mem_bytes as usize];
for (_, off, d) in &segs {
blob[*off as usize..*off as usize + d.len()].copy_from_slice(d);
}
Section::new(&name, ElfSectionType::ProgBits)
.with_flags(SectionFlags::ALLOC | SectionFlags::WRITE)
.with_addr(0)
.with_align(4)
.with_data(blob)
};
elf_builder.add_section(section);
extra_memory_syms.push((format!("__synth_wasm_data_{mem_idx}"), next_section_index));
next_section_index += 1;
}
for (k, _, _) in extra_memory_data_segments {
if !extra_memories.iter().any(|(i, _)| i == k) {
anyhow::bail!(
"multi-memory (#406): active data segment targets memory {k}, \
which has no declared region in this object — refusing to \
drop its init bytes"
);
}
}
let mut sym_indices: HashMap<String, u32> = HashMap::new();
let mut sym_count: u32 = 0;
for (i, func) in funcs.iter().enumerate() {
let internal_label = format!("func_{}", func.wasm_index);
let is_exported = func.name != internal_label;
let export_sym = Symbol::new(&func.name)
.with_value(func_offsets[i])
.with_size(func.code.len() as u32)
.with_binding(if is_exported {
SymbolBinding::Global
} else {
SymbolBinding::Local })
.with_type(SymbolType::Func)
.with_section(4); elf_builder.add_symbol(export_sym);
sym_count += 1;
sym_indices.insert(func.name.clone(), sym_count);
if is_exported {
let internal_sym = Symbol::new(&internal_label)
.with_value(func_offsets[i])
.with_size(func.code.len() as u32)
.with_binding(SymbolBinding::Local) .with_type(SymbolType::Func)
.with_section(4);
elf_builder.add_symbol(internal_sym);
sym_count += 1;
sym_indices.insert(internal_label, sym_count);
}
}
if emit_wasm_data {
let data_sym = Symbol::new("__synth_wasm_data")
.with_value(0)
.with_binding(SymbolBinding::Global)
.with_type(SymbolType::Object)
.with_section(5);
elf_builder.add_symbol(data_sym);
sym_count += 1;
sym_indices.insert("__synth_wasm_data".to_string(), sym_count);
if let Some((packed, _goff, _sz)) = &mixed_layout {
for (k, &poff) in packed.iter().enumerate() {
let seg_sym = Symbol::new(&format!("__synth_wasm_seg_{k}"))
.with_value(poff)
.with_binding(SymbolBinding::Global)
.with_type(SymbolType::Object)
.with_section(6);
elf_builder.add_symbol(seg_sym);
sym_count += 1;
sym_indices.insert(format!("__synth_wasm_seg_{k}"), sym_count);
}
}
if native_layout.is_some() {
let (gv, gsec) = if let Some((_packed, goff, _sz)) = &mixed_layout {
(*goff, 6)
} else if split_linmem_bss {
(0, 6)
} else {
(used_extent, 5)
};
let globals_sym = Symbol::new("__synth_globals")
.with_value(gv)
.with_binding(SymbolBinding::Global)
.with_type(SymbolType::Object)
.with_section(gsec);
elf_builder.add_symbol(globals_sym);
sym_count += 1;
sym_indices.insert("__synth_globals".to_string(), sym_count);
}
}
for (name, shndx) in &extra_memory_syms {
let mem_sym = Symbol::new(name)
.with_value(0)
.with_binding(SymbolBinding::Global)
.with_type(SymbolType::Object)
.with_section(*shndx);
elf_builder.add_symbol(mem_sym);
sym_count += 1;
sym_indices.insert(name.clone(), sym_count);
}
let mut import_label_to_field: HashMap<String, String> = HashMap::new();
for imp in imports {
if matches!(imp.kind, synth_core::ImportKind::Function(_)) {
import_label_to_field.insert(format!("func_{}", imp.index), imp.name.clone());
}
}
let mut external_count = 0usize;
for func in funcs {
for reloc in &func.relocations {
if sym_indices.contains_key(&reloc.symbol) {
continue; }
let effective = import_label_to_field
.get(&reloc.symbol)
.cloned()
.unwrap_or_else(|| reloc.symbol.clone());
let idx = match sym_indices.get(&effective) {
Some(&i) => i,
None => {
let i = elf_builder.add_undefined_symbol(&effective);
sym_indices.insert(effective.clone(), i);
external_count += 1;
i
}
};
sym_indices.insert(reloc.symbol.clone(), idx);
}
}
let mut reloc_count = 0usize;
for (i, func) in funcs.iter().enumerate() {
let func_base = func_offsets[i];
for reloc in &func.relocations {
let sym_name = retarget
.get(&(i, reloc.offset))
.map(|(s, _)| s.as_str())
.unwrap_or(reloc.symbol.as_str());
let sym_idx = sym_indices[sym_name];
let reloc_type = match reloc.kind {
synth_core::backend::RelocKind::ThmCall => ArmRelocationType::ThmCall,
synth_core::backend::RelocKind::MovwAbs => ArmRelocationType::MovwAbsNc,
synth_core::backend::RelocKind::MovtAbs => ArmRelocationType::MovtAbs,
synth_core::backend::RelocKind::Abs32 => {
assert_eq!(
func_base % 4,
0,
"#345: function carrying an R_ARM_ABS32 literal-pool reloc \
must start 4-byte-aligned (func_base={func_base}); the \
PC-relative LDR imm12 assumes it"
);
ArmRelocationType::Abs32
}
synth_core::backend::RelocKind::AArch64Call26
| synth_core::backend::RelocKind::AArch64Jump26
| synth_core::backend::RelocKind::AArch64AdrPrelPgHi21
| synth_core::backend::RelocKind::AArch64AddAbsLo12Nc => {
anyhow::bail!(
"internal error: an AArch64 relocation ({:?}) reached the ARM \
ELF emitter — the aarch64 backend emits its own .rela.text (#851)",
reloc.kind
)
}
synth_core::backend::RelocKind::RiscvCallPlt => {
anyhow::bail!(
"internal error: RISC-V CALL_PLT relocation reached the ARM \
ELF emitter — the riscv backend emits its own .rela.text (#871)"
)
}
};
elf_builder.add_relocation(Relocation {
offset: func_base + reloc.offset,
symbol_index: sym_idx,
reloc_type,
});
reloc_count += 1;
}
}
let extern_sym_indices = (external_count, reloc_count);
if !imports.is_empty() {
let mut import_table_data = Vec::new();
let mut import_func_count = 0u32;
for imp in imports {
if matches!(imp.kind, synth_core::ImportKind::Function(_)) {
import_table_data.extend_from_slice(&imp.index.to_le_bytes());
let mod_bytes = imp.module.as_bytes();
import_table_data.extend_from_slice(&(mod_bytes.len() as u16).to_le_bytes());
import_table_data.extend_from_slice(mod_bytes);
let name_bytes = imp.name.as_bytes();
import_table_data.extend_from_slice(&(name_bytes.len() as u16).to_le_bytes());
import_table_data.extend_from_slice(name_bytes);
import_func_count += 1;
}
}
if !import_table_data.is_empty() {
let mut header = import_func_count.to_le_bytes().to_vec();
header.extend_from_slice(&import_table_data);
let import_section = Section::new(".meld_import_table", ElfSectionType::ProgBits)
.with_flags(0) .with_align(4)
.with_data(header);
elf_builder.add_section(import_section);
}
}
if !table_type_ids.is_empty() {
let mut sidecar = Vec::with_capacity(table_type_ids.len() * 4);
for id in table_type_ids {
sidecar.extend_from_slice(&id.to_le_bytes());
}
let sidecar_section = Section::new(".synth.table_type_ids", ElfSectionType::ProgBits)
.with_flags(0) .with_align(4)
.with_data(sidecar);
elf_builder.add_section(sidecar_section);
}
if let Some(input_dwarf) = dwarf_line
&& !input_dwarf.rows.is_empty()
{
use synth_core::dwarf_line::{SourceLoc, op_offsets_to_source};
let mut table: Vec<(u64, u32, u32)> = Vec::new();
for (i, func) in funcs.iter().enumerate() {
if func.line_map.is_empty() || func.op_offsets.is_empty() {
continue; }
let locs =
op_offsets_to_source(&func.op_offsets, input_dwarf.code_base, &input_dwarf.rows);
for &(machine_off, op_idx) in &func.line_map {
let Some(op_idx) = op_idx else { continue };
if let Some(Some(SourceLoc { line, file })) = locs.get(op_idx)
&& *line != 0
{
let arm_addr = (func_offsets[i] + machine_off) as u64;
table.push((arm_addr, *line, *file));
}
}
}
table.sort_by_key(|&(a, _, _)| a);
table.dedup_by_key(|&mut (a, _, _)| a);
let text_base_sym = Symbol::new("__synth_text_base")
.with_value(0)
.with_binding(SymbolBinding::Global)
.with_type(SymbolType::NoType)
.with_section(4); let text_sym_idx = elf_builder.add_symbol_indexed(text_base_sym);
let subprograms: Vec<synth_core::dwarf_line::SubprogramInfo> = funcs
.iter()
.enumerate()
.map(|(i, func)| synth_core::dwarf_line::SubprogramInfo {
name: func.debug_name.clone().unwrap_or_else(|| func.name.clone()),
low_pc: func_offsets[i] as u64,
high_pc: (func_offsets[i] + func.code.len() as u32) as u64,
})
.collect();
let dwarf_sections = synth_core::dwarf_line::emit_debug_sections(
&table,
text_sym_idx as usize,
&input_dwarf.files,
&subprograms,
);
if !dwarf_sections.is_empty() {
let names: Vec<&str> = dwarf_sections.iter().map(|s| s.name).collect();
let mut total_relocs = 0usize;
for sec in &dwarf_sections {
let dbg_section = Section::new(sec.name, ElfSectionType::ProgBits)
.with_align(1)
.with_data(sec.bytes.clone());
elf_builder.add_section(dbg_section);
if !sec.text_relocs.is_empty() {
let relocs: Vec<Relocation> = sec
.text_relocs
.iter()
.map(|r| Relocation {
offset: r.offset,
symbol_index: text_sym_idx,
reloc_type: ArmRelocationType::Abs32,
})
.collect();
total_relocs += relocs.len();
elf_builder.add_section_relocations(sec.name, relocs);
}
}
info!(
"DWARF: emitted {} sections {:?} ({} address rows, {} .text relocations, --debug-line)",
dwarf_sections.len(),
names,
table.len(),
total_relocs
);
}
}
elf_builder.add_section(arm_build_attributes(target_spec));
let (external_count, reloc_count) = extern_sym_indices;
info!(
"Relocatable ELF: {} functions, {} external symbols, {} relocations",
funcs.len(),
external_count,
reloc_count
);
elf_builder
.build()
.context("Relocatable ELF generation failed")
}
fn encode_thumb_bl(bl_addr: u32, target_addr: u32) -> [u8; 4] {
let offset = (target_addr as i64) - (bl_addr as i64 + 4); let off = (offset >> 1) as i32; let s_bit = ((off >> 24) & 1) as u32;
let i1 = ((off >> 23) & 1) as u32;
let i2 = ((off >> 22) & 1) as u32;
let imm10 = ((off >> 11) & 0x3FF) as u32;
let imm11 = (off & 0x7FF) as u32;
let j1 = (!(i1 ^ s_bit)) & 1;
let j2 = (!(i2 ^ s_bit)) & 1;
let hw1: u16 = (0xF000 | (s_bit << 10) | imm10) as u16;
let hw2: u16 = (0xD000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
let mut bytes = [0u8; 4];
bytes[0..2].copy_from_slice(&hw1.to_le_bytes());
bytes[2..4].copy_from_slice(&hw2.to_le_bytes());
bytes
}
fn build_multi_func_cortex_m_elf(
funcs: &[ElfFunction],
memories: &[WasmMemory],
target: &TargetSpec,
globals_words: &[u32],
stack_layout: StackLayout,
data_segments: &[(u32, Vec<u8>)],
funcref_slots: &[Option<u32>],
type_ids_image: &[u32],
type_ids_byte_offset: Option<u32>,
) -> Result<Vec<u8>> {
let flash_base: u32 = 0x0000_0000;
let ram_base: u32 = 0x2000_0000;
let linear_memory_pages = memories.first().map(|m| m.initial_pages).unwrap_or(1);
let linear_memory_size = linear_memory_pages * 64 * 1024;
let globals_table_bytes = (globals_words.len() as u32) * 4;
if globals_table_bytes > 4096 {
anyhow::bail!(
"globals table ({} bytes) exceeds the startup materializer's \
STR.W #imm12 range (4096 bytes) — refusing to emit a partial \
table (#649)",
globals_table_bytes
);
}
let data_extent_u64: u64 = data_segments
.iter()
.map(|(off, d)| *off as u64 + d.len() as u64)
.max()
.unwrap_or(0);
if data_extent_u64 > linear_memory_size as u64 {
anyhow::bail!(
"active data segment extends to {} bytes but linear memory is only \
{} bytes ({} pages) — instantiation would trap; refusing to \
truncate the initializer (#758)",
data_extent_u64,
linear_memory_size,
linear_memory_pages
);
}
let data_extent: u32 = data_extent_u64 as u32;
let rom_segments: Vec<synth_core::static_data_addr::DataSegment> = data_segments
.iter()
.map(|(off, d)| synth_core::static_data_addr::DataSegment {
linmem_off: *off,
bytes: d.clone(),
})
.collect();
let data_rom_image: Vec<u8> =
synth_core::static_data_addr::pack_rom_image(&rom_segments, true);
debug_assert_eq!(data_rom_image.len() as u64, data_extent_u64);
if let synth_core::static_data_addr::ImageVerdict::Mismatch(mismatches) =
synth_core::static_data_addr::validate_served_image(&rom_segments, &data_rom_image)
{
let detail = mismatches
.iter()
.take(8)
.map(|m| format!(" {}", m.describe()))
.collect::<Vec<_>>()
.join("\n");
anyhow::bail!(
"VCR-VER-003: self-contained ROM data image validation FAILED — {} \
byte(s) of the #758 flash init image disagree with the runtime \
linear-memory image (segments applied in declaration order, \
later-wins). This is a compiler bug in the ROM packing:\n{detail}",
mismatches.len()
);
}
let stack_reserve = stack_layout.stack_reserve();
let linmem_base = stack_layout.startup_linmem_base(ram_base);
let func_visible_gap = stack_layout
.optimized_linmem_base()
.wrapping_sub(linmem_base);
let needed = match stack_layout {
StackLayout::High => {
let min_stack_headroom: u32 = 8 * 1024;
func_visible_gap + linear_memory_size + globals_table_bytes + min_stack_headroom
}
StackLayout::Low { stack_size } => {
stack_size + func_visible_gap + linear_memory_size + globals_table_bytes
}
};
let ram_size: u32 = std::cmp::max(128 * 1024, (needed + 0xFFFF) & !0xFFFF);
let sp_init = match stack_layout {
StackLayout::High => ram_base + ram_size,
StackLayout::Low { .. } => ram_base + stack_reserve,
};
info!(
"RAM layout ({:?}): linear memory {}KB at 0x{:08x}, initial SP 0x{:08x}",
stack_layout,
linear_memory_size / 1024,
linmem_base,
sp_init
);
let vector_table_addr = flash_base;
let vector_table_size: u32 = 128;
let startup_addr = flash_base + vector_table_size;
let func_visible_linmem_base = stack_layout.optimized_linmem_base();
let (startup_code, data_src_patch_off, r9_movw_off) = generate_minimal_startup(
linear_memory_size,
globals_words,
linmem_base,
target.has_fpu(),
data_extent,
func_visible_linmem_base,
);
let startup_size = startup_code.len() as u32;
if let Some(off) = r9_movw_off {
let emitted_globals_base = read_back_r9_base(&startup_code, off).ok_or_else(|| {
anyhow::anyhow!(
"VCR-VER-003: could not decode the emitted MOVW/MOVT R9 globals \
base from the startup blob (#761) — malformed startup emission"
)
})?;
if let synth_core::static_data_addr::LayoutVerdict::Overlap {
globals_base,
overlap_bytes,
..
} = synth_core::static_data_addr::validate_linmem_globals_disjoint(
func_visible_linmem_base,
linear_memory_size,
emitted_globals_base,
globals_table_bytes,
) {
anyhow::bail!(
"VCR-VER-003: self-contained Cortex-M layout OVERLAP — the emitted \
R9 globals table at 0x{globals_base:08x} falls inside the \
function-visible linear-memory page [0x{func_visible_linmem_base:08x}, \
0x{:08x}) by {overlap_bytes} bytes; a store to the top of the page \
would alias a global slot (a silent global<->linmem miscompile). \
This is a compiler layout bug (#761).",
func_visible_linmem_base.wrapping_add(linear_memory_size)
);
}
}
let default_handler_addr = startup_addr + startup_size;
let default_handler = generate_default_handler();
let default_handler_size = default_handler.len() as u32;
let trap_handler_addr = default_handler_addr + default_handler_size;
let trap_handler = generate_trap_handler();
let trap_handler_size = trap_handler.len() as u32;
let funcs_base = (trap_handler_addr + trap_handler_size + 3) & !3;
let mut all_func_code = Vec::new();
let mut func_offsets = Vec::new();
for func in funcs {
while all_func_code.len() % 4 != 0 {
all_func_code.push(0);
}
func_offsets.push(all_func_code.len() as u32);
all_func_code.extend_from_slice(&func.code);
}
let needs_func_table = funcs
.iter()
.flat_map(|f| &f.relocations)
.any(|r| r.symbol == synth_core::backend::FUNC_TABLE_SYMBOL);
let func_table_addr = (funcs_base + all_func_code.len() as u32 + 3) & !3;
let mut func_table_blob: Vec<u8> = Vec::new();
{
use std::collections::HashMap;
let mut label_to_addr: HashMap<&str, u32> = HashMap::new();
for (i, func) in funcs.iter().enumerate() {
let addr = funcs_base + func_offsets[i];
label_to_addr.insert(func.name.as_str(), addr);
}
let index_labels: Vec<(String, u32)> = funcs
.iter()
.enumerate()
.map(|(i, func)| {
(
format!("func_{}", func.wasm_index),
funcs_base + func_offsets[i],
)
})
.collect();
for (label, addr) in &index_labels {
label_to_addr.insert(label.as_str(), *addr);
}
if needs_func_table {
if funcref_slots.is_empty() {
anyhow::bail!(
"call_indirect compiled but the module has no statically \
verifiable funcref-table image — refusing to emit an \
unpopulated table (#275)"
);
}
for (slot, entry) in funcref_slots.iter().enumerate() {
let word: u32 = match entry {
None => 0, Some(fidx) => {
let label = format!("func_{fidx}");
let Some(&addr) = label_to_addr.get(label.as_str()) else {
anyhow::bail!(
"funcref table slot {slot} references function \
{fidx}, which is not in the compiled output \
(loud-skipped or imported) — refusing to link \
a broken dispatch table (#275)"
);
};
addr | 1 }
};
func_table_blob.extend_from_slice(&word.to_le_bytes());
}
match type_ids_byte_offset {
Some(off) if off as usize != func_table_blob.len() => {
anyhow::bail!(
"type-id sidecar offset {off} != pointer-region size {} \
— funcref-region layout drift (#275/#676)",
func_table_blob.len()
);
}
None if !type_ids_image.is_empty() => {
anyhow::bail!(
"type-id sidecar image present without a declared \
offset — funcref-region layout drift (#275/#676)"
);
}
_ => {}
}
for id in type_ids_image {
func_table_blob.extend_from_slice(&id.to_le_bytes());
}
info!(
" #275 funcref table: {} slot(s) + {} sidecar word(s) at 0x{:08x}",
funcref_slots.len(),
type_ids_image.len(),
func_table_addr
);
}
for (i, func) in funcs.iter().enumerate() {
for reloc in &func.relocations {
if reloc.symbol == synth_core::backend::FUNC_TABLE_SYMBOL {
if !matches!(reloc.kind, synth_core::backend::RelocKind::Abs32) {
anyhow::bail!(
"funcref-table relocation at offset {} in '{}' has \
kind {:?}, expected Abs32 — unknown dispatch shape \
(#275)",
reloc.offset,
func.name,
reloc.kind
);
}
let pos = (func_offsets[i] + reloc.offset) as usize;
let addend =
u32::from_le_bytes(all_func_code[pos..pos + 4].try_into().unwrap());
let value = func_table_addr.wrapping_add(addend);
all_func_code[pos..pos + 4].copy_from_slice(&value.to_le_bytes());
info!(
" patched funcref-table literal at 0x{:08x} -> 0x{:08x}",
funcs_base + func_offsets[i] + reloc.offset,
value
);
continue;
}
if !matches!(reloc.kind, synth_core::backend::RelocKind::ThmCall) {
anyhow::bail!(
"relocation against '{}' at offset {} in '{}' has kind \
{:?}, which the standalone Cortex-M builder cannot \
patch (only ThmCall BLs and funcref-table Abs32 \
literals) — refusing a silent mis-patch",
reloc.symbol,
reloc.offset,
func.name,
reloc.kind
);
}
let Some(&callee_addr) = label_to_addr.get(reloc.symbol.as_str()) else {
anyhow::bail!(
"internal call to unknown symbol '{}' in standalone Cortex-M ELF (#170)",
reloc.symbol
);
};
let bl_addr = funcs_base + func_offsets[i] + reloc.offset;
let bytes = encode_thumb_bl(bl_addr, callee_addr);
let pos = (func_offsets[i] + reloc.offset) as usize;
all_func_code[pos..pos + 4].copy_from_slice(&bytes);
info!(
" patched internal BL at 0x{:08x} -> '{}' 0x{:08x}",
bl_addr, reloc.symbol, callee_addr
);
}
}
}
info!("Cortex-M multi-function layout:");
info!(" Vector table: 0x{:08x}", vector_table_addr);
info!(" Startup code: 0x{:08x}", startup_addr);
info!(" Default handler: 0x{:08x}", default_handler_addr);
info!(" Trap handler: 0x{:08x}", trap_handler_addr);
info!(" Functions base: 0x{:08x}", funcs_base);
for (i, func) in funcs.iter().enumerate() {
let addr = funcs_base + func_offsets[i];
info!(
" {}: 0x{:08x} ({} bytes)",
func.name,
addr,
func.code.len()
);
}
info!(" Initial SP: 0x{:08x}", sp_init);
let mut vt = VectorTable::new_cortex_m(sp_init);
vt.reset_handler = startup_addr;
for handler in &mut vt.handlers {
if handler.address == 0 {
if handler.name == "UsageFault_Handler" || handler.name == "HardFault_Handler" {
handler.address = trap_handler_addr;
} else {
handler.address = default_handler_addr;
}
}
}
let vector_table_data = vt
.generate_binary()
.context("Vector table generation failed")?;
let mut flash_image = Vec::new();
flash_image.extend_from_slice(&vector_table_data);
while flash_image.len() < (startup_addr - flash_base) as usize {
flash_image.push(0);
}
let mut patched_startup = startup_code.clone();
let first_func_addr = funcs_base | 1; let lit = patched_startup.len() - 4;
patched_startup[lit..].copy_from_slice(&first_func_addr.to_le_bytes());
flash_image.extend_from_slice(&patched_startup);
flash_image.extend_from_slice(&default_handler);
flash_image.extend_from_slice(&trap_handler);
while flash_image.len() < (funcs_base - flash_base) as usize {
flash_image.push(0);
}
flash_image.extend_from_slice(&all_func_code);
if !func_table_blob.is_empty() {
while !flash_image.len().is_multiple_of(4) {
flash_image.push(0);
}
let laid_out = flash_base + flash_image.len() as u32;
if laid_out != func_table_addr {
anyhow::bail!(
"funcref table laid out at 0x{laid_out:08x} but literals were \
patched with 0x{func_table_addr:08x} — layout drift (#275)"
);
}
flash_image.extend_from_slice(&func_table_blob);
}
if !data_rom_image.is_empty() {
while !flash_image.len().is_multiple_of(4) {
flash_image.push(0);
}
let data_rom_addr = flash_base + flash_image.len() as u32;
flash_image.extend_from_slice(&data_rom_image);
let patch_at = (startup_addr - flash_base) as usize
+ data_src_patch_off.expect(
"#758: data ROM image present but startup emitted no src-patch \
offset — generate_minimal_startup invariant violated",
);
let movw = encode_thumb2_movw(0, (data_rom_addr & 0xFFFF) as u16);
let movt = encode_thumb2_movt(0, (data_rom_addr >> 16) as u16);
flash_image[patch_at..patch_at + 4].copy_from_slice(&movw);
flash_image[patch_at + 4..patch_at + 8].copy_from_slice(&movt);
info!(
" #758 data ROM image: {} bytes at 0x{:08x}, copied to linmem 0x{:08x} at reset",
data_rom_image.len(),
data_rom_addr,
linmem_base
);
}
let flash_size = flash_image.len() as u32;
let mut elf_builder = ElfBuilder::new_arm32().with_entry(startup_addr | 1);
if target.has_fpu() {
elf_builder
.set_flags(synth_backend::EF_ARM_EABI_VER5 | synth_backend::EF_ARM_ABI_FLOAT_HARD);
}
let shstrtab_size = 1 + ".shstrtab\0.strtab\0.symtab\0.text\0".len();
let mut strtab_size = 1 + "Reset_Handler\0Default_Handler\0Trap_Handler\0".len();
for func in funcs {
strtab_size += func.name.len() + 1;
}
let text_file_offset = 52 + 32 + shstrtab_size + strtab_size;
let text_phdr = ProgramHeader::load(
flash_base,
text_file_offset as u32,
flash_size,
ProgramFlags::READ | ProgramFlags::EXEC,
);
elf_builder.add_program_header(text_phdr);
let text_section = Section::new(".text", ElfSectionType::ProgBits)
.with_flags(SectionFlags::ALLOC | SectionFlags::EXEC)
.with_addr(flash_base)
.with_align(4)
.with_data(flash_image);
elf_builder.add_section(text_section);
if linear_memory_size > 0 {
let ram_region_size = linear_memory_size + globals_table_bytes;
let ram_phdr = ProgramHeader::load_nobits(
ram_base,
ram_region_size,
ProgramFlags::READ | ProgramFlags::WRITE,
);
elf_builder.add_program_header(ram_phdr);
let linear_memory_section = Section::new(".linear_memory", ElfSectionType::NoBits)
.with_flags(SectionFlags::ALLOC | SectionFlags::WRITE)
.with_addr(ram_base)
.with_align(4)
.with_size(ram_region_size);
elf_builder.add_section(linear_memory_section);
let mem_sym = Symbol::new("__linear_memory_base")
.with_value(ram_base)
.with_size(linear_memory_size)
.with_binding(SymbolBinding::Global)
.with_type(SymbolType::Object)
.with_section(5); elf_builder.add_symbol(mem_sym);
info!(
"Added .linear_memory section: 0x{:08x} ({} bytes, {} pages)",
ram_base, linear_memory_size, linear_memory_pages
);
}
let reset_sym = Symbol::new("Reset_Handler")
.with_value(startup_addr | 1)
.with_size(startup_size)
.with_binding(SymbolBinding::Global)
.with_type(SymbolType::Func)
.with_section(4);
elf_builder.add_symbol(reset_sym);
let default_sym = Symbol::new("Default_Handler")
.with_value(default_handler_addr | 1)
.with_size(default_handler_size)
.with_binding(SymbolBinding::Global)
.with_type(SymbolType::Func)
.with_section(4);
elf_builder.add_symbol(default_sym);
let trap_sym = Symbol::new("Trap_Handler")
.with_value(trap_handler_addr | 1)
.with_size(trap_handler_size)
.with_binding(SymbolBinding::Global)
.with_type(SymbolType::Func)
.with_section(4);
elf_builder.add_symbol(trap_sym);
for (i, func) in funcs.iter().enumerate() {
let func_addr = funcs_base + func_offsets[i];
let func_sym = Symbol::new(&func.name)
.with_value(func_addr | 1) .with_size(func.code.len() as u32)
.with_binding(SymbolBinding::Global)
.with_type(SymbolType::Func)
.with_section(4);
elf_builder.add_symbol(func_sym);
}
elf_builder.add_section(arm_build_attributes(target));
elf_builder.build().context("ELF generation failed")
}
fn detect_arm_thumb(elf: &[u8]) -> Option<bool> {
if elf.len() < 52 || elf[0..4] != [0x7f, b'E', b'L', b'F'] || elf[4] != 1 || elf[5] != 1 {
return None;
}
let u16le = |off: usize| u16::from_le_bytes(elf[off..off + 2].try_into().unwrap());
let u32le = |off: usize| u32::from_le_bytes(elf[off..off + 4].try_into().unwrap());
if u16le(18) != 40 {
return None; }
let e_shoff = u32le(32) as usize;
let e_shentsize = u16le(46) as usize;
let e_shnum = u16le(48) as usize;
let section = |i: usize| -> Option<(u32, usize, usize)> {
let base = e_shoff.checked_add(i.checked_mul(e_shentsize)?)?;
if base + 40 > elf.len() {
return None;
}
Some((
u32le(base + 4),
u32le(base + 16) as usize,
u32le(base + 20) as usize,
))
};
for i in 0..e_shnum {
let Some((sh_type, off, size)) = section(i) else {
continue;
};
if sh_type == 0x7000_0003
&& let Some(data) = elf.get(off..off + size)
&& let Some(thumb_isa) = parse_aeabi_thumb_isa_use(data)
{
return Some(thumb_isa > 0);
}
}
for i in 0..e_shnum {
let Some((sh_type, off, size)) = section(i) else {
continue;
};
if sh_type != 2 {
continue; }
let mut saw_func = false;
for s in 0..size / 16 {
let base = off + s * 16;
if base + 16 > elf.len() {
break;
}
let st_value = u32le(base + 4);
let st_info = elf[base + 12];
let st_shndx = u16le(base + 14);
if st_info & 0xf == 2 && st_shndx != 0 {
if st_value & 1 == 1 {
return Some(true);
}
saw_func = true;
}
}
if saw_func {
return Some(false); }
}
Some(u32le(24) & 1 == 1)
}
fn parse_aeabi_thumb_isa_use(data: &[u8]) -> Option<u32> {
fn uleb(data: &[u8], pos: &mut usize) -> Option<u32> {
let mut v: u32 = 0;
let mut shift = 0u32;
loop {
let byte = *data.get(*pos)?;
*pos += 1;
v |= u32::from(byte & 0x7f) << shift;
if byte & 0x80 == 0 {
return Some(v);
}
shift += 7;
if shift > 28 {
return None;
}
}
}
if *data.first()? != b'A' {
return None;
}
let mut pos = 1usize;
while pos + 4 <= data.len() {
let sub_start = pos;
let sub_len = u32::from_le_bytes(data.get(pos..pos + 4)?.try_into().ok()?) as usize;
let sub_end = sub_start.checked_add(sub_len)?;
if sub_len < 4 || sub_end > data.len() {
return None;
}
pos += 4;
let name_end = data[pos..sub_end].iter().position(|&b| b == 0)? + pos;
let vendor = &data[pos..name_end];
pos = name_end + 1;
if vendor != b"aeabi" {
pos = sub_end;
continue;
}
while pos < sub_end {
let ss_start = pos;
let tag = uleb(data, &mut pos)?;
let ss_len = u32::from_le_bytes(data.get(pos..pos + 4)?.try_into().ok()?) as usize;
pos += 4;
let ss_end = ss_start.checked_add(ss_len)?;
if ss_end > sub_end || ss_end < pos {
return None;
}
if tag != 1 {
pos = ss_end; continue;
}
while pos < ss_end {
let attr_tag = uleb(data, &mut pos)?;
if matches!(attr_tag, 4 | 5 | 32 | 65 | 67) {
let nul = data[pos..ss_end].iter().position(|&b| b == 0)?;
pos += nul + 1;
} else {
let value = uleb(data, &mut pos)?;
if attr_tag == 9 {
return Some(value); }
}
}
pos = ss_end;
}
pos = sub_end;
}
None
}
fn disasm_command(input: PathBuf) -> Result<()> {
use std::process::Command;
if !input.exists() {
anyhow::bail!("File not found: {}", input.display());
}
info!("Disassembling: {}", input.display());
let bytes = std::fs::read(&input).context("Failed to read input file")?;
let triple = match detect_arm_thumb(&bytes) {
Some(true) => "thumbv7m-none-eabi",
Some(false) | None => "arm-none-eabi",
};
info!("Detected triple: {}", triple);
let output = Command::new("objdump")
.args(["-d", &format!("--triple={triple}")])
.arg(&input)
.output()
.context("Failed to run objdump. Is it installed?")?;
if output.status.success() {
print!("{}", String::from_utf8_lossy(&output.stdout));
} else {
let output = Command::new("objdump")
.arg("-d")
.arg(&input)
.output()
.context("Failed to run objdump")?;
if output.status.success() {
print!("{}", String::from_utf8_lossy(&output.stdout));
} else {
eprintln!("{}", String::from_utf8_lossy(&output.stderr));
anyhow::bail!("objdump failed");
}
}
Ok(())
}
fn backends_command() -> Result<()> {
let registry = build_backend_registry();
let backends = registry.list();
println!("Available backends:\n");
println!(
" {:<12} {:<12} {:<10} {:<10} {:<10}",
"NAME", "STATUS", "ELF", "RULE-VERIFY", "BIN-VERIFY"
);
println!(" {}", "-".repeat(56));
for backend in &backends {
let status = if backend.is_available() {
"available"
} else {
"not found"
};
let caps = backend.capabilities();
println!(
" {:<12} {:<12} {:<10} {:<10} {:<10}",
backend.name(),
status,
if caps.produces_elf { "yes" } else { "no" },
if caps.supports_rule_verification {
"yes"
} else {
"no"
},
if caps.supports_binary_verification {
"yes"
} else {
"no"
},
);
}
println!("\nVerification tiers:");
println!(" RULE-VERIFY: Per-rule SMT proofs (ASIL D) — only custom ARM backend");
println!(" BIN-VERIFY: Binary-level translation validation (ASIL B) — all backends");
Ok(())
}
fn verify_command(wasm_input: PathBuf, elf_input: PathBuf, backend_name: &str) -> Result<()> {
if !wasm_input.exists() {
anyhow::bail!("WASM file not found: {}", wasm_input.display());
}
if !elf_input.exists() {
anyhow::bail!("ELF file not found: {}", elf_input.display());
}
let registry = build_backend_registry();
let backend = registry
.get(backend_name)
.ok_or_else(|| anyhow::anyhow!("Unknown backend '{}'", backend_name))?;
let caps = backend.capabilities();
println!("Translation validation:");
println!(" Source: {}", wasm_input.display());
println!(" Binary: {}", elf_input.display());
println!(" Backend: {}", backend_name);
if caps.supports_rule_verification {
println!(" Strategy: Per-rule SMT verification (ASIL D path)");
#[cfg(feature = "verify")]
{
let file_bytes = std::fs::read(&wasm_input)
.context(format!("Failed to read: {}", wasm_input.display()))?;
let wasm_bytes = if wasm_input.extension().is_some_and(|ext| ext == "wat") {
wat::parse_bytes(&file_bytes)
.context("Failed to parse WAT file")?
.into_owned()
} else if wasm_input.extension().is_some_and(|ext| ext == "wast") {
let contents =
String::from_utf8(file_bytes).context("WAST file is not valid UTF-8")?;
extract_module_from_wast(&contents)?
} else {
file_bytes
};
let functions =
decode_wasm_functions(&wasm_bytes).context("Failed to decode WASM functions")?;
let exports: Vec<_> = functions
.iter()
.filter(|f| f.export_name.is_some())
.collect();
if exports.is_empty() {
println!("\n No exported functions found in WASM module.");
return Ok(());
}
println!("\n Verifying {} exported functions...", exports.len());
for func in &exports {
let name = func.export_name.as_deref().ok_or_else(|| {
anyhow::anyhow!("function at index {} has no export name", func.index)
})?;
run_verification(&func.ops, name)?;
}
println!("\nAll functions verified successfully.");
if backend_name == "arm" {
println!("\n Certifying i64 pseudo-op expansions (shipped Thumb-2 encoder):");
let encoder = synth_backend::ArmEncoder::new_thumb2();
let mut failures = 0u32;
for (wasm, pseudo) in synth_verify::covered_i64_pseudo_selections() {
let code = encoder
.encode(&pseudo)
.context("shipped encoder failed to expand an i64 pseudo-op")?;
match synth_verify::validate_expansion(&wasm, &pseudo, &code) {
Ok(w) => println!(
" ✓ {} expansion certified: {} instrs, {} bytes [unsat, LRAT-checked]",
w.wasm_op_label, w.instr_count, w.byte_len
),
Err(e) => {
println!(" ✗ {wasm:?} expansion FAILED: {e}");
failures += 1;
}
}
}
if failures > 0 {
anyhow::bail!(
"i64 pseudo-op expansion certification failed for {failures} op(s)"
);
}
}
}
#[cfg(not(feature = "verify"))]
{
anyhow::bail!(
"this `synth` binary was built without the `verify` feature — \
SMT translation validation is unavailable.\n \
Rebuild with verification support:\n \
cargo build --features verify\n \
(or `cargo install --path crates/synth-cli --features verify`)"
);
}
} else if caps.supports_binary_verification {
println!(" Strategy: Binary-level translation validation (ASIL B path)");
println!("\n Binary verification not yet implemented.");
println!(" Requires: ARM disassembler + SMT equivalence checking on disassembled output.");
} else {
println!(
" No verification available for backend '{}'.",
backend_name
);
}
Ok(())
}
#[cfg(feature = "riscv")]
fn build_riscv_elf(code: &[u8], func_name: &str) -> Result<Vec<u8>> {
use synth_backend_riscv::{Reg, RiscVElfBuilder, RiscVElfFunction, RiscVOp};
let n_instrs = code.len().div_ceil(4);
let placeholder_ops: Vec<RiscVOp> = (0..n_instrs)
.map(|_| RiscVOp::Addi {
rd: Reg::ZERO,
rs1: Reg::ZERO,
imm: 0,
})
.collect();
let f = RiscVElfFunction {
name: func_name.to_string(),
ops: placeholder_ops,
};
let builder = RiscVElfBuilder::new_relocatable();
let mut elf = builder
.build(&[f])
.context("RISC-V ELF generation failed")?;
let text_offset = 52;
if elf.len() < text_offset + code.len() {
anyhow::bail!("RISC-V ELF is shorter than embedded code");
}
elf[text_offset..text_offset + code.len()].copy_from_slice(code);
Ok(elf)
}
#[cfg(not(feature = "riscv"))]
fn build_riscv_elf(_code: &[u8], _func_name: &str) -> Result<Vec<u8>> {
anyhow::bail!("RISC-V backend was not compiled in (rebuild with --features riscv)")
}
#[cfg(feature = "riscv")]
fn build_multi_func_riscv_elf(
funcs: &[ElfFunction],
imports: &[ImportEntry],
wasm_data: &[u8],
) -> Result<Vec<u8>> {
use synth_backend_riscv::{Reg, RiscVCallReloc, RiscVElfBuilder, RiscVElfFunction, RiscVOp};
let mut label_to_symbol: std::collections::HashMap<String, String> =
std::collections::HashMap::new();
for imp in imports {
if matches!(imp.kind, synth_core::ImportKind::Function(_)) {
label_to_symbol.insert(format!("synth_func_{}", imp.index), imp.name.clone());
}
}
for func in funcs {
label_to_symbol.insert(format!("synth_func_{}", func.wasm_index), func.name.clone());
}
let mut all_code: Vec<u8> = Vec::new();
let mut func_byte_ranges: Vec<(usize, usize)> = Vec::new();
let mut placeholder_funcs: Vec<RiscVElfFunction> = Vec::new();
let mut call_relocs: Vec<RiscVCallReloc> = Vec::new();
for func in funcs {
while !all_code.len().is_multiple_of(4) {
all_code.push(0);
}
let start = all_code.len();
all_code.extend_from_slice(&func.code);
let end = all_code.len();
func_byte_ranges.push((start, end));
for reloc in &func.relocations {
if !matches!(reloc.kind, synth_core::backend::RelocKind::RiscvCallPlt) {
anyhow::bail!(
"internal error: non-CALL_PLT relocation {:?} reached the RISC-V \
ELF emitter (function '{}', offset {}) — the riscv backend only \
produces R_RISCV_CALL_PLT (#871)",
reloc.kind,
func.name,
reloc.offset
);
}
let symbol = label_to_symbol
.get(&reloc.symbol)
.cloned()
.unwrap_or_else(|| reloc.symbol.clone());
call_relocs.push(RiscVCallReloc {
offset: (start as u32) + reloc.offset,
symbol,
});
}
let n_instrs = (end - start).div_ceil(4);
let placeholder_ops: Vec<RiscVOp> = (0..n_instrs)
.map(|_| RiscVOp::Addi {
rd: Reg::ZERO,
rs1: Reg::ZERO,
imm: 0,
})
.collect();
placeholder_funcs.push(RiscVElfFunction {
name: func.name.clone(),
ops: placeholder_ops,
});
}
let builder = RiscVElfBuilder::new_relocatable();
let mut elf = builder
.build_object(&placeholder_funcs, wasm_data, &call_relocs)
.context("RISC-V multi-function ELF generation failed")?;
let text_offset = 52usize;
if elf.len() < text_offset + all_code.len() {
anyhow::bail!("RISC-V ELF too small to embed code");
}
elf[text_offset..text_offset + all_code.len()].copy_from_slice(&all_code);
Ok(elf)
}
#[cfg(not(feature = "riscv"))]
fn build_multi_func_riscv_elf(
_funcs: &[ElfFunction],
_imports: &[ImportEntry],
_wasm_data: &[u8],
) -> Result<Vec<u8>> {
anyhow::bail!("RISC-V backend was not compiled in (rebuild with --features riscv)")
}
fn build_aarch64_elf(code: &[u8], func_name: &str) -> Result<Vec<u8>> {
use synth_backend_aarch64::elf::{ElfFunction as A64ElfFunction, build_relocatable_object};
Ok(build_relocatable_object(&[A64ElfFunction::code(
vec![func_name.to_string()],
code.to_vec(),
Vec::new(),
)]))
}
fn build_multi_func_aarch64_elf(
funcs: &[ElfFunction],
substrate: &synth_backend_aarch64::substrate::Substrate,
) -> Result<Vec<u8>> {
use synth_backend_aarch64::elf::{
ElfFunction as A64ElfFunction, build_relocatable_object_with_data,
};
let a64_funcs: Vec<A64ElfFunction> = funcs
.iter()
.map(|f| {
let func_sym = format!("func_{}", f.wasm_index);
let mut symbols = vec![func_sym.clone()];
if f.name != func_sym {
symbols.push(f.name.clone());
}
A64ElfFunction::code(symbols, f.code.clone(), f.relocations.clone())
})
.collect();
let mut a64_funcs = a64_funcs;
if let Some(table) = substrate.table.clone() {
a64_funcs.push(table);
}
Ok(build_relocatable_object_with_data(
&a64_funcs,
&substrate.globals,
))
}
fn build_simple_elf(code: &[u8], func_name: &str) -> Result<Vec<u8>> {
let mut elf_builder = ElfBuilder::new_arm32().with_entry(0x8000);
let text_section = Section::new(".text", ElfSectionType::ProgBits)
.with_flags(SectionFlags::ALLOC | SectionFlags::EXEC)
.with_addr(0x8000)
.with_align(4)
.with_data(code.to_vec());
elf_builder.add_section(text_section);
let func_sym = Symbol::new(func_name)
.with_value(0x8000)
.with_size(code.len() as u32)
.with_binding(SymbolBinding::Global)
.with_type(SymbolType::Func)
.with_section(4);
elf_builder.add_symbol(func_sym);
elf_builder.build().context("ELF generation failed")
}
fn build_cortex_m_elf(
code: &[u8],
func_name: &str,
target: &TargetSpec,
globals_words: &[u32],
stack_layout: StackLayout,
) -> Result<Vec<u8>> {
let flash_base: u32 = 0x0000_0000;
let ram_base: u32 = 0x2000_0000;
let linear_memory_size: u32 = 64 * 1024;
let linmem_base = stack_layout.startup_linmem_base(ram_base);
let ram_size: u32 = std::cmp::max(
128 * 1024,
(stack_layout.stack_reserve()
+ linear_memory_size
+ (globals_words.len() as u32) * 4
+ 0xFFFF)
& !0xFFFF,
);
let sp_init = match stack_layout {
StackLayout::High => ram_base + ram_size,
StackLayout::Low { stack_size } => ram_base + stack_size,
};
if globals_words.len() * 4 > 4096 {
anyhow::bail!(
"globals table ({} bytes) exceeds the startup materializer's \
STR.W #imm12 range (4096 bytes) — refusing to emit a partial \
table (#649)",
globals_words.len() * 4
);
}
let vector_table_addr = flash_base;
let vector_table_size: u32 = 128;
let startup_addr = flash_base + vector_table_size;
let func_visible_linmem_base = stack_layout.optimized_linmem_base();
let (startup_code, _data_src_patch_off, r9_movw_off) = generate_minimal_startup(
linear_memory_size,
globals_words,
linmem_base,
target.has_fpu(),
0,
func_visible_linmem_base,
);
let startup_size = startup_code.len() as u32;
if let Some(off) = r9_movw_off {
let emitted_globals_base = read_back_r9_base(&startup_code, off).ok_or_else(|| {
anyhow::anyhow!(
"VCR-VER-003: could not decode the emitted MOVW/MOVT R9 globals \
base from the startup blob (#761) — malformed startup emission"
)
})?;
if let synth_core::static_data_addr::LayoutVerdict::Overlap {
globals_base,
overlap_bytes,
..
} = synth_core::static_data_addr::validate_linmem_globals_disjoint(
func_visible_linmem_base,
linear_memory_size,
emitted_globals_base,
(globals_words.len() as u32) * 4,
) {
anyhow::bail!(
"VCR-VER-003: self-contained Cortex-M layout OVERLAP — the emitted \
R9 globals table at 0x{globals_base:08x} falls inside the \
function-visible linear-memory page [0x{func_visible_linmem_base:08x}, \
0x{:08x}) by {overlap_bytes} bytes (#761).",
func_visible_linmem_base.wrapping_add(linear_memory_size)
);
}
}
let default_handler_addr = startup_addr + startup_size;
let default_handler = generate_default_handler();
let default_handler_size = default_handler.len() as u32;
let trap_handler_addr = default_handler_addr + default_handler_size;
let trap_handler = generate_trap_handler();
let trap_handler_size = trap_handler.len() as u32;
let code_addr = (trap_handler_addr + trap_handler_size + 3) & !3;
info!("Cortex-M layout:");
info!(" Vector table: 0x{:08x}", vector_table_addr);
info!(" Startup code: 0x{:08x}", startup_addr);
info!(" Default handler: 0x{:08x}", default_handler_addr);
info!(" Trap handler: 0x{:08x}", trap_handler_addr);
info!(" User code: 0x{:08x}", code_addr);
info!(" Initial SP: 0x{:08x}", sp_init);
let mut vt = VectorTable::new_cortex_m(sp_init);
vt.reset_handler = startup_addr;
for handler in &mut vt.handlers {
if handler.address == 0 {
if handler.name == "UsageFault_Handler" || handler.name == "HardFault_Handler" {
handler.address = trap_handler_addr;
} else {
handler.address = default_handler_addr;
}
}
}
let vector_table_data = vt
.generate_binary()
.context("Vector table generation failed")?;
let mut flash_image = Vec::new();
flash_image.extend_from_slice(&vector_table_data);
while flash_image.len() < (startup_addr - flash_base) as usize {
flash_image.push(0);
}
let mut patched_startup = startup_code.clone();
let func_addr_thumb = code_addr | 1; let lit = patched_startup.len() - 4;
patched_startup[lit..].copy_from_slice(&func_addr_thumb.to_le_bytes());
flash_image.extend_from_slice(&patched_startup);
flash_image.extend_from_slice(&default_handler);
flash_image.extend_from_slice(&trap_handler);
while flash_image.len() < (code_addr - flash_base) as usize {
flash_image.push(0);
}
flash_image.extend_from_slice(code);
let flash_size = flash_image.len() as u32;
let mut elf_builder = ElfBuilder::new_arm32().with_entry(startup_addr | 1);
if target.has_fpu() {
elf_builder
.set_flags(synth_backend::EF_ARM_EABI_VER5 | synth_backend::EF_ARM_ABI_FLOAT_HARD);
}
let shstrtab_size = 1 + ".shstrtab\0.strtab\0.symtab\0.text\0".len(); let strtab_size =
1 + "Reset_Handler\0Default_Handler\0Trap_Handler\0".len() + func_name.len() + 1;
let text_file_offset = 52 + 32 + shstrtab_size + strtab_size;
let text_phdr = ProgramHeader::load(
flash_base, text_file_offset as u32, flash_size, ProgramFlags::READ | ProgramFlags::EXEC, );
elf_builder.add_program_header(text_phdr);
let text_section = Section::new(".text", ElfSectionType::ProgBits)
.with_flags(SectionFlags::ALLOC | SectionFlags::EXEC)
.with_addr(flash_base)
.with_align(4)
.with_data(flash_image);
elf_builder.add_section(text_section);
let reset_sym = Symbol::new("Reset_Handler")
.with_value(startup_addr | 1)
.with_size(startup_size)
.with_binding(SymbolBinding::Global)
.with_type(SymbolType::Func)
.with_section(4);
elf_builder.add_symbol(reset_sym);
let default_sym = Symbol::new("Default_Handler")
.with_value(default_handler_addr | 1)
.with_size(default_handler_size)
.with_binding(SymbolBinding::Global)
.with_type(SymbolType::Func)
.with_section(4);
elf_builder.add_symbol(default_sym);
let trap_sym = Symbol::new("Trap_Handler")
.with_value(trap_handler_addr | 1)
.with_size(trap_handler_size)
.with_binding(SymbolBinding::Global)
.with_type(SymbolType::Func)
.with_section(4);
elf_builder.add_symbol(trap_sym);
let func_sym = Symbol::new(func_name)
.with_value(code_addr | 1)
.with_size(code.len() as u32)
.with_binding(SymbolBinding::Global)
.with_type(SymbolType::Func)
.with_section(4);
elf_builder.add_symbol(func_sym);
elf_builder.add_section(arm_build_attributes(target));
elf_builder.build().context("ELF generation failed")
}
fn generate_minimal_startup(
memory_size: u32,
globals_words: &[u32],
linmem_base: u32,
enable_fpu: bool,
data_copy_bytes: u32,
func_visible_linmem_base: u32,
) -> (Vec<u8>, Option<usize>, Option<usize>) {
let mut code: Vec<u8> = Vec::new();
let src_patch_off: Option<usize> = if data_copy_bytes > 0 {
let off = code.len(); code.extend_from_slice(&encode_thumb2_movw(0, 0));
code.extend_from_slice(&encode_thumb2_movt(0, 0));
code.extend_from_slice(&encode_thumb2_movw(
1,
(func_visible_linmem_base & 0xFFFF) as u16,
));
code.extend_from_slice(&encode_thumb2_movt(
1,
(func_visible_linmem_base >> 16) as u16,
));
code.extend_from_slice(&encode_thumb2_movw(2, (data_copy_bytes & 0xFFFF) as u16));
code.extend_from_slice(&encode_thumb2_movt(2, (data_copy_bytes >> 16) as u16));
code.extend_from_slice(&[0x10, 0xF8, 0x01, 0x3B]);
code.extend_from_slice(&[0x01, 0xF8, 0x01, 0x3B]);
code.extend_from_slice(&[0x01, 0x3A]);
code.extend_from_slice(&[0xF9, 0xD1]);
Some(off)
} else {
None
};
let r10_movw = encode_thumb2_movw(10, (memory_size & 0xFFFF) as u16);
let r10_movt = encode_thumb2_movt(10, (memory_size >> 16) as u16);
if enable_fpu {
code.extend_from_slice(&[0x4E, 0xF6, 0x88, 0x50]); code.extend_from_slice(&[0xCE, 0xF2, 0x00, 0x00]); code.extend_from_slice(&[0xD0, 0xF8, 0x00, 0x10]); code.extend_from_slice(&[0x41, 0xF4, 0x70, 0x01]); code.extend_from_slice(&[0xC0, 0xF8, 0x00, 0x10]); code.extend_from_slice(&[0xBF, 0xF3, 0x4F, 0x8F]); code.extend_from_slice(&[0xBF, 0xF3, 0x6F, 0x8F]); }
code.extend_from_slice(&r10_movw);
code.extend_from_slice(&r10_movt);
code.extend_from_slice(&encode_thumb2_movw(11, (linmem_base & 0xFFFF) as u16));
code.extend_from_slice(&encode_thumb2_movt(11, (linmem_base >> 16) as u16));
let mut r9_movw_off: Option<usize> = None;
if !globals_words.is_empty() {
let base = func_visible_linmem_base.wrapping_add(memory_size);
r9_movw_off = Some(code.len());
code.extend_from_slice(&encode_thumb2_movw(9, (base & 0xFFFF) as u16));
code.extend_from_slice(&encode_thumb2_movt(9, (base >> 16) as u16));
for (i, w) in globals_words.iter().enumerate() {
code.extend_from_slice(&encode_thumb2_movw(12, (w & 0xFFFF) as u16));
code.extend_from_slice(&encode_thumb2_movt(12, (w >> 16) as u16));
let off = (i as u16) * 4;
let hw1: u16 = 0xF8C0 | 9; let hw2: u16 = (12 << 12) | off; code.extend_from_slice(&hw1.to_le_bytes());
code.extend_from_slice(&hw2.to_le_bytes());
}
}
code.extend_from_slice(&[0x01, 0x48]);
code.extend_from_slice(&[0x80, 0x47]);
code.extend_from_slice(&[0xfe, 0xe7]);
code.extend_from_slice(&[0x00, 0x00]);
code.extend_from_slice(&[0x91, 0x00, 0x00, 0x00]);
(code, src_patch_off, r9_movw_off)
}
fn encode_thumb2_movw(rd: u8, imm16: u16) -> [u8; 4] {
let imm4 = ((imm16 >> 12) & 0xF) as u8;
let i = ((imm16 >> 11) & 0x1) as u8;
let imm3 = ((imm16 >> 8) & 0x7) as u8;
let imm8 = (imm16 & 0xFF) as u8;
let hw1: u16 = 0xF240 | ((i as u16) << 10) | (imm4 as u16);
let hw2: u16 = ((imm3 as u16) << 12) | ((rd as u16) << 8) | (imm8 as u16);
let hw1_bytes = hw1.to_le_bytes();
let hw2_bytes = hw2.to_le_bytes();
[hw1_bytes[0], hw1_bytes[1], hw2_bytes[0], hw2_bytes[1]]
}
fn encode_thumb2_movt(rd: u8, imm16: u16) -> [u8; 4] {
let imm4 = ((imm16 >> 12) & 0xF) as u8;
let i = ((imm16 >> 11) & 0x1) as u8;
let imm3 = ((imm16 >> 8) & 0x7) as u8;
let imm8 = (imm16 & 0xFF) as u8;
let hw1: u16 = 0xF2C0 | ((i as u16) << 10) | (imm4 as u16);
let hw2: u16 = ((imm3 as u16) << 12) | ((rd as u16) << 8) | (imm8 as u16);
let hw1_bytes = hw1.to_le_bytes();
let hw2_bytes = hw2.to_le_bytes();
[hw1_bytes[0], hw1_bytes[1], hw2_bytes[0], hw2_bytes[1]]
}
fn decode_thumb2_movw_movt_imm16(code: &[u8], off: usize, movt: bool) -> Option<u16> {
let bytes: [u8; 4] = code.get(off..off + 4)?.try_into().ok()?;
let hw1 = u16::from_le_bytes([bytes[0], bytes[1]]);
let hw2 = u16::from_le_bytes([bytes[2], bytes[3]]);
let base = if movt { 0xF2C0 } else { 0xF240 };
if (hw1 & 0xFBF0) != base {
return None;
}
let imm4 = hw1 & 0xF;
let i = (hw1 >> 10) & 0x1;
let imm3 = (hw2 >> 12) & 0x7;
let imm8 = hw2 & 0xFF;
Some((imm4 << 12) | (i << 11) | (imm3 << 8) | imm8)
}
fn read_back_r9_base(code: &[u8], r9_movw_off: usize) -> Option<u32> {
let lo = decode_thumb2_movw_movt_imm16(code, r9_movw_off, false)? as u32;
let hi = decode_thumb2_movw_movt_imm16(code, r9_movw_off + 4, true)? as u32;
Some((hi << 16) | lo)
}
fn generate_default_handler() -> Vec<u8> {
vec![0xfe, 0xe7]
}
fn generate_trap_handler() -> Vec<u8> {
vec![0xfe, 0xe7]
}
fn link_firmware(
object_path: &std::path::Path,
builtins: Option<&std::path::Path>,
_target_spec: &TargetSpec,
) -> Result<()> {
use std::process::Command;
let gcc = ["arm-none-eabi-gcc", "arm-none-eabi-ld"]
.iter()
.find(|cmd| {
Command::new(cmd)
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
})
.copied();
let gcc = match gcc {
Some(g) => g,
None => {
anyhow::bail!(
"arm-none-eabi-gcc not found in PATH. Install the ARM embedded toolchain:\n \
brew install arm-none-eabi-gcc (macOS)\n \
apt install gcc-arm-none-eabi (Linux)"
);
}
};
info!("Using cross-linker: {}", gcc);
let mut ls_gen = synth_backend::LinkerScriptGenerator::new();
ls_gen.add_region(synth_backend::MemoryRegion {
name: "FLASH".to_string(),
origin: 0x0000_0000,
length: 256 * 1024,
attributes: "rx".to_string(),
});
ls_gen.add_region(synth_backend::MemoryRegion {
name: "RAM".to_string(),
origin: 0x2000_0000,
length: 128 * 1024,
attributes: "rwx".to_string(),
});
let ls_gen = ls_gen.with_stack_size(4096).with_meld_integration();
let linker_script = ls_gen
.generate()
.context("Failed to generate linker script")?;
let ld_script_path = object_path.with_extension("ld");
std::fs::write(&ld_script_path, &linker_script).context("Failed to write linker script")?;
info!("Generated linker script: {}", ld_script_path.display());
let firmware_path = object_path.with_extension("firmware.elf");
let mut cmd = Command::new(gcc);
if gcc == "arm-none-eabi-gcc" {
cmd.args(["-nostartfiles", "-nostdlib", "-mcpu=cortex-m4", "-mthumb"]);
}
cmd.arg("-T").arg(&ld_script_path);
cmd.arg(object_path);
if let Some(builtins_path) = builtins {
cmd.arg(builtins_path);
}
cmd.arg("-o").arg(&firmware_path);
info!("Linking: {:?}", cmd);
let output = cmd.output().context("Failed to invoke cross-linker")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("Linker failed:\n{}", stderr);
}
let _ = std::fs::remove_file(&ld_script_path);
println!(
"Linked firmware: {} ({} bytes)",
firmware_path.display(),
std::fs::metadata(&firmware_path)
.map(|m| m.len())
.unwrap_or(0)
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
const BAKED_10000C: [u8; 8] = [0x40, 0xF2, 0x0C, 0x0C, 0xC0, 0xF2, 0x10, 0x0C];
#[test]
fn baked_static_pair_is_detected_739() {
assert_eq!(
find_baked_static_movw_movt(&BAKED_10000C, &[], 0x100000, 0x110000),
Some((0, 0x10000C)),
"the pre-fix baked 0x10000C pair must be flagged"
);
}
#[test]
fn relocated_pair_is_not_flagged_739() {
assert_eq!(
find_baked_static_movw_movt(&BAKED_10000C, &[0], 0x100000, 0x110000),
None
);
assert_eq!(
find_baked_static_movw_movt(&BAKED_10000C, &[4], 0x100000, 0x110000),
None
);
}
#[test]
fn out_of_window_pair_is_not_flagged_739() {
assert_eq!(
find_baked_static_movw_movt(&BAKED_10000C, &[], 0x200000, 0x300000),
None,
"below-window constant must not be flagged"
);
assert_eq!(
find_baked_static_movw_movt(&BAKED_10000C, &[], 0x100000, 0x10000C),
None,
"hi bound is exclusive"
);
}
#[test]
fn mismatched_rd_pair_is_not_flagged_739() {
let mut bytes = BAKED_10000C;
bytes[7] = 0x05; assert_eq!(
find_baked_static_movw_movt(&bytes, &[], 0x100000, 0x110000),
None
);
}
#[test]
fn volatile_segment_parses_hex_base_decimal_len_543() {
let ranges = parse_volatile_segments(&["0x20001000:4096".to_string()]).unwrap();
assert_eq!(ranges.len(), 1);
assert_eq!(ranges[0].base, 0x2000_1000);
assert_eq!(ranges[0].len, 4096);
}
#[test]
fn volatile_segment_accepts_decimal_and_is_repeatable_543() {
let ranges = parse_volatile_segments(&[
"536875008:0x1000".to_string(), "0x20002000:256".to_string(),
])
.unwrap();
assert_eq!(ranges.len(), 2);
assert_eq!(ranges[0].base, 536_875_008);
assert_eq!(ranges[0].len, 0x1000);
assert_eq!(ranges[1].base, 0x2000_2000);
assert_eq!(ranges[1].len, 256);
}
#[test]
fn volatile_segment_rejects_malformed_543() {
assert!(parse_volatile_segments(&["garbage".to_string()]).is_err());
assert!(parse_volatile_segments(&["0x20001000".to_string()]).is_err());
assert!(parse_volatile_segments(&["nothex:4096".to_string()]).is_err());
assert!(parse_volatile_segments(&["0x1000:notlen".to_string()]).is_err());
assert!(parse_volatile_segments(&["0x1000:0".to_string()]).is_err());
assert!(parse_volatile_segments(&["0xFFFFFFFF:0x10".to_string()]).is_err());
assert!(parse_volatile_segments(&["0x1000:0x10:0x20".to_string()]).is_err());
}
#[test]
fn volatile_segment_empty_by_default_543() {
assert!(parse_volatile_segments(&[]).unwrap().is_empty());
assert!(CompileConfig::default().volatile_segments.is_empty());
}
fn fop(index: u32, export: Option<&str>, ops: Vec<WasmOp>) -> FunctionOps {
FunctionOps {
index,
export_name: export.map(String::from),
debug_name: None,
ops,
op_offsets: Vec::new(),
unsupported: None,
block_arity: Vec::new(),
}
}
#[test]
fn layer2_budget_pipeline_msgq_end_to_end_383() {
use scry_analyze_core::{AnalysisConfig, StackBound, analyze};
use shadow_budget::{BudgetDecision, BudgetSource, StackDepthBound, budget_from_bound};
let fixture = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../scripts/repro/msgq_put_359.wasm");
let bytes = std::fs::read(&fixture).expect("the #359/#383 gust-family fixture is in-tree");
let r = analyze(
bytes,
AnalysisConfig {
widening_threshold: None,
emit_diagnostics: false,
taint_policy: None,
},
)
.expect("scry analyzes a valid Core module");
let bound = match r.stack_usage.max_stack_bytes {
StackBound::Bytes(n) => StackDepthBound::Bytes(n),
StackBound::Unbounded => StackDepthBound::Unbounded,
StackBound::Unknown => StackDepthBound::Unknown,
};
let sp_init = 65_536;
let decision = budget_from_bound(bound, sp_init, Some(4096));
assert_eq!(
decision,
BudgetDecision::Use {
bytes: 32,
source: BudgetSource::ProvenStackDepth
},
"scry-proven 32 B depth -> proven 32 B budget, not the asserted 4096 fallback"
);
}
#[test]
fn layer2_gust_kernel_proven_depth_clears_flashed_budget_383() {
use scry_analyze_core::{AnalysisConfig, StackBound, analyze};
use shadow_budget::{BudgetDecision, BudgetSource, StackDepthBound, budget_from_bound};
let fixture = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../scripts/repro/gust_kernel.wasm");
let bytes = std::fs::read(&fixture).expect("the gale #91 gust_kernel fixture is in-tree");
let r = analyze(
bytes,
AnalysisConfig {
widening_threshold: None,
emit_diagnostics: false,
taint_policy: None,
},
)
.expect("scry analyzes the gust_kernel Core module");
assert_eq!(
r.stack_usage.sp_global,
Some(0),
"gust_kernel's stack-pointer global is identified"
);
assert!(
!r.function_summaries.iter().any(|s| s.recursive),
"gust_kernel has no reachable recursion -> the depth is a finite proof"
);
let proven = match r.stack_usage.max_stack_bytes {
StackBound::Bytes(n) => n,
other => panic!("expected a finite proven depth, got {other:?}"),
};
assert_eq!(proven, 16, "scry-proven gust_kernel shadow-stack depth (B)");
const JESS_FLASHED_BUDGET: u64 = 4096;
assert!(
proven <= JESS_FLASHED_BUDGET,
"proven depth {proven} B must not exceed the flashed {JESS_FLASHED_BUDGET} B budget"
);
let decision = budget_from_bound(
StackDepthBound::Bytes(proven),
1_048_576,
Some(JESS_FLASHED_BUDGET as u32),
);
assert_eq!(
decision,
BudgetDecision::Use {
bytes: 16,
source: BudgetSource::ProvenStackDepth
},
"layer-2 derives a proven 16 B budget for gust_kernel (tighter than the asserted 4096)"
);
}
#[test]
fn layer2_unbounded_recursion_refuses_proven_budget_242() {
use scry_analyze_core::{AnalysisConfig, StackBound, analyze};
use shadow_budget::{BudgetDecision, BudgetSource, StackDepthBound, budget_from_bound};
let wat_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../scripts/repro/recursive_shadow_stack.wat");
let wasm = wat::parse_file(&wat_path).expect("the honest-fail fixture .wat parses");
let r = analyze(
wasm,
AnalysisConfig {
widening_threshold: None,
emit_diagnostics: false,
taint_policy: None,
},
)
.expect("scry analyzes the recursive Core module");
assert!(
r.function_summaries.iter().any(|s| s.recursive),
"scry detects the shadow-stack recursion"
);
assert_eq!(
r.stack_usage.max_stack_bytes,
StackBound::Unbounded,
"recursion through the shadow stack has no finite proven bound"
);
let bound = match r.stack_usage.max_stack_bytes {
StackBound::Bytes(n) => StackDepthBound::Bytes(n),
StackBound::Unbounded => StackDepthBound::Unbounded,
StackBound::Unknown => StackDepthBound::Unknown,
};
assert_eq!(
budget_from_bound(bound, 65_536, Some(4096)),
BudgetDecision::Use {
bytes: 4096,
source: BudgetSource::AssertedFallback
},
"unbounded depth -> asserted fallback, never ProvenStackDepth"
);
match budget_from_bound(bound, 65_536, None) {
BudgetDecision::Refuse(msg) => assert!(
msg.contains("unbounded"),
"refusal names the unbounded cause; got: {msg}"
),
other => panic!("unbounded + no fallback must refuse, got {other:?}"),
}
}
#[test]
fn reachable_from_exports_pulls_in_internal_callees_235() {
let funcs = vec![
fop(1, None, vec![WasmOp::I32Const(1)]),
fop(2, Some("caller"), vec![WasmOp::Call(1), WasmOp::Call(0)]),
fop(3, None, vec![WasmOp::I32Const(9)]),
];
let r = reachable_from_exports(&funcs, 1, &[]); assert!(r.contains(&2), "the export itself is reachable");
assert!(
r.contains(&1),
"the non-exported callee is pulled in (#235)"
);
assert!(!r.contains(&0), "imports stay external, never compiled");
assert!(
!r.contains(&3),
"unreferenced internal functions are not emitted"
);
}
#[test]
fn reachable_from_exports_follows_call_indirect_into_table_275() {
let funcs = vec![
fop(
0,
Some("run"),
vec![
WasmOp::I32Const(0),
WasmOp::CallIndirect {
type_index: 0,
table_index: 0,
},
],
),
fop(1, None, vec![WasmOp::Call(2)]),
fop(2, None, vec![WasmOp::I32Const(42)]),
fop(3, None, vec![WasmOp::I32Const(9)]),
];
let r = reachable_from_exports(&funcs, 0, &[1]);
assert!(r.contains(&0), "the export itself");
assert!(
r.contains(&1),
"the call_indirect target (table entry) is pulled in (#275)"
);
assert!(
r.contains(&2),
"and its transitive direct callee follows too"
);
assert!(!r.contains(&3), "a non-table, uncalled internal stays out");
}
#[test]
fn native_pointer_zero_linmem_lands_in_nobits_bss_345() {
use object::Endianness;
use object::read::elf::{FileHeader, SectionHeader};
let code = vec![0u8; 8]; let func = ElfFunction {
name: "decide".to_string(),
debug_name: None,
wasm_index: 0,
code,
relocations: vec![
synth_core::backend::CodeRelocation {
offset: 0,
symbol: "__synth_wasm_data".to_string(),
kind: synth_core::backend::RelocKind::MovwAbs,
},
synth_core::backend::CodeRelocation {
offset: 4,
symbol: "__synth_wasm_data".to_string(),
kind: synth_core::backend::RelocKind::MovtAbs,
},
],
op_offsets: vec![],
line_map: vec![],
};
let linear_memory_bytes: u32 = 131_072; let native = NativeGlobalsLayout {
globals: vec![(0, 65_536)],
sp_init: 65_536,
sp_alias_indices: vec![0],
shadow_stack_size: None,
};
let elf = build_relocatable_elf(
&[func],
&[],
&[],
linear_memory_bytes,
Some(native),
None,
&TargetSpec::cortex_m3(),
&[],
&[], &[],
)
.expect("#345: native-pointer zero-linmem object builds");
let header = object::elf::FileHeader32::<Endianness>::parse(&*elf).expect("valid ELF32");
let endian = header.endian().expect("endian");
let sections = header.sections(endian, &*elf).expect("sections");
let mut bss_size: Option<u64> = None;
let mut data_size: Option<u64> = None;
let mut bss_is_nobits = false;
for section in sections.iter() {
let name = sections
.section_name(endian, section)
.map(|n| String::from_utf8_lossy(n).into_owned())
.unwrap_or_default();
let sh_type = section.sh_type(endian);
if name == ".bss" {
bss_is_nobits = sh_type == object::elf::SHT_NOBITS;
bss_size = Some(section.sh_size(endian).into());
} else if name == ".data" {
data_size = Some(section.sh_size(endian).into());
assert!(
section.sh_size(endian) < 1024,
"#345: PROGBITS .data must be tiny (global slots only), not the 64 KiB reservation; got {} bytes",
section.sh_size(endian)
);
}
}
let bss = bss_size.expect("#345: a .bss section must be present");
assert!(
bss_is_nobits,
"#345: the linmem reservation must be SHT_NOBITS"
);
assert!(
bss >= 65_536 && bss <= linear_memory_bytes as u64,
"#345: .bss spans the zero-init reservation (got {bss} bytes)"
);
let data = data_size.expect("#345: a small PROGBITS .data (global slots) must be present");
assert!(
data > 0 && data < 1024,
"#345: .data holds only global slots (got {data} bytes)"
);
}
#[test]
fn native_pointer_linmem_addressing_is_abs32_not_movw_movt_345() {
use object::Endianness;
use object::read::elf::{FileHeader, SectionHeader};
let mut code = vec![0u8; 8]; code[0..4].copy_from_slice(&0u32.to_le_bytes()); code[4..8].copy_from_slice(&0u32.to_le_bytes()); let func = ElfFunction {
name: "decide".to_string(),
debug_name: None,
wasm_index: 0,
code,
relocations: vec![
synth_core::backend::CodeRelocation {
offset: 0,
symbol: "__synth_wasm_data".to_string(),
kind: synth_core::backend::RelocKind::Abs32,
},
synth_core::backend::CodeRelocation {
offset: 4,
symbol: "__synth_globals".to_string(),
kind: synth_core::backend::RelocKind::Abs32,
},
],
op_offsets: vec![],
line_map: vec![],
};
let native = NativeGlobalsLayout {
globals: vec![(0, 65_536)],
sp_init: 65_536,
sp_alias_indices: vec![0],
shadow_stack_size: None,
};
let elf = build_relocatable_elf(
&[func],
&[],
&[],
131_072,
Some(native),
None,
&TargetSpec::cortex_m3(),
&[],
&[], &[],
)
.expect("#345: native-pointer literal-pool object builds");
let header = object::elf::FileHeader32::<Endianness>::parse(&*elf).expect("valid ELF32");
let endian = header.endian().expect("endian");
let sections = header.sections(endian, &*elf).expect("sections");
const R_ARM_ABS32: u32 = 2;
const R_ARM_MOVW_ABS_NC: u32 = 43;
const R_ARM_MOVT_ABS: u32 = 44;
let mut abs32 = 0usize;
let mut movw_movt = 0usize;
for section in sections.iter() {
let name = sections
.section_name(endian, section)
.map(|n| String::from_utf8_lossy(n).into_owned())
.unwrap_or_default();
if name != ".rel.text" {
continue;
}
let (rels, _) = section
.rel(endian, &*elf)
.expect("rel section")
.expect("has rel entries");
for rel in rels {
match rel.r_type(endian) {
R_ARM_ABS32 => abs32 += 1,
R_ARM_MOVW_ABS_NC | R_ARM_MOVT_ABS => movw_movt += 1,
_ => {}
}
}
}
assert_eq!(
abs32, 2,
"#345: both linmem-address loads must be R_ARM_ABS32 literal-pool words"
);
assert_eq!(
movw_movt, 0,
"#345: NO inline MOVW/MOVT-ABS relocs may remain on the native-pointer path"
);
}
#[test]
fn mixed_high_offset_segment_splits_per_region_354() {
use object::Endianness;
use object::read::elf::{FileHeader, SectionHeader};
const C: u32 = 65_544;
const SEG_OFF: u32 = 65_536;
let mut code = vec![0u8; 4];
code[0..4].copy_from_slice(&C.to_le_bytes());
let func = ElfFunction {
name: "stack_push_decide".to_string(),
debug_name: None,
wasm_index: 0,
code,
relocations: vec![synth_core::backend::CodeRelocation {
offset: 0,
symbol: "__synth_wasm_data".to_string(),
kind: synth_core::backend::RelocKind::Abs32,
}],
op_offsets: vec![],
line_map: vec![],
};
let seg: Vec<u8> = vec![0, 0, 0, 0, 0, 0, 0, 0, 0xf4, 0xff, 0xff, 0xff];
let data_segments = vec![(SEG_OFF, seg)];
let native = NativeGlobalsLayout {
globals: vec![(0, 65_536)],
sp_init: 65_536,
sp_alias_indices: vec![0],
shadow_stack_size: None,
};
let elf = build_relocatable_elf(
&[func],
&[],
&data_segments,
131_072,
Some(native),
None,
&TargetSpec::cortex_m3(),
&[],
&[], &[],
)
.expect("#354: mixed-case object builds");
let header = object::elf::FileHeader32::<Endianness>::parse(&*elf).expect("valid ELF32");
let endian = header.endian().expect("endian");
let sections = header.sections(endian, &*elf).expect("sections");
let mut bss_size: Option<u64> = None;
let mut bss_is_nobits = false;
let mut data_size: Option<u64> = None;
let mut text_data: Vec<u8> = Vec::new();
for section in sections.iter() {
let name = sections
.section_name(endian, section)
.map(|n| String::from_utf8_lossy(n).into_owned())
.unwrap_or_default();
match name.as_str() {
".bss" => {
bss_is_nobits = section.sh_type(endian) == object::elf::SHT_NOBITS;
bss_size = Some(section.sh_size(endian).into());
}
".data" => data_size = Some(section.sh_size(endian).into()),
".text" => {
text_data = section.data(endian, &*elf).unwrap_or_default().to_vec();
}
_ => {}
}
}
let bss = bss_size.expect("#354: a .bss reservation must be present");
assert!(
bss_is_nobits,
"#354: the zero reservation must be SHT_NOBITS"
);
assert!(
bss >= 65_536,
"#354: .bss spans the zero gap (got {bss} bytes)"
);
let data = data_size.expect("#354: a small PROGBITS .data must be present");
assert!(
data < 256,
"#354: .data is bounded to the init bytes, not the 64 KiB image (got {data} bytes)"
);
{
use object::{Object, ObjectSection, ObjectSymbol};
let file = object::File::parse(&*elf).expect("#354: parse ELF");
assert!(
file.symbols().any(|s| s.name() == Ok("__synth_wasm_seg_0")),
"#354: __synth_wasm_seg_0 symbol must be defined"
);
let mut retargeted = false;
for section in file.sections() {
for (_off, rel) in section.relocations() {
if let object::RelocationTarget::Symbol(idx) = rel.target()
&& file
.symbol_by_index(idx)
.ok()
.and_then(|s| s.name().ok().map(|n| n == "__synth_wasm_seg_0"))
.unwrap_or(false)
{
retargeted = true;
}
}
}
assert!(
retargeted,
"#354: the static-data reloc must retarget to __synth_wasm_seg_0"
);
}
assert!(
text_data.len() >= 4,
"#354: .text must hold the pooled word"
);
let patched = u32::from_le_bytes([text_data[0], text_data[1], text_data[2], text_data[3]]);
assert_eq!(
patched,
C - SEG_OFF,
"#354: in-place addend rewritten to C-seg_off (8); link computes seg0_base+8 = the const"
);
}
#[test]
fn reachable_from_exports_ignores_table_when_no_call_indirect_275() {
let funcs = vec![
fop(0, Some("run"), vec![WasmOp::I32Const(1)]),
fop(1, None, vec![WasmOp::I32Const(42)]), ];
let r = reachable_from_exports(&funcs, 0, &[1]);
assert!(
!r.contains(&1),
"no call_indirect → table entry not pulled in"
);
}
#[test]
fn reachable_from_exports_leaf_is_exports_only_235() {
let funcs = vec![
fop(0, Some("a"), vec![WasmOp::I32Const(1)]),
fop(1, Some("b"), vec![WasmOp::LocalGet(0)]),
fop(2, None, vec![WasmOp::I32Const(7)]), ];
let r = reachable_from_exports(&funcs, 0, &[]);
assert_eq!(
r.into_iter().collect::<Vec<_>>(),
vec![0, 1],
"exports only"
);
}
#[test]
fn test_encode_thumb_bl_matches_gas() {
assert_eq!(encode_thumb_bl(0x6, 0x4), [0xff, 0xf7, 0xfd, 0xff]);
assert_eq!(encode_thumb_bl(0xa, 0x12), [0x00, 0xf0, 0x02, 0xf8]);
assert_eq!(encode_thumb_bl(0x138a, 0x0), [0xfe, 0xf7, 0x39, 0xfe]);
assert_eq!(encode_thumb_bl(0x138e, 0x2b02), [0x01, 0xf0, 0xb8, 0xfb]);
}
#[test]
fn test_cortex_m_binary_structure() {
let code = vec![
0x00, 0x80, 0x80, 0xe0, 0x1e, 0xff, 0x2f, 0xe1, ];
let elf_data = build_cortex_m_elf(
&code,
"test_func",
&TargetSpec::cortex_m3(),
&[],
StackLayout::High,
)
.unwrap();
assert_eq!(&elf_data[0..4], b"\x7fELF", "Invalid ELF magic");
assert_eq!(elf_data[4], 1, "Should be 32-bit ELF");
assert_eq!(elf_data[5], 1, "Should be little-endian");
assert_eq!(elf_data[16], 2, "Should be ET_EXEC");
assert_eq!(elf_data[18], 0x28, "Should be ARM architecture");
}
#[test]
fn test_vector_table_structure() {
let code = vec![0x00, 0x80, 0x80, 0xe0];
let elf_data = build_cortex_m_elf(
&code,
"test",
&TargetSpec::cortex_m3(),
&[],
StackLayout::High,
)
.unwrap();
let mut found_sp = false;
for i in 0..elf_data.len().saturating_sub(4) {
let word = u32::from_le_bytes([
elf_data[i],
elf_data[i + 1],
elf_data[i + 2],
elf_data[i + 3],
]);
if word == 0x20020000 {
found_sp = true;
let reset = u32::from_le_bytes([
elf_data[i + 4],
elf_data[i + 5],
elf_data[i + 6],
elf_data[i + 7],
]);
assert_eq!(reset, 0x81, "Reset handler should be 0x81 (0x80 | 1)");
break;
}
}
assert!(found_sp, "Stack pointer (0x20020000) not found in ELF");
}
#[test]
fn test_simple_elf_generation() {
let code = vec![0x00, 0x80, 0x80, 0xe0];
let elf_data = build_simple_elf(&code, "simple_func").unwrap();
assert_eq!(&elf_data[0..4], b"\x7fELF", "Invalid ELF magic");
let entry = u32::from_le_bytes([elf_data[24], elf_data[25], elf_data[26], elf_data[27]]);
assert_eq!(
entry, 0x8001,
"Entry point should be 0x8001 (0x8000 | Thumb bit)"
);
}
#[test]
fn test_startup_code_patching() {
let code = vec![0x00, 0x80, 0x80, 0xe0];
let elf_data = build_cortex_m_elf(
&code,
"patched",
&TargetSpec::cortex_m3(),
&[],
StackLayout::High,
)
.unwrap();
let mut found_literal = false;
for i in 0..elf_data.len().saturating_sub(4) {
let word = u32::from_le_bytes([
elf_data[i],
elf_data[i + 1],
elf_data[i + 2],
elf_data[i + 3],
]);
if word == 0xA1 {
found_literal = true;
break;
}
}
assert!(
found_literal,
"Literal pool should contain 0xA1 (0xA0 | 1 for Thumb)"
);
}
#[test]
fn test_minimal_startup_generation() {
let memory_size: u32 = 64 * 1024;
let (startup, patch, r9_off) =
generate_minimal_startup(memory_size, &[], 0x2000_0000, false, 0, 0x2000_0100);
assert!(patch.is_none(), "no copy loop when data_copy_bytes == 0");
assert!(r9_off.is_none(), "no R9 block when there are no globals");
assert_eq!(startup.len(), 28, "Startup code should be 28 bytes");
assert_eq!(startup[8], 0x40);
assert_eq!(startup[9], 0xF2);
assert_eq!(startup[10], 0x00);
assert_eq!(startup[11], 0x0B);
assert_eq!(startup[12], 0xC2);
assert_eq!(startup[13], 0xF2);
assert_eq!(startup[14], 0x00);
assert_eq!(startup[15], 0x0B);
assert_eq!(startup[16], 0x01);
assert_eq!(startup[17], 0x48);
assert_eq!(startup[18], 0x80);
assert_eq!(startup[19], 0x47);
assert_eq!(startup[20], 0xfe);
assert_eq!(startup[21], 0xe7);
}
#[test]
fn test_startup_globals_materializer_649() {
let memory_size: u32 = 64 * 1024;
let words = [0x9ABCDEF0u32, 0x12345678, 0x0C0FFEE1];
let (startup, _, r9_off) =
generate_minimal_startup(memory_size, &words, 0x2000_0000, false, 0, 0x2000_0100);
let empty =
generate_minimal_startup(memory_size, &[], 0x2000_0000, false, 0, 0x2000_0100).0;
let r9_off = r9_off.expect("R9 block present with globals");
assert_eq!(
read_back_r9_base(&startup, r9_off),
Some(0x2001_0100),
"read-back R9 base must decode to func_visible + memory_size"
);
assert_eq!(startup.len(), 16 + 8 + 36 + 12, "materializer size");
assert_eq!(&startup[..16], &empty[..16], "R10/R11 scaffold unchanged");
assert_eq!(&startup[16..20], &encode_thumb2_movw(9, 0x0100));
assert_eq!(&startup[20..24], &encode_thumb2_movt(9, 0x2001));
assert_eq!(&startup[24..28], &encode_thumb2_movw(12, 0xDEF0));
assert_eq!(&startup[28..32], &encode_thumb2_movt(12, 0x9ABC));
assert_eq!(&startup[32..36], &[0xC9, 0xF8, 0x00, 0xC0]);
assert_eq!(&startup[36..40], &encode_thumb2_movw(12, 0x5678));
assert_eq!(&startup[40..44], &encode_thumb2_movt(12, 0x1234));
assert_eq!(&startup[44..48], &[0xC9, 0xF8, 0x04, 0xC0]);
assert_eq!(&startup[56..60], &[0xC9, 0xF8, 0x08, 0xC0]);
assert_eq!(&startup[60..], &empty[16..], "call scaffold unchanged");
assert_eq!(empty.len(), 28);
}
#[test]
fn test_startup_low_layout_shifted_bases_687() {
let memory_size: u32 = 64 * 1024;
let base_high = 0x2000_0000u32;
let base_low = base_high + DEFAULT_LOW_STACK_SIZE; let words = [0x0C0FFEE1u32];
let low = generate_minimal_startup(memory_size, &words, base_low, false, 0, base_low).0;
let high = generate_minimal_startup(memory_size, &words, base_high, false, 0, base_high).0;
assert_eq!(low.len(), high.len(), "same shape, shifted constants");
assert_eq!(&low[..8], &high[..8]);
assert_eq!(&low[8..12], &encode_thumb2_movw(11, 0x1000));
assert_eq!(&low[12..16], &encode_thumb2_movt(11, 0x2000));
assert_eq!(&low[16..20], &encode_thumb2_movw(9, 0x1000));
assert_eq!(&low[20..24], &encode_thumb2_movt(9, 0x2001));
assert_eq!(&low[24..], &high[24..]);
}
#[test]
fn test_cortex_m_elf_sp_init_by_layout_687() {
let code = vec![0x70, 0x47]; let high = build_cortex_m_elf(&code, "f", &TargetSpec::cortex_m3(), &[], StackLayout::High)
.unwrap();
let low = build_cortex_m_elf(
&code,
"f",
&TargetSpec::cortex_m3(),
&[],
StackLayout::Low { stack_size: 4096 },
)
.unwrap();
let find_sp = |elf: &[u8], sp: u32| elf.windows(4).any(|w| w == sp.to_le_bytes());
assert!(
find_sp(&high, 0x2002_0000),
"high layout: initial SP = top of 128KB RAM"
);
assert!(
find_sp(&low, 0x2000_1000),
"low layout: initial SP = SRAM start + 4KB stack"
);
assert!(
!find_sp(&low, 0x2002_0000),
"low layout must not carry the high-layout SP"
);
}
#[test]
fn test_resolve_stack_layout_contract_687() {
use StackLayoutArg::{High, Low};
assert_eq!(
resolve_stack_layout(High, None, false, true, "arm").unwrap(),
StackLayout::High
);
assert_eq!(
resolve_stack_layout(High, Some(8192), false, true, "arm").unwrap(),
StackLayout::High
);
assert_eq!(
resolve_stack_layout(Low, None, false, true, "arm").unwrap(),
StackLayout::Low { stack_size: 4096 }
);
assert_eq!(
resolve_stack_layout(Low, Some(8192), false, true, "arm").unwrap(),
StackLayout::Low { stack_size: 8192 }
);
assert!(resolve_stack_layout(Low, None, true, true, "arm").is_err());
assert!(resolve_stack_layout(Low, None, false, false, "riscv").is_err());
assert!(resolve_stack_layout(Low, None, false, true, "aarch64").is_err());
assert!(resolve_stack_layout(Low, Some(4097), false, true, "arm").is_err());
assert!(resolve_stack_layout(Low, Some(128), false, true, "arm").is_err());
}
#[test]
fn test_globals_table_words_layout_649() {
let globals = vec![
WasmGlobal {
index: 0,
init: Some(GlobalInit::I64(0x123456789ABCDEF0u64 as i64)),
mutable: true,
slot_bytes: 8,
},
WasmGlobal {
index: 1,
init: Some(GlobalInit::I32(7)),
mutable: true,
slot_bytes: 4,
},
WasmGlobal {
index: 2,
init: None,
mutable: true,
slot_bytes: 8,
},
];
assert_eq!(
globals_table_words(&globals),
vec![0x9ABCDEF0, 0x12345678, 7, 0, 0]
);
assert!(globals_table_words(&[]).is_empty());
}
#[test]
fn test_default_handler_generation() {
let handler = generate_default_handler();
assert_eq!(handler.len(), 2);
assert_eq!(handler[0], 0xfe);
assert_eq!(handler[1], 0xe7);
}
#[test]
fn test_target_info_command_imxrt1062() {
let result = target_info_command("imxrt1062".to_string());
assert!(result.is_ok(), "imxrt1062 target_info should succeed");
}
#[test]
fn test_target_info_command_stm32h743() {
let result = target_info_command("stm32h743".to_string());
assert!(result.is_ok(), "stm32h743 target_info should succeed");
}
#[test]
fn test_target_info_command_existing_targets_still_work() {
assert!(target_info_command("nrf52840".to_string()).is_ok());
assert!(target_info_command("stm32f407".to_string()).is_ok());
}
#[test]
fn test_target_info_command_unknown_target_errors() {
let err = target_info_command("not-a-real-mcu".to_string()).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("not-a-real-mcu"));
assert!(msg.contains("nrf52840"));
assert!(msg.contains("stm32f407"));
assert!(
msg.contains("stm32h743"),
"error message should advertise stm32h743"
);
assert!(
msg.contains("imxrt1062"),
"error message should advertise imxrt1062"
);
}
#[test]
fn test_synthesize_command_unsupported_hardware_message() {
let bad_path = std::path::PathBuf::from("/tmp/__non_existent_wasm__");
let out_path = std::path::PathBuf::from("/tmp/__non_existent_out__");
let names = ["nrf52840", "stm32f407", "stm32h743", "imxrt1062"];
for n in names {
let caps = match n {
"nrf52840" => HardwareCapabilities::nrf52840(),
"stm32f407" => HardwareCapabilities::stm32f407(),
"stm32h743" => HardwareCapabilities::stm32h743(),
"imxrt1062" => HardwareCapabilities::imxrt1062(),
_ => unreachable!(),
};
assert!(caps.mpu_regions > 0, "{} should have MPU regions", n);
}
let _ = (bad_path, out_path);
}
#[test]
fn test_resolve_target_spec_default_no_cortex_m() {
let spec = resolve_target_spec(None, false, "arm", false).unwrap();
assert_eq!(spec.isa, synth_core::target::IsaVariant::Arm32);
}
#[test]
fn test_resolve_target_spec_cortex_m_flag() {
let spec = resolve_target_spec(None, true, "arm", false).unwrap();
assert_eq!(spec.triple, "thumbv7m-none-eabi");
}
#[test]
fn test_resolve_target_spec_explicit_target_wins_over_cortex_m() {
let spec = resolve_target_spec(Some("cortex-m7"), true, "arm", false).unwrap();
assert_eq!(spec.triple, "thumbv7em-none-eabihf");
}
#[test]
fn test_resolve_target_spec_unknown_triple_errors() {
let err =
resolve_target_spec(Some("totally-bogus-triple"), false, "arm", false).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("totally-bogus-triple"));
assert!(msg.contains("Supported"));
}
#[test]
fn test_resolve_target_spec_unknown_triple_lists_backend_targets_882() {
let err =
resolve_target_spec(Some("totally-bogus-triple"), false, "riscv", true).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("Supported"), "{msg}");
assert!(
msg.contains("targets accepted by backend 'riscv'"),
"explicit -b must list its accepted targets: {msg}"
);
assert!(msg.contains("esp32c3"), "{msg}");
}
#[test]
fn test_resolve_target_spec_riscv_target_arm_default_backend_errors_882() {
for name in [
"riscv32",
"rv32imac",
"esp32c3",
"riscv32imac-unknown-none-elf",
] {
let err = resolve_target_spec(Some(name), false, "arm", false).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("RISC-V"), "{name}: {msg}");
assert!(msg.contains("-b riscv"), "{name}: {msg}");
}
}
#[test]
fn test_resolve_target_spec_explicit_backend_mismatch_errors_882() {
let err = resolve_target_spec(Some("cortex-m3"), false, "riscv", true).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("does not accept --target cortex-m3"), "{msg}");
assert!(msg.contains("rv32imac"), "{msg}");
assert!(msg.contains("-b arm"), "{msg}");
let err = resolve_target_spec(Some("riscv32"), false, "arm", true).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("does not accept --target riscv32"), "{msg}");
assert!(msg.contains("cortex-m3"), "{msg}");
assert!(msg.contains("-b riscv"), "{msg}");
let err = resolve_target_spec(Some("cortex-a53"), false, "arm", true).unwrap_err();
assert!(err.to_string().contains("-b aarch64"), "{err}");
}
#[test]
fn test_resolve_target_spec_good_input_unchanged_882() {
let arm_targets = [
"cortex-m3",
"cortex-m4",
"cortex-m4f",
"cortex-m7",
"cortex-m7dp",
"cortex-m55",
"cortex-r5",
];
for name in arm_targets {
for explicit in [false, true] {
let spec = resolve_target_spec(Some(name), false, "arm", explicit)
.unwrap_or_else(|e| panic!("{name} must resolve on arm: {e}"));
assert_eq!(spec, TargetSpec::from_triple(name).unwrap(), "{name}");
}
}
let rv_targets = [
"rv32imac", "rv32imc", "rv32im", "rv32i", "rv32gc", "esp32c3", "riscv32",
];
for name in rv_targets {
let spec = resolve_target_spec(Some(name), false, "riscv", true)
.unwrap_or_else(|e| panic!("{name} must resolve on riscv: {e}"));
assert_eq!(spec.family, synth_core::target::ArchFamily::RiscV, "{name}");
}
let spec = resolve_target_spec(Some("cortex-a53"), false, "aarch64", true).unwrap();
assert_eq!(spec.family, synth_core::target::ArchFamily::ArmCortexA);
let spec = resolve_target_spec(Some("riscv32"), false, "w2c2", true).unwrap();
assert_eq!(spec.family, synth_core::target::ArchFamily::RiscV);
}
#[test]
fn test_resolve_target_spec_riscv_default_218() {
let spec = resolve_target_spec(None, false, "riscv", true).unwrap();
assert_eq!(spec.family, synth_core::target::ArchFamily::RiscV);
assert_eq!(
spec.isa,
synth_core::target::IsaVariant::RiscV32 {
extensions: "imac".to_string()
}
);
}
}