Skip to main content

synth_core/
backend.rs

1//! Backend trait and registry for multi-backend compilation
2//!
3//! Every compiler backend (ARM, aWsm, wasker, w2c2) implements the `Backend`
4//! trait, allowing the CLI and verification framework to treat them uniformly.
5
6use crate::target::TargetSpec;
7use crate::wasm_decoder::DecodedModule;
8use crate::wasm_op::WasmOp;
9use std::collections::HashMap;
10use thiserror::Error;
11
12/// Errors from backend compilation
13#[derive(Debug, Error)]
14pub enum BackendError {
15    #[error("compilation failed: {0}")]
16    CompilationFailed(String),
17
18    #[error("backend not available: {0}")]
19    NotAvailable(String),
20
21    #[error("unsupported configuration: {0}")]
22    UnsupportedConfig(String),
23
24    #[error("external tool error: {0}")]
25    ExternalToolError(String),
26}
27
28/// Memory-bounds safety strategy. Phase 1 of `docs/binary-safety-design.md` ยง3.1.
29///
30/// - `Mpu`/PMP: rely on hardware (ARM MPU or RV32 PMP) โ€” no inline check.
31/// - `Software`: emit a `CMP/BHS Trap_Handler` (ARM) or `bgeu addr, mem_size, ebreak` (RV32)
32///   before every load/store.
33/// - `Mask`: emit `AND addr, addr, #(mem_size - 1)` โ€” only valid when memory size
34///   is a power of two. Wraps on OOB rather than trapping (fuzz-profile semantics).
35/// - `None`: no bounds enforcement.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
37pub enum SafetyBounds {
38    /// No bounds check (caller assumes the WASM module is trusted)
39    #[default]
40    None,
41    /// ARM MPU / RV32 PMP โ€” hardware enforcement, no inline guard
42    Mpu,
43    /// Software CMP/BHS (ARM) or BGEU+EBREAK (RV32) per access
44    Software,
45    /// AND-mask, requires power-of-two memory size
46    Mask,
47}
48
49impl SafetyBounds {
50    /// Parse the `--safety-bounds` argument value.
51    pub fn parse(s: &str) -> std::result::Result<Self, String> {
52        match s {
53            "none" => Ok(SafetyBounds::None),
54            "mpu" | "pmp" => Ok(SafetyBounds::Mpu),
55            "software" | "soft" => Ok(SafetyBounds::Software),
56            "mask" | "masking" => Ok(SafetyBounds::Mask),
57            other => Err(format!(
58                "unknown --safety-bounds value '{}'; expected one of: none, mpu, software, mask",
59                other
60            )),
61        }
62    }
63
64    /// String form used in the safety manifest.
65    pub fn as_str(self) -> &'static str {
66        match self {
67            SafetyBounds::None => "none",
68            SafetyBounds::Mpu => "mpu",
69            SafetyBounds::Software => "software",
70            SafetyBounds::Mask => "mask",
71        }
72    }
73}
74
75/// Configuration for a compilation run
76#[derive(Debug, Clone)]
77pub struct CompileConfig {
78    /// Optimization level (0 = none, 1 = fast, 2 = default, 3 = aggressive)
79    pub opt_level: u8,
80    /// Target specification
81    pub target: TargetSpec,
82    /// Legacy: enable software bounds checking for memory operations.
83    /// Deprecated in favor of `safety_bounds`. When set, equivalent to
84    /// `SafetyBounds::Software`. Kept for backwards compatibility with
85    /// callers that haven't migrated yet.
86    pub bounds_check: bool,
87    /// Phase-1 unified safety-bounds knob. If `bounds_check` is `true` and
88    /// this is `None`, the legacy field wins (back-compat). If both are set,
89    /// `safety_bounds` wins.
90    pub safety_bounds: SafetyBounds,
91    /// Hardware profile name (e.g. "nrf52840", "stm32f407")
92    pub hardware: String,
93    /// Skip optimization passes (direct instruction selection)
94    pub no_optimize: bool,
95    /// Use Loom-compatible optimization preset
96    pub loom_compat: bool,
97    /// Number of imported functions (calls to indices below this use Meld dispatch)
98    pub num_imports: u32,
99}
100
101impl CompileConfig {
102    /// Resolve the effective safety-bounds setting, honouring the legacy
103    /// `bounds_check` field as a fallback. Used by backends to pick the
104    /// inline-check shape.
105    pub fn effective_safety_bounds(&self) -> SafetyBounds {
106        match (self.safety_bounds, self.bounds_check) {
107            (SafetyBounds::None, true) => SafetyBounds::Software,
108            (s, _) => s,
109        }
110    }
111}
112
113impl Default for CompileConfig {
114    fn default() -> Self {
115        Self {
116            opt_level: 2,
117            target: TargetSpec::cortex_m4(),
118            bounds_check: false,
119            safety_bounds: SafetyBounds::None,
120            hardware: String::new(),
121            no_optimize: false,
122            loom_compat: false,
123            num_imports: 0,
124        }
125    }
126}
127
128/// A relocation entry produced during compilation
129///
130/// Records that a BL instruction at `offset` bytes into the function's code
131/// targets an external symbol (e.g., `__meld_dispatch_import`). The linker
132/// resolves these when combining the Synth object with the Kiln bridge.
133#[derive(Debug, Clone, PartialEq, Eq)]
134pub struct CodeRelocation {
135    /// Byte offset within the function's machine code where the BL resides
136    pub offset: u32,
137    /// Target symbol name (e.g., "__meld_dispatch_import")
138    pub symbol: String,
139}
140
141/// A single compiled function
142#[derive(Debug, Clone)]
143pub struct CompiledFunction {
144    /// Function name (from WASM export or generated)
145    pub name: String,
146    /// Raw machine code bytes
147    pub code: Vec<u8>,
148    /// Original WASM ops (retained for verification)
149    pub wasm_ops: Vec<WasmOp>,
150    /// Relocations for external symbol references (BL to bridge functions)
151    pub relocations: Vec<CodeRelocation>,
152}
153
154/// Result of compiling a full module
155#[derive(Debug)]
156pub struct CompilationResult {
157    /// Compiled functions
158    pub functions: Vec<CompiledFunction>,
159    /// Complete ELF binary (if backend produces one directly)
160    pub elf: Option<Vec<u8>>,
161    /// Name of the backend that produced this result
162    pub backend_name: String,
163}
164
165/// What a backend can and cannot do
166#[derive(Debug, Clone)]
167pub struct BackendCapabilities {
168    /// Backend produces complete ELF files (external backends like aWsm)
169    pub produces_elf: bool,
170    /// Backend supports per-rule verification (only our custom ARM backend)
171    pub supports_rule_verification: bool,
172    /// Backend supports binary-level verification (all backends via disassembly)
173    pub supports_binary_verification: bool,
174    /// Backend is an external tool (not a library)
175    pub is_external: bool,
176}
177
178/// Trait that every compilation backend implements
179pub trait Backend: Send + Sync {
180    /// Human-readable backend name
181    fn name(&self) -> &str;
182
183    /// What this backend can do
184    fn capabilities(&self) -> BackendCapabilities;
185
186    /// Which targets this backend supports
187    fn supported_targets(&self) -> Vec<TargetSpec>;
188
189    /// Compile an entire decoded WASM module
190    fn compile_module(
191        &self,
192        module: &DecodedModule,
193        config: &CompileConfig,
194    ) -> std::result::Result<CompilationResult, BackendError>;
195
196    /// Compile a single function from WASM ops to machine code
197    fn compile_function(
198        &self,
199        name: &str,
200        ops: &[WasmOp],
201        config: &CompileConfig,
202    ) -> std::result::Result<CompiledFunction, BackendError>;
203
204    /// Check if this backend is available (external tools installed, etc.)
205    fn is_available(&self) -> bool;
206}
207
208/// Registry of available backends
209pub struct BackendRegistry {
210    backends: HashMap<String, Box<dyn Backend>>,
211}
212
213impl BackendRegistry {
214    pub fn new() -> Self {
215        Self {
216            backends: HashMap::new(),
217        }
218    }
219
220    /// Register a backend under its name
221    pub fn register(&mut self, backend: Box<dyn Backend>) {
222        let name = backend.name().to_string();
223        self.backends.insert(name, backend);
224    }
225
226    /// Get a backend by name
227    pub fn get(&self, name: &str) -> Option<&dyn Backend> {
228        self.backends.get(name).map(|b| b.as_ref())
229    }
230
231    /// List all registered backends
232    pub fn list(&self) -> Vec<&dyn Backend> {
233        self.backends.values().map(|b| b.as_ref()).collect()
234    }
235
236    /// List backends that are actually available (installed and working)
237    pub fn available(&self) -> Vec<&dyn Backend> {
238        self.backends
239            .values()
240            .filter(|b| b.is_available())
241            .map(|b| b.as_ref())
242            .collect()
243    }
244}
245
246impl Default for BackendRegistry {
247    fn default() -> Self {
248        Self::new()
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255
256    #[test]
257    fn test_registry_empty() {
258        let reg = BackendRegistry::new();
259        assert!(reg.list().is_empty());
260        assert!(reg.available().is_empty());
261        assert!(reg.get("arm").is_none());
262    }
263
264    #[test]
265    fn test_compile_config_default() {
266        let config = CompileConfig::default();
267        assert_eq!(config.opt_level, 2);
268        assert!(!config.bounds_check);
269        assert_eq!(config.safety_bounds, SafetyBounds::None);
270        assert!(!config.no_optimize);
271    }
272
273    #[test]
274    fn safety_bounds_parse_round_trip() {
275        for s in ["none", "mpu", "software", "mask"] {
276            let sb = SafetyBounds::parse(s).unwrap();
277            assert_eq!(sb.as_str(), s);
278        }
279        assert_eq!(SafetyBounds::parse("pmp").unwrap(), SafetyBounds::Mpu);
280        assert_eq!(SafetyBounds::parse("soft").unwrap(), SafetyBounds::Software);
281        assert!(SafetyBounds::parse("nonsense").is_err());
282    }
283
284    #[test]
285    fn effective_safety_bounds_legacy_promotes_to_software() {
286        let cfg = CompileConfig {
287            bounds_check: true,
288            ..Default::default()
289        };
290        assert_eq!(cfg.effective_safety_bounds(), SafetyBounds::Software);
291    }
292
293    #[test]
294    fn effective_safety_bounds_new_field_wins() {
295        let cfg = CompileConfig {
296            bounds_check: true,
297            safety_bounds: SafetyBounds::Mpu,
298            ..Default::default()
299        };
300        assert_eq!(cfg.effective_safety_bounds(), SafetyBounds::Mpu);
301    }
302}