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};
use synth_core::target::TargetSpec;
use synth_core::wasm_decoder::ImportEntry;
use synth_synthesis::{FunctionOps, WasmMemory, WasmOp, decode_wasm_functions, decode_wasm_module};
use tracing::{Level, info};
use wast::parser::{self, ParseBuffer};
use wast::{Wast, WastDirective};
mod sign;
const SBOM_DEFAULT_SENTINEL: &str = "\u{0}sbom-default\u{0}";
#[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,
}
#[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, default_value = "arm")]
backend: String,
#[arg(long)]
verify: bool,
#[arg(long)]
link: bool,
#[arg(long, value_name = "BUILTINS")]
builtins: Option<PathBuf>,
#[arg(long)]
relocatable: bool,
#[arg(
long,
value_name = "PATH",
num_args = 0..=1,
default_missing_value = SBOM_DEFAULT_SENTINEL
)]
sbom: Option<PathBuf>,
#[arg(long)]
sign_output: bool,
},
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,
sbom,
sign_output,
} => {
let target_spec = resolve_target_spec(target.as_deref(), cortex_m)?;
let is_cortex_m =
cortex_m || target_spec.family == synth_core::target::ArchFamily::ArmCortexM;
let loom_compat = loom_compat || loom;
let resolved_safety_bounds =
resolve_safety_bounds(safety_bounds.as_deref(), bounds_check)?;
let sbom_path = resolve_sbom_path(sbom, &output);
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,
sbom_path,
sign_output,
)?;
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,
code: Vec<u8>,
relocations: Vec<synth_core::backend::CodeRelocation>,
}
fn resolve_target_spec(target: Option<&str>, cortex_m: bool) -> Result<TargetSpec> {
match target {
Some(name) => TargetSpec::from_triple(name).map_err(|e| {
anyhow::anyhow!(
"{e}. Supported: cortex-m3, cortex-m4, cortex-m4f, cortex-m7, cortex-m7dp"
)
}),
None if cortex_m => Ok(TargetSpec::cortex_m3()),
None => {
Ok(TargetSpec {
isa: synth_core::target::IsaVariant::Arm32,
..TargetSpec::cortex_m4()
})
}
}
}
fn build_backend_registry() -> BackendRegistry {
let mut registry = BackendRegistry::new();
registry.register(Box::new(ArmBackend::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,
) -> Result<SafetyBounds> {
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()
);
}
return Ok(parsed);
}
if legacy_bounds_check {
eprintln!("warning: --bounds-check is deprecated; use --safety-bounds=software instead");
return Ok(SafetyBounds::Software);
}
Ok(SafetyBounds::None)
}
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 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,
sbom_path: Option<PathBuf>,
sign_output: bool,
) -> 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 {
return compile_all_exports(
input,
output,
cortex_m,
no_optimize,
loom_compat,
loom,
safety_bounds,
backend,
verify,
target_spec,
relocatable,
sbom_path,
sign_output,
);
}
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 (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)?;
if sbom_path.is_some() {
if let Ok(module) = decode_wasm_module(&wasm_bytes) {
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());
(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(),
)
}
};
info!("WASM operations: {:?}", wasm_ops);
let config = CompileConfig {
no_optimize,
loom_compat,
safety_bounds,
target: target_spec.clone(),
..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 matches!(target_spec.family, synth_core::target::ArchFamily::RiscV) {
build_riscv_elf(&code, &func_name)?
} else if cortex_m {
build_cortex_m_elf(&code, &func_name, target_spec)?
} 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, 0)?;
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 z3-solver not available.");
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_z3_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"))
}
#[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,
sbom_path: Option<PathBuf>,
sign_output: bool,
) -> Result<()> {
let path = input.context("--all-exports requires an input file")?;
info!("Compiling all exports from: {}", path.display());
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) =
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;
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();
sbom_wasm_bytes = Some(wasm_bytes.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)
} 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 module = decode_wasm_module(&wasm_bytes).context("Failed to decode WASM module")?;
sbom_wasm_bytes = Some(wasm_bytes);
let exports: Vec<_> = module
.functions
.into_iter()
.filter(|f| f.export_name.is_some())
.collect();
let memories = module.memories;
let imports = module.imports;
let num_imports = module.num_imported_funcs;
(exports, memories, imports, num_imports)
};
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 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 config = CompileConfig {
no_optimize,
loom_compat,
safety_bounds,
num_imports: max_num_imported_funcs,
target: target_spec.clone(),
..CompileConfig::default()
};
let mut compiled_funcs = Vec::new();
for func in &all_exports {
let name = func.export_name.clone().ok_or_else(|| {
anyhow::anyhow!("function at index {} has no export name", func.index)
})?;
info!(
"Compiling function '{}' via backend '{}'...",
name,
backend.name()
);
let compiled = backend
.compile_function(&name, &func.ops, &config)
.map_err(|e| {
anyhow::anyhow!("Backend '{}' failed on '{}': {}", backend.name(), name, e)
})?;
info!(" {} bytes of machine code", compiled.code.len());
if !compiled.relocations.is_empty() {
info!(
" {} relocations (external symbol references)",
compiled.relocations.len()
);
}
compiled_funcs.push(ElfFunction {
name: name.clone(),
code: compiled.code,
relocations: compiled.relocations,
});
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");
}
}
}
let has_relocations = compiled_funcs.iter().any(|f| !f.relocations.is_empty());
let is_riscv = matches!(target_spec.family, synth_core::target::ArchFamily::RiscV);
let elf_data = if is_riscv {
info!("Building RISC-V multi-function relocatable object (EM_RISCV)");
build_multi_func_riscv_elf(&compiled_funcs)?
} else if has_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)?
} else if cortex_m {
build_multi_func_cortex_m_elf(&compiled_funcs, &all_memories, target_spec)?
} 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 has_relocations {
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()
);
}
println!("\nFunction addresses:");
println!(
" Use 'synth disasm {}' or 'objdump -t {}' to see symbols",
output.display(),
output.display()
);
Ok(())
}
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")
}
fn build_relocatable_elf(funcs: &[ElfFunction], imports: &[ImportEntry]) -> Result<Vec<u8>> {
use std::collections::HashMap;
let mut elf_builder = ElfBuilder::new_arm32()
.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 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);
for (i, func) in funcs.iter().enumerate() {
let func_sym = Symbol::new(&func.name)
.with_value(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);
}
let mut extern_sym_indices: HashMap<String, u32> = HashMap::new();
for func in funcs {
for reloc in &func.relocations {
if !extern_sym_indices.contains_key(&reloc.symbol) {
let idx = elf_builder.add_undefined_symbol(&reloc.symbol);
extern_sym_indices.insert(reloc.symbol.clone(), idx);
}
}
}
for (i, func) in funcs.iter().enumerate() {
let func_base = func_offsets[i];
for reloc in &func.relocations {
let sym_idx = extern_sym_indices[&reloc.symbol];
elf_builder.add_relocation(Relocation {
offset: func_base + reloc.offset,
symbol_index: sym_idx,
reloc_type: ArmRelocationType::Call, });
}
}
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);
}
}
info!(
"Relocatable ELF: {} functions, {} external symbols, {} relocations",
funcs.len(),
extern_sym_indices.len(),
funcs.iter().map(|f| f.relocations.len()).sum::<usize>()
);
elf_builder
.build()
.context("Relocatable ELF generation failed")
}
fn build_multi_func_cortex_m_elf(
funcs: &[ElfFunction],
memories: &[WasmMemory],
target: &TargetSpec,
) -> 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 min_stack_size: u32 = 8 * 1024;
let needed = linear_memory_size + min_stack_size;
let ram_size: u32 = std::cmp::max(128 * 1024, (needed + 0xFFFF) & !0xFFFF);
let stack_top = ram_base + ram_size;
info!(
"RAM layout: linear memory {}KB at 0x{:08x}, stack at 0x{:08x}",
linear_memory_size / 1024,
ram_base,
stack_top
);
let vector_table_addr = flash_base;
let vector_table_size: u32 = 128;
let startup_addr = flash_base + vector_table_size;
let startup_code = generate_minimal_startup(linear_memory_size);
let startup_size = startup_code.len() as u32;
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);
}
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!(" Stack top: 0x{:08x}", stack_top);
let mut vt = VectorTable::new_cortex_m(stack_top);
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; patched_startup[24..28].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);
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_phdr = ProgramHeader::load_nobits(
ram_base,
linear_memory_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(linear_memory_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.build().context("ELF generation failed")
}
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 output = Command::new("objdump")
.args(["-d", "--triple=arm-none-eabi"])
.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().map_or(false, |ext| ext == "wat") {
wat::parse_bytes(&file_bytes)
.context("Failed to parse WAT file")?
.into_owned()
} else if wasm_input.extension().map_or(false, |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.");
}
#[cfg(not(feature = "verify"))]
{
anyhow::bail!(
"this `synth` binary was built without the `verify` feature — \
Z3 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]) -> Result<Vec<u8>> {
use synth_backend_riscv::{Reg, RiscVElfBuilder, RiscVElfFunction, RiscVOp};
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();
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));
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(&placeholder_funcs)
.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]) -> Result<Vec<u8>> {
anyhow::bail!("RISC-V backend was not compiled in (rebuild with --features riscv)")
}
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) -> Result<Vec<u8>> {
let flash_base: u32 = 0x0000_0000;
let ram_base: u32 = 0x2000_0000;
let ram_size: u32 = 128 * 1024; let stack_top = ram_base + ram_size;
let linear_memory_size: u32 = 64 * 1024;
let vector_table_addr = flash_base;
let vector_table_size: u32 = 128;
let startup_addr = flash_base + vector_table_size;
let startup_code = generate_minimal_startup(linear_memory_size);
let startup_size = startup_code.len() as u32;
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!(" Stack top: 0x{:08x}", stack_top);
let mut vt = VectorTable::new_cortex_m(stack_top);
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; patched_startup[24..28].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.build().context("ELF generation failed")
}
fn generate_minimal_startup(memory_size: u32) -> Vec<u8> {
let r10_movw = encode_thumb2_movw(10, (memory_size & 0xFFFF) as u16);
let r10_movt = encode_thumb2_movt(10, (memory_size >> 16) as u16);
vec![
r10_movw[0],
r10_movw[1],
r10_movw[2],
r10_movw[3],
r10_movt[0],
r10_movt[1],
r10_movt[2],
r10_movt[3],
0x40,
0xF2,
0x00,
0x0B,
0xC2,
0xF2,
0x00,
0x0B,
0x01,
0x48,
0x80,
0x47,
0xfe,
0xe7,
0x00,
0x00,
0x91,
0x00,
0x00,
0x00,
]
}
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 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::*;
#[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()).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()).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()).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 = generate_minimal_startup(memory_size);
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_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).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).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).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).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("totally-bogus-triple"));
assert!(msg.contains("Supported"));
}
}