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 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"),
_ => 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(_) => (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, note, instruction_offset) = match oi {
Some(idx) => match ops.get(*idx) {
Some(WasmOp::BrIf(_)) | Some(WasmOp::BrTable { .. }) => {
(true, None, op_offsets.get(*idx).copied())
}
Some(other) => (
false,
Some(format!(
"object conditional branch from non-branch source op {other:?} \
(uncovered in v1: i64-expansion / trap-guard / bounds-check branch)"
)),
op_offsets.get(*idx).copied(),
),
None => (
false,
Some(
"object conditional branch traces to an out-of-range op index".to_string(),
),
None,
),
},
None => (
false,
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,
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());
}
#[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);
}
}