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), `if` (preserved — the structured-decision conditional
44//! branch, #944). `eliminated-constant` is WIRED (schema + emitter, correct
45//! byte-offset join key) but not yet gate-exercised — a fixture that drops a
46//! covered branch op is a v1 follow-up. Every object conditional branch that does
47//! NOT resolve to one of the covered source ops is surfaced in
48//! `object_cond_branches` with `resolved: false` — and, since #944, with a
49//! machine-readable `origin` naming WHY synth introduced it when the introducing
50//! op family is one whose lowering is verified to emit exactly that control flow
51//! (see [`introduced_branch_origin`]). An introduced branch whose family is NOT
52//! in that verified map stays `origin: None` with the op named in the note — an
53//! unexplained-but-declared branch beats a confident wrong label (#944's own
54//! finding: a plausible "guard" label for these was tested and found wrong).
55//!
56//! ## Compiler-introduced branch origins (#944)
57//!
58//! `origin` values are kebab-case and each is backed by disassembly-verified
59//! lowering shape on the direct/relocatable ARM path:
60//! - `bulk-memory-fill-loop` — `memory.fill` expands to a byte-store loop; its
61//!   one conditional branch is the loop bound test (`cmp; bhs` — zero-trip safe).
62//! - `bulk-memory-copy-loop` — `memory.copy` (memmove semantics) expands to an
63//!   overlap-direction test (`cmp dst,src; bhi`) plus a forward- and a
64//!   backward-copy loop bound test: exactly three conditional branches.
65//! - `division-trap-guard` — `i32.div_s` emits the divide-by-zero guard plus the
66//!   two-test `INT_MIN / -1` overflow guard (three branches, each skipping a
67//!   `udf`); `i32.div_u` / `i32.rem_s` / `i32.rem_u` emit the zero guard alone.
68//!
69//! These fields are ADDITIVE on the `synth-provenance-v1` wire format: the
70//! deployed consumer (witness `object-disposition`, whose serde ignores unknown
71//! fields) keeps parsing maps that carry them.
72
73use serde::{Deserialize, Serialize};
74
75use crate::backend::{BranchClass, BranchMap, LineMap};
76use crate::wasm_op::WasmOp;
77
78/// The schema version string embedded at the top of the sidecar.
79pub const SCHEMA: &str = "synth-provenance-v1";
80
81/// The transformation a source branch/condition underwent on the way to object code.
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
83#[serde(rename_all = "kebab-case")]
84pub enum ProvKind {
85    /// 1:1 `br_if`/`br` that stayed a real object branch.
86    Preserved,
87    /// `select` fused to predicated moves (no object branch).
88    FoldedPredication,
89    /// `br_table` split into N object branches (`count` = N).
90    SplitIntoObjectBranches,
91    /// Source branch/condition dropped before codegen (constant / fact-spec).
92    EliminatedConstant,
93}
94
95/// One source-level branch/condition and what it became in the object.
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
97pub struct ProvEntry {
98    /// Witness join key: ABSOLUTE wasm byte offset of the source op.
99    pub instruction_offset: u32,
100    /// Index of the source op within the (compiled) op stream — diagnostic.
101    pub wasm_op_index: usize,
102    /// The source WASM op mnemonic (e.g. `"BrIf"`, `"Select"`, `"BrTable"`).
103    pub op: String,
104    /// How synth transformed it.
105    pub kind: ProvKind,
106    /// Object PCs (function-relative machine offsets) that realize this source
107    /// op's control flow. Empty for `eliminated-constant`.
108    pub object_pcs: Vec<u32>,
109    /// For `split-into-object-branches`: the object-branch count. Omitted otherwise.
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub count: Option<usize>,
112    /// Optional scry#51 reachability evidence for an `eliminated-constant` entry
113    /// (justified-infeasible). Reserved for a later increment; `None` in v1.
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub scry_evidence: Option<String>,
116}
117
118/// One object-level conditional branch, and whether it reconciled to a covered
119/// source condition. This is the (a)-clause carrier: derived from the REAL
120/// object-branch side-table, so a branch synth emitted that no covered source op
121/// explains shows up here with `resolved: false` (surfaced, not hidden).
122#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
123pub struct ObjectCondBranch {
124    /// Function-relative machine offset of the conditional branch.
125    pub pc: u32,
126    /// The wasm op index this branch traces back to (via `line_map`), if any.
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub wasm_op_index: Option<usize>,
129    /// Absolute wasm byte offset of that source op, if resolvable.
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub instruction_offset: Option<u32>,
132    /// True iff this branch resolves to a covered source condition (`br_if` /
133    /// `br_table` / `if`). False = a compiler-introduced object branch —
134    /// surfaced not hidden, and carrying `origin` when its introducing op
135    /// family is in the verified classification (#944).
136    pub resolved: bool,
137    /// #944: machine-readable origin for a compiler-introduced branch
138    /// (`resolved: false`), derived from the source op the branch's encode-time
139    /// `line_map` entry traces to — never guessed. `None` for a resolved branch,
140    /// and for an introduced branch whose op family is not in the verified map
141    /// (declared-unattributed; the gate pins that count). Additive field: absent
142    /// on the wire when `None`, so pre-#944 consumers are unaffected.
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub origin: Option<String>,
145    /// Human note when `resolved` is false.
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub note: Option<String>,
148}
149
150/// Provenance for one compiled function.
151#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
152pub struct FunctionProvenance {
153    pub func_index: u32,
154    pub name: String,
155    pub entries: Vec<ProvEntry>,
156    pub object_cond_branches: Vec<ObjectCondBranch>,
157}
158
159/// The whole-module `synth-provenance-v1` map.
160#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
161pub struct ProvenanceMap {
162    pub schema: String,
163    pub module: String,
164    pub functions: Vec<FunctionProvenance>,
165}
166
167impl ProvenanceMap {
168    pub fn new(module: impl Into<String>) -> Self {
169        ProvenanceMap {
170            schema: SCHEMA.to_string(),
171            module: module.into(),
172            functions: Vec::new(),
173        }
174    }
175
176    /// Serialize to pretty JSON.
177    pub fn to_json(&self) -> String {
178        serde_json::to_string_pretty(self).expect("ProvenanceMap serializes")
179    }
180}
181
182/// Is this a source op the map covers as a branch/condition? Returns its
183/// mnemonic if so. Public so the CLI can classify eliminated (fact-spec-dropped)
184/// ops with the SAME coverage predicate the emitter uses.
185pub fn covered_source_op_name(op: &WasmOp) -> Option<&'static str> {
186    source_op_name(op)
187}
188
189/// Is this a source op the map covers as a branch/condition?
190fn source_op_name(op: &WasmOp) -> Option<&'static str> {
191    match op {
192        WasmOp::BrIf(_) => Some("BrIf"),
193        WasmOp::Br(_) => Some("Br"),
194        WasmOp::BrTable { .. } => Some("BrTable"),
195        WasmOp::Select => Some("Select"),
196        // #944: `if` is a real source decision — its conditional branch was
197        // previously mis-bucketed with the compiler-introduced branches.
198        WasmOp::If => Some("If"),
199        _ => None,
200    }
201}
202
203/// #944: the machine-readable origin of a compiler-introduced conditional
204/// branch, keyed by the source op whose LOWERING emits it (known exactly — the
205/// encode-time `line_map` records which op each machine instruction came from).
206///
207/// Deliberately narrow: an op family is listed ONLY once its lowering's branch
208/// shape has been verified by disassembly (see the module header for each
209/// family's shape). Anything else returns `None` and stays declared-unattributed
210/// — mislabelling a branch is worse than declaring it unexplained (#944).
211pub fn introduced_branch_origin(op: &WasmOp) -> Option<&'static str> {
212    match op {
213        // memory.fill byte-store loop: one bound-test branch (zero-trip safe).
214        WasmOp::MemoryFill => Some("bulk-memory-fill-loop"),
215        // memory.copy memmove expansion: overlap-direction test + forward- and
216        // backward-copy loop bound tests (three branches).
217        WasmOp::MemoryCopy => Some("bulk-memory-copy-loop"),
218        // WASM trap semantics: divide-by-zero guard (all four), plus the
219        // INT_MIN/-1 overflow guard pair for `i32.div_s`.
220        WasmOp::I32DivS | WasmOp::I32DivU | WasmOp::I32RemS | WasmOp::I32RemU => {
221            Some("division-trap-guard")
222        }
223        _ => None,
224    }
225}
226
227/// Derive provenance for one function from the CLI-available data.
228///
229/// - `ops` / `op_offsets` are index-aligned (the stream the backend compiled and
230///   its per-op absolute wasm byte offsets, already fact-spec-filtered upstream).
231/// - `line_map` / `branch_map` are index-aligned (one entry per emitted machine
232///   instruction: `(pc, wasm_op_index)` and `(pc, class)`).
233/// - `eliminated`: `(wasm_op_index_in_original_stream, op_name,
234///   absolute_wasm_byte_offset)` for branch/condition ops that constant-folding
235///   / fact-spec dropped before codegen. The offset is the ORIGINAL-stream byte
236///   offset (the witness join key), NOT derivable from `op_offsets` here (which
237///   is the filtered/kept table) — the caller looks it up in the unfiltered
238///   side-table.
239pub fn derive_function_provenance(
240    func_index: u32,
241    name: &str,
242    ops: &[WasmOp],
243    op_offsets: &[u32],
244    line_map: &LineMap,
245    branch_map: &BranchMap,
246    eliminated: &[(usize, String, u32)],
247) -> FunctionProvenance {
248    // For each op index, collect the object PCs whose branch_map class matters.
249    // line_map and branch_map are parallel; zip them.
250    let mut entries: Vec<ProvEntry> = Vec::new();
251
252    for (op_idx, op) in ops.iter().enumerate() {
253        let Some(op_name) = source_op_name(op) else {
254            continue;
255        };
256        let instruction_offset = op_offsets.get(op_idx).copied().unwrap_or(0);
257
258        // Object realizations of this op: the machine instructions whose
259        // line_map op-index == op_idx AND whose branch class is a branch or a
260        // predicated move (skip the data-processing setup instructions).
261        let mut cond_pcs = Vec::new();
262        let mut uncond_pcs = Vec::new();
263        let mut pred_pcs = Vec::new();
264        for ((pc, oi), (_pc2, class)) in line_map.iter().zip(branch_map.iter()) {
265            if *oi != Some(op_idx) {
266                continue;
267            }
268            match class {
269                BranchClass::CondBranch => cond_pcs.push(*pc),
270                BranchClass::UncondBranch => uncond_pcs.push(*pc),
271                BranchClass::Predicated => pred_pcs.push(*pc),
272                BranchClass::Other => {}
273            }
274        }
275
276        let (kind, object_pcs, count) = match op {
277            // #944: an `if` decision is realized by its conditional branch, like
278            // a `br_if` (its then-end unconditional jump is control flow, not
279            // the decision).
280            WasmOp::BrIf(_) | WasmOp::If => (ProvKind::Preserved, cond_pcs.clone(), None),
281            WasmOp::Br(_) => (ProvKind::Preserved, uncond_pcs.clone(), None),
282            WasmOp::BrTable { .. } => {
283                let n = cond_pcs.len();
284                let mut all = cond_pcs.clone();
285                all.extend(uncond_pcs.iter().copied());
286                (ProvKind::SplitIntoObjectBranches, all, Some(n))
287            }
288            WasmOp::Select => (ProvKind::FoldedPredication, pred_pcs.clone(), None),
289            _ => unreachable!("source_op_name gated the match"),
290        };
291
292        entries.push(ProvEntry {
293            instruction_offset,
294            wasm_op_index: op_idx,
295            op: op_name.to_string(),
296            kind,
297            object_pcs,
298            count,
299            scry_evidence: None,
300        });
301    }
302
303    // Eliminated-constant entries: branch/condition ops dropped before codegen.
304    for (orig_idx, op_name, byte_offset) in eliminated {
305        entries.push(ProvEntry {
306            instruction_offset: *byte_offset,
307            wasm_op_index: *orig_idx,
308            op: op_name.clone(),
309            kind: ProvKind::EliminatedConstant,
310            object_pcs: Vec::new(),
311            count: None,
312            scry_evidence: None,
313        });
314    }
315
316    // (a)-clause carrier: enumerate the REAL object conditional branches and
317    // reconcile each back to its source op via line_map. A branch that traces to
318    // a covered condition (BrIf / BrTable) is resolved; anything else is an
319    // uncovered/only-in-synth branch, surfaced with a note.
320    let mut object_cond_branches: Vec<ObjectCondBranch> = Vec::new();
321    for ((pc, oi), (_pc2, class)) in line_map.iter().zip(branch_map.iter()) {
322        if *class != BranchClass::CondBranch {
323            continue;
324        }
325        let (resolved, origin, note, instruction_offset) = match oi {
326            Some(idx) => match ops.get(*idx) {
327                Some(WasmOp::BrIf(_)) | Some(WasmOp::BrTable { .. }) | Some(WasmOp::If) => {
328                    (true, None, None, op_offsets.get(*idx).copied())
329                }
330                Some(other) => {
331                    // #944: a compiler-introduced branch. When the introducing
332                    // op family's branch shape is verified, carry its
333                    // machine-readable origin; otherwise declare it
334                    // unattributed with the op named — never guess a label.
335                    let origin = introduced_branch_origin(other);
336                    let note = match origin {
337                        Some(o) => format!(
338                            "compiler-introduced: {o} — emitted lowering source op {other:?} \
339                             (serves that op's WASM semantics; not a source-level decision)"
340                        ),
341                        None => format!(
342                            "object conditional branch from non-branch source op {other:?} \
343                             (unattributed: op family not in the verified origin map, #944)"
344                        ),
345                    };
346                    (
347                        false,
348                        origin.map(str::to_string),
349                        Some(note),
350                        op_offsets.get(*idx).copied(),
351                    )
352                }
353                None => (
354                    false,
355                    None,
356                    Some(
357                        "object conditional branch traces to an out-of-range op index".to_string(),
358                    ),
359                    None,
360                ),
361            },
362            None => (
363                false,
364                None,
365                Some(
366                    "object conditional branch with no source op (prologue/epilogue synth branch)"
367                        .to_string(),
368                ),
369                None,
370            ),
371        };
372        object_cond_branches.push(ObjectCondBranch {
373            pc: *pc,
374            wasm_op_index: *oi,
375            instruction_offset,
376            resolved,
377            origin,
378            note,
379        });
380    }
381
382    FunctionProvenance {
383        func_index,
384        name: name.to_string(),
385        entries,
386        object_cond_branches,
387    }
388}
389
390#[cfg(test)]
391mod tests {
392    use super::*;
393
394    #[test]
395    fn preserved_brif_and_folded_select() {
396        // op[0] BrIf, op[1] Select.
397        let ops = vec![WasmOp::BrIf(0), WasmOp::Select];
398        let op_offsets = vec![10u32, 20u32];
399        // Object: BrIf → cmp(other)@0x00 + bcc(cond)@0x04; Select → predicated mov@0x08.
400        let line_map: LineMap = vec![(0x00, Some(0)), (0x04, Some(0)), (0x08, Some(1))];
401        let branch_map: BranchMap = vec![
402            (0x00, BranchClass::Other),
403            (0x04, BranchClass::CondBranch),
404            (0x08, BranchClass::Predicated),
405        ];
406        let fp = derive_function_provenance(0, "f", &ops, &op_offsets, &line_map, &branch_map, &[]);
407
408        let brif = &fp.entries[0];
409        assert_eq!(brif.kind, ProvKind::Preserved);
410        assert_eq!(brif.object_pcs, vec![0x04]);
411        assert_eq!(brif.instruction_offset, 10);
412
413        let sel = &fp.entries[1];
414        assert_eq!(sel.kind, ProvKind::FoldedPredication);
415        assert_eq!(sel.object_pcs, vec![0x08]);
416
417        // (a): the one object cond branch resolves to the BrIf.
418        assert_eq!(fp.object_cond_branches.len(), 1);
419        assert!(fp.object_cond_branches[0].resolved);
420        assert_eq!(fp.object_cond_branches[0].instruction_offset, Some(10));
421    }
422
423    #[test]
424    fn unresolved_object_branch_is_surfaced_not_hidden() {
425        // An I32DivU whose object lowering emits a trap-guard conditional branch.
426        let ops = vec![WasmOp::I32DivU];
427        let op_offsets = vec![30u32];
428        let line_map: LineMap = vec![(0x00, Some(0))];
429        let branch_map: BranchMap = vec![(0x00, BranchClass::CondBranch)];
430        let fp = derive_function_provenance(0, "g", &ops, &op_offsets, &line_map, &branch_map, &[]);
431        // No covered source entries (I32DivU isn't a covered source branch)...
432        assert!(fp.entries.is_empty());
433        // ...but the object branch is NOT missing: it's surfaced unresolved,
434        // and (#944) with its verified machine-readable origin.
435        assert_eq!(fp.object_cond_branches.len(), 1);
436        assert!(!fp.object_cond_branches[0].resolved);
437        assert!(fp.object_cond_branches[0].note.is_some());
438        assert_eq!(
439            fp.object_cond_branches[0].origin.as_deref(),
440            Some("division-trap-guard")
441        );
442    }
443
444    /// #944: an `if` decision's conditional branch is a SOURCE decision —
445    /// covered (preserved entry) and resolved, not a compiler-introduced branch.
446    #[test]
447    fn if_decision_branch_is_covered_and_resolved() {
448        let ops = vec![WasmOp::If];
449        let op_offsets = vec![50u32];
450        let line_map: LineMap = vec![(0x00, Some(0)), (0x04, Some(0))];
451        let branch_map: BranchMap = vec![
452            (0x00, BranchClass::Other),      // the cmp
453            (0x04, BranchClass::CondBranch), // the beq to the else/end arm
454        ];
455        let fp = derive_function_provenance(0, "f", &ops, &op_offsets, &line_map, &branch_map, &[]);
456        assert_eq!(fp.entries.len(), 1);
457        assert_eq!(fp.entries[0].op, "If");
458        assert_eq!(fp.entries[0].kind, ProvKind::Preserved);
459        assert_eq!(fp.entries[0].object_pcs, vec![0x04]);
460        assert_eq!(fp.object_cond_branches.len(), 1);
461        assert!(fp.object_cond_branches[0].resolved);
462        assert!(fp.object_cond_branches[0].origin.is_none());
463    }
464
465    /// #944 classified origins: bulk-memory expansion branches carry the
466    /// verified origin of the op whose lowering emitted them.
467    #[test]
468    fn bulk_memory_branches_carry_verified_origin() {
469        let ops = vec![WasmOp::MemoryFill, WasmOp::MemoryCopy];
470        let op_offsets = vec![10u32, 20u32];
471        // fill: one loop-bound branch; copy: direction test + two loop bounds.
472        let line_map: LineMap = vec![
473            (0x00, Some(0)),
474            (0x08, Some(1)),
475            (0x10, Some(1)),
476            (0x20, Some(1)),
477        ];
478        let branch_map: BranchMap = vec![
479            (0x00, BranchClass::CondBranch),
480            (0x08, BranchClass::CondBranch),
481            (0x10, BranchClass::CondBranch),
482            (0x20, BranchClass::CondBranch),
483        ];
484        let fp = derive_function_provenance(0, "b", &ops, &op_offsets, &line_map, &branch_map, &[]);
485        let origins: Vec<_> = fp
486            .object_cond_branches
487            .iter()
488            .map(|b| b.origin.as_deref())
489            .collect();
490        assert_eq!(
491            origins,
492            vec![
493                Some("bulk-memory-fill-loop"),
494                Some("bulk-memory-copy-loop"),
495                Some("bulk-memory-copy-loop"),
496                Some("bulk-memory-copy-loop"),
497            ]
498        );
499        // Each also carries the introducing op's byte offset — the join anchor.
500        assert_eq!(
501            fp.object_cond_branches[0].instruction_offset,
502            Some(10),
503            "fill branch anchors at the memory.fill op offset"
504        );
505    }
506
507    /// #944 negative control (non-vacuity): the origin map must NOT blanket-label.
508    /// A conditional branch tracing to an op family whose lowering shape has not
509    /// been disassembly-verified stays declared-unattributed (`origin: None`) —
510    /// widening the map without verification is exactly the failure mode the
511    /// gate exists to prevent.
512    #[test]
513    fn unverified_op_family_stays_declared_unattributed() {
514        let ops = vec![WasmOp::I64Shl];
515        let op_offsets = vec![70u32];
516        let line_map: LineMap = vec![(0x00, Some(0))];
517        let branch_map: BranchMap = vec![(0x00, BranchClass::CondBranch)];
518        let fp = derive_function_provenance(0, "u", &ops, &op_offsets, &line_map, &branch_map, &[]);
519        let b = &fp.object_cond_branches[0];
520        assert!(!b.resolved);
521        assert!(
522            b.origin.is_none(),
523            "must not invent an origin: {:?}",
524            b.origin
525        );
526        assert!(
527            b.note.as_deref().unwrap_or("").contains("unattributed"),
528            "the note must declare the gap, not guess: {:?}",
529            b.note
530        );
531    }
532
533    /// #944: a branch with NO source op at all (prologue/epilogue) is declared,
534    /// not silently labeled.
535    #[test]
536    fn no_source_op_branch_is_declared() {
537        let ops: Vec<WasmOp> = vec![];
538        let op_offsets: Vec<u32> = vec![];
539        let line_map: LineMap = vec![(0x00, None)];
540        let branch_map: BranchMap = vec![(0x00, BranchClass::CondBranch)];
541        let fp = derive_function_provenance(0, "p", &ops, &op_offsets, &line_map, &branch_map, &[]);
542        let b = &fp.object_cond_branches[0];
543        assert!(!b.resolved);
544        assert!(b.origin.is_none());
545        assert!(b.note.is_some());
546    }
547
548    #[test]
549    fn eliminated_constant_is_recorded() {
550        let ops: Vec<WasmOp> = vec![];
551        let op_offsets: Vec<u32> = vec![];
552        let line_map: LineMap = vec![];
553        let branch_map: BranchMap = vec![];
554        let fp = derive_function_provenance(
555            0,
556            "h",
557            &ops,
558            &op_offsets,
559            &line_map,
560            &branch_map,
561            &[(3, "BrIf".to_string(), 42)],
562        );
563        assert_eq!(fp.entries.len(), 1);
564        assert_eq!(fp.entries[0].kind, ProvKind::EliminatedConstant);
565        assert!(fp.entries[0].object_pcs.is_empty());
566        // The witness join key is the real byte offset, not a hardcoded 0.
567        assert_eq!(fp.entries[0].instruction_offset, 42);
568    }
569}