use serde::{Deserialize, Serialize};
use crate::backend::{BranchClass, BranchMap, LineMap};
use crate::wasm_op::WasmOp;
pub const SCHEMA: &str = "synth-provenance-v1";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ProvKind {
Preserved,
FoldedPredication,
SplitIntoObjectBranches,
EliminatedConstant,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProvEntry {
pub instruction_offset: u32,
pub wasm_op_index: usize,
pub op: String,
pub kind: ProvKind,
pub object_pcs: Vec<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub count: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scry_evidence: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ObjectCondBranch {
pub pc: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub wasm_op_index: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub instruction_offset: Option<u32>,
pub resolved: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub origin: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FunctionProvenance {
pub func_index: u32,
pub name: String,
pub entries: Vec<ProvEntry>,
pub object_cond_branches: Vec<ObjectCondBranch>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProvenanceMap {
pub schema: String,
pub module: String,
pub functions: Vec<FunctionProvenance>,
}
impl ProvenanceMap {
pub fn new(module: impl Into<String>) -> Self {
ProvenanceMap {
schema: SCHEMA.to_string(),
module: module.into(),
functions: Vec::new(),
}
}
pub fn to_json(&self) -> String {
serde_json::to_string_pretty(self).expect("ProvenanceMap serializes")
}
}
pub fn covered_source_op_name(op: &WasmOp) -> Option<&'static str> {
source_op_name(op)
}
fn source_op_name(op: &WasmOp) -> Option<&'static str> {
match op {
WasmOp::BrIf(_) => Some("BrIf"),
WasmOp::Br(_) => Some("Br"),
WasmOp::BrTable { .. } => Some("BrTable"),
WasmOp::Select => Some("Select"),
WasmOp::If => Some("If"),
_ => None,
}
}
pub fn introduced_branch_origin(op: &WasmOp) -> Option<&'static str> {
match op {
WasmOp::MemoryFill => Some("bulk-memory-fill-loop"),
WasmOp::MemoryCopy => Some("bulk-memory-copy-loop"),
WasmOp::I32DivS | WasmOp::I32DivU | WasmOp::I32RemS | WasmOp::I32RemU => {
Some("division-trap-guard")
}
_ => None,
}
}
pub fn derive_function_provenance(
func_index: u32,
name: &str,
ops: &[WasmOp],
op_offsets: &[u32],
line_map: &LineMap,
branch_map: &BranchMap,
eliminated: &[(usize, String, u32)],
) -> FunctionProvenance {
let mut entries: Vec<ProvEntry> = Vec::new();
for (op_idx, op) in ops.iter().enumerate() {
let Some(op_name) = source_op_name(op) else {
continue;
};
let instruction_offset = op_offsets.get(op_idx).copied().unwrap_or(0);
let mut cond_pcs = Vec::new();
let mut uncond_pcs = Vec::new();
let mut pred_pcs = Vec::new();
for ((pc, oi), (_pc2, class)) in line_map.iter().zip(branch_map.iter()) {
if *oi != Some(op_idx) {
continue;
}
match class {
BranchClass::CondBranch => cond_pcs.push(*pc),
BranchClass::UncondBranch => uncond_pcs.push(*pc),
BranchClass::Predicated => pred_pcs.push(*pc),
BranchClass::Other => {}
}
}
let (kind, object_pcs, count) = match op {
WasmOp::BrIf(_) | WasmOp::If => (ProvKind::Preserved, cond_pcs.clone(), None),
WasmOp::Br(_) => (ProvKind::Preserved, uncond_pcs.clone(), None),
WasmOp::BrTable { .. } => {
let n = cond_pcs.len();
let mut all = cond_pcs.clone();
all.extend(uncond_pcs.iter().copied());
(ProvKind::SplitIntoObjectBranches, all, Some(n))
}
WasmOp::Select => (ProvKind::FoldedPredication, pred_pcs.clone(), None),
_ => unreachable!("source_op_name gated the match"),
};
entries.push(ProvEntry {
instruction_offset,
wasm_op_index: op_idx,
op: op_name.to_string(),
kind,
object_pcs,
count,
scry_evidence: None,
});
}
for (orig_idx, op_name, byte_offset) in eliminated {
entries.push(ProvEntry {
instruction_offset: *byte_offset,
wasm_op_index: *orig_idx,
op: op_name.clone(),
kind: ProvKind::EliminatedConstant,
object_pcs: Vec::new(),
count: None,
scry_evidence: None,
});
}
let mut object_cond_branches: Vec<ObjectCondBranch> = Vec::new();
for ((pc, oi), (_pc2, class)) in line_map.iter().zip(branch_map.iter()) {
if *class != BranchClass::CondBranch {
continue;
}
let (resolved, origin, note, instruction_offset) = match oi {
Some(idx) => match ops.get(*idx) {
Some(WasmOp::BrIf(_)) | Some(WasmOp::BrTable { .. }) | Some(WasmOp::If) => {
(true, None, None, op_offsets.get(*idx).copied())
}
Some(other) => {
let origin = introduced_branch_origin(other);
let note = match origin {
Some(o) => format!(
"compiler-introduced: {o} — emitted lowering source op {other:?} \
(serves that op's WASM semantics; not a source-level decision)"
),
None => format!(
"object conditional branch from non-branch source op {other:?} \
(unattributed: op family not in the verified origin map, #944)"
),
};
(
false,
origin.map(str::to_string),
Some(note),
op_offsets.get(*idx).copied(),
)
}
None => (
false,
None,
Some(
"object conditional branch traces to an out-of-range op index".to_string(),
),
None,
),
},
None => (
false,
None,
Some(
"object conditional branch with no source op (prologue/epilogue synth branch)"
.to_string(),
),
None,
),
};
object_cond_branches.push(ObjectCondBranch {
pc: *pc,
wasm_op_index: *oi,
instruction_offset,
resolved,
origin,
note,
});
}
FunctionProvenance {
func_index,
name: name.to_string(),
entries,
object_cond_branches,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn preserved_brif_and_folded_select() {
let ops = vec![WasmOp::BrIf(0), WasmOp::Select];
let op_offsets = vec![10u32, 20u32];
let line_map: LineMap = vec![(0x00, Some(0)), (0x04, Some(0)), (0x08, Some(1))];
let branch_map: BranchMap = vec![
(0x00, BranchClass::Other),
(0x04, BranchClass::CondBranch),
(0x08, BranchClass::Predicated),
];
let fp = derive_function_provenance(0, "f", &ops, &op_offsets, &line_map, &branch_map, &[]);
let brif = &fp.entries[0];
assert_eq!(brif.kind, ProvKind::Preserved);
assert_eq!(brif.object_pcs, vec![0x04]);
assert_eq!(brif.instruction_offset, 10);
let sel = &fp.entries[1];
assert_eq!(sel.kind, ProvKind::FoldedPredication);
assert_eq!(sel.object_pcs, vec![0x08]);
assert_eq!(fp.object_cond_branches.len(), 1);
assert!(fp.object_cond_branches[0].resolved);
assert_eq!(fp.object_cond_branches[0].instruction_offset, Some(10));
}
#[test]
fn unresolved_object_branch_is_surfaced_not_hidden() {
let ops = vec![WasmOp::I32DivU];
let op_offsets = vec![30u32];
let line_map: LineMap = vec![(0x00, Some(0))];
let branch_map: BranchMap = vec![(0x00, BranchClass::CondBranch)];
let fp = derive_function_provenance(0, "g", &ops, &op_offsets, &line_map, &branch_map, &[]);
assert!(fp.entries.is_empty());
assert_eq!(fp.object_cond_branches.len(), 1);
assert!(!fp.object_cond_branches[0].resolved);
assert!(fp.object_cond_branches[0].note.is_some());
assert_eq!(
fp.object_cond_branches[0].origin.as_deref(),
Some("division-trap-guard")
);
}
#[test]
fn if_decision_branch_is_covered_and_resolved() {
let ops = vec![WasmOp::If];
let op_offsets = vec![50u32];
let line_map: LineMap = vec![(0x00, Some(0)), (0x04, Some(0))];
let branch_map: BranchMap = vec![
(0x00, BranchClass::Other), (0x04, BranchClass::CondBranch), ];
let fp = derive_function_provenance(0, "f", &ops, &op_offsets, &line_map, &branch_map, &[]);
assert_eq!(fp.entries.len(), 1);
assert_eq!(fp.entries[0].op, "If");
assert_eq!(fp.entries[0].kind, ProvKind::Preserved);
assert_eq!(fp.entries[0].object_pcs, vec![0x04]);
assert_eq!(fp.object_cond_branches.len(), 1);
assert!(fp.object_cond_branches[0].resolved);
assert!(fp.object_cond_branches[0].origin.is_none());
}
#[test]
fn bulk_memory_branches_carry_verified_origin() {
let ops = vec![WasmOp::MemoryFill, WasmOp::MemoryCopy];
let op_offsets = vec![10u32, 20u32];
let line_map: LineMap = vec![
(0x00, Some(0)),
(0x08, Some(1)),
(0x10, Some(1)),
(0x20, Some(1)),
];
let branch_map: BranchMap = vec![
(0x00, BranchClass::CondBranch),
(0x08, BranchClass::CondBranch),
(0x10, BranchClass::CondBranch),
(0x20, BranchClass::CondBranch),
];
let fp = derive_function_provenance(0, "b", &ops, &op_offsets, &line_map, &branch_map, &[]);
let origins: Vec<_> = fp
.object_cond_branches
.iter()
.map(|b| b.origin.as_deref())
.collect();
assert_eq!(
origins,
vec![
Some("bulk-memory-fill-loop"),
Some("bulk-memory-copy-loop"),
Some("bulk-memory-copy-loop"),
Some("bulk-memory-copy-loop"),
]
);
assert_eq!(
fp.object_cond_branches[0].instruction_offset,
Some(10),
"fill branch anchors at the memory.fill op offset"
);
}
#[test]
fn unverified_op_family_stays_declared_unattributed() {
let ops = vec![WasmOp::I64Shl];
let op_offsets = vec![70u32];
let line_map: LineMap = vec![(0x00, Some(0))];
let branch_map: BranchMap = vec![(0x00, BranchClass::CondBranch)];
let fp = derive_function_provenance(0, "u", &ops, &op_offsets, &line_map, &branch_map, &[]);
let b = &fp.object_cond_branches[0];
assert!(!b.resolved);
assert!(
b.origin.is_none(),
"must not invent an origin: {:?}",
b.origin
);
assert!(
b.note.as_deref().unwrap_or("").contains("unattributed"),
"the note must declare the gap, not guess: {:?}",
b.note
);
}
#[test]
fn no_source_op_branch_is_declared() {
let ops: Vec<WasmOp> = vec![];
let op_offsets: Vec<u32> = vec![];
let line_map: LineMap = vec![(0x00, None)];
let branch_map: BranchMap = vec![(0x00, BranchClass::CondBranch)];
let fp = derive_function_provenance(0, "p", &ops, &op_offsets, &line_map, &branch_map, &[]);
let b = &fp.object_cond_branches[0];
assert!(!b.resolved);
assert!(b.origin.is_none());
assert!(b.note.is_some());
}
#[test]
fn eliminated_constant_is_recorded() {
let ops: Vec<WasmOp> = vec![];
let op_offsets: Vec<u32> = vec![];
let line_map: LineMap = vec![];
let branch_map: BranchMap = vec![];
let fp = derive_function_provenance(
0,
"h",
&ops,
&op_offsets,
&line_map,
&branch_map,
&[(3, "BrIf".to_string(), 42)],
);
assert_eq!(fp.entries.len(), 1);
assert_eq!(fp.entries[0].kind, ProvKind::EliminatedConstant);
assert!(fp.entries[0].object_pcs.is_empty());
assert_eq!(fp.entries[0].instruction_offset, 42);
}
}