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 /// AAPCS integer-argument count per function, indexed by full WASM function
100 /// index (imports first, then locals). Lets `Call` marshal the right number
101 /// of operand-stack values into R0–R3 (issue #195). Empty = pass no args
102 /// (pre-#195 behaviour).
103 pub func_arg_counts: Vec<u32>,
104 /// AAPCS integer-argument count per function type, indexed by type index.
105 /// Used by `call_indirect` (issue #195).
106 pub type_arg_counts: Vec<u32>,
107 /// Produce relocatable (ET_REL) host-link output. When set, the backend
108 /// uses the direct instruction selector (`select_with_stack`) rather than
109 /// the optimized path: the optimizer materializes an *absolute* linear-
110 /// memory base (0x20000100) and does not preserve caller-saved registers
111 /// across calls, both wrong for a host-linked object where the linmem base
112 /// is supplied via `fp` at runtime and callees follow AAPCS. Imports are
113 /// also emitted as direct `func_N` BLs (resolved to the wasm field name)
114 /// instead of `__meld_dispatch_import`. (#197 — follow-up to #188/#171.)
115 pub relocatable: bool,
116}
117
118impl CompileConfig {
119 /// Resolve the effective safety-bounds setting, honouring the legacy
120 /// `bounds_check` field as a fallback. Used by backends to pick the
121 /// inline-check shape.
122 pub fn effective_safety_bounds(&self) -> SafetyBounds {
123 match (self.safety_bounds, self.bounds_check) {
124 (SafetyBounds::None, true) => SafetyBounds::Software,
125 (s, _) => s,
126 }
127 }
128}
129
130impl Default for CompileConfig {
131 fn default() -> Self {
132 Self {
133 opt_level: 2,
134 target: TargetSpec::cortex_m4(),
135 bounds_check: false,
136 safety_bounds: SafetyBounds::None,
137 hardware: String::new(),
138 no_optimize: false,
139 loom_compat: false,
140 num_imports: 0,
141 func_arg_counts: Vec::new(),
142 type_arg_counts: Vec::new(),
143 relocatable: false,
144 }
145 }
146}
147
148/// A relocation entry produced during compilation
149///
150/// Records that a BL instruction at `offset` bytes into the function's code
151/// targets an external symbol (e.g., `__meld_dispatch_import`). The linker
152/// resolves these when combining the Synth object with the Kiln bridge.
153#[derive(Debug, Clone, PartialEq, Eq)]
154pub struct CodeRelocation {
155 /// Byte offset within the function's machine code where the BL resides
156 pub offset: u32,
157 /// Target symbol name (e.g., "__meld_dispatch_import")
158 pub symbol: String,
159}
160
161/// A single compiled function
162#[derive(Debug, Clone)]
163pub struct CompiledFunction {
164 /// Function name (from WASM export or generated)
165 pub name: String,
166 /// Raw machine code bytes
167 pub code: Vec<u8>,
168 /// Original WASM ops (retained for verification)
169 pub wasm_ops: Vec<WasmOp>,
170 /// Relocations for external symbol references (BL to bridge functions)
171 pub relocations: Vec<CodeRelocation>,
172}
173
174/// Result of compiling a full module
175#[derive(Debug)]
176pub struct CompilationResult {
177 /// Compiled functions
178 pub functions: Vec<CompiledFunction>,
179 /// Complete ELF binary (if backend produces one directly)
180 pub elf: Option<Vec<u8>>,
181 /// Name of the backend that produced this result
182 pub backend_name: String,
183}
184
185/// What a backend can and cannot do
186#[derive(Debug, Clone)]
187pub struct BackendCapabilities {
188 /// Backend produces complete ELF files (external backends like aWsm)
189 pub produces_elf: bool,
190 /// Backend supports per-rule verification (only our custom ARM backend)
191 pub supports_rule_verification: bool,
192 /// Backend supports binary-level verification (all backends via disassembly)
193 pub supports_binary_verification: bool,
194 /// Backend is an external tool (not a library)
195 pub is_external: bool,
196}
197
198/// Trait that every compilation backend implements
199pub trait Backend: Send + Sync {
200 /// Human-readable backend name
201 fn name(&self) -> &str;
202
203 /// What this backend can do
204 fn capabilities(&self) -> BackendCapabilities;
205
206 /// Which targets this backend supports
207 fn supported_targets(&self) -> Vec<TargetSpec>;
208
209 /// Compile an entire decoded WASM module
210 fn compile_module(
211 &self,
212 module: &DecodedModule,
213 config: &CompileConfig,
214 ) -> std::result::Result<CompilationResult, BackendError>;
215
216 /// Compile a single function from WASM ops to machine code
217 fn compile_function(
218 &self,
219 name: &str,
220 ops: &[WasmOp],
221 config: &CompileConfig,
222 ) -> std::result::Result<CompiledFunction, BackendError>;
223
224 /// Check if this backend is available (external tools installed, etc.)
225 fn is_available(&self) -> bool;
226}
227
228/// Registry of available backends
229pub struct BackendRegistry {
230 backends: HashMap<String, Box<dyn Backend>>,
231}
232
233impl BackendRegistry {
234 pub fn new() -> Self {
235 Self {
236 backends: HashMap::new(),
237 }
238 }
239
240 /// Register a backend under its name
241 pub fn register(&mut self, backend: Box<dyn Backend>) {
242 let name = backend.name().to_string();
243 self.backends.insert(name, backend);
244 }
245
246 /// Get a backend by name
247 pub fn get(&self, name: &str) -> Option<&dyn Backend> {
248 self.backends.get(name).map(|b| b.as_ref())
249 }
250
251 /// List all registered backends
252 pub fn list(&self) -> Vec<&dyn Backend> {
253 self.backends.values().map(|b| b.as_ref()).collect()
254 }
255
256 /// List backends that are actually available (installed and working)
257 pub fn available(&self) -> Vec<&dyn Backend> {
258 self.backends
259 .values()
260 .filter(|b| b.is_available())
261 .map(|b| b.as_ref())
262 .collect()
263 }
264}
265
266impl Default for BackendRegistry {
267 fn default() -> Self {
268 Self::new()
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 #[test]
277 fn test_registry_empty() {
278 let reg = BackendRegistry::new();
279 assert!(reg.list().is_empty());
280 assert!(reg.available().is_empty());
281 assert!(reg.get("arm").is_none());
282 }
283
284 #[test]
285 fn test_compile_config_default() {
286 let config = CompileConfig::default();
287 assert_eq!(config.opt_level, 2);
288 assert!(!config.bounds_check);
289 assert_eq!(config.safety_bounds, SafetyBounds::None);
290 assert!(!config.no_optimize);
291 }
292
293 #[test]
294 fn safety_bounds_parse_round_trip() {
295 for s in ["none", "mpu", "software", "mask"] {
296 let sb = SafetyBounds::parse(s).unwrap();
297 assert_eq!(sb.as_str(), s);
298 }
299 assert_eq!(SafetyBounds::parse("pmp").unwrap(), SafetyBounds::Mpu);
300 assert_eq!(SafetyBounds::parse("soft").unwrap(), SafetyBounds::Software);
301 assert!(SafetyBounds::parse("nonsense").is_err());
302 }
303
304 #[test]
305 fn effective_safety_bounds_legacy_promotes_to_software() {
306 let cfg = CompileConfig {
307 bounds_check: true,
308 ..Default::default()
309 };
310 assert_eq!(cfg.effective_safety_bounds(), SafetyBounds::Software);
311 }
312
313 #[test]
314 fn effective_safety_bounds_new_field_wins() {
315 let cfg = CompileConfig {
316 bounds_check: true,
317 safety_bounds: SafetyBounds::Mpu,
318 ..Default::default()
319 };
320 assert_eq!(cfg.effective_safety_bounds(), SafetyBounds::Mpu);
321 }
322}