Skip to main content

synth_core/
provenance.rs

1//! VCR-DEC-003 (#396, witness#130) — the `synth-provenance-v1` branch-transformation map.
2//!
3//! synth's lowering changes the branch structure between the WASM source and the
4//! ARM object: cmp→select fusion collapses a `select` into a predicated IT-block
5//! move (no branch), `br_table` splits one WASM branch into a cascade of object
6//! branches, constant-condition guards get elided. witness measures MC/DC on the
7//! WASM component; to certify the OBJECT it must reconcile each object-level
8//! branch/condition back to its source condition. This module emits the map that
9//! makes that reconciliation possible.
10//!
11//! ## Schema (`synth-provenance-v1`)
12//!
13//! A JSON sidecar. The witness-facing join key is `(func_index,
14//! instruction_offset)` where `instruction_offset` is the ABSOLUTE wasm byte
15//! offset of the source op (synth's `op_offsets`, same origin as walrus
16//! `InstrLocId` — see VCR-DEC-003). Each entry additionally carries the OBJECT
17//! realization (`object_pcs`) so a consumer can map a source condition to the
18//! machine code it became — the piece the roadmap's terse schema left implicit
19//! but the reconciliation gate needs.
20//!
21//! `kind ∈`
22//! - `preserved` — a 1:1 `br_if`/`br` that stayed a real object branch.
23//! - `folded-predication` — a `select` fused to predicated (IT-block) moves; a
24//!   decision with NO object branch. `object_pcs` point at the predicated moves.
25//! - `split-into-object-branches` — a `br_table` that became N object branches;
26//!   `count` = N.
27//! - `eliminated-constant` — a source branch/condition dropped before codegen
28//!   (constant-fold / fact-spec guard elision). `object_pcs` is empty; the
29//!   omission is RECORDED, not silently missing.
30//!
31//! ## What the map lets a consumer prove (the non-vacuous gate)
32//!
33//! (a) every object-level conditional branch resolves to a source WASM condition
34//!     — carried in `object_cond_branches`, derived from the real object-branch
35//!     side-table ([`crate::backend::BranchMap`]), NOT re-walked from the wasm
36//!     branch ops (which would be vacuous);
37//! (b) a folded/eliminated source condition is explicitly recorded with its
38//!     object realization (or its absence), NOT dropped.
39//!
40//! ## Bounded v1 — covered vs uncovered
41//!
42//! GATE-EXERCISED: `br_if`, `br` (preserved), `select` (folded-predication),
43//! `br_table` (split). `eliminated-constant` is WIRED (schema + emitter, correct
44//! byte-offset join key) but not yet gate-exercised — a fixture that drops a
45//! covered branch op is a v1 follow-up. Every object conditional branch that does
46//! NOT resolve to one of the covered source ops is surfaced in
47//! `object_cond_branches` with `resolved: false` and a `note` (e.g. an `i64`
48//! expansion branch or a div/mem trap guard) — an "only-in-synth"-style
49//! divergence the consumer SEES rather than a silent gap. Naming the uncovered
50//! ones is the deliverable's explicit follow-up boundary.
51
52use serde::{Deserialize, Serialize};
53
54use crate::backend::{BranchClass, BranchMap, LineMap};
55use crate::wasm_op::WasmOp;
56
57/// The schema version string embedded at the top of the sidecar.
58pub const SCHEMA: &str = "synth-provenance-v1";
59
60/// The transformation a source branch/condition underwent on the way to object code.
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
62#[serde(rename_all = "kebab-case")]
63pub enum ProvKind {
64    /// 1:1 `br_if`/`br` that stayed a real object branch.
65    Preserved,
66    /// `select` fused to predicated moves (no object branch).
67    FoldedPredication,
68    /// `br_table` split into N object branches (`count` = N).
69    SplitIntoObjectBranches,
70    /// Source branch/condition dropped before codegen (constant / fact-spec).
71    EliminatedConstant,
72}
73
74/// One source-level branch/condition and what it became in the object.
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
76pub struct ProvEntry {
77    /// Witness join key: ABSOLUTE wasm byte offset of the source op.
78    pub instruction_offset: u32,
79    /// Index of the source op within the (compiled) op stream — diagnostic.
80    pub wasm_op_index: usize,
81    /// The source WASM op mnemonic (e.g. `"BrIf"`, `"Select"`, `"BrTable"`).
82    pub op: String,
83    /// How synth transformed it.
84    pub kind: ProvKind,
85    /// Object PCs (function-relative machine offsets) that realize this source
86    /// op's control flow. Empty for `eliminated-constant`.
87    pub object_pcs: Vec<u32>,
88    /// For `split-into-object-branches`: the object-branch count. Omitted otherwise.
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub count: Option<usize>,
91    /// Optional scry#51 reachability evidence for an `eliminated-constant` entry
92    /// (justified-infeasible). Reserved for a later increment; `None` in v1.
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub scry_evidence: Option<String>,
95}
96
97/// One object-level conditional branch, and whether it reconciled to a covered
98/// source condition. This is the (a)-clause carrier: derived from the REAL
99/// object-branch side-table, so a branch synth emitted that no covered source op
100/// explains shows up here with `resolved: false` (surfaced, not hidden).
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102pub struct ObjectCondBranch {
103    /// Function-relative machine offset of the conditional branch.
104    pub pc: u32,
105    /// The wasm op index this branch traces back to (via `line_map`), if any.
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub wasm_op_index: Option<usize>,
108    /// Absolute wasm byte offset of that source op, if resolvable.
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub instruction_offset: Option<u32>,
111    /// True iff this branch resolves to a covered source condition (`br_if` /
112    /// `br_table`). False = an uncovered/only-in-synth object branch (e.g. an
113    /// i64-expansion or trap-guard branch) — a v1 follow-up, surfaced not hidden.
114    pub resolved: bool,
115    /// Human note when `resolved` is false.
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub note: Option<String>,
118}
119
120/// Provenance for one compiled function.
121#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
122pub struct FunctionProvenance {
123    pub func_index: u32,
124    pub name: String,
125    pub entries: Vec<ProvEntry>,
126    pub object_cond_branches: Vec<ObjectCondBranch>,
127}
128
129/// The whole-module `synth-provenance-v1` map.
130#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
131pub struct ProvenanceMap {
132    pub schema: String,
133    pub module: String,
134    pub functions: Vec<FunctionProvenance>,
135}
136
137impl ProvenanceMap {
138    pub fn new(module: impl Into<String>) -> Self {
139        ProvenanceMap {
140            schema: SCHEMA.to_string(),
141            module: module.into(),
142            functions: Vec::new(),
143        }
144    }
145
146    /// Serialize to pretty JSON.
147    pub fn to_json(&self) -> String {
148        serde_json::to_string_pretty(self).expect("ProvenanceMap serializes")
149    }
150}
151
152/// Is this a source op the map covers as a branch/condition? Returns its
153/// mnemonic if so. Public so the CLI can classify eliminated (fact-spec-dropped)
154/// ops with the SAME coverage predicate the emitter uses.
155pub fn covered_source_op_name(op: &WasmOp) -> Option<&'static str> {
156    source_op_name(op)
157}
158
159/// Is this a source op the map covers as a branch/condition?
160fn source_op_name(op: &WasmOp) -> Option<&'static str> {
161    match op {
162        WasmOp::BrIf(_) => Some("BrIf"),
163        WasmOp::Br(_) => Some("Br"),
164        WasmOp::BrTable { .. } => Some("BrTable"),
165        WasmOp::Select => Some("Select"),
166        _ => None,
167    }
168}
169
170/// Derive provenance for one function from the CLI-available data.
171///
172/// - `ops` / `op_offsets` are index-aligned (the stream the backend compiled and
173///   its per-op absolute wasm byte offsets, already fact-spec-filtered upstream).
174/// - `line_map` / `branch_map` are index-aligned (one entry per emitted machine
175///   instruction: `(pc, wasm_op_index)` and `(pc, class)`).
176/// - `eliminated`: `(wasm_op_index_in_original_stream, op_name,
177///   absolute_wasm_byte_offset)` for branch/condition ops that constant-folding
178///   / fact-spec dropped before codegen. The offset is the ORIGINAL-stream byte
179///   offset (the witness join key), NOT derivable from `op_offsets` here (which
180///   is the filtered/kept table) — the caller looks it up in the unfiltered
181///   side-table.
182pub fn derive_function_provenance(
183    func_index: u32,
184    name: &str,
185    ops: &[WasmOp],
186    op_offsets: &[u32],
187    line_map: &LineMap,
188    branch_map: &BranchMap,
189    eliminated: &[(usize, String, u32)],
190) -> FunctionProvenance {
191    // For each op index, collect the object PCs whose branch_map class matters.
192    // line_map and branch_map are parallel; zip them.
193    let mut entries: Vec<ProvEntry> = Vec::new();
194
195    for (op_idx, op) in ops.iter().enumerate() {
196        let Some(op_name) = source_op_name(op) else {
197            continue;
198        };
199        let instruction_offset = op_offsets.get(op_idx).copied().unwrap_or(0);
200
201        // Object realizations of this op: the machine instructions whose
202        // line_map op-index == op_idx AND whose branch class is a branch or a
203        // predicated move (skip the data-processing setup instructions).
204        let mut cond_pcs = Vec::new();
205        let mut uncond_pcs = Vec::new();
206        let mut pred_pcs = Vec::new();
207        for ((pc, oi), (_pc2, class)) in line_map.iter().zip(branch_map.iter()) {
208            if *oi != Some(op_idx) {
209                continue;
210            }
211            match class {
212                BranchClass::CondBranch => cond_pcs.push(*pc),
213                BranchClass::UncondBranch => uncond_pcs.push(*pc),
214                BranchClass::Predicated => pred_pcs.push(*pc),
215                BranchClass::Other => {}
216            }
217        }
218
219        let (kind, object_pcs, count) = match op {
220            WasmOp::BrIf(_) => (ProvKind::Preserved, cond_pcs.clone(), None),
221            WasmOp::Br(_) => (ProvKind::Preserved, uncond_pcs.clone(), None),
222            WasmOp::BrTable { .. } => {
223                let n = cond_pcs.len();
224                let mut all = cond_pcs.clone();
225                all.extend(uncond_pcs.iter().copied());
226                (ProvKind::SplitIntoObjectBranches, all, Some(n))
227            }
228            WasmOp::Select => (ProvKind::FoldedPredication, pred_pcs.clone(), None),
229            _ => unreachable!("source_op_name gated the match"),
230        };
231
232        entries.push(ProvEntry {
233            instruction_offset,
234            wasm_op_index: op_idx,
235            op: op_name.to_string(),
236            kind,
237            object_pcs,
238            count,
239            scry_evidence: None,
240        });
241    }
242
243    // Eliminated-constant entries: branch/condition ops dropped before codegen.
244    for (orig_idx, op_name, byte_offset) in eliminated {
245        entries.push(ProvEntry {
246            instruction_offset: *byte_offset,
247            wasm_op_index: *orig_idx,
248            op: op_name.clone(),
249            kind: ProvKind::EliminatedConstant,
250            object_pcs: Vec::new(),
251            count: None,
252            scry_evidence: None,
253        });
254    }
255
256    // (a)-clause carrier: enumerate the REAL object conditional branches and
257    // reconcile each back to its source op via line_map. A branch that traces to
258    // a covered condition (BrIf / BrTable) is resolved; anything else is an
259    // uncovered/only-in-synth branch, surfaced with a note.
260    let mut object_cond_branches: Vec<ObjectCondBranch> = Vec::new();
261    for ((pc, oi), (_pc2, class)) in line_map.iter().zip(branch_map.iter()) {
262        if *class != BranchClass::CondBranch {
263            continue;
264        }
265        let (resolved, note, instruction_offset) = match oi {
266            Some(idx) => match ops.get(*idx) {
267                Some(WasmOp::BrIf(_)) | Some(WasmOp::BrTable { .. }) => {
268                    (true, None, op_offsets.get(*idx).copied())
269                }
270                Some(other) => (
271                    false,
272                    Some(format!(
273                        "object conditional branch from non-branch source op {other:?} \
274                         (uncovered in v1: i64-expansion / trap-guard / bounds-check branch)"
275                    )),
276                    op_offsets.get(*idx).copied(),
277                ),
278                None => (
279                    false,
280                    Some(
281                        "object conditional branch traces to an out-of-range op index".to_string(),
282                    ),
283                    None,
284                ),
285            },
286            None => (
287                false,
288                Some(
289                    "object conditional branch with no source op (prologue/epilogue synth branch)"
290                        .to_string(),
291                ),
292                None,
293            ),
294        };
295        object_cond_branches.push(ObjectCondBranch {
296            pc: *pc,
297            wasm_op_index: *oi,
298            instruction_offset,
299            resolved,
300            note,
301        });
302    }
303
304    FunctionProvenance {
305        func_index,
306        name: name.to_string(),
307        entries,
308        object_cond_branches,
309    }
310}
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315
316    #[test]
317    fn preserved_brif_and_folded_select() {
318        // op[0] BrIf, op[1] Select.
319        let ops = vec![WasmOp::BrIf(0), WasmOp::Select];
320        let op_offsets = vec![10u32, 20u32];
321        // Object: BrIf → cmp(other)@0x00 + bcc(cond)@0x04; Select → predicated mov@0x08.
322        let line_map: LineMap = vec![(0x00, Some(0)), (0x04, Some(0)), (0x08, Some(1))];
323        let branch_map: BranchMap = vec![
324            (0x00, BranchClass::Other),
325            (0x04, BranchClass::CondBranch),
326            (0x08, BranchClass::Predicated),
327        ];
328        let fp = derive_function_provenance(0, "f", &ops, &op_offsets, &line_map, &branch_map, &[]);
329
330        let brif = &fp.entries[0];
331        assert_eq!(brif.kind, ProvKind::Preserved);
332        assert_eq!(brif.object_pcs, vec![0x04]);
333        assert_eq!(brif.instruction_offset, 10);
334
335        let sel = &fp.entries[1];
336        assert_eq!(sel.kind, ProvKind::FoldedPredication);
337        assert_eq!(sel.object_pcs, vec![0x08]);
338
339        // (a): the one object cond branch resolves to the BrIf.
340        assert_eq!(fp.object_cond_branches.len(), 1);
341        assert!(fp.object_cond_branches[0].resolved);
342        assert_eq!(fp.object_cond_branches[0].instruction_offset, Some(10));
343    }
344
345    #[test]
346    fn unresolved_object_branch_is_surfaced_not_hidden() {
347        // An I32DivU whose object lowering emits a trap-guard conditional branch.
348        let ops = vec![WasmOp::I32DivU];
349        let op_offsets = vec![30u32];
350        let line_map: LineMap = vec![(0x00, Some(0))];
351        let branch_map: BranchMap = vec![(0x00, BranchClass::CondBranch)];
352        let fp = derive_function_provenance(0, "g", &ops, &op_offsets, &line_map, &branch_map, &[]);
353        // No covered source entries (I32DivU isn't a covered source branch)...
354        assert!(fp.entries.is_empty());
355        // ...but the object branch is NOT missing: it's surfaced unresolved.
356        assert_eq!(fp.object_cond_branches.len(), 1);
357        assert!(!fp.object_cond_branches[0].resolved);
358        assert!(fp.object_cond_branches[0].note.is_some());
359    }
360
361    #[test]
362    fn eliminated_constant_is_recorded() {
363        let ops: Vec<WasmOp> = vec![];
364        let op_offsets: Vec<u32> = vec![];
365        let line_map: LineMap = vec![];
366        let branch_map: BranchMap = vec![];
367        let fp = derive_function_provenance(
368            0,
369            "h",
370            &ops,
371            &op_offsets,
372            &line_map,
373            &branch_map,
374            &[(3, "BrIf".to_string(), 42)],
375        );
376        assert_eq!(fp.entries.len(), 1);
377        assert_eq!(fp.entries[0].kind, ProvKind::EliminatedConstant);
378        assert!(fp.entries[0].object_pcs.is_empty());
379        // The witness join key is the real byte offset, not a hardcoded 0.
380        assert_eq!(fp.entries[0].instruction_offset, 42);
381    }
382}