synth-core 0.11.9

Core types, error handling, and backend trait for the Synth compiler
Documentation
//! Backend trait and registry for multi-backend compilation
//!
//! Every compiler backend (ARM, aWsm, wasker, w2c2) implements the `Backend`
//! trait, allowing the CLI and verification framework to treat them uniformly.

use crate::target::TargetSpec;
use crate::wasm_decoder::DecodedModule;
use crate::wasm_op::WasmOp;
use std::collections::HashMap;
use thiserror::Error;

/// Errors from backend compilation
#[derive(Debug, Error)]
pub enum BackendError {
    #[error("compilation failed: {0}")]
    CompilationFailed(String),

    #[error("backend not available: {0}")]
    NotAvailable(String),

    #[error("unsupported configuration: {0}")]
    UnsupportedConfig(String),

    #[error("external tool error: {0}")]
    ExternalToolError(String),
}

/// Memory-bounds safety strategy. Phase 1 of `docs/binary-safety-design.md` ยง3.1.
///
/// - `Mpu`/PMP: rely on hardware (ARM MPU or RV32 PMP) โ€” no inline check.
/// - `Software`: emit a `CMP/BHS Trap_Handler` (ARM) or `bgeu addr, mem_size, ebreak` (RV32)
///   before every load/store.
/// - `Mask`: emit `AND addr, addr, #(mem_size - 1)` โ€” only valid when memory size
///   is a power of two. Wraps on OOB rather than trapping (fuzz-profile semantics).
/// - `None`: no bounds enforcement.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SafetyBounds {
    /// No bounds check (caller assumes the WASM module is trusted)
    #[default]
    None,
    /// ARM MPU / RV32 PMP โ€” hardware enforcement, no inline guard
    Mpu,
    /// Software CMP/BHS (ARM) or BGEU+EBREAK (RV32) per access
    Software,
    /// AND-mask, requires power-of-two memory size
    Mask,
}

impl SafetyBounds {
    /// Parse the `--safety-bounds` argument value.
    pub fn parse(s: &str) -> std::result::Result<Self, String> {
        match s {
            "none" => Ok(SafetyBounds::None),
            "mpu" | "pmp" => Ok(SafetyBounds::Mpu),
            "software" | "soft" => Ok(SafetyBounds::Software),
            "mask" | "masking" => Ok(SafetyBounds::Mask),
            other => Err(format!(
                "unknown --safety-bounds value '{}'; expected one of: none, mpu, software, mask",
                other
            )),
        }
    }

    /// String form used in the safety manifest.
    pub fn as_str(self) -> &'static str {
        match self {
            SafetyBounds::None => "none",
            SafetyBounds::Mpu => "mpu",
            SafetyBounds::Software => "software",
            SafetyBounds::Mask => "mask",
        }
    }
}

/// Configuration for a compilation run
#[derive(Debug, Clone)]
pub struct CompileConfig {
    /// Optimization level (0 = none, 1 = fast, 2 = default, 3 = aggressive)
    pub opt_level: u8,
    /// Target specification
    pub target: TargetSpec,
    /// Legacy: enable software bounds checking for memory operations.
    /// Deprecated in favor of `safety_bounds`. When set, equivalent to
    /// `SafetyBounds::Software`. Kept for backwards compatibility with
    /// callers that haven't migrated yet.
    pub bounds_check: bool,
    /// Phase-1 unified safety-bounds knob. If `bounds_check` is `true` and
    /// this is `None`, the legacy field wins (back-compat). If both are set,
    /// `safety_bounds` wins.
    pub safety_bounds: SafetyBounds,
    /// Hardware profile name (e.g. "nrf52840", "stm32f407")
    pub hardware: String,
    /// Skip optimization passes (direct instruction selection)
    pub no_optimize: bool,
    /// Use Loom-compatible optimization preset
    pub loom_compat: bool,
    /// Number of imported functions (calls to indices below this use Meld dispatch)
    pub num_imports: u32,
    /// AAPCS integer-argument count per function, indexed by full WASM function
    /// index (imports first, then locals). Lets `Call` marshal the right number
    /// of operand-stack values into R0โ€“R3 (issue #195). Empty = pass no args
    /// (pre-#195 behaviour).
    pub func_arg_counts: Vec<u32>,
    /// AAPCS integer-argument count per function type, indexed by type index.
    /// Used by `call_indirect` (issue #195).
    pub type_arg_counts: Vec<u32>,
}

impl CompileConfig {
    /// Resolve the effective safety-bounds setting, honouring the legacy
    /// `bounds_check` field as a fallback. Used by backends to pick the
    /// inline-check shape.
    pub fn effective_safety_bounds(&self) -> SafetyBounds {
        match (self.safety_bounds, self.bounds_check) {
            (SafetyBounds::None, true) => SafetyBounds::Software,
            (s, _) => s,
        }
    }
}

impl Default for CompileConfig {
    fn default() -> Self {
        Self {
            opt_level: 2,
            target: TargetSpec::cortex_m4(),
            bounds_check: false,
            safety_bounds: SafetyBounds::None,
            hardware: String::new(),
            no_optimize: false,
            loom_compat: false,
            num_imports: 0,
            func_arg_counts: Vec::new(),
            type_arg_counts: Vec::new(),
        }
    }
}

/// A relocation entry produced during compilation
///
/// Records that a BL instruction at `offset` bytes into the function's code
/// targets an external symbol (e.g., `__meld_dispatch_import`). The linker
/// resolves these when combining the Synth object with the Kiln bridge.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CodeRelocation {
    /// Byte offset within the function's machine code where the BL resides
    pub offset: u32,
    /// Target symbol name (e.g., "__meld_dispatch_import")
    pub symbol: String,
}

/// A single compiled function
#[derive(Debug, Clone)]
pub struct CompiledFunction {
    /// Function name (from WASM export or generated)
    pub name: String,
    /// Raw machine code bytes
    pub code: Vec<u8>,
    /// Original WASM ops (retained for verification)
    pub wasm_ops: Vec<WasmOp>,
    /// Relocations for external symbol references (BL to bridge functions)
    pub relocations: Vec<CodeRelocation>,
}

/// Result of compiling a full module
#[derive(Debug)]
pub struct CompilationResult {
    /// Compiled functions
    pub functions: Vec<CompiledFunction>,
    /// Complete ELF binary (if backend produces one directly)
    pub elf: Option<Vec<u8>>,
    /// Name of the backend that produced this result
    pub backend_name: String,
}

/// What a backend can and cannot do
#[derive(Debug, Clone)]
pub struct BackendCapabilities {
    /// Backend produces complete ELF files (external backends like aWsm)
    pub produces_elf: bool,
    /// Backend supports per-rule verification (only our custom ARM backend)
    pub supports_rule_verification: bool,
    /// Backend supports binary-level verification (all backends via disassembly)
    pub supports_binary_verification: bool,
    /// Backend is an external tool (not a library)
    pub is_external: bool,
}

/// Trait that every compilation backend implements
pub trait Backend: Send + Sync {
    /// Human-readable backend name
    fn name(&self) -> &str;

    /// What this backend can do
    fn capabilities(&self) -> BackendCapabilities;

    /// Which targets this backend supports
    fn supported_targets(&self) -> Vec<TargetSpec>;

    /// Compile an entire decoded WASM module
    fn compile_module(
        &self,
        module: &DecodedModule,
        config: &CompileConfig,
    ) -> std::result::Result<CompilationResult, BackendError>;

    /// Compile a single function from WASM ops to machine code
    fn compile_function(
        &self,
        name: &str,
        ops: &[WasmOp],
        config: &CompileConfig,
    ) -> std::result::Result<CompiledFunction, BackendError>;

    /// Check if this backend is available (external tools installed, etc.)
    fn is_available(&self) -> bool;
}

/// Registry of available backends
pub struct BackendRegistry {
    backends: HashMap<String, Box<dyn Backend>>,
}

impl BackendRegistry {
    pub fn new() -> Self {
        Self {
            backends: HashMap::new(),
        }
    }

    /// Register a backend under its name
    pub fn register(&mut self, backend: Box<dyn Backend>) {
        let name = backend.name().to_string();
        self.backends.insert(name, backend);
    }

    /// Get a backend by name
    pub fn get(&self, name: &str) -> Option<&dyn Backend> {
        self.backends.get(name).map(|b| b.as_ref())
    }

    /// List all registered backends
    pub fn list(&self) -> Vec<&dyn Backend> {
        self.backends.values().map(|b| b.as_ref()).collect()
    }

    /// List backends that are actually available (installed and working)
    pub fn available(&self) -> Vec<&dyn Backend> {
        self.backends
            .values()
            .filter(|b| b.is_available())
            .map(|b| b.as_ref())
            .collect()
    }
}

impl Default for BackendRegistry {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_registry_empty() {
        let reg = BackendRegistry::new();
        assert!(reg.list().is_empty());
        assert!(reg.available().is_empty());
        assert!(reg.get("arm").is_none());
    }

    #[test]
    fn test_compile_config_default() {
        let config = CompileConfig::default();
        assert_eq!(config.opt_level, 2);
        assert!(!config.bounds_check);
        assert_eq!(config.safety_bounds, SafetyBounds::None);
        assert!(!config.no_optimize);
    }

    #[test]
    fn safety_bounds_parse_round_trip() {
        for s in ["none", "mpu", "software", "mask"] {
            let sb = SafetyBounds::parse(s).unwrap();
            assert_eq!(sb.as_str(), s);
        }
        assert_eq!(SafetyBounds::parse("pmp").unwrap(), SafetyBounds::Mpu);
        assert_eq!(SafetyBounds::parse("soft").unwrap(), SafetyBounds::Software);
        assert!(SafetyBounds::parse("nonsense").is_err());
    }

    #[test]
    fn effective_safety_bounds_legacy_promotes_to_software() {
        let cfg = CompileConfig {
            bounds_check: true,
            ..Default::default()
        };
        assert_eq!(cfg.effective_safety_bounds(), SafetyBounds::Software);
    }

    #[test]
    fn effective_safety_bounds_new_field_wins() {
        let cfg = CompileConfig {
            bounds_check: true,
            safety_bounds: SafetyBounds::Mpu,
            ..Default::default()
        };
        assert_eq!(cfg.effective_safety_bounds(), SafetyBounds::Mpu);
    }
}