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 crate::wsc_facts::WscFact;
10use std::collections::HashMap;
11use thiserror::Error;
12
13/// Errors from backend compilation
14#[derive(Debug, Error)]
15pub enum BackendError {
16    #[error("compilation failed: {0}")]
17    CompilationFailed(String),
18
19    #[error("backend not available: {0}")]
20    NotAvailable(String),
21
22    #[error("unsupported configuration: {0}")]
23    UnsupportedConfig(String),
24
25    #[error("external tool error: {0}")]
26    ExternalToolError(String),
27}
28
29/// Memory-bounds safety strategy. Phase 1 of `docs/binary-safety-design.md` §3.1.
30///
31/// - `Mpu`/PMP: rely on hardware (ARM MPU or RV32 PMP) — no inline check.
32/// - `Software`: emit a `CMP/BHS Trap_Handler` (ARM) or `bgeu addr, mem_size, ebreak` (RV32)
33///   before every load/store.
34/// - `Mask`: emit `AND addr, addr, #(mem_size - 1)` — only valid when memory size
35///   is a power of two. Wraps on OOB rather than trapping (fuzz-profile semantics).
36/// - `None`: no bounds enforcement.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
38pub enum SafetyBounds {
39    /// No bounds check (caller assumes the WASM module is trusted)
40    #[default]
41    None,
42    /// ARM MPU / RV32 PMP — hardware enforcement, no inline guard
43    Mpu,
44    /// Software CMP/BHS (ARM) or BGEU+EBREAK (RV32) per access
45    Software,
46    /// AND-mask, requires power-of-two memory size
47    Mask,
48}
49
50impl SafetyBounds {
51    /// Parse the `--safety-bounds` argument value.
52    pub fn parse(s: &str) -> std::result::Result<Self, String> {
53        match s {
54            "none" => Ok(SafetyBounds::None),
55            "mpu" | "pmp" => Ok(SafetyBounds::Mpu),
56            "software" | "soft" => Ok(SafetyBounds::Software),
57            "mask" | "masking" => Ok(SafetyBounds::Mask),
58            other => Err(format!(
59                "unknown --safety-bounds value '{}'; expected one of: none, mpu, software, mask",
60                other
61            )),
62        }
63    }
64
65    /// String form used in the safety manifest.
66    pub fn as_str(self) -> &'static str {
67        match self {
68            SafetyBounds::None => "none",
69            SafetyBounds::Mpu => "mpu",
70            SafetyBounds::Software => "software",
71            SafetyBounds::Mask => "mask",
72        }
73    }
74}
75
76/// The absolute SRAM address the OPTIMIZED (non-relocatable) ARM path
77/// materializes as its linear-memory base (`MOVW/MOVT R12, #base` before each
78/// const-address access, and the #468 base-CSE R11 hoist). Historical value:
79/// 256 bytes above the SRAM start — the differential-harness contract for
80/// optimized-path fixtures maps linmem here. `CompileConfig::linmem_base`
81/// defaults to this; `--stack-layout=low` (#687) shifts it up by the reserved
82/// stack size so the moved layout reaches user code, not just the startup.
83pub const OPTIMIZED_LINMEM_BASE: u32 = 0x2000_0100;
84
85/// Configuration for a compilation run
86#[derive(Debug, Clone)]
87pub struct CompileConfig {
88    /// Optimization level (0 = none, 1 = fast, 2 = default, 3 = aggressive)
89    pub opt_level: u8,
90    /// Target specification
91    pub target: TargetSpec,
92    /// Legacy: enable software bounds checking for memory operations.
93    /// Deprecated in favor of `safety_bounds`. When set, equivalent to
94    /// `SafetyBounds::Software`. Kept for backwards compatibility with
95    /// callers that haven't migrated yet.
96    pub bounds_check: bool,
97    /// Phase-1 unified safety-bounds knob. If `bounds_check` is `true` and
98    /// this is `None`, the legacy field wins (back-compat). If both are set,
99    /// `safety_bounds` wins.
100    pub safety_bounds: SafetyBounds,
101    /// Hardware profile name (e.g. "nrf52840", "stm32f407")
102    pub hardware: String,
103    /// Skip optimization passes (direct instruction selection)
104    pub no_optimize: bool,
105    /// Use Loom-compatible optimization preset
106    pub loom_compat: bool,
107    /// Number of imported functions (calls to indices below this use Meld dispatch)
108    pub num_imports: u32,
109    /// AAPCS integer-argument count per function, indexed by full WASM function
110    /// index (imports first, then locals). Lets `Call` marshal the right number
111    /// of operand-stack values into R0–R3 (issue #195). Empty = pass no args
112    /// (pre-#195 behaviour).
113    pub func_arg_counts: Vec<u32>,
114    /// AAPCS integer-argument count per function type, indexed by type index.
115    /// Used by `call_indirect` (issue #195).
116    pub type_arg_counts: Vec<u32>,
117    /// Produce relocatable (ET_REL) host-link output. When set, the backend
118    /// uses the direct instruction selector (`select_with_stack`) rather than
119    /// the optimized path: the optimizer materializes an *absolute* linear-
120    /// memory base (0x20000100) and does not preserve caller-saved registers
121    /// across calls, both wrong for a host-linked object where the linmem base
122    /// is supplied via `fp` at runtime and callees follow AAPCS. Imports are
123    /// also emitted as direct `func_N` BLs (resolved to the wasm field name)
124    /// instead of `__meld_dispatch_import`. (#197 — follow-up to #188/#171.)
125    pub relocatable: bool,
126
127    /// #275: the SELF-CONTAINED Thumb-2 `--cortex-m` image path lowers
128    /// `call_indirect` through a flash-resident funcref table addressed
129    /// PC-RELATIVE (an `LdrSym` literal-pool pointer to
130    /// [`FUNC_TABLE_SYMBOL`]) — NEVER through R11, which is the linear-memory
131    /// base (the v0.42 #717 collision). Set by the CLI ONLY when the image
132    /// builder that emits and patches that table
133    /// (`build_multi_func_cortex_m_elf`) will run: Cortex-M family, not
134    /// `--relocatable`, no imported functions. Every other self-contained
135    /// configuration keeps the loud #275 decline. Default `false`.
136    pub self_contained_funcref_table: bool,
137
138    /// #687 (`--stack-layout=low`): the absolute linear-memory base the
139    /// OPTIMIZED ARM path materializes into user code. Defaults to
140    /// [`OPTIMIZED_LINMEM_BASE`] (`0x2000_0100`, byte-identical to every
141    /// pre-#687 compile). Under the low stack layout the CLI shifts it up by
142    /// the reserved stack size so const-address loads/stores land in the moved
143    /// linear memory instead of the stack region. Only the optimized
144    /// (non-relocatable) path consumes it — the direct selector is R11/fp
145    /// - relative and follows the startup's R11 init instead.
146    pub linmem_base: u32,
147
148    /// #237: emit wasm function-static data as a base-independent `.data`
149    /// section (`__synth_wasm_data`) addressed via MOVW/MOVT symbol relocations,
150    /// so a host-pointer drop-in (linmem base = 0 for native `*ptr` derefs)
151    /// doesn't mis-resolve the statics. Off by default — only the leaves'
152    /// base-relative `[R11+const]` path is used unless explicitly requested.
153    pub native_pointer_abi: bool,
154
155    /// #237: wasm linear-memory minimum size in bytes — the full static-data
156    /// extent (initialized `(data)` segments plus the zero-init/BSS region).
157    /// Under `native_pointer_abi`, a const memory address below this is a wasm
158    /// static → symbol-relative; any address beyond it is a runtime host pointer
159    /// → `[R11=0 + addr]`.
160    pub linear_memory_bytes: u32,
161
162    /// VCR-MEM-002 phase 1 (#406): initial size in 64 KiB pages of EACH linear
163    /// memory, indexed by memory index. Consulted only by the multi-memory
164    /// lowering arms (loads/stores wrapped in `WasmOp::MultiMemory`,
165    /// `memory.size`/`grow` with a non-zero index) — memory-0 lowering never
166    /// reads it, so single-memory output is byte-identical whether it is set
167    /// or empty. Empty (the default) means "no multi-memory context": any
168    /// multi-memory op then declines loudly.
169    pub memory_pages: Vec<u32>,
170
171    /// #237: the wasm stack-pointer global as `(index, init_value)`, if the
172    /// module has one. Under `native_pointer_abi` the backend register-promotes
173    /// it: `global.get` materializes `__synth_wasm_data + init` (the real stack
174    /// top) and the init value doubles as the static-data base that separates
175    /// pointer consts (`>= init`) from frame-size scalars (`< init`).
176    pub stack_pointer_global: Option<(u32, i32)>,
177    /// #311: per-function (full index) / per-type "returns i64" — the call
178    /// lowering must tag i64 results as a register pair or the hi half is
179    /// invisible to liveness.
180    pub func_ret_i64: Vec<bool>,
181    pub type_ret_i64: Vec<bool>,
182    /// #643: byte width of each defined global's storage slot, indexed by
183    /// global index — 4 for i32/f32, 8 for i64/f64, 16 for v128 (from the
184    /// module's global section). The globals table is laid out by SUMMING
185    /// these widths: an i64 global needs a register-PAIR store/load at
186    /// `[R9, off]`/`[R9, off+4]`, and every later global's offset shifts.
187    /// Empty ⇒ every global assumed 4 bytes (the legacy `idx * 4` layout;
188    /// hand-built op streams and i32-only modules are byte-identical).
189    pub global_widths: Vec<u32>,
190    /// #359: declared parameter widths per *function* (full index, imports
191    /// first): `func_params_i64[f][k]` is true when param `k` of function `f` is
192    /// i64/f64. The AAPCS stack-argument path needs the *declared* widths
193    /// (op-stream inference can't see an unused i64 param that still shifts the
194    /// incoming-stack layout). The source of truth — a per-function driver loop
195    /// (`compile_module` / the CLI loop) indexes it by `func.index` and copies
196    /// the slice into [`current_func_params_i64`] before each `compile_function`.
197    /// Empty → every param assumed i32 (the legacy path; keeps every function
198    /// with <=4 params, or all-i32 params, byte-identical).
199    pub func_params_i64: Vec<Vec<bool>>,
200    /// #359: declared parameter widths of the function CURRENTLY being compiled
201    /// — `current_func_params_i64[k]` is true when param `k` is i64/f64. Set per
202    /// function (a cheap clone of the config) from [`func_params_i64`] by the
203    /// driver loop, because `compile_function` is shared across backends and
204    /// carries no function index. Empty → assume i32.
205    pub current_func_params_i64: Vec<bool>,
206    /// GI-FPU-002 (#619/#369): per-function declared f32-param mask (full index,
207    /// imports first). The driver copies `func_params_f32[f]` into
208    /// [`current_func_params_f32`] before each `compile_function`. Empty ⇒
209    /// all-non-f32 (byte-identical to before).
210    pub func_params_f32: Vec<Vec<bool>>,
211    /// GI-FPU-002: declared f32-param mask of the function CURRENTLY being
212    /// compiled — `current_func_params_f32[k]` is true when param `k` is f32.
213    /// Set per function from [`func_params_f32`], mirroring
214    /// [`current_func_params_i64`]. Empty ⇒ no f32 params.
215    pub current_func_params_f32: Vec<bool>,
216    /// GI-FPU-002 phase 2 (#369): per-function declared f64-param mask (full
217    /// index, imports first) and the CURRENT function's slice. Hard-float
218    /// targets decline f64-param functions loudly — the legacy width
219    /// inference treats an f64 param as an i64 CORE-register pair, which
220    /// reads the wrong registers under AAPCS-VFP (the caller put it in a
221    /// D-register). Empty ⇒ no f64 params (byte-identical legacy path).
222    pub func_params_f64: Vec<Vec<bool>>,
223    /// See [`func_params_f64`](Self::func_params_f64).
224    pub current_func_params_f64: Vec<bool>,
225    /// GI-FPU-002 phase 2 (#719/#369): whether the function CURRENTLY being
226    /// compiled returns f32. Set per function from the decoder's `func_ret_f32`.
227    /// The direct selector's epilogue uses it to loudly decline a result that
228    /// reaches the return in a core register instead of an S-register (a call
229    /// that returned f32 as integer-tagged R0 would otherwise be a silent
230    /// miscompile — the AAPCS-VFP caller reads S0). `false` for hand-built op
231    /// streams / non-f32 returns (byte-identical to before).
232    pub current_func_ret_f32: bool,
233    /// GI-FPU-002 phase 2 (#719/#369): whether the function CURRENTLY being
234    /// compiled returns f64 (D0 under AAPCS-VFP). Same epilogue-soundness role.
235    pub current_func_ret_f64: bool,
236    /// GI-FPU-002 phase 2 (#719/#369): per-function (full index, imports first)
237    /// "returns f32/f64" tables. The direct selector declines a `call` to an
238    /// f32/f64-returning callee LOUDLY at the call site — the result arrives in
239    /// S0/D0 (AAPCS-VFP), which this increment does not marshal into the operand
240    /// stack; tagging it as an integer R0 would be a silent miscompile. Also the
241    /// source for [`current_func_ret_f32`]/[`current_func_ret_f64`] in the
242    /// per-function driver loops. Empty ⇒ callees assumed non-float-returning
243    /// (hand-built op streams; byte-identical legacy behaviour).
244    pub func_ret_f32: Vec<bool>,
245    /// See [`func_ret_f32`](Self::func_ret_f32).
246    pub func_ret_f64: Vec<bool>,
247    /// GI-FPU-002 phase 2 (#719/#369): per-type "returns f32/f64" — the
248    /// `call_indirect` analogue of [`func_ret_f32`](Self::func_ret_f32).
249    pub type_ret_f32: Vec<bool>,
250    /// See [`type_ret_f32`](Self::type_ret_f32).
251    pub type_ret_f64: Vec<bool>,
252    /// #457: DECLARED parameter count of the function CURRENTLY being compiled,
253    /// from the module's type section (`func_arg_counts[func.index]`). Set per
254    /// function by the driver loops like [`current_func_params_i64`].
255    ///
256    /// The backends otherwise INFER the param count from local-access patterns
257    /// (`count_params`: a local whose first access is a read is assumed to be a
258    /// param) — which cannot distinguish a param from a read-before-write
259    /// non-param local. WASM zero-initializes non-param locals, so such a local
260    /// must read 0; the inference instead homed it in a parameter register and
261    /// read caller garbage (#457). The backends cap the inferred count at this
262    /// declared count when it is present, which reclassifies exactly the
263    /// read-before-write locals (an inferred count can only exceed the declared
264    /// one via a read-first index >= the declared count) and leaves every other
265    /// function's codegen byte-identical.
266    ///
267    /// `None` → declared signature unknown (hand-built op streams, direct
268    /// `compile_function` callers) → pure inference, the legacy behaviour.
269    pub current_func_param_count: Option<u32>,
270    /// #509: blocktype-arity side-table of the function CURRENTLY being compiled
271    /// — `(param_count, result_count)` of the k-th `Block`/`Loop`/`If` in its op
272    /// stream (ordinal-keyed; see [`FunctionOps::block_arity`]). Set per function
273    /// by the driver loop (like [`current_func_params_i64`]). The direct selector
274    /// uses it to land a value carried by `br`/`br_if`/`br_table` in the target
275    /// block's designated result register instead of dropping it. Empty → every
276    /// block treated as void (the legacy lowering; hand-built op streams).
277    ///
278    /// [`FunctionOps::block_arity`]: crate::wasm_decoder::FunctionOps::block_arity
279    pub current_func_block_arity: Vec<(u8, u8)>,
280
281    /// #543 Phase 1 — integrator-marked volatile linear-memory segments (the DMA
282    /// transfer window). Each range `[base, base+len)` names a region of the fused
283    /// linear memory that an EXTERNAL agent (the DMA engine, modelled by gale as a
284    /// Component-Model `own<buffer>` handoff — gale decision `DD-DMA-REGION-001`,
285    /// gale#124) rewrites out-of-band. Loads and stores whose address falls inside
286    /// a marked range must eventually be treated as VOLATILE: not cached, hoisted,
287    /// or reordered across the transfer boundary.
288    ///
289    /// PHASE-2 CONTRACT (implemented — issue #543): the optimizer's
290    /// address-caching passes HONOR these ranges. Consumption points:
291    ///  - the #468 base-CSE / const-address-fold
292    ///    (`optimizer_bridge::plan_base_cse`, DEFAULT-ON, opt-out
293    ///    `SYNTH_BASE_CSE=0`): a const-address access whose 4-byte window
294    ///    intersects a marked range is EXCLUDED from the fold set — it keeps
295    ///    its verbatim per-access materialize-and-access codegen, while
296    ///    accesses outside the range still fold;
297    ///  - const-CSE (`liveness::apply_const_cse` wired in `arm_backend.rs`,
298    ///    DEFAULT-ON, opt-out `SYNTH_CONST_CSE=0`; the former bridge-level
299    ///    inline cache is retired, #242): declines WHOLESALE while any range is
300    ///    marked — a cached constant cannot be classified address-vs-data at
301    ///    that level, so the conservative stance for statically-unknown
302    ///    addressing is to re-materialize every constant at each occurrence.
303    ///
304    /// Passes that only touch SP-relative frame slots (stack-reload forwarding,
305    /// frame-slot DCE, spill re-choice) are unaffected by design: these ranges
306    /// are LINEAR-MEMORY addresses, and frame slots are never linmem. Nothing on
307    /// the pipeline deletes, forwards, or reorders a linear-memory access (IR CSE
308    /// deliberately never CSEs `MemLoad`s; DCE removes only unreachable blocks),
309    /// so every marked access is issued verbatim, in program order.
310    ///
311    /// Empty (the default): zero behavior change by construction — every gate
312    /// reduces to the pre-#543 path, so the emitted `.text` is byte-identical
313    /// with or without this code (the frozen-codegen gate holds). See rivet
314    /// `VCR-DMA-001`.
315    pub volatile_segments: Vec<VolatileRange>,
316
317    /// #778 phase 2 — the parsed `--wcet-hints` file (UNTRUSTED per-function
318    /// loop-bound hints, the scry seam). Consulted ONLY by the WCET sidecar
319    /// computation over the final instruction stream; NEVER by codegen — the
320    /// emitted bytes are byte-identical with or without hints. Every hint is
321    /// soundly verified before use and rejected with a machine reason
322    /// otherwise.
323    pub wcet_hints: Option<crate::wcet::WcetHints>,
324
325    /// VCR-PERF-002 Phase 1 (#494) — proven invariants forwarded by loom in
326    /// the `wsc.facts` custom section (encoding:
327    /// `docs/design/wsc-facts-encoding.md`; program:
328    /// `docs/design/proof-carrying-specialization.md`), whole-module table
329    /// keyed by `(func_index, value_id)`. The compile driver copies the
330    /// current function's slice into [`current_func_facts`] (the
331    /// `func_params_i64` → `current_func_params_i64` pattern), because
332    /// `compile_function` carries no function index.
333    ///
334    /// PHASE-1 CONTRACT: threaded but NOT consumed — no codegen path reads
335    /// facts, so emitted bytes are unchanged whether or not the module
336    /// carries the section (locked by `wsc_facts_ingestion_494.rs`). Phase 2
337    /// turns each fact into a premise for a flag-gated (`SYNTH_FACT_SPEC`),
338    /// per-elision ordeal-validated specialization; the facts-absent compile
339    /// stays byte-identical by construction (empty ⇒ every gate vacuous).
340    ///
341    /// [`current_func_facts`]: CompileConfig::current_func_facts
342    pub wsc_facts: Vec<WscFact>,
343    /// VCR-PERF-002 Phase 1 (#494): the `wsc.facts` invariants of the function
344    /// CURRENTLY being compiled (`fact.func_index == func.index`), set per
345    /// function by the driver loops like [`current_func_params_i64`]. This is
346    /// the field a Phase-2 selector pass will read its premises from. Empty →
347    /// no facts → no specialization may ever fire (the fail-safe default).
348    ///
349    /// [`current_func_params_i64`]: CompileConfig::current_func_params_i64
350    pub current_func_facts: Vec<WscFact>,
351    /// VCR-PERF-002 Phase 2b (#494, divisor-nonzero): op indices (into the op
352    /// stream passed to `compile_function`) of `div`/`rem` ops whose
353    /// DIVIDE-BY-ZERO trap guard is proven dead — the fact-spec pass
354    /// discharged `UNSAT(P ∧ divisor == 0)` per site through the
355    /// certificate-checked ordeal solver BEFORE the driver set this field.
356    /// Consumed by the ARM direct selector (`select_with_stack`); every other
357    /// path ignores it (guards stay — sound). Empty (the default) ⇒ every
358    /// guard is emitted, byte-identical to today.
359    pub fact_div_zero_elide: Vec<usize>,
360    /// VCR-PERF-002 Phase 2b (#494): op indices of `div_s` ops whose
361    /// `INT_MIN / -1` OVERFLOW trap guard is proven dead — a SEPARATE
362    /// obligation (`UNSAT(P ∧ dividend == INT_MIN ∧ divisor == -1)`). A
363    /// divisor-nonzero fact alone NEVER lands here: divisor ≠ 0 does not
364    /// exclude -1 (#633/#634 two-guard distinction). Empty ⇒ guard emitted.
365    pub fact_div_ovf_elide: Vec<usize>,
366    /// #494 bounds-elision (#390 `guard_bool`): op indices of i32 memory
367    /// accesses whose `--safety-bounds software` inline guard is proven dead
368    /// — the fact-spec pass discharged
369    /// `UNSAT(P ∧ trap_mem_oob(zext64(index) + offset, size,
370    /// min_memory_bytes))` per site through the certificate-checked ordeal
371    /// solver BEFORE the driver set this field (ordeal 0.9.1 `trap_mem_oob`
372    /// shape, wraparound-safe 64-bit extension). Consumed by the ARM direct
373    /// selector (`select_with_stack`); every other path ignores it (guards
374    /// stay — sound). Empty (the default) ⇒ every guard is emitted,
375    /// byte-identical to today.
376    pub fact_mem_bounds_elide: Vec<usize>,
377    /// #642: `call_indirect` guard inputs — the compile-time table size for
378    /// the runtime bounds check and the per-expected-type closed-world type
379    /// verdicts — computed from the decoded module by
380    /// [`crate::wasm_decoder::DecodedModule::call_indirect_guards`] and set by
381    /// the driver loops. The default (`table_size: None`, empty verdicts)
382    /// DECLINES every `call_indirect` lowering: an unchecked indirect branch
383    /// is never emitted (WASM Core §4.4.8 requires OOB/type-mismatch traps).
384    pub call_indirect_guards: crate::wasm_decoder::CallIndirectGuards,
385}
386
387/// #543 — an integrator-marked volatile linear-memory segment (the DMA transfer
388/// window): the half-open byte range `[base, base + len)` of the fused linear
389/// memory that an external agent rewrites out-of-band. Parsed from the CLI
390/// `--volatile-segment <base>:<len>` flag. See [`CompileConfig::volatile_segments`]
391/// for the Phase-1/Phase-2 split.
392#[derive(Debug, Clone, Copy, PartialEq, Eq)]
393pub struct VolatileRange {
394    /// Start address of the volatile region, in linear-memory bytes.
395    pub base: u32,
396    /// Length of the volatile region, in bytes. The region is `[base, base+len)`.
397    pub len: u32,
398}
399
400impl CompileConfig {
401    /// Resolve the effective safety-bounds setting, honouring the legacy
402    /// `bounds_check` field as a fallback. Used by backends to pick the
403    /// inline-check shape.
404    pub fn effective_safety_bounds(&self) -> SafetyBounds {
405        match (self.safety_bounds, self.bounds_check) {
406            (SafetyBounds::None, true) => SafetyBounds::Software,
407            (s, _) => s,
408        }
409    }
410}
411
412impl Default for CompileConfig {
413    fn default() -> Self {
414        Self {
415            opt_level: 2,
416            target: TargetSpec::cortex_m4(),
417            bounds_check: false,
418            safety_bounds: SafetyBounds::None,
419            hardware: String::new(),
420            no_optimize: false,
421            loom_compat: false,
422            num_imports: 0,
423            func_arg_counts: Vec::new(),
424            type_arg_counts: Vec::new(),
425            relocatable: false,
426            // #275: self-contained funcref-table dispatch is opt-in by the
427            // CLI's cortex-m image path; everything else keeps the decline.
428            self_contained_funcref_table: false,
429            // #687: the historical optimized-path absolute base — every
430            // default compile stays byte-identical.
431            linmem_base: OPTIMIZED_LINMEM_BASE,
432            native_pointer_abi: false,
433            linear_memory_bytes: 0,
434            // #406: empty ⇒ no multi-memory context ⇒ multi-memory ops decline
435            // loudly; memory-0 lowering never reads it.
436            memory_pages: Vec::new(),
437            stack_pointer_global: None,
438            func_ret_i64: Vec::new(),
439            type_ret_i64: Vec::new(),
440            // #643: empty ⇒ legacy all-4-byte global slots (i32-only modules).
441            global_widths: Vec::new(),
442            func_params_i64: Vec::new(),
443            current_func_params_i64: Vec::new(),
444            func_params_f32: Vec::new(),
445            current_func_params_f32: Vec::new(),
446            // GI-FPU-002 phase 2 (#719/#369): false ⇒ non-float return (or a
447            // hand-built op stream); driver loops set it per function.
448            current_func_ret_f32: false,
449            current_func_ret_f64: false,
450            // GI-FPU-002 phase 2 (#719/#369): empty ⇒ callees assumed
451            // non-float-returning (hand-built op streams).
452            func_params_f64: Vec::new(),
453            current_func_params_f64: Vec::new(),
454            func_ret_f32: Vec::new(),
455            func_ret_f64: Vec::new(),
456            type_ret_f32: Vec::new(),
457            type_ret_f64: Vec::new(),
458            // #457: None ⇒ declared signature unknown ⇒ param-count inference
459            // only (unit tests / hand-built op streams); driver loops fill it.
460            current_func_param_count: None,
461            // #509: empty ⇒ legacy void-block lowering (unit tests / hand-built
462            // op streams); the driver loops fill it per function.
463            current_func_block_arity: Vec::new(),
464            // #543 Phase 1: no volatile segments unless the CLI flag names them.
465            // Empty ⇒ inert ⇒ emitted bytes unchanged.
466            volatile_segments: Vec::new(),
467            // VCR-PERF-002 Phase 1 (#494): no facts unless the module carries
468            // a parseable `wsc.facts` section. Empty ⇒ inert (and Phase 1 has
469            // no consumer anyway) ⇒ emitted bytes unchanged.
470            wsc_facts: Vec::new(),
471            current_func_facts: Vec::new(),
472            // VCR-PERF-002 Phase 2b (#494): no guard-elision marks unless the
473            // fact-spec pass discharged the per-site obligations. Empty ⇒
474            // every div/rem trap guard is emitted, byte-identical.
475            fact_div_zero_elide: Vec::new(),
476            fact_div_ovf_elide: Vec::new(),
477            fact_mem_bounds_elide: Vec::new(),
478            // #642: no guard inputs ⇒ every call_indirect lowering declines
479            // loudly (never an unchecked indirect branch). Driver loops fill
480            // this from the decoded module.
481            call_indirect_guards: crate::wasm_decoder::CallIndirectGuards::default(),
482            // #778 phase 2: no --wcet-hints file ⇒ no hints. Consulted ONLY by
483            // the WCET sidecar computation — never by codegen (the emitted
484            // bytes are byte-identical with or without hints).
485            wcet_hints: None,
486        }
487    }
488}
489
490/// #275: the base symbol of the SELF-CONTAINED funcref table — the
491/// flash-resident region `build_multi_func_cortex_m_elf` appends after the
492/// function code: one 4-byte code pointer per table slot across ALL tables in
493/// declaration order (the same contiguous layout the `--relocatable` R11
494/// contract uses — `TableGuards::base_byte_offset` stays valid verbatim),
495/// null slots as ZERO words (#664), followed by the #676 type-id sidecar at
496/// `type_ids_byte_offset` when a heterogeneous table needs it. The dispatch
497/// reaches it through an `LdrSym` literal-pool word (an `Abs32` reloc against
498/// this symbol) that the image builder patches post-layout — never through
499/// R11, which is the linear-memory base (the #717 collision).
500pub const FUNC_TABLE_SYMBOL: &str = "__synth_func_table";
501
502/// A relocation entry produced during compilation
503///
504/// Records that a BL instruction at `offset` bytes into the function's code
505/// targets an external symbol (e.g., `__meld_dispatch_import`). The linker
506/// resolves these when combining the Synth object with the Kiln bridge.
507#[derive(Debug, Clone, Copy, PartialEq, Eq)]
508pub enum RelocKind {
509    /// R_ARM_THM_CALL — a Thumb BL call site (the default; #167).
510    ThmCall,
511    /// R_ARM_MOVW_ABS_NC — the MOVW half of a symbol-relative address (#237).
512    MovwAbs,
513    /// R_ARM_MOVT_ABS — the MOVT half of a symbol-relative address (#237).
514    MovtAbs,
515    /// R_ARM_ABS32 — a 32-bit absolute address held in a `.text` literal-pool
516    /// word, loaded via `LDR rX, [pc, #off]` (#345). The link-survivable
517    /// replacement for the inline-immediate MOVW/MOVT-ABS pair: `ld`/bfd patches
518    /// the data word at link time (`S + A`, the addend living in the word, REL
519    /// semantics), which survives placement into a large multi-object image —
520    /// whereas an inline-instruction MOVW_ABS immediate can be mangled.
521    Abs32,
522}
523
524#[derive(Debug, Clone, PartialEq, Eq)]
525pub struct CodeRelocation {
526    /// Byte offset within the function's machine code where the reloc applies
527    pub offset: u32,
528    /// Target symbol name (e.g., "__meld_dispatch_import", "__synth_wasm_data")
529    pub symbol: String,
530    /// Which ARM relocation type to emit for this site.
531    pub kind: RelocKind,
532}
533
534/// VCR-DBG-001: a per-instruction source map — `(machine_offset_within_code,
535/// wasm_op_index)` pairs, one per emitted machine instruction. A `None` op-index
536/// marks an instruction with no originating wasm op (prologue/epilogue, literal
537/// pool). Consumed by the DWARF `.debug_line` emitter; empty when no source map
538/// was produced.
539pub type LineMap = Vec<(u32, Option<usize>)>;
540
541/// VCR-DEC-003 (#396, witness#130): the object-level control-flow class of one
542/// emitted machine instruction, captured at encode time alongside [`LineMap`].
543/// It is the piece post-hoc CLI derivation cannot recover — `line_map` records
544/// which wasm op an instruction came from, but not whether that instruction IS a
545/// conditional branch, an unconditional branch, or a predicated (IT-block) move.
546/// The `synth-provenance-v1` emitter needs it to enumerate the ACTUAL object
547/// conditional branches (so it can prove "every object branch resolves to a
548/// source condition", not just "every source branch has an object PC").
549#[derive(Debug, Clone, Copy, PartialEq, Eq)]
550pub enum BranchClass {
551    /// A conditional branch (`Bcc`/`Blo`/`Bhs`/`BCondOffset`) — an object-level
552    /// decision point MC/DC must account for.
553    CondBranch,
554    /// An unconditional branch (`B`/`BOffset`) — control flow, not a decision.
555    UncondBranch,
556    /// A predicated conditional move (`SelectMove`, the IT-block form the
557    /// cmp→select fuse produces) — a folded decision with no branch.
558    Predicated,
559    /// Anything else (data-processing, load/store, call, prologue/epilogue).
560    Other,
561}
562
563/// VCR-DEC-003: per-instruction object-branch class, parallel to [`LineMap`]
564/// (same length, same order — one entry per emitted machine instruction).
565/// `(machine_offset_within_code, class)`. Empty when provenance is not being
566/// produced (never serialized into `.text`; frozen-safe additive metadata).
567pub type BranchMap = Vec<(u32, BranchClass)>;
568
569/// A single compiled function
570#[derive(Debug, Clone)]
571pub struct CompiledFunction {
572    /// Function name (from WASM export or generated)
573    pub name: String,
574    /// Raw machine code bytes
575    pub code: Vec<u8>,
576    /// Original WASM ops (retained for verification)
577    pub wasm_ops: Vec<WasmOp>,
578    /// Relocations for external symbol references (BL to bridge functions)
579    pub relocations: Vec<CodeRelocation>,
580    /// VCR-DBG-001: per-instruction source map for DWARF `.debug_line` emission —
581    /// `(machine_offset_within_code, wasm_op_index)` captured at encode time, one
582    /// entry per emitted machine instruction. A `None` op-index marks an
583    /// instruction with no originating wasm op (prologue/epilogue, literal-pool
584    /// word). This is purely additive metadata: it is never serialized unless
585    /// `.debug_line` emission is requested, so the emitted `.text` is
586    /// byte-identical with or without it. Empty for backends/paths that do not
587    /// yet produce a source map (RISC-V, the optimized ARM path).
588    pub line_map: LineMap,
589    /// VCR-DEC-003 (#396): per-instruction object-branch class, parallel to
590    /// `line_map`. Lets the `synth-provenance-v1` emitter enumerate the real
591    /// object conditional branches (not just re-walk the wasm branch ops).
592    /// Purely additive metadata: never serialized into `.text`, so emitted bytes
593    /// are byte-identical with or without it. Empty for backends/paths that do
594    /// not produce it (RISC-V, the optimized ARM path).
595    pub branch_map: BranchMap,
596    /// #778 (v0.46): the SOUND static worst-case-cycle bound for this function,
597    /// or a loud decline, computed over the final Thumb-2 instruction stream (see
598    /// [`crate::wcet`]). `Some` only when the ARM backend produced it (the RISC-V
599    /// and AArch64 backends carry no cycle model yet → `None`). Purely additive
600    /// metadata: derived from the already-decided instruction list, never
601    /// serialized into `.text`, so emitted bytes are byte-identical with or
602    /// without it (frozen-safe). Emitted as the `<output>.wcet.json` sidecar only
603    /// under `--emit-wcet`.
604    pub wcet: Option<crate::wcet::WcetFunction>,
605    /// #778 phase 3: the per-function WCET INTERMEDIATE (own-body cycles + direct
606    /// call sites, or a composition-independent decline) BEFORE inter-procedural
607    /// composition. The module driver composes these across the direct call graph
608    /// into the final per-function bounds (a caller's bound = its own body + each
609    /// direct callee's bound × the call site's proven execution count). `Some` only
610    /// on the Thumb-2 path that produced `wcet`. Purely additive, `.text`-invisible
611    /// (frozen-safe) — derived from the already-decided instruction list.
612    pub wcet_intermediate: Option<crate::wcet::WcetIntermediate>,
613}
614
615/// Result of compiling a full module
616#[derive(Debug)]
617pub struct CompilationResult {
618    /// Compiled functions
619    pub functions: Vec<CompiledFunction>,
620    /// Complete ELF binary (if backend produces one directly)
621    pub elf: Option<Vec<u8>>,
622    /// Name of the backend that produced this result
623    pub backend_name: String,
624}
625
626/// What a backend can and cannot do
627#[derive(Debug, Clone)]
628pub struct BackendCapabilities {
629    /// Backend produces complete ELF files (external backends like aWsm)
630    pub produces_elf: bool,
631    /// Backend supports per-rule verification (only our custom ARM backend)
632    pub supports_rule_verification: bool,
633    /// Backend supports binary-level verification (all backends via disassembly)
634    pub supports_binary_verification: bool,
635    /// Backend is an external tool (not a library)
636    pub is_external: bool,
637}
638
639/// Trait that every compilation backend implements
640pub trait Backend: Send + Sync {
641    /// Human-readable backend name
642    fn name(&self) -> &str;
643
644    /// What this backend can do
645    fn capabilities(&self) -> BackendCapabilities;
646
647    /// Which targets this backend supports
648    fn supported_targets(&self) -> Vec<TargetSpec>;
649
650    /// Compile an entire decoded WASM module
651    fn compile_module(
652        &self,
653        module: &DecodedModule,
654        config: &CompileConfig,
655    ) -> std::result::Result<CompilationResult, BackendError>;
656
657    /// Compile a single function from WASM ops to machine code
658    fn compile_function(
659        &self,
660        name: &str,
661        ops: &[WasmOp],
662        config: &CompileConfig,
663    ) -> std::result::Result<CompiledFunction, BackendError>;
664
665    /// Check if this backend is available (external tools installed, etc.)
666    fn is_available(&self) -> bool;
667}
668
669/// Registry of available backends
670pub struct BackendRegistry {
671    backends: HashMap<String, Box<dyn Backend>>,
672}
673
674impl BackendRegistry {
675    pub fn new() -> Self {
676        Self {
677            backends: HashMap::new(),
678        }
679    }
680
681    /// Register a backend under its name
682    pub fn register(&mut self, backend: Box<dyn Backend>) {
683        let name = backend.name().to_string();
684        self.backends.insert(name, backend);
685    }
686
687    /// Get a backend by name
688    pub fn get(&self, name: &str) -> Option<&dyn Backend> {
689        self.backends.get(name).map(|b| b.as_ref())
690    }
691
692    /// List all registered backends
693    pub fn list(&self) -> Vec<&dyn Backend> {
694        self.backends.values().map(|b| b.as_ref()).collect()
695    }
696
697    /// List backends that are actually available (installed and working)
698    pub fn available(&self) -> Vec<&dyn Backend> {
699        self.backends
700            .values()
701            .filter(|b| b.is_available())
702            .map(|b| b.as_ref())
703            .collect()
704    }
705}
706
707impl Default for BackendRegistry {
708    fn default() -> Self {
709        Self::new()
710    }
711}
712
713#[cfg(test)]
714mod tests {
715    use super::*;
716
717    #[test]
718    fn test_registry_empty() {
719        let reg = BackendRegistry::new();
720        assert!(reg.list().is_empty());
721        assert!(reg.available().is_empty());
722        assert!(reg.get("arm").is_none());
723    }
724
725    #[test]
726    fn test_compile_config_default() {
727        let config = CompileConfig::default();
728        assert_eq!(config.opt_level, 2);
729        assert!(!config.bounds_check);
730        assert_eq!(config.safety_bounds, SafetyBounds::None);
731        assert!(!config.no_optimize);
732    }
733
734    #[test]
735    fn safety_bounds_parse_round_trip() {
736        for s in ["none", "mpu", "software", "mask"] {
737            let sb = SafetyBounds::parse(s).unwrap();
738            assert_eq!(sb.as_str(), s);
739        }
740        assert_eq!(SafetyBounds::parse("pmp").unwrap(), SafetyBounds::Mpu);
741        assert_eq!(SafetyBounds::parse("soft").unwrap(), SafetyBounds::Software);
742        assert!(SafetyBounds::parse("nonsense").is_err());
743    }
744
745    #[test]
746    fn effective_safety_bounds_legacy_promotes_to_software() {
747        let cfg = CompileConfig {
748            bounds_check: true,
749            ..Default::default()
750        };
751        assert_eq!(cfg.effective_safety_bounds(), SafetyBounds::Software);
752    }
753
754    #[test]
755    fn effective_safety_bounds_new_field_wins() {
756        let cfg = CompileConfig {
757            bounds_check: true,
758            safety_bounds: SafetyBounds::Mpu,
759            ..Default::default()
760        };
761        assert_eq!(cfg.effective_safety_bounds(), SafetyBounds::Mpu);
762    }
763}