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    /// 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    /// #237: emit wasm function-static data as a base-independent `.data`
118    /// section (`__synth_wasm_data`) addressed via MOVW/MOVT symbol relocations,
119    /// so a host-pointer drop-in (linmem base = 0 for native `*ptr` derefs)
120    /// doesn't mis-resolve the statics. Off by default — only the leaves'
121    /// base-relative `[R11+const]` path is used unless explicitly requested.
122    pub native_pointer_abi: bool,
123
124    /// #237: wasm linear-memory minimum size in bytes — the full static-data
125    /// extent (initialized `(data)` segments plus the zero-init/BSS region).
126    /// Under `native_pointer_abi`, a const memory address below this is a wasm
127    /// static → symbol-relative; any address beyond it is a runtime host pointer
128    /// → `[R11=0 + addr]`.
129    pub linear_memory_bytes: u32,
130
131    /// #237: the wasm stack-pointer global as `(index, init_value)`, if the
132    /// module has one. Under `native_pointer_abi` the backend register-promotes
133    /// it: `global.get` materializes `__synth_wasm_data + init` (the real stack
134    /// top) and the init value doubles as the static-data base that separates
135    /// pointer consts (`>= init`) from frame-size scalars (`< init`).
136    pub stack_pointer_global: Option<(u32, i32)>,
137    /// #311: per-function (full index) / per-type "returns i64" — the call
138    /// lowering must tag i64 results as a register pair or the hi half is
139    /// invisible to liveness.
140    pub func_ret_i64: Vec<bool>,
141    pub type_ret_i64: Vec<bool>,
142    /// #359: declared parameter widths per *function* (full index, imports
143    /// first): `func_params_i64[f][k]` is true when param `k` of function `f` is
144    /// i64/f64. The AAPCS stack-argument path needs the *declared* widths
145    /// (op-stream inference can't see an unused i64 param that still shifts the
146    /// incoming-stack layout). The source of truth — a per-function driver loop
147    /// (`compile_module` / the CLI loop) indexes it by `func.index` and copies
148    /// the slice into [`current_func_params_i64`] before each `compile_function`.
149    /// Empty → every param assumed i32 (the legacy path; keeps every function
150    /// with <=4 params, or all-i32 params, byte-identical).
151    pub func_params_i64: Vec<Vec<bool>>,
152    /// #359: declared parameter widths of the function CURRENTLY being compiled
153    /// — `current_func_params_i64[k]` is true when param `k` is i64/f64. Set per
154    /// function (a cheap clone of the config) from [`func_params_i64`] by the
155    /// driver loop, because `compile_function` is shared across backends and
156    /// carries no function index. Empty → assume i32.
157    pub current_func_params_i64: Vec<bool>,
158    /// #509: blocktype-arity side-table of the function CURRENTLY being compiled
159    /// — `(param_count, result_count)` of the k-th `Block`/`Loop`/`If` in its op
160    /// stream (ordinal-keyed; see [`FunctionOps::block_arity`]). Set per function
161    /// by the driver loop (like [`current_func_params_i64`]). The direct selector
162    /// uses it to land a value carried by `br`/`br_if`/`br_table` in the target
163    /// block's designated result register instead of dropping it. Empty → every
164    /// block treated as void (the legacy lowering; hand-built op streams).
165    ///
166    /// [`FunctionOps::block_arity`]: crate::wasm_decoder::FunctionOps::block_arity
167    pub current_func_block_arity: Vec<(u8, u8)>,
168
169    /// #543 Phase 1 — integrator-marked volatile linear-memory segments (the DMA
170    /// transfer window). Each range `[base, base+len)` names a region of the fused
171    /// linear memory that an EXTERNAL agent (the DMA engine, modelled by gale as a
172    /// Component-Model `own<buffer>` handoff — gale decision `DD-DMA-REGION-001`,
173    /// gale#124) rewrites out-of-band. Loads and stores whose address falls inside
174    /// a marked range must eventually be treated as VOLATILE: not cached, hoisted,
175    /// or reordered across the transfer boundary.
176    ///
177    /// PHASE-1 CONTRACT (this field is plumbing only): it is populated by the CLI
178    /// `--volatile-segment <base>:<len>` flag and threaded to codegen, but is NOT
179    /// yet consumed by any pass. Empty by default, so the emitted `.text` is
180    /// byte-identical with or without the flag (the frozen-codegen gate holds).
181    ///
182    /// PHASE-2 CONSUMPTION POINT (deferred, gated — issue #543): the optimizer's
183    /// address-caching passes must BACK OFF for any access inside these ranges —
184    /// specifically const-CSE (`SYNTH_CONST_CSE`, aliasing repeated address
185    /// constants) and the #468 base-CSE / const-address-fold (hoisting the linmem
186    /// base into R11 and folding `[R11,#imm]` loads), plus any load-reuse /
187    /// reorder. A load or store overlapping a volatile range must re-materialize
188    /// its address and re-issue the memory access at each occurrence, and no such
189    /// access may move across a marked boundary. See rivet `VCR-DMA-001`.
190    pub volatile_segments: Vec<VolatileRange>,
191}
192
193/// #543 — an integrator-marked volatile linear-memory segment (the DMA transfer
194/// window): the half-open byte range `[base, base + len)` of the fused linear
195/// memory that an external agent rewrites out-of-band. Parsed from the CLI
196/// `--volatile-segment <base>:<len>` flag. See [`CompileConfig::volatile_segments`]
197/// for the Phase-1/Phase-2 split.
198#[derive(Debug, Clone, Copy, PartialEq, Eq)]
199pub struct VolatileRange {
200    /// Start address of the volatile region, in linear-memory bytes.
201    pub base: u32,
202    /// Length of the volatile region, in bytes. The region is `[base, base+len)`.
203    pub len: u32,
204}
205
206impl CompileConfig {
207    /// Resolve the effective safety-bounds setting, honouring the legacy
208    /// `bounds_check` field as a fallback. Used by backends to pick the
209    /// inline-check shape.
210    pub fn effective_safety_bounds(&self) -> SafetyBounds {
211        match (self.safety_bounds, self.bounds_check) {
212            (SafetyBounds::None, true) => SafetyBounds::Software,
213            (s, _) => s,
214        }
215    }
216}
217
218impl Default for CompileConfig {
219    fn default() -> Self {
220        Self {
221            opt_level: 2,
222            target: TargetSpec::cortex_m4(),
223            bounds_check: false,
224            safety_bounds: SafetyBounds::None,
225            hardware: String::new(),
226            no_optimize: false,
227            loom_compat: false,
228            num_imports: 0,
229            func_arg_counts: Vec::new(),
230            type_arg_counts: Vec::new(),
231            relocatable: false,
232            native_pointer_abi: false,
233            linear_memory_bytes: 0,
234            stack_pointer_global: None,
235            func_ret_i64: Vec::new(),
236            type_ret_i64: Vec::new(),
237            func_params_i64: Vec::new(),
238            current_func_params_i64: Vec::new(),
239            // #509: empty ⇒ legacy void-block lowering (unit tests / hand-built
240            // op streams); the driver loops fill it per function.
241            current_func_block_arity: Vec::new(),
242            // #543 Phase 1: no volatile segments unless the CLI flag names them.
243            // Empty ⇒ inert ⇒ emitted bytes unchanged.
244            volatile_segments: Vec::new(),
245        }
246    }
247}
248
249/// A relocation entry produced during compilation
250///
251/// Records that a BL instruction at `offset` bytes into the function's code
252/// targets an external symbol (e.g., `__meld_dispatch_import`). The linker
253/// resolves these when combining the Synth object with the Kiln bridge.
254#[derive(Debug, Clone, Copy, PartialEq, Eq)]
255pub enum RelocKind {
256    /// R_ARM_THM_CALL — a Thumb BL call site (the default; #167).
257    ThmCall,
258    /// R_ARM_MOVW_ABS_NC — the MOVW half of a symbol-relative address (#237).
259    MovwAbs,
260    /// R_ARM_MOVT_ABS — the MOVT half of a symbol-relative address (#237).
261    MovtAbs,
262    /// R_ARM_ABS32 — a 32-bit absolute address held in a `.text` literal-pool
263    /// word, loaded via `LDR rX, [pc, #off]` (#345). The link-survivable
264    /// replacement for the inline-immediate MOVW/MOVT-ABS pair: `ld`/bfd patches
265    /// the data word at link time (`S + A`, the addend living in the word, REL
266    /// semantics), which survives placement into a large multi-object image —
267    /// whereas an inline-instruction MOVW_ABS immediate can be mangled.
268    Abs32,
269}
270
271#[derive(Debug, Clone, PartialEq, Eq)]
272pub struct CodeRelocation {
273    /// Byte offset within the function's machine code where the reloc applies
274    pub offset: u32,
275    /// Target symbol name (e.g., "__meld_dispatch_import", "__synth_wasm_data")
276    pub symbol: String,
277    /// Which ARM relocation type to emit for this site.
278    pub kind: RelocKind,
279}
280
281/// VCR-DBG-001: a per-instruction source map — `(machine_offset_within_code,
282/// wasm_op_index)` pairs, one per emitted machine instruction. A `None` op-index
283/// marks an instruction with no originating wasm op (prologue/epilogue, literal
284/// pool). Consumed by the DWARF `.debug_line` emitter; empty when no source map
285/// was produced.
286pub type LineMap = Vec<(u32, Option<usize>)>;
287
288/// A single compiled function
289#[derive(Debug, Clone)]
290pub struct CompiledFunction {
291    /// Function name (from WASM export or generated)
292    pub name: String,
293    /// Raw machine code bytes
294    pub code: Vec<u8>,
295    /// Original WASM ops (retained for verification)
296    pub wasm_ops: Vec<WasmOp>,
297    /// Relocations for external symbol references (BL to bridge functions)
298    pub relocations: Vec<CodeRelocation>,
299    /// VCR-DBG-001: per-instruction source map for DWARF `.debug_line` emission —
300    /// `(machine_offset_within_code, wasm_op_index)` captured at encode time, one
301    /// entry per emitted machine instruction. A `None` op-index marks an
302    /// instruction with no originating wasm op (prologue/epilogue, literal-pool
303    /// word). This is purely additive metadata: it is never serialized unless
304    /// `.debug_line` emission is requested, so the emitted `.text` is
305    /// byte-identical with or without it. Empty for backends/paths that do not
306    /// yet produce a source map (RISC-V, the optimized ARM path).
307    pub line_map: LineMap,
308}
309
310/// Result of compiling a full module
311#[derive(Debug)]
312pub struct CompilationResult {
313    /// Compiled functions
314    pub functions: Vec<CompiledFunction>,
315    /// Complete ELF binary (if backend produces one directly)
316    pub elf: Option<Vec<u8>>,
317    /// Name of the backend that produced this result
318    pub backend_name: String,
319}
320
321/// What a backend can and cannot do
322#[derive(Debug, Clone)]
323pub struct BackendCapabilities {
324    /// Backend produces complete ELF files (external backends like aWsm)
325    pub produces_elf: bool,
326    /// Backend supports per-rule verification (only our custom ARM backend)
327    pub supports_rule_verification: bool,
328    /// Backend supports binary-level verification (all backends via disassembly)
329    pub supports_binary_verification: bool,
330    /// Backend is an external tool (not a library)
331    pub is_external: bool,
332}
333
334/// Trait that every compilation backend implements
335pub trait Backend: Send + Sync {
336    /// Human-readable backend name
337    fn name(&self) -> &str;
338
339    /// What this backend can do
340    fn capabilities(&self) -> BackendCapabilities;
341
342    /// Which targets this backend supports
343    fn supported_targets(&self) -> Vec<TargetSpec>;
344
345    /// Compile an entire decoded WASM module
346    fn compile_module(
347        &self,
348        module: &DecodedModule,
349        config: &CompileConfig,
350    ) -> std::result::Result<CompilationResult, BackendError>;
351
352    /// Compile a single function from WASM ops to machine code
353    fn compile_function(
354        &self,
355        name: &str,
356        ops: &[WasmOp],
357        config: &CompileConfig,
358    ) -> std::result::Result<CompiledFunction, BackendError>;
359
360    /// Check if this backend is available (external tools installed, etc.)
361    fn is_available(&self) -> bool;
362}
363
364/// Registry of available backends
365pub struct BackendRegistry {
366    backends: HashMap<String, Box<dyn Backend>>,
367}
368
369impl BackendRegistry {
370    pub fn new() -> Self {
371        Self {
372            backends: HashMap::new(),
373        }
374    }
375
376    /// Register a backend under its name
377    pub fn register(&mut self, backend: Box<dyn Backend>) {
378        let name = backend.name().to_string();
379        self.backends.insert(name, backend);
380    }
381
382    /// Get a backend by name
383    pub fn get(&self, name: &str) -> Option<&dyn Backend> {
384        self.backends.get(name).map(|b| b.as_ref())
385    }
386
387    /// List all registered backends
388    pub fn list(&self) -> Vec<&dyn Backend> {
389        self.backends.values().map(|b| b.as_ref()).collect()
390    }
391
392    /// List backends that are actually available (installed and working)
393    pub fn available(&self) -> Vec<&dyn Backend> {
394        self.backends
395            .values()
396            .filter(|b| b.is_available())
397            .map(|b| b.as_ref())
398            .collect()
399    }
400}
401
402impl Default for BackendRegistry {
403    fn default() -> Self {
404        Self::new()
405    }
406}
407
408#[cfg(test)]
409mod tests {
410    use super::*;
411
412    #[test]
413    fn test_registry_empty() {
414        let reg = BackendRegistry::new();
415        assert!(reg.list().is_empty());
416        assert!(reg.available().is_empty());
417        assert!(reg.get("arm").is_none());
418    }
419
420    #[test]
421    fn test_compile_config_default() {
422        let config = CompileConfig::default();
423        assert_eq!(config.opt_level, 2);
424        assert!(!config.bounds_check);
425        assert_eq!(config.safety_bounds, SafetyBounds::None);
426        assert!(!config.no_optimize);
427    }
428
429    #[test]
430    fn safety_bounds_parse_round_trip() {
431        for s in ["none", "mpu", "software", "mask"] {
432            let sb = SafetyBounds::parse(s).unwrap();
433            assert_eq!(sb.as_str(), s);
434        }
435        assert_eq!(SafetyBounds::parse("pmp").unwrap(), SafetyBounds::Mpu);
436        assert_eq!(SafetyBounds::parse("soft").unwrap(), SafetyBounds::Software);
437        assert!(SafetyBounds::parse("nonsense").is_err());
438    }
439
440    #[test]
441    fn effective_safety_bounds_legacy_promotes_to_software() {
442        let cfg = CompileConfig {
443            bounds_check: true,
444            ..Default::default()
445        };
446        assert_eq!(cfg.effective_safety_bounds(), SafetyBounds::Software);
447    }
448
449    #[test]
450    fn effective_safety_bounds_new_field_wins() {
451        let cfg = CompileConfig {
452            bounds_check: true,
453            safety_bounds: SafetyBounds::Mpu,
454            ..Default::default()
455        };
456        assert_eq!(cfg.effective_safety_bounds(), SafetyBounds::Mpu);
457    }
458}