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}