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" => Ok(SafetyBounds::Mpu),
56            // RQ-69-PMPLIB (#1317): `pmp` was a SILENT ALIAS for `mpu` here.
57            // v0.68 (#1284) made the CLI refuse it on every backend, but this
58            // is `synth-core`, a PUBLISHED crate — a library caller still got
59            // the alias the CLI rejects, and a unit test PINNED that. synth
60            // emits no RV32 PMP programming, so accepting the spelling and
61            // meaning something else is the accepted-and-dropped shape #1284
62            // closed at the outer layer and left open here.
63            //
64            // The WORDING is #1284's, verbatim. Those tests pin what the user
65            // sees; that contract did not change — only the LAYER enforcing it
66            // did. Keeping the phrasing means #1284's tests pass UNEDITED,
67            // which is the evidence that this moved the check without
68            // weakening it.
69            "pmp" => Err(
70                "--safety-bounds pmp is refused on every backend: synth emits no RV32 PMP \
71                 programming, and `pmp` is never a silent alias for `mpu`. The \
72                 only accepted MPU mode is --safety-bounds mpu --embedder-mpu on an \
73                 ARM relocatable object, where YOUR startup programs the MPU \
74                 from the region table (#1145, #1284, #1317)"
75                    .to_string(),
76            ),
77            "software" | "soft" => Ok(SafetyBounds::Software),
78            "mask" | "masking" => Ok(SafetyBounds::Mask),
79            other => Err(format!(
80                "unknown --safety-bounds value '{}'; expected one of: none, mpu, software, mask",
81                other
82            )),
83        }
84    }
85
86    /// String form used in the safety manifest.
87    pub fn as_str(self) -> &'static str {
88        match self {
89            SafetyBounds::None => "none",
90            SafetyBounds::Mpu => "mpu",
91            SafetyBounds::Software => "software",
92            SafetyBounds::Mask => "mask",
93        }
94    }
95}
96
97/// The absolute SRAM address the OPTIMIZED (non-relocatable) ARM path
98/// materializes as its linear-memory base (`MOVW/MOVT R12, #base` before each
99/// const-address access, and the #468 base-CSE R11 hoist). Historical value:
100/// 256 bytes above the SRAM start — the differential-harness contract for
101/// optimized-path fixtures maps linmem here. `CompileConfig::linmem_base`
102/// defaults to this; `--stack-layout=low` (#687) shifts it up by the reserved
103/// stack size so the moved layout reaches user code, not just the startup.
104pub const OPTIMIZED_LINMEM_BASE: u32 = 0x2000_0100;
105
106/// Configuration for a compilation run
107#[derive(Debug, Clone)]
108pub struct CompileConfig {
109    /// Optimization level (0 = none, 1 = fast, 2 = default, 3 = aggressive)
110    pub opt_level: u8,
111    /// Target specification
112    pub target: TargetSpec,
113    /// Legacy: enable software bounds checking for memory operations.
114    /// Deprecated in favor of `safety_bounds`. When set, equivalent to
115    /// `SafetyBounds::Software`. Kept for backwards compatibility with
116    /// callers that haven't migrated yet.
117    pub bounds_check: bool,
118    /// Phase-1 unified safety-bounds knob. If `bounds_check` is `true` and
119    /// this is `None`, the legacy field wins (back-compat). If both are set,
120    /// `safety_bounds` wins.
121    pub safety_bounds: SafetyBounds,
122    /// Hardware profile name (e.g. "nrf52840", "stm32f407")
123    pub hardware: String,
124    /// Skip optimization passes (direct instruction selection)
125    pub no_optimize: bool,
126    /// Use Loom-compatible optimization preset
127    pub loom_compat: bool,
128    /// Number of imported functions (calls to indices below this use Meld dispatch)
129    pub num_imports: u32,
130    /// AAPCS integer-argument count per function, indexed by full WASM function
131    /// index (imports first, then locals). Lets `Call` marshal the right number
132    /// of operand-stack values into R0–R3 (issue #195). Empty = pass no args
133    /// (pre-#195 behaviour).
134    pub func_arg_counts: Vec<u32>,
135    /// #851: result (return-value) count per function, indexed by full WASM
136    /// function index (imports first). `0` = void, `1` = one value. The AArch64
137    /// direct-`call` lowering needs the 0-vs-1 distinction to decide whether to
138    /// push the `x0` result — `func_ret_i64/f32/f64` carry the result TYPE but
139    /// conflate void and i32. Empty on backends/paths that do not lower calls
140    /// this way (byte-invisible there).
141    pub func_result_counts: Vec<u32>,
142    /// AAPCS integer-argument count per function type, indexed by type index.
143    /// Used by `call_indirect` (issue #195).
144    pub type_arg_counts: Vec<u32>,
145    /// Produce relocatable (ET_REL) host-link output. When set, the backend
146    /// uses the direct instruction selector (`select_with_stack`) rather than
147    /// the optimized path: the optimizer materializes an *absolute* linear-
148    /// memory base (0x20000100) and does not preserve caller-saved registers
149    /// across calls, both wrong for a host-linked object where the linmem base
150    /// is supplied via `fp` at runtime and callees follow AAPCS. Imports are
151    /// also emitted as direct `func_N` BLs (resolved to the wasm field name)
152    /// instead of `__meld_dispatch_import`. (#197 — follow-up to #188/#171.)
153    pub relocatable: bool,
154
155    /// #275: the SELF-CONTAINED Thumb-2 `--cortex-m` image path lowers
156    /// `call_indirect` through a flash-resident funcref table addressed
157    /// PC-RELATIVE (an `LdrSym` literal-pool pointer to
158    /// [`FUNC_TABLE_SYMBOL`]) — NEVER through R11, which is the linear-memory
159    /// base (the v0.42 #717 collision). Set by the CLI ONLY when the image
160    /// builder that emits and patches that table
161    /// (`build_multi_func_cortex_m_elf`) will run: Cortex-M family, not
162    /// `--relocatable`, no imported functions. Every other self-contained
163    /// configuration keeps the loud #275 decline. Default `false`.
164    pub self_contained_funcref_table: bool,
165
166    /// #687 (`--stack-layout=low`): the absolute linear-memory base the
167    /// OPTIMIZED ARM path materializes into user code. Defaults to
168    /// [`OPTIMIZED_LINMEM_BASE`] (`0x2000_0100`, byte-identical to every
169    /// pre-#687 compile). Under the low stack layout the CLI shifts it up by
170    /// the reserved stack size so const-address loads/stores land in the moved
171    /// linear memory instead of the stack region. Only the optimized
172    /// (non-relocatable) path consumes it — the direct selector is R11/fp
173    /// - relative and follows the startup's R11 init instead.
174    pub linmem_base: u32,
175
176    /// #237: emit wasm function-static data as a base-independent `.data`
177    /// section (`__synth_wasm_data`) addressed via MOVW/MOVT symbol relocations,
178    /// so a host-pointer drop-in (linmem base = 0 for native `*ptr` derefs)
179    /// doesn't mis-resolve the statics. Off by default — only the leaves'
180    /// base-relative `[R11+const]` path is used unless explicitly requested.
181    pub native_pointer_abi: bool,
182
183    /// #237: wasm linear-memory minimum size in bytes — the full static-data
184    /// extent (initialized `(data)` segments plus the zero-init/BSS region).
185    /// Under `native_pointer_abi`, a const memory address below this is a wasm
186    /// static → symbol-relative; any address beyond it is a runtime host pointer
187    /// → `[R11=0 + addr]`.
188    pub linear_memory_bytes: u32,
189
190    /// VCR-MEM-002 phase 1 (#406): initial size in 64 KiB pages of EACH linear
191    /// memory, indexed by memory index. Consulted only by the multi-memory
192    /// lowering arms (loads/stores wrapped in `WasmOp::MultiMemory`,
193    /// `memory.size`/`grow` with a non-zero index) — memory-0 lowering never
194    /// reads it, so single-memory output is byte-identical whether it is set
195    /// or empty. Empty (the default) means "no multi-memory context": any
196    /// multi-memory op then declines loudly.
197    pub memory_pages: Vec<u32>,
198
199    /// #1441: log2 of memory 0's DECLARED page size — 16 for the default
200    /// 64 KiB page, 0 for `(pagesize 1)` (the custom-page-sizes proposal
201    /// permits exactly those two). R10 holds memory 0's size in BYTES while
202    /// `memory.size` must answer in DECLARED pages, so this is the shift the
203    /// ARM lowering applies. Memory k > 0 needs no equivalent: its
204    /// `memory.size` materializes the declared PAGE COUNT as a constant, which
205    /// is page-size-independent.
206    pub memory0_page_log2: u32,
207
208    /// #237: the wasm stack-pointer global as `(index, init_value)`, if the
209    /// module has one. Under `native_pointer_abi` the backend register-promotes
210    /// it: `global.get` materializes `__synth_wasm_data + init` (the real stack
211    /// top) and the init value doubles as the static-data base that separates
212    /// pointer consts (`>= init`) from frame-size scalars (`< init`).
213    pub stack_pointer_global: Option<(u32, i32)>,
214    /// #311: per-function (full index) / per-type "returns i64" — the call
215    /// lowering must tag i64 results as a register pair or the hi half is
216    /// invisible to liveness.
217    pub func_ret_i64: Vec<bool>,
218    pub type_ret_i64: Vec<bool>,
219    /// #643: byte width of each defined global's storage slot, indexed by
220    /// global index — 4 for i32/f32, 8 for i64/f64, 16 for v128 (from the
221    /// module's global section). The globals table is laid out by SUMMING
222    /// these widths: an i64 global needs a register-PAIR store/load at
223    /// `[R9, off]`/`[R9, off+4]`, and every later global's offset shifts.
224    /// Empty ⇒ every global assumed 4 bytes (the legacy `idx * 4` layout;
225    /// hand-built op streams and i32-only modules are byte-identical).
226    pub global_widths: Vec<u32>,
227    /// #359: declared parameter widths per *function* (full index, imports
228    /// first): `func_params_i64[f][k]` is true when param `k` of function `f` is
229    /// i64/f64. The AAPCS stack-argument path needs the *declared* widths
230    /// (op-stream inference can't see an unused i64 param that still shifts the
231    /// incoming-stack layout). The source of truth — a per-function driver loop
232    /// (`compile_module` / the CLI loop) indexes it by `func.index` and copies
233    /// the slice into [`current_func_params_i64`] before each `compile_function`.
234    /// Empty → every param assumed i32 (the legacy path; keeps every function
235    /// with <=4 params, or all-i32 params, byte-identical).
236    pub func_params_i64: Vec<Vec<bool>>,
237    /// #359: declared parameter widths of the function CURRENTLY being compiled
238    /// — `current_func_params_i64[k]` is true when param `k` is i64/f64. Set per
239    /// function (a cheap clone of the config) from [`func_params_i64`] by the
240    /// driver loop, because `compile_function` is shared across backends and
241    /// carries no function index. Empty → assume i32.
242    pub current_func_params_i64: Vec<bool>,
243    /// GI-FPU-002 (#619/#369): per-function declared f32-param mask (full index,
244    /// imports first). The driver copies `func_params_f32[f]` into
245    /// [`current_func_params_f32`] before each `compile_function`. Empty ⇒
246    /// all-non-f32 (byte-identical to before).
247    pub func_params_f32: Vec<Vec<bool>>,
248    /// GI-FPU-002: declared f32-param mask of the function CURRENTLY being
249    /// compiled — `current_func_params_f32[k]` is true when param `k` is f32.
250    /// Set per function from [`func_params_f32`], mirroring
251    /// [`current_func_params_i64`]. Empty ⇒ no f32 params.
252    pub current_func_params_f32: Vec<bool>,
253    /// GI-FPU-002 phase 2 (#369): per-function declared f64-param mask (full
254    /// index, imports first) and the CURRENT function's slice. Hard-float
255    /// targets decline f64-param functions loudly — the legacy width
256    /// inference treats an f64 param as an i64 CORE-register pair, which
257    /// reads the wrong registers under AAPCS-VFP (the caller put it in a
258    /// D-register). Empty ⇒ no f64 params (byte-identical legacy path).
259    pub func_params_f64: Vec<Vec<bool>>,
260    /// See [`func_params_f64`](Self::func_params_f64).
261    pub current_func_params_f64: Vec<bool>,
262    /// GI-FPU-002 phase 2 (#719/#369): whether the function CURRENTLY being
263    /// compiled returns f32. Set per function from the decoder's `func_ret_f32`.
264    /// The direct selector's epilogue uses it to loudly decline a result that
265    /// reaches the return in a core register instead of an S-register (a call
266    /// that returned f32 as integer-tagged R0 would otherwise be a silent
267    /// miscompile — the AAPCS-VFP caller reads S0). `false` for hand-built op
268    /// streams / non-f32 returns (byte-identical to before).
269    pub current_func_ret_f32: bool,
270    /// GI-FPU-002 phase 2 (#719/#369): whether the function CURRENTLY being
271    /// compiled returns f64 (D0 under AAPCS-VFP). Same epilogue-soundness role.
272    pub current_func_ret_f64: bool,
273    /// GI-FPU-002 phase 2 (#719/#369): per-function (full index, imports first)
274    /// "returns f32/f64" tables. The direct selector declines a `call` to an
275    /// f32/f64-returning callee LOUDLY at the call site — the result arrives in
276    /// S0/D0 (AAPCS-VFP), which this increment does not marshal into the operand
277    /// stack; tagging it as an integer R0 would be a silent miscompile. Also the
278    /// source for [`current_func_ret_f32`]/[`current_func_ret_f64`] in the
279    /// per-function driver loops. Empty ⇒ callees assumed non-float-returning
280    /// (hand-built op streams; byte-identical legacy behaviour).
281    pub func_ret_f32: Vec<bool>,
282    /// See [`func_ret_f32`](Self::func_ret_f32).
283    pub func_ret_f64: Vec<bool>,
284    /// GI-FPU-002 phase 2 (#719/#369): per-type "returns f32/f64" — the
285    /// `call_indirect` analogue of [`func_ret_f32`](Self::func_ret_f32).
286    pub type_ret_f32: Vec<bool>,
287    /// See [`type_ret_f32`](Self::type_ret_f32).
288    pub type_ret_f64: Vec<bool>,
289    /// #457: DECLARED parameter count of the function CURRENTLY being compiled,
290    /// from the module's type section (`func_arg_counts[func.index]`). Set per
291    /// function by the driver loops like [`current_func_params_i64`].
292    ///
293    /// The backends otherwise INFER the param count from local-access patterns
294    /// (`count_params`: a local whose first access is a read is assumed to be a
295    /// param) — which cannot distinguish a param from a read-before-write
296    /// non-param local. WASM zero-initializes non-param locals, so such a local
297    /// must read 0; the inference instead homed it in a parameter register and
298    /// read caller garbage (#457).
299    ///
300    /// When this is `Some(declared)`, every backend uses
301    /// `min(`[`referenced_locals`](crate::referenced_locals)`(ops), declared)`
302    /// — the highest index the body touches, clamped by the signature. That is
303    /// EXACT in both directions: a genuine non-param local can never be
304    /// mistaken for a param (the clamp), and a param can never be demoted to a
305    /// local (the max over ALL accesses, reads and writes alike). The earlier
306    /// rule capped the READ-FIRST inference instead, which demoted a
307    /// conditionally-written param and produced an uninitialised-frame-slot
308    /// read on ARM and RISC-V and a zero-init local on AArch64 (#970/#851).
309    ///
310    /// `None` → declared signature unknown (hand-built op streams, direct
311    /// `compile_function` callers) → pure inference, the legacy behaviour.
312    /// HONEST RESIDUAL (#970, unchanged from #851): on that path a write-first
313    /// index is genuinely AMBIGUOUS — a param whose incoming value is dead, or
314    /// a non-param local — and both readings can be wrong. The read-first rule
315    /// keeps the #457 behaviour rather than reading caller garbage for a
316    /// zero-init local. The CLI always supplies a declared count.
317    pub current_func_param_count: Option<u32>,
318    /// (#778 phase 4 / #49) The WASM index of the function CURRENTLY being compiled,
319    /// so the WCET pass can identify this function's OWN `func_<idx>` self-call label
320    /// (a self-recursive `BL func_N` where N == this index) and prove/decline the
321    /// self-recursion depth. Set per function by the driver loop (like
322    /// [`current_func_params_i64`]). `None` → unknown (hand-built op streams, direct
323    /// `compile_function` callers) → no self-recursion certificate is attempted.
324    pub current_func_index: Option<u32>,
325    /// #509: blocktype-arity side-table of the function CURRENTLY being compiled
326    /// — `(param_count, result_count)` of the k-th `Block`/`Loop`/`If` in its op
327    /// stream (ordinal-keyed; see [`FunctionOps::block_arity`]). Set per function
328    /// by the driver loop (like [`current_func_params_i64`]). The direct selector
329    /// uses it to land a value carried by `br`/`br_if`/`br_table` in the target
330    /// block's designated result register instead of dropping it. Empty → every
331    /// block treated as void (the legacy lowering; hand-built op streams).
332    ///
333    /// [`FunctionOps::block_arity`]: crate::wasm_decoder::FunctionOps::block_arity
334    pub current_func_block_arity: Vec<(u8, u8)>,
335
336    /// #1214: THIS function's declared-i64-local side-table — see
337    /// [`FunctionOps::declared_i64_locals`]. Set per function by the driver
338    /// loop (like [`current_func_block_arity`]). Empty ⇒ no declared-width
339    /// correction (hand-built op streams / unit tests): behavior is exactly
340    /// the prior dataflow-only inference in `infer_i64_locals`.
341    ///
342    /// [`FunctionOps::declared_i64_locals`]: crate::wasm_decoder::FunctionOps::declared_i64_locals
343    pub current_func_declared_i64_locals: Vec<bool>,
344
345    /// #543 Phase 1 — integrator-marked volatile linear-memory segments (the DMA
346    /// transfer window). Each range `[base, base+len)` names a region of the fused
347    /// linear memory that an EXTERNAL agent (the DMA engine, modelled by gale as a
348    /// Component-Model `own<buffer>` handoff — gale decision `DD-DMA-REGION-001`,
349    /// gale#124) rewrites out-of-band. Loads and stores whose address falls inside
350    /// a marked range must eventually be treated as VOLATILE: not cached, hoisted,
351    /// or reordered across the transfer boundary.
352    ///
353    /// PHASE-2 CONTRACT (implemented — issue #543): the optimizer's
354    /// address-caching passes HONOR these ranges. Consumption points:
355    ///  - the #468 base-CSE / const-address-fold
356    ///    (`optimizer_bridge::plan_base_cse`, DEFAULT-ON, opt-out
357    ///    `SYNTH_BASE_CSE=0`): a const-address access whose 4-byte window
358    ///    intersects a marked range is EXCLUDED from the fold set — it keeps
359    ///    its verbatim per-access materialize-and-access codegen, while
360    ///    accesses outside the range still fold;
361    ///  - const-CSE (`liveness::apply_const_cse` wired in `arm_backend.rs`,
362    ///    DEFAULT-ON, opt-out `SYNTH_CONST_CSE=0`; the former bridge-level
363    ///    inline cache is retired, #242): declines WHOLESALE while any range is
364    ///    marked — a cached constant cannot be classified address-vs-data at
365    ///    that level, so the conservative stance for statically-unknown
366    ///    addressing is to re-materialize every constant at each occurrence.
367    ///
368    /// Passes that only touch SP-relative frame slots (stack-reload forwarding,
369    /// frame-slot DCE, spill re-choice) are unaffected by design: these ranges
370    /// are LINEAR-MEMORY addresses, and frame slots are never linmem. Nothing on
371    /// the pipeline deletes, forwards, or reorders a linear-memory access (IR CSE
372    /// deliberately never CSEs `MemLoad`s; DCE removes only unreachable blocks),
373    /// so every marked access is issued verbatim, in program order.
374    ///
375    /// Empty (the default): zero behavior change by construction — every gate
376    /// reduces to the pre-#543 path, so the emitted `.text` is byte-identical
377    /// with or without this code (the frozen-codegen gate holds). See rivet
378    /// `VCR-DMA-001`.
379    pub volatile_segments: Vec<VolatileRange>,
380
381    /// #778 phase 2 — the parsed `--wcet-hints` file (UNTRUSTED per-function
382    /// loop-bound hints, the scry seam). Consulted ONLY by the WCET sidecar
383    /// computation over the final instruction stream; NEVER by codegen — the
384    /// emitted bytes are byte-identical with or without hints. Every hint is
385    /// soundly verified before use and rejected with a machine reason
386    /// otherwise.
387    pub wcet_hints: Option<crate::wcet::WcetHints>,
388
389    /// VCR-PERF-002 Phase 1 (#494) — proven invariants forwarded by loom in
390    /// the `wsc.facts` custom section (encoding:
391    /// `docs/design/wsc-facts-encoding.md`; program:
392    /// `docs/design/proof-carrying-specialization.md`), whole-module table
393    /// keyed by `(func_index, value_id)`. The compile driver copies the
394    /// current function's slice into [`current_func_facts`] (the
395    /// `func_params_i64` → `current_func_params_i64` pattern), because
396    /// `compile_function` carries no function index.
397    ///
398    /// PHASE-1 CONTRACT: threaded but NOT consumed — no codegen path reads
399    /// facts, so emitted bytes are unchanged whether or not the module
400    /// carries the section (locked by `wsc_facts_ingestion_494.rs`). Phase 2
401    /// turns each fact into a premise for a flag-gated (`SYNTH_FACT_SPEC`),
402    /// per-elision ordeal-validated specialization; the facts-absent compile
403    /// stays byte-identical by construction (empty ⇒ every gate vacuous).
404    ///
405    /// [`current_func_facts`]: CompileConfig::current_func_facts
406    pub wsc_facts: Vec<WscFact>,
407    /// VCR-PERF-002 Phase 1 (#494): the `wsc.facts` invariants of the function
408    /// CURRENTLY being compiled (`fact.func_index == func.index`), set per
409    /// function by the driver loops like [`current_func_params_i64`]. This is
410    /// the field a Phase-2 selector pass will read its premises from. Empty →
411    /// no facts → no specialization may ever fire (the fail-safe default).
412    ///
413    /// [`current_func_params_i64`]: CompileConfig::current_func_params_i64
414    pub current_func_facts: Vec<WscFact>,
415    /// VCR-PERF-002 Phase 2b (#494, divisor-nonzero): op indices (into the op
416    /// stream passed to `compile_function`) of `div`/`rem` ops whose
417    /// DIVIDE-BY-ZERO trap guard is proven dead — the fact-spec pass
418    /// discharged `UNSAT(P ∧ divisor == 0)` per site through the
419    /// certificate-checked ordeal solver BEFORE the driver set this field.
420    /// Consumed by the ARM direct selector (`select_with_stack`); every other
421    /// path ignores it (guards stay — sound). Empty (the default) ⇒ every
422    /// guard is emitted, byte-identical to today.
423    pub fact_div_zero_elide: Vec<usize>,
424    /// VCR-PERF-002 Phase 2b (#494): op indices of `div_s` ops whose
425    /// `INT_MIN / -1` OVERFLOW trap guard is proven dead — a SEPARATE
426    /// obligation (`UNSAT(P ∧ dividend == INT_MIN ∧ divisor == -1)`). A
427    /// divisor-nonzero fact alone NEVER lands here: divisor ≠ 0 does not
428    /// exclude -1 (#633/#634 two-guard distinction). Empty ⇒ guard emitted.
429    pub fact_div_ovf_elide: Vec<usize>,
430    /// #494 bounds-elision (#390 `guard_bool`): op indices of i32 memory
431    /// accesses whose `--safety-bounds software` inline guard is proven dead
432    /// — the fact-spec pass discharged
433    /// `UNSAT(P ∧ trap_mem_oob(zext64(index) + offset, size,
434    /// min_memory_bytes))` per site through the certificate-checked ordeal
435    /// solver BEFORE the driver set this field (ordeal 0.9.1 `trap_mem_oob`
436    /// shape, wraparound-safe 64-bit extension). Consumed by the ARM direct
437    /// selector (`select_with_stack`); every other path ignores it (guards
438    /// stay — sound). Empty (the default) ⇒ every guard is emitted,
439    /// byte-identical to today.
440    pub fact_mem_bounds_elide: Vec<usize>,
441    /// VCR-MEM-004 (#901): op indices of linear-memory accesses whose
442    /// `--safety-bounds software` inline guard is elided on an EXTERNAL proof
443    /// — scry's sound abstract interpretation proved the access in-bounds
444    /// against the memory's guaranteed minimum size, and the verdict file
445    /// cleared every fail-closed gate ([`crate::proven_safe::ingest`]:
446    /// `module_sha256` bound to the exact bytes being compiled,
447    /// `memory_min_bytes` equal to this module's declared floor, and each
448    /// entry's `(func, pc)` key validated against the decoded operator at
449    /// that index).
450    ///
451    /// Kept SEPARATE from [`CompileConfig::fact_mem_bounds_elide`] on purpose:
452    /// the two strip the same guard at the same consumption point, but on
453    /// different AUTHORITIES (a per-site ordeal certificate vs a whole-module
454    /// external AI), and the `synth-proven-safe-elisions-v1` attestation
455    /// records which one covered each site. The ARM backend unions them.
456    /// Empty (the default) ⇒ every guard is emitted, byte-identical to today.
457    pub proven_safe_mem_elide: Vec<usize>,
458    /// #642: `call_indirect` guard inputs — the compile-time table size for
459    /// the runtime bounds check and the per-expected-type closed-world type
460    /// verdicts — computed from the decoded module by
461    /// [`crate::wasm_decoder::DecodedModule::call_indirect_guards`] and set by
462    /// the driver loops. The default (`table_size: None`, empty verdicts)
463    /// DECLINES every `call_indirect` lowering: an unchecked indirect branch
464    /// is never emitted (WASM Core §4.4.8 requires OOB/type-mismatch traps).
465    pub call_indirect_guards: crate::wasm_decoder::CallIndirectGuards,
466    /// #851 lane L3: result count per FUNCTION TYPE (see
467    /// [`crate::wasm_decoder::DecodedModule::type_result_counts`]). The aarch64
468    /// `call_indirect` lowering needs the 0-vs-1 result distinction for a callee
469    /// it knows only by its static type.
470    pub type_result_counts: Vec<u32>,
471    /// #851 lane L3: the STRUCTURAL signature class id per function type (see
472    /// [`crate::wasm_decoder::DecodedModule::structural_type_class_ids`]). The
473    /// aarch64 `call_indirect` type check compares this, not the raw type index
474    /// — WASM type equality is structural. Distinct from
475    /// `call_indirect_guards.type_class_ids`, which the ARM path populates only
476    /// when its heterogeneous-table sidecar exists.
477    pub type_class_ids: Vec<u32>,
478    /// #851 lane L3, aarch64 only — the driver has EMITTED the module-level
479    /// substrate the globals and `call_indirect` lowerings address: the `.data`
480    /// globals image (`__synth_globals`) and the `.text` funcref table
481    /// (`__synth_func_table`), both produced by
482    /// `synth_backend_aarch64::substrate::plan`.
483    ///
484    /// FAIL-SAFE BY DEFAULT (`false`): the aarch64 selector LOUD-DECLINES
485    /// `global.get`/`global.set`/`call_indirect` unless this is set, so a driver
486    /// that compiles function bodies but never emits the regions cannot ship
487    /// code addressing a symbol that does not exist. Set only on the two paths
488    /// that call `plan()` and place its output in the object.
489    pub a64_substrate_emitted: bool,
490    /// RQ-63-RVGLOBAL (#242): how many globals the module IMPORTS. The op
491    /// stream's `global.get`/`global.set` index space is imports-FIRST, while
492    /// [`CompileConfig::global_widths`] / [`CompileConfig::global_mutable`]
493    /// are indexed by DEFINED global (the decoder's `WasmGlobal::index`), so a
494    /// backend maps an op index `i` to defined global `i - num_imported_globals`
495    /// and must decline `i < num_imported_globals` (an imported global's value
496    /// arrives at instantiation, which a synth-emitted region cannot bind).
497    pub num_imported_globals: u32,
498    /// RQ-63-RVGLOBAL (#242): per DEFINED global, its declared mutability.
499    /// Indexed like [`CompileConfig::global_widths`]. A validated module never
500    /// `global.set`s an immutable global, but the RV32 lowering declines it
501    /// anyway rather than write through a `const` — defence in depth, since
502    /// the decoder does not run the wasm validator. Empty ⇒ every global is
503    /// treated as mutable (hand-built op streams).
504    pub global_mutable: Vec<bool>,
505    /// RQ-63-RVGLOBAL (#242): whether the driver WILL place the RV32 globals
506    /// region (`__synth_globals`, a synth-emitted `.data` image carrying every
507    /// defined global's decoded initializer — `synth_backend_riscv::globals`)
508    /// in the object it assembles. FAIL-SAFE BY DEFAULT (`false`): the RV32
509    /// selector LOUD-DECLINES `global.get`/`global.set` unless this is set,
510    /// so a driver that compiles function bodies but never emits the region
511    /// cannot ship code relocating against a symbol nothing defines (the
512    /// #1102 dangling-reference class). The aarch64 `a64_substrate_emitted`
513    /// contract, ported.
514    pub rv32_globals_emitted: bool,
515}
516
517/// #543 — an integrator-marked volatile linear-memory segment (the DMA transfer
518/// window): the half-open byte range `[base, base + len)` of the fused linear
519/// memory that an external agent rewrites out-of-band. Parsed from the CLI
520/// `--volatile-segment <base>:<len>` flag. See [`CompileConfig::volatile_segments`]
521/// for the Phase-1/Phase-2 split.
522#[derive(Debug, Clone, Copy, PartialEq, Eq)]
523pub struct VolatileRange {
524    /// Start address of the volatile region, in linear-memory bytes.
525    pub base: u32,
526    /// Length of the volatile region, in bytes. The region is `[base, base+len)`.
527    pub len: u32,
528}
529
530impl CompileConfig {
531    /// Resolve the effective safety-bounds setting, honouring the legacy
532    /// `bounds_check` field as a fallback. Used by backends to pick the
533    /// inline-check shape.
534    pub fn effective_safety_bounds(&self) -> SafetyBounds {
535        match (self.safety_bounds, self.bounds_check) {
536            (SafetyBounds::None, true) => SafetyBounds::Software,
537            (s, _) => s,
538        }
539    }
540}
541
542impl Default for CompileConfig {
543    fn default() -> Self {
544        Self {
545            opt_level: 2,
546            target: TargetSpec::cortex_m4(),
547            bounds_check: false,
548            safety_bounds: SafetyBounds::None,
549            hardware: String::new(),
550            no_optimize: false,
551            loom_compat: false,
552            num_imports: 0,
553            func_arg_counts: Vec::new(),
554            func_result_counts: Vec::new(),
555            type_arg_counts: Vec::new(),
556            relocatable: false,
557            // #275: self-contained funcref-table dispatch is opt-in by the
558            // CLI's cortex-m image path; everything else keeps the decline.
559            self_contained_funcref_table: false,
560            // #687: the historical optimized-path absolute base — every
561            // default compile stays byte-identical.
562            linmem_base: OPTIMIZED_LINMEM_BASE,
563            native_pointer_abi: false,
564            linear_memory_bytes: 0,
565            // #406: empty ⇒ no multi-memory context ⇒ multi-memory ops decline
566            // loudly; memory-0 lowering never reads it.
567            memory_pages: Vec::new(),
568            // #1441: 16 = the default 64 KiB page, correct for every module
569            // that declares no page size and for every legacy caller.
570            memory0_page_log2: 16,
571            stack_pointer_global: None,
572            func_ret_i64: Vec::new(),
573            type_ret_i64: Vec::new(),
574            // #643: empty ⇒ legacy all-4-byte global slots (i32-only modules).
575            global_widths: Vec::new(),
576            func_params_i64: Vec::new(),
577            current_func_params_i64: Vec::new(),
578            func_params_f32: Vec::new(),
579            current_func_params_f32: Vec::new(),
580            // GI-FPU-002 phase 2 (#719/#369): false ⇒ non-float return (or a
581            // hand-built op stream); driver loops set it per function.
582            current_func_ret_f32: false,
583            current_func_ret_f64: false,
584            // GI-FPU-002 phase 2 (#719/#369): empty ⇒ callees assumed
585            // non-float-returning (hand-built op streams).
586            func_params_f64: Vec::new(),
587            current_func_params_f64: Vec::new(),
588            func_ret_f32: Vec::new(),
589            func_ret_f64: Vec::new(),
590            type_ret_f32: Vec::new(),
591            type_ret_f64: Vec::new(),
592            // #457: None ⇒ declared signature unknown ⇒ param-count inference
593            // only (unit tests / hand-built op streams); driver loops fill it.
594            current_func_param_count: None,
595            current_func_index: None,
596            // #509: empty ⇒ legacy void-block lowering (unit tests / hand-built
597            // op streams); the driver loops fill it per function.
598            current_func_block_arity: Vec::new(),
599            // #1214: empty ⇒ no declared-width correction (unit tests /
600            // hand-built op streams); the driver loops fill it per function.
601            current_func_declared_i64_locals: Vec::new(),
602            // #543 Phase 1: no volatile segments unless the CLI flag names them.
603            // Empty ⇒ inert ⇒ emitted bytes unchanged.
604            volatile_segments: Vec::new(),
605            // VCR-PERF-002 Phase 1 (#494): no facts unless the module carries
606            // a parseable `wsc.facts` section. Empty ⇒ inert (and Phase 1 has
607            // no consumer anyway) ⇒ emitted bytes unchanged.
608            wsc_facts: Vec::new(),
609            current_func_facts: Vec::new(),
610            // VCR-PERF-002 Phase 2b (#494): no guard-elision marks unless the
611            // fact-spec pass discharged the per-site obligations. Empty ⇒
612            // every div/rem trap guard is emitted, byte-identical.
613            fact_div_zero_elide: Vec::new(),
614            fact_div_ovf_elide: Vec::new(),
615            fact_mem_bounds_elide: Vec::new(),
616            proven_safe_mem_elide: Vec::new(),
617            // #642: no guard inputs ⇒ every call_indirect lowering declines
618            // loudly (never an unchecked indirect branch). Driver loops fill
619            // this from the decoded module.
620            call_indirect_guards: crate::wasm_decoder::CallIndirectGuards::default(),
621            type_result_counts: Vec::new(),
622            type_class_ids: Vec::new(),
623            a64_substrate_emitted: false,
624            // RQ-63-RVGLOBAL: no imports, every global mutable, and — fail-safe
625            // — NO globals region placed, so the RV32 selector declines.
626            num_imported_globals: 0,
627            global_mutable: Vec::new(),
628            rv32_globals_emitted: false,
629            // #778 phase 2: no --wcet-hints file ⇒ no hints. Consulted ONLY by
630            // the WCET sidecar computation — never by codegen (the emitted
631            // bytes are byte-identical with or without hints).
632            wcet_hints: None,
633        }
634    }
635}
636
637/// #275: the base symbol of the SELF-CONTAINED funcref table — the
638/// flash-resident region `build_multi_func_cortex_m_elf` appends after the
639/// function code: one 4-byte code pointer per table slot across ALL tables in
640/// declaration order (the same contiguous layout the `--relocatable` R11
641/// contract uses — `TableGuards::base_byte_offset` stays valid verbatim),
642/// null slots as ZERO words (#664), followed by the #676 type-id sidecar at
643/// `type_ids_byte_offset` when a heterogeneous table needs it. The dispatch
644/// reaches it through an `LdrSym` literal-pool word (an `Abs32` reloc against
645/// this symbol) that the image builder patches post-layout — never through
646/// R11, which is the linear-memory base (the #717 collision).
647pub const FUNC_TABLE_SYMBOL: &str = "__synth_func_table";
648
649/// A relocation entry produced during compilation
650///
651/// Records that a BL instruction at `offset` bytes into the function's code
652/// targets an external symbol (e.g., `__meld_dispatch_import`). The linker
653/// resolves these when combining the Synth object with the Kiln bridge.
654#[derive(Debug, Clone, Copy, PartialEq, Eq)]
655pub enum RelocKind {
656    /// R_ARM_THM_CALL (ELF type 10) — a THUMB BL call site (#167). Correct
657    /// only for a Thumb-state `bl`, whose 32-bit placeholder is `f7ff fffe`
658    /// (branch-to-self, addend -4 for the +4 pipeline bias).
659    ThmCall,
660    /// R_ARM_CALL (ELF type 28) — an ARM-STATE (A32) BL call site (#1040).
661    /// The A32 analogue of [`RelocKind::ThmCall`], exactly as
662    /// [`RelocKind::AArch64Call26`] and [`RelocKind::RiscvCallPlt`] are the
663    /// analogues for their ISAs — the ISA is fixed at the site that KNOWS it,
664    /// never re-derived at the ELF emitter where the information is gone.
665    ///
666    /// Emitting `ThmCall` for an A32 `bl` was #1040: a consumer that trusts
667    /// the declared type patches Thumb halfwords into an ARM-state word (or
668    /// emits an interwork veneer), producing an invalid instruction. The
669    /// matching A32 placeholder is `ebfffffe` (branch-to-self, addend -8 for
670    /// the +8 pipeline bias) — `gas` emits exactly that for `bl <extern>` in
671    /// ARM mode, and `eb000000` (addend 0) lands two instructions past the
672    /// callee entry, the A32 twin of #174.
673    ArmCall,
674    /// R_ARM_MOVW_ABS_NC — the MOVW half of a symbol-relative address (#237).
675    MovwAbs,
676    /// R_ARM_MOVT_ABS — the MOVT half of a symbol-relative address (#237).
677    MovtAbs,
678    /// R_ARM_ABS32 — a 32-bit absolute address held in a `.text` literal-pool
679    /// word, loaded via `LDR rX, [pc, #off]` (#345). The link-survivable
680    /// replacement for the inline-immediate MOVW/MOVT-ABS pair: `ld`/bfd patches
681    /// the data word at link time (`S + A`, the addend living in the word, REL
682    /// semantics), which survives placement into a large multi-object image —
683    /// whereas an inline-instruction MOVW_ABS immediate can be mangled.
684    Abs32,
685    /// R_AARCH64_CALL26 (ELF type 283) — an AArch64 `BL` call site (#851). The
686    /// AArch64 analogue of [`RelocKind::ThmCall`]: the linker patches the 26-bit
687    /// word-offset immediate of the `bl` at `offset` to reach the target symbol.
688    /// Emitted only by the `EM_AARCH64` backend's `.rela.text`.
689    AArch64Call26,
690    /// R_AARCH64_JUMP26 (ELF type 282) — an AArch64 `B` (tail-branch) site
691    /// (#851 lane L3). Same 26-bit word-offset immediate as
692    /// [`RelocKind::AArch64Call26`], but for a branch that does NOT set `x30`:
693    /// the aarch64 `call_indirect` funcref table is a `.text`-resident array of
694    /// `b func_N` trampolines, so the dispatch's `blr` sets the return address
695    /// and the trampoline tail-branches into the callee (which returns straight
696    /// to the dispatcher).
697    AArch64Jump26,
698    /// R_AARCH64_ADR_PREL_PG_HI21 (ELF type 275) — the `adrp` half of a
699    /// PC-relative symbol address (#851 lane L3). Patches the 21-bit page delta
700    /// (`immlo`[30:29] + `immhi`[23:5]) so `adrp xd, sym` reaches the 4 KiB page
701    /// containing `sym`. Always paired with an
702    /// [`RelocKind::AArch64AddAbsLo12Nc`] on the next instruction. This pair is
703    /// how aarch64 reaches a synth-EMITTED region (the globals `.data` image,
704    /// the funcref table) with NO dedicated base register — so neither feature
705    /// adds an embedder precondition alongside `x28`.
706    AArch64AdrPrelPgHi21,
707    /// R_AARCH64_ADD_ABS_LO12_NC (ELF type 277) — the `add xd, xd, :lo12:sym`
708    /// half of a PC-relative symbol address (#851 lane L3). Patches the 12-bit
709    /// immediate field [21:10] with `(S + A) & 0xFFF`.
710    AArch64AddAbsLo12Nc,
711    /// R_RISCV_CALL_PLT (ELF type 19) — a RISC-V `auipc`+`jalr` call pair
712    /// (#871). The RV32 analogue of [`RelocKind::ThmCall`]: `offset` points at
713    /// the `auipc` of an 8-byte `auipc ra, 0 ; jalr ra, 0(ra)` placeholder and
714    /// the linker patches BOTH instructions' immediates to reach the target
715    /// symbol (the modern form; `R_RISCV_CALL` is deprecated). Emitted only by
716    /// the `EM_RISCV` backend's `.rela.text`.
717    RiscvCallPlt,
718    /// R_RISCV_HI20 (ELF type 26) — the `lui` half of an ABSOLUTE symbol
719    /// address (RQ-63-RVGLOBAL, #242). `offset` points at a `lui rd, 0`
720    /// placeholder whose 20-bit immediate the linker patches to
721    /// `((S + A) + 0x800) >> 12`. Always paired with an
722    /// [`RelocKind::RiscvLo12I`] on the next instruction. This pair is how
723    /// RV32 reaches a synth-EMITTED region (the globals `.data` image,
724    /// `__synth_globals`) with NO dedicated base register — the RV32 twin of
725    /// the aarch64 `adrp`+`add :lo12:` pair, so globals add no embedder
726    /// precondition beside `s11`. Absolute rather than PC-relative because the
727    /// RV32 object is always statically host-linked into a fixed-address
728    /// bare-metal image (the `medlow` code model), which needs no per-site
729    /// local symbol.
730    RiscvHi20,
731    /// R_RISCV_LO12_I (ELF type 27) — the `addi rd, rd, 0` half of an absolute
732    /// symbol address (RQ-63-RVGLOBAL). The linker patches the 12-bit I-type
733    /// immediate with the low bits of `S + A`.
734    RiscvLo12I,
735}
736
737#[derive(Debug, Clone, PartialEq, Eq)]
738pub struct CodeRelocation {
739    /// Byte offset within the function's machine code where the reloc applies
740    pub offset: u32,
741    /// Target symbol name (e.g., "__meld_dispatch_import", "__synth_wasm_data")
742    pub symbol: String,
743    /// Which ARM relocation type to emit for this site.
744    pub kind: RelocKind,
745}
746
747/// Symbol BINDING — whether a symbol takes part in cross-object resolution
748/// (#656 ARM, #1180 / RQ-65-FUNCN aarch64).
749///
750/// This is a CONTAINER-INDEPENDENT concept, which is why it lives here and
751/// not in a writer: ELF spells it `STB_LOCAL` / `STB_GLOBAL` / `STB_WEAK` in
752/// `st_info`'s high nibble (the discriminants below are exactly those
753/// values), Mach-O spells `Local` as `N_EXT` CLEAR and `Global` as `N_EXT`
754/// SET on the `nlist_64`. The decision that a synth-invented name — a
755/// `func_N` call label, `__synth_globals`, `__synth_func_table` — is
756/// `Local` while a wasm export or import is `Global` is made ONCE, at the
757/// object PLAN, and every container reads it from there. Two independently
758/// compiled synth objects both define `func_1`; a `Global` binding made
759/// linking them into one program a `duplicate symbol` refusal, in every
760/// container (measured on ELF `ld.lld` and Mach-O Apple `ld`, #1180).
761#[derive(Debug, Clone, Copy, PartialEq, Eq)]
762pub enum SymbolBinding {
763    /// File-local: resolves relocations within its own object (they bind by
764    /// symbol INDEX, not by name) and is invisible to every other object.
765    Local = 0,
766    /// Visible to the link: the name an embedder calls, or the import it
767    /// defines.
768    Global = 1,
769    /// Weak (ELF `STB_WEAK`); no synth writer emits it today.
770    Weak = 2,
771}
772
773/// The locals-first symbol order, computed ONCE for every container that
774/// needs it (#656, #1180).
775///
776/// ELF requires every `STB_LOCAL` symbol to precede every non-local one in
777/// `.symtab`, with the section's `sh_info` = index of the first non-local.
778/// Mach-O's `LC_DYSYMTAB` requires the same partition (locals, then external
779/// defined, then undefined). The ARM ELF32 builder (`synth-backend`,
780/// `ElfBuilder::build`) and the aarch64 object plan (`synth-backend-aarch64`,
781/// `plan_object`) both apply THIS permutation, so the rule is written once —
782/// a second hand-written copy in a second writer is the mirror the North
783/// Star forbids, and the way the two backends diverged in the first place.
784///
785/// The permutation is a STABLE sort on `binding != Local`: with zero locals
786/// it is the identity, which is what keeps every pre-#656 / pre-#1180 object
787/// byte-identical.
788#[derive(Debug, Clone, PartialEq, Eq)]
789pub struct LocalsFirst {
790    /// `order[new] = old`: the symbol emitted at position `new` is the caller's
791    /// symbol `old`.
792    pub order: Vec<usize>,
793    /// `old_to_new[old] = new`: where the caller's symbol `old` landed. Every
794    /// relocation that names a symbol by index is rewritten through this.
795    pub old_to_new: Vec<usize>,
796    /// The number of `Local` symbols — the length of the local prefix. ELF's
797    /// `sh_info` is `local_count + 1` (the null symbol at index 0 counts as
798    /// local); Mach-O's `nlocalsym` is `local_count`.
799    pub local_count: usize,
800}
801
802/// Compute the [`LocalsFirst`] permutation for `bindings` (in the caller's
803/// current order).
804pub fn locals_first(bindings: impl IntoIterator<Item = SymbolBinding>) -> LocalsFirst {
805    let bindings: Vec<SymbolBinding> = bindings.into_iter().collect();
806    let mut order: Vec<usize> = (0..bindings.len()).collect();
807    // Stable: locals keep their relative order, and so do non-locals.
808    order.sort_by_key(|&i| bindings[i] != SymbolBinding::Local);
809    let mut old_to_new = vec![0usize; bindings.len()];
810    for (new, &old) in order.iter().enumerate() {
811        old_to_new[old] = new;
812    }
813    let local_count = bindings
814        .iter()
815        .filter(|b| **b == SymbolBinding::Local)
816        .count();
817    LocalsFirst {
818        order,
819        old_to_new,
820        local_count,
821    }
822}
823
824/// VCR-DBG-001: a per-instruction source map — `(machine_offset_within_code,
825/// wasm_op_index)` pairs, one per emitted machine instruction. A `None` op-index
826/// marks an instruction with no originating wasm op (prologue/epilogue, literal
827/// pool). Consumed by the DWARF `.debug_line` emitter; empty when no source map
828/// was produced.
829pub type LineMap = Vec<(u32, Option<usize>)>;
830
831/// VCR-DEC-003 (#396, witness#130): the object-level control-flow class of one
832/// emitted machine instruction, captured at encode time alongside [`LineMap`].
833/// It is the piece post-hoc CLI derivation cannot recover — `line_map` records
834/// which wasm op an instruction came from, but not whether that instruction IS a
835/// conditional branch, an unconditional branch, or a predicated (IT-block) move.
836/// The `synth-provenance-v1` emitter needs it to enumerate the ACTUAL object
837/// conditional branches (so it can prove "every object branch resolves to a
838/// source condition", not just "every source branch has an object PC").
839#[derive(Debug, Clone, Copy, PartialEq, Eq)]
840pub enum BranchClass {
841    /// A conditional branch (`Bcc`/`Blo`/`Bhs`/`BCondOffset`) — an object-level
842    /// decision point MC/DC must account for.
843    CondBranch,
844    /// An unconditional branch (`B`/`BOffset`) — control flow, not a decision.
845    UncondBranch,
846    /// A predicated conditional move (`SelectMove`, the IT-block form the
847    /// cmp→select fuse produces) — a folded decision with no branch.
848    Predicated,
849    /// Anything else (data-processing, load/store, call, prologue/epilogue).
850    Other,
851}
852
853/// VCR-DEC-003: per-instruction object-branch class, parallel to [`LineMap`]
854/// (same length, same order — one entry per emitted machine instruction).
855/// `(machine_offset_within_code, class)`. Empty when provenance is not being
856/// produced (never serialized into `.text`; frozen-safe additive metadata).
857pub type BranchMap = Vec<(u32, BranchClass)>;
858
859/// A single compiled function
860#[derive(Debug, Clone)]
861pub struct CompiledFunction {
862    /// Function name (from WASM export or generated)
863    pub name: String,
864    /// Raw machine code bytes
865    pub code: Vec<u8>,
866    /// Original WASM ops (retained for verification)
867    pub wasm_ops: Vec<WasmOp>,
868    /// Relocations for external symbol references (BL to bridge functions)
869    pub relocations: Vec<CodeRelocation>,
870    /// VCR-DBG-001: per-instruction source map for DWARF `.debug_line` emission —
871    /// `(machine_offset_within_code, wasm_op_index)` captured at encode time, one
872    /// entry per emitted machine instruction. A `None` op-index marks an
873    /// instruction with no originating wasm op (prologue/epilogue, literal-pool
874    /// word). This is purely additive metadata: it is never serialized unless
875    /// `.debug_line` emission is requested, so the emitted `.text` is
876    /// byte-identical with or without it. Empty for backends/paths that do not
877    /// yet produce a source map (RISC-V, the optimized ARM path).
878    pub line_map: LineMap,
879    /// VCR-DEC-003 (#396): per-instruction object-branch class, parallel to
880    /// `line_map`. Lets the `synth-provenance-v1` emitter enumerate the real
881    /// object conditional branches (not just re-walk the wasm branch ops).
882    /// Purely additive metadata: never serialized into `.text`, so emitted bytes
883    /// are byte-identical with or without it. Empty for backends/paths that do
884    /// not produce it (RISC-V, the optimized ARM path).
885    pub branch_map: BranchMap,
886    /// #778 (v0.46): the SOUND static worst-case-cycle bound for this function,
887    /// or a loud decline, computed over the final Thumb-2 instruction stream (see
888    /// [`crate::wcet`]). `Some` only when the ARM backend produced it (the RISC-V
889    /// and AArch64 backends carry no cycle model yet → `None`). Purely additive
890    /// metadata: derived from the already-decided instruction list, never
891    /// serialized into `.text`, so emitted bytes are byte-identical with or
892    /// without it (frozen-safe). Emitted as the `<output>.wcet.json` sidecar only
893    /// under `--emit-wcet`.
894    pub wcet: Option<crate::wcet::WcetFunction>,
895    /// #778 phase 3: the per-function WCET INTERMEDIATE (own-body cycles + direct
896    /// call sites, or a composition-independent decline) BEFORE inter-procedural
897    /// composition. The module driver composes these across the direct call graph
898    /// into the final per-function bounds (a caller's bound = its own body + each
899    /// direct callee's bound × the call site's proven execution count). `Some` only
900    /// on the Thumb-2 path that produced `wcet`. Purely additive, `.text`-invisible
901    /// (frozen-safe) — derived from the already-decided instruction list.
902    pub wcet_intermediate: Option<crate::wcet::WcetIntermediate>,
903    /// RQ-71-STACKDEPTH (#1341): this function's own native-stack profile,
904    /// read off the SAME final stream `wcet_intermediate` is read from.
905    /// `None` on backends that emit no ArmInstruction stream.
906    pub stack_frame: Option<crate::stack_depth::StackFrame>,
907}
908
909/// Result of compiling a full module
910#[derive(Debug)]
911pub struct CompilationResult {
912    /// Compiled functions
913    pub functions: Vec<CompiledFunction>,
914    /// Complete ELF binary (if backend produces one directly)
915    pub elf: Option<Vec<u8>>,
916    /// Name of the backend that produced this result
917    pub backend_name: String,
918}
919
920/// What a backend can and cannot do
921#[derive(Debug, Clone)]
922pub struct BackendCapabilities {
923    /// Backend produces complete ELF files (external backends like aWsm)
924    pub produces_elf: bool,
925    /// Backend supports per-rule verification (only our custom ARM backend)
926    pub supports_rule_verification: bool,
927    /// Backend supports binary-level verification (all backends via disassembly)
928    pub supports_binary_verification: bool,
929    /// Backend is an external tool (not a library)
930    pub is_external: bool,
931}
932
933/// Trait that every compilation backend implements
934pub trait Backend: Send + Sync {
935    /// Human-readable backend name
936    fn name(&self) -> &str;
937
938    /// What this backend can do
939    fn capabilities(&self) -> BackendCapabilities;
940
941    /// Which targets this backend supports
942    fn supported_targets(&self) -> Vec<TargetSpec>;
943
944    /// Compile an entire decoded WASM module
945    fn compile_module(
946        &self,
947        module: &DecodedModule,
948        config: &CompileConfig,
949    ) -> std::result::Result<CompilationResult, BackendError>;
950
951    /// Compile a single function from WASM ops to machine code
952    fn compile_function(
953        &self,
954        name: &str,
955        ops: &[WasmOp],
956        config: &CompileConfig,
957    ) -> std::result::Result<CompiledFunction, BackendError>;
958
959    /// Check if this backend is available (external tools installed, etc.)
960    fn is_available(&self) -> bool;
961}
962
963/// Registry of available backends
964pub struct BackendRegistry {
965    backends: HashMap<String, Box<dyn Backend>>,
966}
967
968impl BackendRegistry {
969    pub fn new() -> Self {
970        Self {
971            backends: HashMap::new(),
972        }
973    }
974
975    /// Register a backend under its name
976    pub fn register(&mut self, backend: Box<dyn Backend>) {
977        let name = backend.name().to_string();
978        self.backends.insert(name, backend);
979    }
980
981    /// Get a backend by name
982    pub fn get(&self, name: &str) -> Option<&dyn Backend> {
983        self.backends.get(name).map(|b| b.as_ref())
984    }
985
986    /// List all registered backends
987    pub fn list(&self) -> Vec<&dyn Backend> {
988        self.backends.values().map(|b| b.as_ref()).collect()
989    }
990
991    /// List backends that are actually available (installed and working)
992    pub fn available(&self) -> Vec<&dyn Backend> {
993        self.backends
994            .values()
995            .filter(|b| b.is_available())
996            .map(|b| b.as_ref())
997            .collect()
998    }
999}
1000
1001impl Default for BackendRegistry {
1002    fn default() -> Self {
1003        Self::new()
1004    }
1005}
1006
1007#[cfg(test)]
1008mod tests {
1009    use super::*;
1010
1011    #[test]
1012    fn test_registry_empty() {
1013        let reg = BackendRegistry::new();
1014        assert!(reg.list().is_empty());
1015        assert!(reg.available().is_empty());
1016        assert!(reg.get("arm").is_none());
1017    }
1018
1019    #[test]
1020    fn test_compile_config_default() {
1021        let config = CompileConfig::default();
1022        assert_eq!(config.opt_level, 2);
1023        assert!(!config.bounds_check);
1024        assert_eq!(config.safety_bounds, SafetyBounds::None);
1025        assert!(!config.no_optimize);
1026    }
1027
1028    #[test]
1029    fn safety_bounds_parse_round_trip() {
1030        for s in ["none", "mpu", "software", "mask"] {
1031            let sb = SafetyBounds::parse(s).unwrap();
1032            assert_eq!(sb.as_str(), s);
1033        }
1034        // RQ-69-PMPLIB (#1317): this line PINNED the silent alias. Flipped —
1035        // the library refuses `pmp` exactly as the CLI has since v0.68, and
1036        // the message names the issue so a caller can find out why.
1037        let pmp = SafetyBounds::parse("pmp").expect_err("pmp must be refused (#1317)");
1038        assert!(pmp.contains("#1317"), "refusal must name the issue: {pmp}");
1039        assert!(
1040            pmp.contains("never a silent alias"),
1041            "refusal must say what it is not: {pmp}"
1042        );
1043        assert_eq!(SafetyBounds::parse("soft").unwrap(), SafetyBounds::Software);
1044        assert!(SafetyBounds::parse("nonsense").is_err());
1045    }
1046
1047    #[test]
1048    fn effective_safety_bounds_legacy_promotes_to_software() {
1049        let cfg = CompileConfig {
1050            bounds_check: true,
1051            ..Default::default()
1052        };
1053        assert_eq!(cfg.effective_safety_bounds(), SafetyBounds::Software);
1054    }
1055
1056    #[test]
1057    fn effective_safety_bounds_new_field_wins() {
1058        let cfg = CompileConfig {
1059            bounds_check: true,
1060            safety_bounds: SafetyBounds::Mpu,
1061            ..Default::default()
1062        };
1063        assert_eq!(cfg.effective_safety_bounds(), SafetyBounds::Mpu);
1064    }
1065}