use alloc::{collections::BTreeMap, sync::Arc, vec::Vec};
use miden_core::{
Felt, Word,
advice::AdviceMap,
chiplets::hasher,
mast::{
BasicBlockNode, BasicBlockNodeBuilder, CallNode, DynNode, JoinNode, LoopNode, MastForest,
MastForestRootMap, MastNode, MastNodeExt, OpBatch, SplitNode, error_code_from_msg,
},
operations::{AssemblyOp, DebugVarInfo, Operation},
serde::Serializable,
utils::{Idx, IndexVec, bytes_to_packed_u32_elements},
};
use miden_mast_package::debug_info::PackageDebugInfo;
use super::{GlobalItemIndex, LinkerError, Procedure};
use crate::{
diagnostics::{IntoDiagnostic, Report, WrapErr},
report,
};
mod finalizer;
use finalizer::{BuiltMastForest, MastForestFinalizer};
mod node_identity_policy;
use node_identity_policy::FinalForestLayout;
mod pending_record;
pub(crate) use pending_record::{AsmOpRef, DebugVarRef, MastNodeRef, SourceNodeRef};
use pending_record::{
MastNodeKey, PendingMastNode, PendingMastNodeDraft, PendingMastNodeKind, PendingSourceNode,
};
mod source_debug_graph;
pub(crate) use source_debug_graph::{SourceDebugGraph, SourceNode, SourceNodeId};
mod static_import;
const PROCEDURE_INLINING_THRESHOLD: usize = 32;
const BASIC_BLOCK_ERROR_CODE_KEY_DOMAIN: Felt = Felt::new_unchecked(0x2473_0001);
const CHILD_KEY_DOMAIN: Felt = Felt::new_unchecked(0x2473_0002);
#[derive(Clone, Debug, Default)]
pub struct MastForestBuilder {
advice_map: AdviceMap,
procedures: BTreeMap<GlobalItemIndex, Procedure>,
proc_gid_by_mast_root: BTreeMap<Word, GlobalItemIndex>,
procedure_root_refs: Vec<MastNodeRef>,
procedure_source_root_refs: Vec<SourceNodeRef>,
procedure_source_root_count_by_node_ref: BTreeMap<MastNodeRef, usize>,
node_ref_by_key: BTreeMap<MastNodeKey, MastNodeRef>,
nodes: IndexVec<MastNodeRef, PendingMastNode>,
source_nodes: IndexVec<SourceNodeRef, PendingSourceNode>,
latest_source_ref_by_node_ref: BTreeMap<MastNodeRef, SourceNodeRef>,
source_refs_by_node_ref: BTreeMap<MastNodeRef, Vec<SourceNodeRef>>,
asm_op_by_ref: IndexVec<AsmOpRef, AssemblyOp>,
debug_vars: IndexVec<DebugVarRef, DebugVarInfo>,
error_messages: BTreeMap<u64, Arc<str>>,
statically_linked_mast: Arc<MastForest>,
statically_linked_source_forests: Vec<Arc<MastForest>>,
statically_linked_package_debug_info: Vec<Option<PackageDebugInfo>>,
statically_linked_forest_indices_by_commitment: BTreeMap<Word, Vec<usize>>,
statically_linked_root_map: MastForestRootMap,
}
pub(crate) struct StaticLibrary<'a> {
pub(crate) mast: &'a MastForest,
pub(crate) debug_info: Option<PackageDebugInfo>,
}
impl<'a> StaticLibrary<'a> {
pub(crate) fn new(mast: &'a MastForest, debug_info: Option<PackageDebugInfo>) -> Self {
Self { mast, debug_info }
}
}
impl MastForestBuilder {
#[cfg(test)]
pub fn new<'a>(
static_libraries: impl IntoIterator<Item = &'a MastForest>,
) -> Result<Self, Report> {
Self::new_with_static_libraries(
static_libraries.into_iter().map(|mast| StaticLibrary::new(mast, None)),
)
}
pub(crate) fn new_with_static_libraries<'a>(
static_libraries: impl IntoIterator<Item = StaticLibrary<'a>>,
) -> Result<Self, Report> {
let static_libraries = static_libraries.into_iter().collect::<Vec<_>>();
let forests = static_libraries.iter().map(|library| library.mast).collect::<Vec<_>>();
let statically_linked_source_forests = static_libraries
.iter()
.map(|library| Arc::new(library.mast.clone()))
.collect::<Vec<_>>();
let mut error_messages = BTreeMap::new();
let statically_linked_package_debug_info = static_libraries
.into_iter()
.map(|library| {
if let Some(debug_info) = library.debug_info.as_ref()
&& let Some(section) = debug_info.error_messages()
{
for row in section.messages() {
error_messages.entry(row.err_code).or_insert_with(|| row.message.clone());
}
}
library.debug_info
})
.collect::<Vec<_>>();
let mut statically_linked_forest_indices_by_commitment = BTreeMap::new();
for (idx, forest) in forests.iter().enumerate() {
statically_linked_forest_indices_by_commitment
.entry(forest.commitment())
.or_insert_with(Vec::new)
.push(idx);
}
let (statically_linked_mast, statically_linked_root_map) =
MastForest::merge(forests.iter().copied()).into_diagnostic()?;
Ok(MastForestBuilder {
advice_map: statically_linked_mast.advice_map().clone(),
statically_linked_mast: Arc::new(statically_linked_mast),
statically_linked_source_forests,
statically_linked_package_debug_info,
statically_linked_forest_indices_by_commitment,
statically_linked_root_map,
error_messages,
..Self::default()
})
}
fn push_pending_node_record_ref(
&mut self,
key: MastNodeKey,
draft: PendingMastNodeDraft,
) -> Result<MastNodeRef, Report> {
let node_ref = self
.nodes
.push(PendingMastNode {
key,
digest: draft.digest,
kind: draft.kind,
child_refs: draft.child_refs,
asm_ops: draft.asm_ops,
debug_vars: draft.debug_vars,
})
.into_diagnostic()
.wrap_err("assembler created too many MAST nodes")?;
Ok(node_ref)
}
fn insert_pending_node_record_ref(
&mut self,
key: MastNodeKey,
draft: PendingMastNodeDraft,
) -> Result<MastNodeRef, Report> {
let node_ref = self.push_pending_node_record_ref(key, draft)?;
self.node_ref_by_key.insert(key, node_ref);
Ok(node_ref)
}
fn dedup_key_for_pending_data(&self, draft: &PendingMastNodeDraft) -> MastNodeKey {
self.key_for_pending_record(draft.digest, &draft.kind, &draft.child_refs)
}
fn intern_pending_node(&mut self, draft: PendingMastNodeDraft) -> Result<MastNodeRef, Report> {
let dedup_key = self.dedup_key_for_pending_data(&draft);
let source_child_refs = self.source_child_refs_for_node_refs(&draft.child_refs);
let node_ref = if let Some(node_ref) = self.find_node_ref_by_key(&dedup_key) {
if self.should_replace_pending_node(node_ref, &draft) {
self.replace_pending_node_record_ref(node_ref, dedup_key, draft.clone());
}
node_ref
} else {
self.insert_pending_node_record_ref(dedup_key, draft.clone())?
};
self.record_source_occurrence(node_ref, source_child_refs, &draft)?;
Ok(node_ref)
}
fn insert_pending_node_with_allocated_metadata_refs(
&mut self,
dedup_key: MastNodeKey,
draft: PendingMastNodeDraft,
asm_op_checkpoint: usize,
debug_var_checkpoint: usize,
) -> Result<MastNodeRef, Report> {
match self.insert_or_replace_pending_node_record_ref(dedup_key, draft) {
Ok(node_ref) => Ok(node_ref),
Err(err) => {
truncate_index_vec(&mut self.asm_op_by_ref, asm_op_checkpoint);
truncate_index_vec(&mut self.debug_vars, debug_var_checkpoint);
Err(err)
},
}
}
fn find_node_ref_by_key(&self, key: &MastNodeKey) -> Option<MastNodeRef> {
self.node_ref_by_key.get(key).copied()
}
fn find_reusable_node_ref_by_key(
&self,
key: &MastNodeKey,
draft: &PendingMastNodeDraft,
) -> Option<MastNodeRef> {
self.find_node_ref_by_key(key)
.filter(|&node_ref| !self.should_replace_pending_node(node_ref, draft))
}
fn should_replace_pending_node(
&self,
existing_ref: MastNodeRef,
draft: &PendingMastNodeDraft,
) -> bool {
self.nodes[existing_ref].kind.is_external() && !draft.kind.is_external()
}
fn replace_pending_node_record_ref(
&mut self,
node_ref: MastNodeRef,
key: MastNodeKey,
draft: PendingMastNodeDraft,
) {
self.nodes[node_ref] = PendingMastNode {
key,
digest: draft.digest,
kind: draft.kind,
child_refs: draft.child_refs,
asm_ops: draft.asm_ops,
debug_vars: draft.debug_vars,
};
self.node_ref_by_key.insert(key, node_ref);
}
fn insert_or_replace_pending_node_record_ref(
&mut self,
key: MastNodeKey,
draft: PendingMastNodeDraft,
) -> Result<MastNodeRef, Report> {
if let Some(node_ref) = self.find_node_ref_by_key(&key) {
if self.should_replace_pending_node(node_ref, &draft) {
self.replace_pending_node_record_ref(node_ref, key, draft);
}
Ok(node_ref)
} else {
self.insert_pending_node_record_ref(key, draft)
}
}
fn key_from_pending_refs(&self, node_digest: Word, child_refs: &[MastNodeRef]) -> MastNodeKey {
let mut has_non_digest_child = false;
let mut elements = Vec::with_capacity(1 + 4 + child_refs.len() * 4);
elements.push(CHILD_KEY_DOMAIN);
elements.extend_from_slice(node_digest.as_elements());
for &child_ref in child_refs {
let child = &self.nodes[child_ref];
has_non_digest_child |= child.key != child.digest;
elements.extend_from_slice(child.key.as_elements());
}
if has_non_digest_child {
hasher::hash_elements(&elements)
} else {
node_digest
}
}
fn key_for_pending_record(
&self,
digest: Word,
kind: &PendingMastNodeKind,
child_refs: &[MastNodeRef],
) -> MastNodeKey {
if let Some(op_batches) = kind.basic_block_op_batches() {
self.key_for_pending_basic_block(digest, op_batches)
} else {
self.key_from_pending_refs(digest, child_refs)
}
}
fn key_for_pending_basic_block(
&self,
block_digest: Word,
op_batches: &[OpBatch],
) -> MastNodeKey {
debug_assert!(!op_batches.is_empty());
let error_code_data = serialize_basic_block_error_codes(op_batches);
if error_code_data.is_empty() {
return block_digest;
}
let data_len = error_code_data.len() as u64;
let mut elements = Vec::with_capacity(7 + error_code_data.len().div_ceil(4));
elements.push(BASIC_BLOCK_ERROR_CODE_KEY_DOMAIN);
elements.extend_from_slice(block_digest.as_elements());
elements.push(Felt::from_u32(data_len as u32));
elements.push(Felt::from_u32((data_len >> 32) as u32));
elements.extend(bytes_to_packed_u32_elements(&error_code_data));
hasher::hash_elements(&elements)
}
pub(crate) fn add_asm_op_ref(&mut self, asm_op: AssemblyOp) -> Result<AsmOpRef, Report> {
self.asm_op_by_ref
.push(asm_op)
.into_diagnostic()
.wrap_err("assembler created too many assembly op refs")
}
fn push_debug_var_ref(&mut self, debug_var: DebugVarInfo) -> Result<DebugVarRef, Report> {
self.debug_vars
.push(debug_var)
.into_diagnostic()
.wrap_err("assembler created too many debug variables")
}
fn indexed_asm_op_refs(
&mut self,
asm_ops: Vec<(usize, AssemblyOp)>,
) -> Result<Vec<(usize, AsmOpRef)>, Report> {
asm_ops
.into_iter()
.map(|(op_idx, asm_op)| self.add_asm_op_ref(asm_op).map(|asm_ref| (op_idx, asm_ref)))
.collect()
}
fn indexed_debug_var_refs(
&mut self,
debug_vars: Vec<(usize, DebugVarInfo)>,
) -> Result<Vec<(usize, DebugVarRef)>, Report> {
debug_vars
.into_iter()
.map(|(op_idx, debug_var)| {
self.add_debug_var_ref(debug_var).map(|debug_var_ref| (op_idx, debug_var_ref))
})
.collect()
}
fn intern_pending_node_with_asm_op(
&mut self,
mut draft: PendingMastNodeDraft,
asm_op: AssemblyOp,
) -> Result<MastNodeRef, Report> {
let dedup_key = self.dedup_key_for_pending_data(&draft);
let source_child_refs = self.source_child_refs_for_node_refs(&draft.child_refs);
let asm_op_checkpoint = self.asm_op_by_ref.len();
let debug_var_checkpoint = self.debug_vars.len();
draft.asm_ops = self.indexed_asm_op_refs(vec![(0, asm_op)])?;
let node_ref =
if let Some(node_ref) = self.find_reusable_node_ref_by_key(&dedup_key, &draft) {
node_ref
} else {
self.insert_pending_node_with_allocated_metadata_refs(
dedup_key,
draft.clone(),
asm_op_checkpoint,
debug_var_checkpoint,
)?
};
if let Err(err) = self.record_source_occurrence(node_ref, source_child_refs, &draft) {
truncate_index_vec(&mut self.asm_op_by_ref, asm_op_checkpoint);
truncate_index_vec(&mut self.debug_vars, debug_var_checkpoint);
return Err(err);
}
Ok(node_ref)
}
fn source_refs_for_node_ref_occurrences(
&self,
node_refs: &[MastNodeRef],
) -> Vec<SourceNodeRef> {
let mut child_counts = BTreeMap::<MastNodeRef, usize>::new();
for node_ref in node_refs {
*child_counts.entry(*node_ref).or_default() += 1;
}
let mut child_seen = BTreeMap::<MastNodeRef, usize>::new();
node_refs
.iter()
.map(|node_ref| {
let history = self
.source_refs_by_node_ref
.get(node_ref)
.expect("execution ref must have a source occurrence");
let needed = child_counts[node_ref];
let seen = child_seen.entry(*node_ref).or_default();
let start = history.len().saturating_sub(needed);
let source_ref = history
.get((start + *seen).min(history.len() - 1))
.copied()
.expect("execution ref must have at least one source occurrence");
*seen += 1;
source_ref
})
.collect()
}
fn source_child_refs_for_node_refs(&self, child_refs: &[MastNodeRef]) -> Vec<SourceNodeRef> {
self.source_refs_for_node_ref_occurrences(child_refs)
}
fn record_source_occurrence(
&mut self,
exec_ref: MastNodeRef,
child_refs: Vec<SourceNodeRef>,
draft: &PendingMastNodeDraft,
) -> Result<SourceNodeRef, Report> {
let (op_start, op_end) = self.source_op_range_for_draft(draft);
self.push_source_occurrence(
exec_ref,
child_refs,
op_start,
op_end,
draft.asm_ops.clone(),
draft.debug_vars.clone(),
true,
)
}
fn source_op_range_for_draft(&self, draft: &PendingMastNodeDraft) -> (usize, usize) {
let op_count = if let Some(op_batches) = draft.kind.basic_block_op_batches() {
op_batches.iter().flat_map(OpBatch::raw_ops).count()
} else {
draft
.asm_ops
.iter()
.map(|(op_idx, _)| op_idx + 1)
.chain(draft.debug_vars.iter().map(|(op_idx, _)| op_idx + 1))
.max()
.unwrap_or(0)
};
(0, op_count)
}
fn push_source_occurrence(
&mut self,
exec_ref: MastNodeRef,
child_refs: Vec<SourceNodeRef>,
op_start: usize,
op_end: usize,
asm_ops: Vec<(usize, AsmOpRef)>,
debug_vars: Vec<(usize, DebugVarRef)>,
update_latest: bool,
) -> Result<SourceNodeRef, Report> {
let source_ref = self
.source_nodes
.push(PendingSourceNode {
exec_ref,
child_refs,
op_start,
op_end,
asm_ops,
debug_vars,
})
.into_diagnostic()
.wrap_err("assembler created too many source MAST node refs")?;
if update_latest {
self.latest_source_ref_by_node_ref.insert(exec_ref, source_ref);
}
self.source_refs_by_node_ref.entry(exec_ref).or_default().push(source_ref);
Ok(source_ref)
}
pub(crate) fn build(mut self) -> Result<BuiltMastForest, Report> {
let procedure_root_refs = core::mem::take(&mut self.procedure_root_refs);
let procedure_source_root_refs = core::mem::take(&mut self.procedure_source_root_refs);
let layout = FinalForestLayout::plan(procedure_root_refs, &self.nodes);
let mut finalizer = MastForestFinalizer::new();
finalizer.materialize_live_nodes(&layout.live_node_refs, &self.nodes)?;
let error_messages = core::mem::take(&mut self.error_messages);
finalizer
.into_built_forest(
&layout.procedure_root_refs,
&procedure_source_root_refs,
&self.source_nodes,
&self.asm_op_by_ref,
&self.debug_vars,
self.advice_map,
)
.map(|built| built.with_error_messages(error_messages))
}
}
fn compute_operations_and_adjust_mappings<T: Copy>(
node: &MastNode,
mappings: Vec<(usize, T)>,
) -> (usize, Vec<(usize, T)>) {
match node {
MastNode::Block(block) => (
block.num_operations() as usize,
BasicBlockNode::adjust_asm_op_indices(mappings, block.op_batches()),
),
_ => {
let num_ops = mappings.iter().map(|(idx, _)| idx + 1).max().unwrap_or(0);
(num_ops, mappings)
},
}
}
fn batch_basic_block_operations(
operations: Vec<Operation>,
) -> Result<(Vec<OpBatch>, Word), Report> {
let block = BasicBlockNodeBuilder::new(operations)
.build()
.into_diagnostic()
.wrap_err("assembler failed to build new basic block")?;
Ok((block.op_batches().to_vec(), block.digest()))
}
fn serialize_basic_block_error_codes(op_batches: &[OpBatch]) -> Vec<u8> {
let mut data = Vec::new();
for (raw_op_idx, op) in op_batches.iter().flat_map(OpBatch::raw_ops).enumerate() {
if matches!(op, Operation::Assert(_) | Operation::U32assert2(_) | Operation::MpVerify(_)) {
data.extend_from_slice(&(raw_op_idx as u64).to_le_bytes());
op.write_into(&mut data);
}
}
data
}
fn truncate_index_vec<I: Idx, T>(items: &mut IndexVec<I, T>, len: usize) {
while items.len() > len {
items.swap_remove(items.len() - 1);
}
}
impl MastForestBuilder {
#[inline(always)]
pub fn get_procedure(&self, gid: GlobalItemIndex) -> Option<&Procedure> {
self.procedures.get(&gid)
}
#[inline(always)]
pub fn find_procedure_by_mast_root(&self, mast_root: &Word) -> Option<&Procedure> {
self.proc_gid_by_mast_root
.get(mast_root)
.and_then(|gid| self.get_procedure(*gid))
}
pub(crate) fn mast_root_for_ref(&self, node_ref: MastNodeRef) -> Option<Word> {
self.nodes.get(node_ref).map(|pending_node| pending_node.digest)
}
pub(crate) fn latest_source_ref_for_node_ref(
&self,
node_ref: MastNodeRef,
) -> Option<SourceNodeRef> {
self.latest_source_ref_by_node_ref.get(&node_ref).copied()
}
fn pending_node_mast_root(&self, node_ref: MastNodeRef) -> Word {
self.nodes[node_ref].digest
}
fn pending_node_is_basic_block(&self, node_ref: MastNodeRef) -> bool {
self.nodes[node_ref].kind.is_basic_block()
}
fn pending_basic_block_op_batches(&self, node_ref: MastNodeRef) -> Option<&[OpBatch]> {
self.nodes[node_ref].kind.basic_block_op_batches()
}
}
impl MastForestBuilder {
pub fn insert_procedure(
&mut self,
gid: GlobalItemIndex,
procedure: Procedure,
) -> Result<(), Report> {
if let Some(cached) = self.procedures.get(&gid) {
if cached.mast_root() != procedure.mast_root() {
return Err(report!(
"procedure '{}' was compiled more than once with different MAST roots",
procedure.path()
));
}
log::warn!(
target: "assembler::mast_forest_builder",
"procedure '{}' was compiled more than once; reusing the cached MAST root",
procedure.path(),
);
return Ok(());
}
if let Some(cached) = self.find_procedure_by_mast_root(&procedure.mast_root()) {
let cached_locals = cached.num_locals();
let procedure_locals = procedure.num_locals();
let mismatched_locals = cached_locals != procedure_locals;
let is_valid =
!mismatched_locals || core::cmp::min(cached_locals, procedure_locals) == 0;
if !is_valid {
let first = cached.path();
let second = procedure.path();
return Err(report!(
"two procedures found with same mast root, but conflicting definitions ('{}' and '{}')",
first,
second
));
}
}
self.record_procedure_root_ref(procedure.body_node_ref());
self.proc_gid_by_mast_root.insert(procedure.mast_root(), gid);
self.procedures.insert(gid, procedure);
Ok(())
}
pub(crate) fn record_procedure_root_ref(&mut self, root_ref: MastNodeRef) {
if !self.procedure_root_refs.contains(&root_ref) {
self.procedure_root_refs.push(root_ref);
}
if let Some(history) = self.source_refs_by_node_ref.get(&root_ref)
&& let Some(source_ref) = history
.get(*self.procedure_source_root_count_by_node_ref.entry(root_ref).or_default())
.copied()
.or_else(|| history.last().copied())
&& !self.procedure_source_root_refs.contains(&source_ref)
{
self.procedure_source_root_refs.push(source_ref);
*self.procedure_source_root_count_by_node_ref.entry(root_ref).or_default() += 1;
}
}
fn is_procedure_root_ref(&self, node_ref: MastNodeRef) -> bool {
self.procedure_root_refs.contains(&node_ref)
}
}
impl MastForestBuilder {
pub(crate) fn join_node_refs(
&mut self,
node_refs: Vec<MastNodeRef>,
asm_op: Option<AssemblyOp>,
) -> Result<MastNodeRef, Report> {
debug_assert!(!node_refs.is_empty(), "cannot combine empty MAST node ref list");
let mut node_refs = self.merge_contiguous_basic_block_refs(node_refs)?;
while node_refs.len() > 1 {
let last_mast_node_ref = if node_refs.len().is_multiple_of(2) {
None
} else {
node_refs.pop()
};
let mut source_node_refs = Vec::new();
core::mem::swap(&mut node_refs, &mut source_node_refs);
let mut source_mast_node_iter = source_node_refs.drain(0..);
while let (Some(left), Some(right)) =
(source_mast_node_iter.next(), source_mast_node_iter.next())
{
let left_digest = self.pending_node_mast_root(left);
let right_digest = self.pending_node_mast_root(right);
let join_digest =
hasher::merge_in_domain(&[left_digest, right_digest], JoinNode::DOMAIN);
let child_refs = vec![left, right];
let draft =
PendingMastNodeDraft::new(PendingMastNodeKind::Join, join_digest, child_refs);
let join_mast_node_ref = if let Some(ref asm_op) = asm_op {
self.intern_pending_node_with_asm_op(draft, asm_op.clone())?
} else {
self.intern_pending_node(draft)?
};
node_refs.push(join_mast_node_ref);
}
if let Some(mast_node_ref) = last_mast_node_ref {
node_refs.push(mast_node_ref);
}
}
Ok(node_refs.remove(0))
}
pub(crate) fn ensure_split_node_ref(
&mut self,
branches: [MastNodeRef; 2],
asm_op: AssemblyOp,
) -> Result<MastNodeRef, Report> {
let branch_digests = branches.map(|node_ref| self.pending_node_mast_root(node_ref));
let split_digest = hasher::merge_in_domain(&branch_digests, SplitNode::DOMAIN);
let child_refs = Vec::from(branches);
self.intern_pending_node_with_asm_op(
PendingMastNodeDraft::new(PendingMastNodeKind::Split, split_digest, child_refs),
asm_op,
)
}
pub(crate) fn ensure_loop_node_ref(
&mut self,
body: MastNodeRef,
asm_op: AssemblyOp,
) -> Result<MastNodeRef, Report> {
let body_digest = self.pending_node_mast_root(body);
let loop_digest =
hasher::merge_in_domain(&[body_digest, Word::default()], LoopNode::DOMAIN);
let child_refs = vec![body];
self.intern_pending_node_with_asm_op(
PendingMastNodeDraft::new(PendingMastNodeKind::Loop, loop_digest, child_refs),
asm_op,
)
}
pub(crate) fn ensure_call_node_ref(
&mut self,
callee: MastNodeRef,
is_syscall: bool,
asm_op: AssemblyOp,
) -> Result<MastNodeRef, Report> {
let callee_digest = self.pending_node_mast_root(callee);
let call_domain = if is_syscall {
CallNode::SYSCALL_DOMAIN
} else {
CallNode::CALL_DOMAIN
};
let call_digest = hasher::merge_in_domain(&[callee_digest, Word::default()], call_domain);
let child_refs = vec![callee];
self.intern_pending_node_with_asm_op(
PendingMastNodeDraft::new(
PendingMastNodeKind::Call { is_syscall },
call_digest,
child_refs,
),
asm_op,
)
}
pub(crate) fn ensure_dyn_node_ref(
&mut self,
is_dyncall: bool,
asm_op: AssemblyOp,
) -> Result<MastNodeRef, Report> {
let dyn_digest = if is_dyncall {
DynNode::DYNCALL_DEFAULT_DIGEST
} else {
DynNode::DYN_DEFAULT_DIGEST
};
let child_refs = Vec::new();
self.intern_pending_node_with_asm_op(
PendingMastNodeDraft::new(
PendingMastNodeKind::Dyn { is_dyncall },
dyn_digest,
child_refs,
),
asm_op,
)
}
fn merge_contiguous_basic_block_refs(
&mut self,
node_refs: Vec<MastNodeRef>,
) -> Result<Vec<MastNodeRef>, Report> {
let mut merged_node_refs = Vec::with_capacity(node_refs.len());
let mut contiguous_basic_block_refs: Vec<MastNodeRef> = Vec::new();
for node_ref in node_refs {
if self.pending_node_is_basic_block(node_ref) {
contiguous_basic_block_refs.push(node_ref);
} else {
merged_node_refs.extend(self.merge_basic_block_refs(&contiguous_basic_block_refs)?);
contiguous_basic_block_refs.clear();
merged_node_refs.push(node_ref);
}
}
merged_node_refs.extend(self.merge_basic_block_refs(&contiguous_basic_block_refs)?);
Ok(merged_node_refs)
}
fn record_merged_source_occurrences(
&mut self,
merged_ref: MastNodeRef,
merged_source_occurrences: &[(SourceNodeRef, usize)],
) -> Result<(), Report> {
for &(source_ref, new_start) in merged_source_occurrences {
let source_node = self.source_nodes[source_ref].clone();
let old_start = source_node.op_start;
let op_len = source_node.op_end.saturating_sub(old_start);
let remap_op_idx = |op_idx: usize| {
debug_assert!(op_idx >= old_start);
op_idx - old_start + new_start
};
self.push_source_occurrence(
merged_ref,
source_node.child_refs,
new_start,
new_start + op_len,
source_node
.asm_ops
.into_iter()
.map(|(op_idx, asm_op_ref)| (remap_op_idx(op_idx), asm_op_ref))
.collect(),
source_node
.debug_vars
.into_iter()
.map(|(op_idx, debug_var_ref)| (remap_op_idx(op_idx), debug_var_ref))
.collect(),
false,
)?;
}
Ok(())
}
fn merge_basic_block_refs(
&mut self,
contiguous_basic_block_refs: &[MastNodeRef],
) -> Result<Vec<MastNodeRef>, Report> {
if contiguous_basic_block_refs.is_empty() {
return Ok(Vec::new());
}
if contiguous_basic_block_refs.len() == 1 {
return Ok(contiguous_basic_block_refs.to_vec());
}
let mut operations: Vec<Operation> = Vec::new();
let mut merged_asm_ops: Vec<(usize, AsmOpRef)> = Vec::new();
let mut merged_debug_vars: Vec<(usize, DebugVarRef)> = Vec::new();
let mut merged_source_occurrences: Vec<(SourceNodeRef, usize)> = Vec::new();
let mut merged_basic_block_refs: Vec<MastNodeRef> = Vec::new();
let source_refs = self.source_refs_for_node_ref_occurrences(contiguous_basic_block_refs);
for (&basic_block_ref, source_ref) in contiguous_basic_block_refs.iter().zip(source_refs) {
if should_merge(
self.is_procedure_root_ref(basic_block_ref),
self.pending_basic_block_op_batches(basic_block_ref)
.expect("merge_basic_blocks: expected BasicBlockNode")
.len(),
) {
let block_ops = {
let pending_node = &self.nodes[basic_block_ref];
let op_batches = pending_node
.kind
.basic_block_op_batches()
.expect("merge_basic_blocks: expected BasicBlockNode");
op_batches.iter().flat_map(|b| b.raw_ops().copied()).collect::<Vec<_>>()
};
let ops_offset = operations.len();
merged_source_occurrences.push((source_ref, ops_offset));
let pending_node = &self.nodes[basic_block_ref];
merged_asm_ops.extend(
pending_node
.asm_ops
.iter()
.map(|(op_idx, asm_op_id)| (op_idx + ops_offset, *asm_op_id)),
);
merged_debug_vars.extend(
pending_node
.debug_vars
.iter()
.map(|(op_idx, var_id)| (op_idx + ops_offset, *var_id)),
);
operations.extend(block_ops);
} else {
if !operations.is_empty() {
let block_ops = core::mem::take(&mut operations);
let block_asm_ops = core::mem::take(&mut merged_asm_ops);
let block_debug_vars = core::mem::take(&mut merged_debug_vars);
let block_source_occurrences = core::mem::take(&mut merged_source_occurrences);
let merged_basic_block_ref =
self.ensure_block_ref(block_ops, block_asm_ops, block_debug_vars)?;
self.record_merged_source_occurrences(
merged_basic_block_ref,
&block_source_occurrences,
)?;
merged_basic_block_refs.push(merged_basic_block_ref);
}
merged_basic_block_refs.push(basic_block_ref);
}
}
if !operations.is_empty() {
let merged_basic_block =
self.ensure_block_ref(operations, merged_asm_ops, merged_debug_vars)?;
self.record_merged_source_occurrences(merged_basic_block, &merged_source_occurrences)?;
merged_basic_block_refs.push(merged_basic_block);
}
Ok(merged_basic_block_refs)
}
pub(crate) fn ensure_block_ref(
&mut self,
operations: Vec<Operation>,
asm_op_refs: Vec<(usize, AsmOpRef)>,
debug_vars: Vec<(usize, DebugVarRef)>,
) -> Result<MastNodeRef, Report> {
let (op_batches, digest) = batch_basic_block_operations(operations)?;
let kind = PendingMastNodeKind::BasicBlock { op_batches };
self.intern_pending_node(PendingMastNodeDraft {
kind,
digest,
child_refs: Vec::new(),
asm_ops: asm_op_refs,
debug_vars,
})
}
}
impl MastForestBuilder {
pub(crate) fn add_debug_var_ref(
&mut self,
debug_var: DebugVarInfo,
) -> Result<DebugVarRef, Report> {
self.push_debug_var_ref(debug_var)
}
}
impl MastForestBuilder {
pub fn register_error(&mut self, msg: Arc<str>) -> Felt {
let err_code = error_code_from_msg(&msg);
self.error_messages.entry(err_code.as_canonical_u64()).or_insert(msg);
err_code
}
}
impl MastForestBuilder {
pub fn merge_advice_map(&mut self, other: &AdviceMap) -> Result<(), Report> {
self.advice_map
.merge(other)
.map_err(|((key, prev_values), new_values)| LinkerError::AdviceMapKeyAlreadyPresent {
key,
prev_values: prev_values.to_vec(),
new_values: new_values.to_vec(),
})
.into_diagnostic()
}
}
fn should_merge(is_procedure: bool, num_op_batches: usize) -> bool {
!is_procedure || num_op_batches < PROCEDURE_INLINING_THRESHOLD
}
#[cfg(test)]
mod tests {
use alloc::{
collections::BTreeSet,
string::{String, ToString},
};
use miden_core::{
mast::{MastNodeBuilder, MastNodeId},
operations::{DebugVarLocation, Operation},
};
use miden_mast_package::debug_info::{
DebugSourceAsmOp, DebugSourceGraphSection, DebugSourceMapSection, DebugSourceNode,
DebugSourceNodeId, DebugSourceVar, PackageDebugInfo,
};
use proptest::prelude::*;
use super::*;
fn record_test_root(builder: &mut MastForestBuilder, node_ref: MastNodeRef) -> MastNodeRef {
builder.record_procedure_root_ref(node_ref);
node_ref
}
fn add_test_asm_op(builder: &mut MastForestBuilder, asm_op: AssemblyOp) -> AsmOpRef {
builder.add_asm_op_ref(asm_op).unwrap()
}
fn test_asm_op(context: impl Into<String>, op: impl Into<String>) -> AssemblyOp {
AssemblyOp::new(None, context.into(), 1, op.into())
}
fn test_word(value: u64) -> Word {
Word::from([Felt::new_unchecked(value), Felt::ZERO, Felt::ZERO, Felt::ZERO])
}
fn source_nodes_for_exec(
source_graph: &SourceDebugGraph,
exec_node: MastNodeId,
) -> Vec<&SourceNode> {
source_graph
.nodes()
.as_slice()
.iter()
.filter(|source_node| source_node.exec_node() == exec_node)
.collect()
}
fn source_debug_var_names(
source_graph: &SourceDebugGraph,
exec_node: MastNodeId,
) -> Vec<String> {
source_nodes_for_exec(source_graph, exec_node)
.into_iter()
.flat_map(|source_node| {
source_node
.debug_vars()
.iter()
.map(|(_, debug_var)| debug_var.name().to_string())
.collect::<Vec<_>>()
})
.collect()
}
fn source_asm_contexts(source_graph: &SourceDebugGraph, exec_node: MastNodeId) -> Vec<String> {
source_nodes_for_exec(source_graph, exec_node)
.into_iter()
.flat_map(|source_node| {
source_node
.asm_ops()
.iter()
.map(|(_, asm_op)| asm_op.context_name().to_string())
.collect::<Vec<_>>()
})
.collect()
}
fn package_debug_info_from_source_graph(source_graph: &SourceDebugGraph) -> PackageDebugInfo {
let source_nodes = source_graph
.nodes()
.as_slice()
.iter()
.map(|source_node| {
DebugSourceNode::new(
source_node.exec_node(),
source_node
.children()
.iter()
.map(|child| DebugSourceNodeId::from(u32::from(*child)))
.collect(),
source_node.op_start() as u32,
source_node.op_end() as u32,
)
})
.collect();
let roots = source_graph
.roots()
.iter()
.map(|root| DebugSourceNodeId::from(u32::from(*root)))
.collect();
let mut asm_ops = Vec::new();
let mut debug_vars = Vec::new();
for (source_idx, source_node) in source_graph.nodes().as_slice().iter().enumerate() {
let source_node_id = DebugSourceNodeId::from(source_idx as u32);
asm_ops.extend(source_node.asm_ops().iter().map(|(op_idx, asm_op)| {
DebugSourceAsmOp::new(
source_node_id,
*op_idx as u32,
asm_op.location().cloned(),
asm_op.context_name().to_string(),
asm_op.op().to_string(),
asm_op.num_cycles(),
)
}));
debug_vars.extend(source_node.debug_vars().iter().map(|(op_idx, debug_var)| {
DebugSourceVar::new(source_node_id, *op_idx as u32, debug_var.clone())
}));
}
PackageDebugInfo::with_source_debug(
DebugSourceGraphSection::from_parts(source_nodes, roots),
DebugSourceMapSection::from_parts(asm_ops, debug_vars),
)
}
#[derive(Debug, Clone)]
struct GeneratedBuildStep {
tag: u8,
first: usize,
second: usize,
flags: u8,
}
fn generated_build_steps() -> impl Strategy<Value = Vec<GeneratedBuildStep>> {
proptest::collection::vec((0u8..5, any::<usize>(), any::<usize>(), any::<u8>()), 1..24)
.prop_map(|steps| {
steps
.into_iter()
.map(|(tag, first, second, flags)| GeneratedBuildStep {
tag,
first,
second,
flags,
})
.collect()
})
}
fn assert_finalization_invariants(
forest: &MastForest,
remapping: &BTreeMap<MastNodeRef, MastNodeId>,
) {
let mut final_ids = BTreeSet::new();
let node_count = forest.num_nodes() as usize;
for &node_id in remapping.values() {
assert!(node_id.to_usize() < node_count, "final node ID {node_id} must be in bounds");
assert!(final_ids.insert(node_id), "final node ID {node_id} must resolve once");
}
for &root_id in forest.procedure_roots() {
assert!(root_id.to_usize() < node_count, "procedure root {root_id} must be in bounds");
}
for node_idx in 0..forest.num_nodes() {
let node_id = MastNodeId::new_unchecked(node_idx);
let mut children = Vec::new();
forest[node_id].append_children_to(&mut children);
for child_id in children {
assert!(
child_id.to_usize() < node_idx as usize,
"child {child_id} must precede parent {node_id}"
);
}
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(32))]
#[test]
fn finalization_invariants_hold_for_generated_builder_shapes(
steps in generated_build_steps()
) {
let mut builder = MastForestBuilder::new(&[]).unwrap();
let shared_asm_op = add_test_asm_op(&mut builder, test_asm_op("generated::shared", "add"));
let shared_debug_var = builder
.add_debug_var_ref(DebugVarInfo::new(
"shared",
DebugVarLocation::Stack(0),
))
.unwrap();
let seed_ref = builder
.ensure_block_ref(
vec![Operation::Add, Operation::Mul],
vec![(0, shared_asm_op), (1, shared_asm_op)],
vec![(0, shared_debug_var)],
)
.unwrap();
record_test_root(&mut builder, seed_ref);
let mut node_refs = vec![seed_ref];
for (step_idx, step) in steps.iter().enumerate() {
let first_ref = node_refs[step.first % node_refs.len()];
let second_ref = node_refs[step.second % node_refs.len()];
let context = format!("generated::{step_idx}");
let next_ref = match step.tag {
0 => {
let asm_op = add_test_asm_op(
&mut builder,
test_asm_op(context.clone(), "add"),
);
builder
.ensure_block_ref(
vec![Operation::Add],
vec![(0, asm_op)],
vec![],
)
.unwrap()
},
1 => builder
.ensure_split_node_ref(
[first_ref, second_ref],
test_asm_op(context.clone(), "if.true"),
)
.unwrap(),
2 => builder
.ensure_loop_node_ref(
first_ref,
test_asm_op(context.clone(), "begin"),
)
.unwrap(),
3 => builder
.ensure_call_node_ref(
first_ref,
step.flags & 1 == 1,
test_asm_op(context.clone(), "call"),
)
.unwrap(),
_ => builder
.join_node_refs(
vec![first_ref, second_ref],
Some(test_asm_op(context.clone(), "begin")),
)
.unwrap(),
};
if step.flags & 2 == 2 {
record_test_root(&mut builder, next_ref);
}
node_refs.push(next_ref);
}
record_test_root(&mut builder, *node_refs.last().unwrap());
let (forest, remapping) = builder.build().unwrap().into_parts();
assert_finalization_invariants(&forest, &remapping);
}
}
#[test]
fn test_build_without_roots_prunes_all_nodes() {
let mut builder = MastForestBuilder::new(&[]).unwrap();
let dead_ref = builder.ensure_block_ref(vec![Operation::Add], vec![], vec![]).unwrap();
let (forest, remapping) = builder.build().unwrap().into_parts();
assert!(!remapping.contains_key(&dead_ref));
assert_eq!(forest.num_nodes(), 0);
assert_eq!(forest.procedure_roots().len(), 0);
}
#[test]
fn test_build_prunes_unreachable_nodes() {
let mut builder = MastForestBuilder::new(&[]).unwrap();
let root_ref = builder.ensure_block_ref(vec![Operation::Add], vec![], vec![]).unwrap();
let dead_ref = builder.ensure_block_ref(vec![Operation::Mul], vec![], vec![]).unwrap();
builder.record_procedure_root_ref(root_ref);
let (forest, remapping) = builder.build().unwrap().into_parts();
assert!(remapping.contains_key(&root_ref));
assert!(!remapping.contains_key(&dead_ref));
assert_eq!(forest.num_nodes(), 1);
assert_eq!(forest.procedure_roots().len(), 1);
}
#[test]
fn test_merge_basic_blocks_keeps_non_mergeable_block_standalone() {
let mut builder = MastForestBuilder::new(&[]).unwrap();
let num_ops = PROCEDURE_INLINING_THRESHOLD * 1024;
let large_ops = vec![Operation::Add; num_ops];
let large_block_ref = builder.ensure_block_ref(large_ops, vec![], vec![]).unwrap();
builder.record_procedure_root_ref(large_block_ref);
let small_block_ref =
builder.ensure_block_ref(vec![Operation::Add], vec![], vec![]).unwrap();
let merged_blocks =
builder.merge_basic_block_refs(&[large_block_ref, small_block_ref]).unwrap();
assert_eq!(merged_blocks.len(), 2);
assert_eq!(merged_blocks[0], large_block_ref);
assert_eq!(merged_blocks[1], small_block_ref);
}
#[test]
fn test_build_keeps_existing_forest_root_after_merge() {
let mut builder = MastForestBuilder::new(&[]).unwrap();
let root_block_ref =
builder.ensure_block_ref(vec![Operation::Add], vec![], vec![]).unwrap();
builder.record_procedure_root_ref(root_block_ref);
let root_digest = builder.nodes[root_block_ref].digest;
let tail_block_ref =
builder.ensure_block_ref(vec![Operation::Mul], vec![], vec![]).unwrap();
let merged_blocks =
builder.merge_basic_block_refs(&[root_block_ref, tail_block_ref]).unwrap();
assert_eq!(merged_blocks.len(), 1);
assert_ne!(merged_blocks[0], root_block_ref);
let (forest, remapping) = builder.build().unwrap().into_parts();
let final_root_id = remapping[&root_block_ref];
assert!(forest.is_procedure_root(final_root_id));
assert_eq!(forest[final_root_id].digest(), root_digest);
}
#[test]
fn test_source_graph_preserves_pre_merge_block_ranges() {
let mut builder = MastForestBuilder::new(&[]).unwrap();
let first_asm_op = add_test_asm_op(&mut builder, test_asm_op("merge::first", "add"));
let second_asm_op = add_test_asm_op(&mut builder, test_asm_op("merge::second", "mul"));
let first_block_ref = builder
.ensure_block_ref(vec![Operation::Add], vec![(0, first_asm_op)], vec![])
.unwrap();
let second_block_ref = builder
.ensure_block_ref(vec![Operation::Mul], vec![(0, second_asm_op)], vec![])
.unwrap();
let merged_blocks =
builder.merge_basic_block_refs(&[first_block_ref, second_block_ref]).unwrap();
assert_eq!(merged_blocks.len(), 1);
let merged_ref = record_test_root(&mut builder, merged_blocks[0]);
let (_, remapping, source_graph, _) =
builder.build().unwrap().into_parts_with_source_graph();
let final_merged_id = remapping[&merged_ref];
let source_nodes = source_nodes_for_exec(&source_graph, final_merged_id);
assert_eq!(
source_graph.roots().len(),
1,
"pre-merge source blocks should not become procedure roots",
);
assert!(
source_nodes.iter().any(|source_node| {
source_node.op_start() == 0
&& source_node.op_end() == 1
&& source_node
.asm_ops()
.iter()
.any(|(_, asm_op)| asm_op.context_name() == "merge::first")
}),
"first pre-merge source block should survive as range 0..1",
);
assert!(
source_nodes.iter().any(|source_node| {
source_node.op_start() == 1
&& source_node.op_end() == 2
&& source_node
.asm_ops()
.iter()
.any(|(_, asm_op)| asm_op.context_name() == "merge::second")
}),
"second pre-merge source block should survive as range 1..2",
);
}
#[test]
fn test_source_graph_preserves_repeated_deduped_block_ranges_in_merge_window() {
let mut builder = MastForestBuilder::new(&[]).unwrap();
let first_asm_op = add_test_asm_op(&mut builder, test_asm_op("merge::first", "add"));
let second_asm_op = add_test_asm_op(&mut builder, test_asm_op("merge::second", "add"));
let first_block_ref = builder
.ensure_block_ref(vec![Operation::Add], vec![(0, first_asm_op)], vec![])
.unwrap();
let second_block_ref = builder
.ensure_block_ref(vec![Operation::Add], vec![(0, second_asm_op)], vec![])
.unwrap();
assert_eq!(
first_block_ref, second_block_ref,
"identical execution blocks should dedup to one execution ref",
);
let merged_blocks =
builder.merge_basic_block_refs(&[first_block_ref, second_block_ref]).unwrap();
assert_eq!(merged_blocks.len(), 1);
let merged_ref = record_test_root(&mut builder, merged_blocks[0]);
let (_, remapping, source_graph, _) =
builder.build().unwrap().into_parts_with_source_graph();
let final_merged_id = remapping[&merged_ref];
let source_nodes = source_nodes_for_exec(&source_graph, final_merged_id);
assert!(
source_nodes.iter().any(|source_node| {
source_node.op_start() == 0
&& source_node.op_end() == 1
&& source_node
.asm_ops()
.iter()
.any(|(_, asm_op)| asm_op.context_name() == "merge::first")
}),
"first deduped source block should survive as range 0..1",
);
assert!(
source_nodes.iter().any(|source_node| {
source_node.op_start() == 1
&& source_node.op_end() == 2
&& source_node
.asm_ops()
.iter()
.any(|(_, asm_op)| asm_op.context_name() == "merge::second")
}),
"second deduped source block should survive as range 1..2",
);
}
#[test]
fn test_build_orders_external_nodes_before_non_external_nodes() {
let mut builder = MastForestBuilder::new(&[]).unwrap();
let block_ref = builder.ensure_block_ref(vec![Operation::Add], vec![], vec![]).unwrap();
record_test_root(&mut builder, block_ref);
let external_a = builder
.ensure_external_link_with_source_ref(test_word(2), None, None, None)
.unwrap();
let external_b = builder
.ensure_external_link_with_source_ref(test_word(1), None, None, None)
.unwrap();
builder.record_procedure_root_ref(external_a);
builder.record_procedure_root_ref(external_b);
let mut expected_external_refs = [
(external_a, builder.nodes[external_a].key),
(external_b, builder.nodes[external_b].key),
];
expected_external_refs.sort_by_key(|(_, key)| *key);
let (forest, remapping) = builder.build().unwrap().into_parts();
assert_eq!(remapping[&expected_external_refs[0].0], MastNodeId::new_unchecked(0));
assert_eq!(remapping[&expected_external_refs[1].0], MastNodeId::new_unchecked(1));
assert!(forest[MastNodeId::new_unchecked(0)].is_external());
assert!(forest[MastNodeId::new_unchecked(1)].is_external());
}
#[test]
fn test_concrete_node_replaces_same_digest_external_placeholder() {
let mut builder = MastForestBuilder::new(&[]).unwrap();
let block_digest =
BasicBlockNodeBuilder::new(vec![Operation::Add]).build().unwrap().digest();
let external_ref = builder
.ensure_external_link_with_source_ref(block_digest, None, None, None)
.unwrap();
builder.record_procedure_root_ref(external_ref);
let concrete_ref = builder.ensure_block_ref(vec![Operation::Add], vec![], vec![]).unwrap();
assert_eq!(external_ref, concrete_ref);
assert!(!builder.nodes[external_ref].kind.is_external());
let (forest, remapping) = builder.build().unwrap().into_parts();
let final_root = remapping[&external_ref];
assert!(forest.is_procedure_root(final_root));
assert!(matches!(forest[final_root], MastNode::Block(_)));
}
#[test]
fn test_merge_basic_blocks_keeps_recorded_root_block_standalone() {
let mut builder = MastForestBuilder::new(&[]).unwrap();
let num_ops = PROCEDURE_INLINING_THRESHOLD * 1024;
let large_ops = vec![Operation::Add; num_ops];
let large_block_ref = builder.ensure_block_ref(large_ops, vec![], vec![]).unwrap();
builder.record_procedure_root_ref(large_block_ref);
let small_block_ref =
builder.ensure_block_ref(vec![Operation::Add], vec![], vec![]).unwrap();
let merged_blocks =
builder.merge_basic_block_refs(&[large_block_ref, small_block_ref]).unwrap();
assert_eq!(merged_blocks.len(), 2);
assert_eq!(merged_blocks[0], large_block_ref);
assert_eq!(merged_blocks[1], small_block_ref);
}
#[test]
fn test_ensure_node_preserving_debug_vars_prevents_aliasing() {
use miden_core::operations::{DebugVarInfo, DebugVarLocation};
let mut builder = MastForestBuilder::new(&[]).unwrap();
let var_x_ref = builder
.add_debug_var_ref(DebugVarInfo::new("x", DebugVarLocation::Stack(0)))
.unwrap();
let var_y_ref = builder
.add_debug_var_ref(DebugVarInfo::new("y", DebugVarLocation::Stack(1)))
.unwrap();
let block_a_ref = builder
.ensure_block_ref(vec![Operation::Add], Vec::new(), vec![(0, var_x_ref)])
.unwrap();
let block_b_ref = builder
.ensure_block_ref(vec![Operation::Add], Vec::new(), vec![(0, var_y_ref)])
.unwrap();
assert_eq!(block_a_ref, block_b_ref);
record_test_root(&mut builder, block_a_ref);
let (_forest, remapping, source_graph, _) =
builder.build().unwrap().into_parts_with_source_graph();
let final_block_a = remapping[&block_a_ref];
let final_block_b = remapping[&block_b_ref];
let var_names = source_debug_var_names(&source_graph, final_block_a);
assert_eq!(var_names, vec!["x", "y"]);
assert_eq!(final_block_a, final_block_b);
}
#[test]
fn test_source_graph_distinguishes_same_exec_debug_var_occurrences() {
use miden_core::operations::{DebugVarInfo, DebugVarLocation};
let mut builder = MastForestBuilder::new(&[]).unwrap();
let var_x_ref = builder
.add_debug_var_ref(DebugVarInfo::new("x", DebugVarLocation::Stack(0)))
.unwrap();
let var_y_ref = builder
.add_debug_var_ref(DebugVarInfo::new("y", DebugVarLocation::Stack(1)))
.unwrap();
let block_a_ref = builder
.ensure_block_ref(vec![Operation::Add], Vec::new(), vec![(0, var_x_ref)])
.unwrap();
let block_b_ref = builder
.ensure_block_ref(vec![Operation::Add], Vec::new(), vec![(0, var_y_ref)])
.unwrap();
assert_eq!(block_a_ref, block_b_ref);
record_test_root(&mut builder, block_a_ref);
let (forest, remapping, source_graph, _) =
builder.build().unwrap().into_parts_with_source_graph();
let final_block = remapping[&block_a_ref];
let debug_var_names = source_nodes_for_exec(&source_graph, final_block)
.into_iter()
.flat_map(|source_node| {
source_node
.debug_vars()
.iter()
.map(|(_, debug_var)| debug_var.name().to_string())
})
.collect::<BTreeSet<_>>();
assert_eq!(final_block, remapping[&block_b_ref]);
assert_eq!(forest.num_nodes(), 1);
assert_eq!(source_graph.roots().len(), 1);
assert_eq!(debug_var_names, BTreeSet::from(["x".to_string(), "y".to_string()]));
}
#[test]
fn test_ensure_block_dedups_identical_debug_var_payloads() {
use miden_core::operations::{DebugVarInfo, DebugVarLocation};
let mut builder = MastForestBuilder::new(&[]).unwrap();
let var_a = builder
.add_debug_var_ref(DebugVarInfo::new("x", DebugVarLocation::Stack(0)))
.unwrap();
let var_b = builder
.add_debug_var_ref(DebugVarInfo::new("x", DebugVarLocation::Stack(0)))
.unwrap();
let block_a = builder
.ensure_block_ref(vec![Operation::Add], Vec::new(), vec![(0, var_a)])
.unwrap();
let block_b = builder
.ensure_block_ref(vec![Operation::Add], Vec::new(), vec![(0, var_b)])
.unwrap();
assert_eq!(
block_a, block_b,
"same op stream plus same DebugVarInfo payload should dedup to one node"
);
}
#[test]
fn test_error_code_bearing_basic_blocks_do_not_dedup_by_digest_only() {
fn error_code_for_final_block(forest: &MastForest, node_id: MastNodeId) -> Felt {
let MastNode::Block(block) = &forest[node_id] else {
panic!("expected a basic block")
};
let op = block
.op_batches()
.iter()
.flat_map(OpBatch::raw_ops)
.next()
.expect("expected one operation");
match op {
Operation::Assert(code)
| Operation::U32assert2(code)
| Operation::MpVerify(code) => *code,
other => panic!("expected error-code-bearing operation, got {other:?}"),
}
}
for make_op in [
Operation::Assert as fn(Felt) -> Operation,
Operation::U32assert2 as fn(Felt) -> Operation,
Operation::MpVerify as fn(Felt) -> Operation,
] {
let mut builder = MastForestBuilder::new(&[]).unwrap();
let first_code = Felt::from_u32(1);
let second_code = Felt::from_u32(2);
let first_ref = builder
.ensure_block_ref(vec![make_op(first_code)], Vec::new(), Vec::new())
.unwrap();
let duplicate_first_ref = builder
.ensure_block_ref(vec![make_op(first_code)], Vec::new(), Vec::new())
.unwrap();
let second_ref = builder
.ensure_block_ref(vec![make_op(second_code)], Vec::new(), Vec::new())
.unwrap();
assert_eq!(first_ref, duplicate_first_ref);
assert_ne!(
first_ref, second_ref,
"same-digest blocks with different runtime error codes must remain distinct",
);
record_test_root(&mut builder, first_ref);
record_test_root(&mut builder, second_ref);
let (forest, remapping) = builder.build().unwrap().into_parts();
let final_first_id = remapping[&first_ref];
let final_second_id = remapping[&second_ref];
assert_ne!(final_first_id, final_second_id);
assert_eq!(forest[final_first_id].digest(), forest[final_second_id].digest());
assert_eq!(error_code_for_final_block(&forest, final_first_id), first_code);
assert_eq!(error_code_for_final_block(&forest, final_second_id), second_code);
}
}
#[test]
fn test_control_nodes_include_child_error_code_keys() {
let mut builder = MastForestBuilder::new(&[]).unwrap();
let first_block = builder
.ensure_block_ref(vec![Operation::Assert(Felt::from_u32(1))], Vec::new(), Vec::new())
.unwrap();
let second_block = builder
.ensure_block_ref(vec![Operation::Assert(Felt::from_u32(2))], Vec::new(), Vec::new())
.unwrap();
assert_eq!(
builder.pending_node_mast_root(first_block),
builder.pending_node_mast_root(second_block)
);
let first_call = builder
.ensure_call_node_ref(first_block, false, test_asm_op("test", "call"))
.unwrap();
let second_call = builder
.ensure_call_node_ref(second_block, false, test_asm_op("test", "call"))
.unwrap();
assert_ne!(
first_call, second_call,
"same-digest control nodes must not dedup when their children differ by runtime error code",
);
}
#[test]
fn test_build_assigns_final_debug_var_ids_to_used_refs() {
use miden_core::operations::{DebugVarInfo, DebugVarLocation};
let mut builder = MastForestBuilder::new(&[]).unwrap();
let _unused_var = builder
.add_debug_var_ref(DebugVarInfo::new("unused", DebugVarLocation::Stack(0)))
.unwrap();
let used_var = builder
.add_debug_var_ref(DebugVarInfo::new("used", DebugVarLocation::Stack(1)))
.unwrap();
let block_ref = builder
.ensure_block_ref(vec![Operation::Add], Vec::new(), vec![(0, used_var)])
.unwrap();
record_test_root(&mut builder, block_ref);
let (_forest, remapping, source_graph, _) =
builder.build().unwrap().into_parts_with_source_graph();
let final_block_id = remapping[&block_ref];
let var_names = source_debug_var_names(&source_graph, final_block_id);
assert_eq!(var_names, vec!["used"]);
}
#[test]
fn test_ensure_block_keeps_different_asm_ops_distinct() {
let mut builder = MastForestBuilder::new(&[]).unwrap();
let asm_op_a =
add_test_asm_op(&mut builder, AssemblyOp::new(None, "ctx_a".into(), 1, "add".into()));
let asm_op_b =
add_test_asm_op(&mut builder, AssemblyOp::new(None, "ctx_b".into(), 1, "add".into()));
let block_a_ref = builder
.ensure_block_ref(vec![Operation::Add], vec![(0, asm_op_a)], Vec::new())
.unwrap();
let block_b_ref = builder
.ensure_block_ref(vec![Operation::Add], vec![(0, asm_op_b)], Vec::new())
.unwrap();
assert_eq!(
block_a_ref, block_b_ref,
"AssemblyOp payload must not affect execution node identity"
);
record_test_root(&mut builder, block_a_ref);
let (_forest, remapping, source_graph, _) =
builder.build().unwrap().into_parts_with_source_graph();
let final_block_a = remapping[&block_a_ref];
assert!(source_asm_contexts(&source_graph, final_block_a).contains(&"ctx_a".to_string()));
assert_eq!(final_block_a, remapping[&block_b_ref]);
}
#[test]
fn test_source_graph_distinguishes_same_exec_asm_op_occurrences() {
let mut builder = MastForestBuilder::new(&[]).unwrap();
let asm_op_a =
add_test_asm_op(&mut builder, AssemblyOp::new(None, "ctx_a".into(), 1, "add".into()));
let asm_op_b =
add_test_asm_op(&mut builder, AssemblyOp::new(None, "ctx_b".into(), 1, "add".into()));
let block_a_ref = builder
.ensure_block_ref(vec![Operation::Add], vec![(0, asm_op_a)], Vec::new())
.unwrap();
let block_b_ref = builder
.ensure_block_ref(vec![Operation::Add], vec![(0, asm_op_b)], Vec::new())
.unwrap();
assert_eq!(block_a_ref, block_b_ref);
record_test_root(&mut builder, block_a_ref);
let (forest, remapping, source_graph, _) =
builder.build().unwrap().into_parts_with_source_graph();
let final_block = remapping[&block_a_ref];
let asm_contexts = source_nodes_for_exec(&source_graph, final_block)
.into_iter()
.flat_map(|source_node| {
source_node
.asm_ops()
.iter()
.map(|(_, asm_op)| asm_op.context_name().to_string())
})
.collect::<BTreeSet<_>>();
assert_eq!(final_block, remapping[&block_b_ref]);
assert_eq!(forest.num_nodes(), 1);
assert_eq!(source_graph.roots().len(), 1);
assert_eq!(asm_contexts, BTreeSet::from(["ctx_a".to_string(), "ctx_b".to_string()]));
}
#[test]
fn test_source_graph_preserves_repeated_same_exec_child_occurrences() {
let mut builder = MastForestBuilder::new(&[]).unwrap();
let asm_op_a =
add_test_asm_op(&mut builder, AssemblyOp::new(None, "ctx_a".into(), 1, "add".into()));
let asm_op_b =
add_test_asm_op(&mut builder, AssemblyOp::new(None, "ctx_b".into(), 1, "add".into()));
let block_a_ref = builder
.ensure_block_ref(vec![Operation::Add], vec![(0, asm_op_a)], Vec::new())
.unwrap();
let block_b_ref = builder
.ensure_block_ref(vec![Operation::Add], vec![(0, asm_op_b)], Vec::new())
.unwrap();
assert_eq!(block_a_ref, block_b_ref);
let split_ref = builder
.ensure_split_node_ref(
[block_a_ref, block_b_ref],
AssemblyOp::new(None, "split".into(), 1, "if.true".into()),
)
.unwrap();
record_test_root(&mut builder, split_ref);
let (_forest, _remapping, source_graph, _) =
builder.build().unwrap().into_parts_with_source_graph();
let root = source_graph.roots()[0];
let child_contexts = source_graph.nodes()[root]
.children()
.iter()
.map(|child| {
let child_node = &source_graph.nodes()[*child];
child_node.asm_ops()[0].1.context_name().to_string()
})
.collect::<Vec<_>>();
assert_eq!(child_contexts, vec!["ctx_a".to_string(), "ctx_b".to_string()]);
}
#[test]
fn test_source_graph_reuses_source_occurrence_for_duplicate_child_refs() {
let mut builder = MastForestBuilder::new(&[]).unwrap();
let block_ref =
builder.ensure_block_ref(vec![Operation::Add], Vec::new(), Vec::new()).unwrap();
let split_ref = builder
.ensure_split_node_ref(
[block_ref, block_ref],
AssemblyOp::new(None, "split".into(), 1, "if.true".into()),
)
.unwrap();
record_test_root(&mut builder, split_ref);
let (_forest, _remapping, source_graph, _) =
builder.build().unwrap().into_parts_with_source_graph();
let root = source_graph.roots()[0];
let children = source_graph.nodes()[root].children();
assert_eq!(children.len(), 2);
assert_eq!(children[0], children[1]);
}
#[test]
fn test_non_block_nodes_keep_different_asm_ops_distinct() {
let mut builder = MastForestBuilder::new(&[]).unwrap();
let callee_ref = builder.ensure_block_ref(vec![Operation::Add], vec![], vec![]).unwrap();
let call_a_ref = builder
.ensure_call_node_ref(
callee_ref,
false,
AssemblyOp::new(None, "ctx_a".into(), 1, "call.foo".into()),
)
.unwrap();
let call_b_ref = builder
.ensure_call_node_ref(
callee_ref,
false,
AssemblyOp::new(None, "ctx_b".into(), 1, "call.foo".into()),
)
.unwrap();
assert_eq!(
call_a_ref, call_b_ref,
"AssemblyOp payload must not affect execution node identity"
);
record_test_root(&mut builder, call_a_ref);
let (_forest, remapping, source_graph, _) =
builder.build().unwrap().into_parts_with_source_graph();
let final_call_a = remapping[&call_a_ref];
assert!(source_asm_contexts(&source_graph, final_call_a).contains(&"ctx_a".to_string()));
assert_eq!(final_call_a, remapping[&call_b_ref]);
}
#[test]
fn test_statically_linked_nodes_preserve_metadata_in_dedup() {
use miden_core::operations::{DebugVarInfo, DebugVarLocation};
let static_block_id = MastNodeId::new_unchecked(0);
let mut nodes = IndexVec::new();
let inserted_node_id = nodes
.push(
MastNodeBuilder::BasicBlock(BasicBlockNodeBuilder::new(vec![Operation::Add]))
.build_linked()
.unwrap(),
)
.unwrap();
assert_eq!(inserted_node_id, static_block_id);
let static_forest =
MastForest::from_raw_parts(nodes, vec![static_block_id], AdviceMap::default()).unwrap();
let mut builder = MastForestBuilder::new([&static_forest]).unwrap();
let copied_block_ref = builder
.ensure_external_link_with_source_ref(
static_forest[static_block_id].digest(),
None,
None,
None,
)
.unwrap();
let local_var_ref = builder
.add_debug_var_ref(DebugVarInfo::new("y", DebugVarLocation::Stack(1)))
.unwrap();
let local_asm_op_ref = add_test_asm_op(
&mut builder,
AssemblyOp::new(None, "local_ctx".into(), 1, "add".into()),
);
let local_block_ref = builder
.ensure_block_ref(
vec![Operation::Add],
vec![(0, local_asm_op_ref)],
vec![(0, local_var_ref)],
)
.unwrap();
assert_eq!(
copied_block_ref, local_block_ref,
"source metadata must not affect execution node identity"
);
record_test_root(&mut builder, copied_block_ref);
let (_forest, remapping, source_graph, _) =
builder.build().unwrap().into_parts_with_source_graph();
let final_copied_block_id = remapping[&copied_block_ref];
assert_eq!(final_copied_block_id, remapping[&local_block_ref]);
assert!(
source_asm_contexts(&source_graph, final_copied_block_id)
.contains(&"local_ctx".to_string())
);
assert!(
source_debug_var_names(&source_graph, final_copied_block_id).contains(&"y".to_string())
);
}
#[test]
fn test_statically_linked_padded_block_dedups_with_equivalent_local_block() {
use miden_core::operations::{DebugVarInfo, DebugVarLocation};
let mut source_builder = MastForestBuilder::new(&[]).unwrap();
let ops = vec![
Operation::Push(Felt::from_u32(1)),
Operation::Drop,
Operation::Drop,
Operation::Drop,
Operation::Drop,
Operation::Drop,
Operation::Drop,
Operation::Push(Felt::from_u32(2)),
Operation::Push(Felt::from_u32(3)),
];
let asm_op = AssemblyOp::new(None, "padded_ctx".into(), 1, "push.3".into());
let debug_var = DebugVarInfo::new("padded_var", DebugVarLocation::Stack(0));
let static_asm_op_ref = add_test_asm_op(&mut source_builder, asm_op.clone());
let static_debug_var_ref = source_builder.add_debug_var_ref(debug_var).unwrap();
let static_block_ref = source_builder
.ensure_block_ref(
ops.clone(),
vec![(8, static_asm_op_ref)],
vec![(8, static_debug_var_ref)],
)
.unwrap();
record_test_root(&mut source_builder, static_block_ref);
let (static_forest, source_remapping, static_source_graph, _) =
source_builder.build().unwrap().into_parts_with_source_graph();
let final_static_block = source_remapping[&static_block_ref];
let static_source_root = static_source_graph.roots()[0];
let expected_padded_idx = static_source_graph.nodes()[static_source_root].asm_ops()[0].0;
assert_eq!(
static_source_graph.nodes()[static_source_root].debug_vars()[0].0,
expected_padded_idx
);
let package_debug_info = package_debug_info_from_source_graph(&static_source_graph);
let mut builder = MastForestBuilder::new_with_static_libraries([StaticLibrary::new(
&static_forest,
Some(package_debug_info),
)])
.unwrap();
let copied_block_ref = builder
.ensure_external_link_with_source_ref(
static_forest[final_static_block].digest(),
Some(static_forest.commitment()),
Some(final_static_block),
Some(DebugSourceNodeId::from(u32::from(static_source_root))),
)
.unwrap();
let local_asm_op_ref = add_test_asm_op(&mut builder, asm_op);
let local_block_ref =
builder.ensure_block_ref(ops, vec![(8, local_asm_op_ref)], vec![]).unwrap();
assert_eq!(
copied_block_ref, local_block_ref,
"copied padded blocks should dedup with equivalent local blocks",
);
record_test_root(&mut builder, copied_block_ref);
let (_forest, remapping, source_graph, _) =
builder.build().unwrap().into_parts_with_source_graph();
let final_block_id = remapping[&copied_block_ref];
let source_nodes = source_nodes_for_exec(&source_graph, final_block_id);
assert!(source_nodes.iter().any(|source_node| {
source_node.asm_ops().iter().any(|(op_idx, asm_op)| {
*op_idx == expected_padded_idx && asm_op.context_name() == "padded_ctx"
})
}));
assert!(source_nodes.iter().any(|source_node| {
source_node.debug_vars().iter().any(|(op_idx, debug_var)| {
*op_idx == expected_padded_idx && debug_var.name() == "padded_var"
})
}));
}
#[test]
fn test_statically_linked_package_source_range_is_preserved() {
let mut source_builder = MastForestBuilder::new(&[]).unwrap();
let ops = vec![
Operation::Push(Felt::from_u32(1)),
Operation::Drop,
Operation::Drop,
Operation::Drop,
Operation::Push(Felt::from_u32(2)),
];
let asm_op = AssemblyOp::new(None, "partial_ctx".into(), 1, "push.2".into());
let static_asm_op_ref = add_test_asm_op(&mut source_builder, asm_op);
let static_block_ref = source_builder
.ensure_block_ref(ops, vec![(4, static_asm_op_ref)], vec![])
.unwrap();
record_test_root(&mut source_builder, static_block_ref);
let (static_forest, source_remapping, static_source_graph, _) =
source_builder.build().unwrap().into_parts_with_source_graph();
let final_static_block = source_remapping[&static_block_ref];
let static_source_root = static_source_graph.roots()[0];
let expected_partial_start = static_source_graph.nodes()[static_source_root].asm_ops()[0].0;
let mut package_debug_info = package_debug_info_from_source_graph(&static_source_graph);
let package_source_root = DebugSourceNodeId::from(u32::from(static_source_root));
let package_source_graph =
package_debug_info.source_graph().expect("source graph should be present");
let mut package_source_nodes = package_source_graph.nodes().to_vec();
package_source_nodes[u32::from(package_source_root) as usize].op_start =
expected_partial_start as u32;
package_source_nodes[u32::from(package_source_root) as usize].op_end =
expected_partial_start as u32 + 1;
let package_source_roots = package_source_graph.roots().to_vec();
package_debug_info = package_debug_info.with_source_graph(
DebugSourceGraphSection::from_parts(package_source_nodes, package_source_roots),
);
let mut builder = MastForestBuilder::new_with_static_libraries([StaticLibrary::new(
&static_forest,
Some(package_debug_info),
)])
.unwrap();
let copied_block_ref = builder
.ensure_external_link_with_source_ref(
static_forest[final_static_block].digest(),
Some(static_forest.commitment()),
Some(final_static_block),
Some(package_source_root),
)
.unwrap();
record_test_root(&mut builder, copied_block_ref);
let (_forest, remapping, source_graph, _) =
builder.build().unwrap().into_parts_with_source_graph();
let final_block_id = remapping[&copied_block_ref];
let linked_source_node = source_nodes_for_exec(&source_graph, final_block_id)
.into_iter()
.find(|source_node| {
source_node
.asm_ops()
.iter()
.any(|(_, asm_op)| asm_op.context_name() == "partial_ctx")
})
.expect("linked source node should preserve package metadata");
assert_eq!(linked_source_node.op_start(), expected_partial_start);
assert_eq!(linked_source_node.op_end(), expected_partial_start + 1);
}
#[test]
fn test_static_link_rejects_package_debug_child_exec_mismatch() {
let mut source_builder = MastForestBuilder::new(&[]).unwrap();
let left_ref =
source_builder.ensure_block_ref(vec![Operation::Add], vec![], vec![]).unwrap();
let right_ref =
source_builder.ensure_block_ref(vec![Operation::Mul], vec![], vec![]).unwrap();
let split_ref = source_builder
.ensure_split_node_ref(
[left_ref, right_ref],
AssemblyOp::new(None, "split_ctx".into(), 1, "if.true".into()),
)
.unwrap();
record_test_root(&mut source_builder, split_ref);
let (static_forest, source_remapping, static_source_graph, _) =
source_builder.build().unwrap().into_parts_with_source_graph();
let final_split = source_remapping[&split_ref];
let package_source_root =
DebugSourceNodeId::from(u32::from(static_source_graph.roots()[0]));
let mut package_debug_info = package_debug_info_from_source_graph(&static_source_graph);
let package_source_graph =
package_debug_info.source_graph().expect("source graph should be present");
let mut package_source_nodes = package_source_graph.nodes().to_vec();
package_source_nodes[u32::from(package_source_root) as usize]
.children
.swap(0, 1);
let package_source_roots = package_source_graph.roots().to_vec();
package_debug_info = package_debug_info.with_source_graph(
DebugSourceGraphSection::from_parts(package_source_nodes, package_source_roots),
);
let mut builder = MastForestBuilder::new_with_static_libraries([StaticLibrary::new(
&static_forest,
Some(package_debug_info),
)])
.unwrap();
let error = builder
.ensure_external_link_with_source_ref(
static_forest[final_split].digest(),
Some(static_forest.commitment()),
Some(final_split),
Some(package_source_root),
)
.expect_err("statically linked package debug graph with swapped children is invalid");
assert!(error.to_string().contains("child 0 maps"), "unexpected error: {error}");
}
#[test]
fn test_merged_root_block_keeps_metadata() {
use miden_core::operations::{AssemblyOp, DebugVarInfo, DebugVarLocation};
let mut builder = MastForestBuilder::new(&[]).unwrap();
let var_ref = builder
.add_debug_var_ref(DebugVarInfo::new("x", DebugVarLocation::Stack(0)))
.unwrap();
let asm_op_ref =
add_test_asm_op(&mut builder, AssemblyOp::new(None, "test".into(), 1, "add".into()));
let root_block_ref = builder
.ensure_block_ref(vec![Operation::Add], vec![(0, asm_op_ref)], vec![(0, var_ref)])
.unwrap();
builder.record_procedure_root_ref(root_block_ref);
let other_block_ref =
builder.ensure_block_ref(vec![Operation::Mul], vec![], vec![]).unwrap();
let merged = builder.merge_basic_block_refs(&[root_block_ref, other_block_ref]).unwrap();
assert_eq!(merged.len(), 1);
let merged_ref = merged[0];
assert_ne!(merged_ref, root_block_ref);
let (forest, remapping, source_graph, _) =
builder.build().unwrap().into_parts_with_source_graph();
let final_root_id = remapping[&root_block_ref];
assert!(forest.is_procedure_root(final_root_id), "root should survive");
let root_vars = source_debug_var_names(&source_graph, final_root_id);
assert_eq!(root_vars, vec!["x"], "root must keep its debug vars after merge");
assert!(
source_asm_contexts(&source_graph, final_root_id).contains(&"test".to_string()),
"root must keep its asm op after merge"
);
}
#[test]
fn test_static_link_exact_node_preserves_alias_metadata() {
let mut source_builder = MastForestBuilder::new(&[]).unwrap();
let alias_a_asm_op = add_test_asm_op(
&mut source_builder,
AssemblyOp::new(None, "alias_a".into(), 1, "add".into()),
);
let alias_b_asm_op = add_test_asm_op(
&mut source_builder,
AssemblyOp::new(None, "alias_b".into(), 1, "add".into()),
);
let alias_a_ref = source_builder
.ensure_block_ref(vec![Operation::Add], vec![(0, alias_a_asm_op)], vec![])
.unwrap();
let alias_b_ref = source_builder
.ensure_block_ref(vec![Operation::Add], vec![(0, alias_b_asm_op)], vec![])
.unwrap();
record_test_root(&mut source_builder, alias_a_ref);
record_test_root(&mut source_builder, alias_b_ref);
let (static_forest, source_remapping) = source_builder.build().unwrap().into_parts();
let final_alias_a = source_remapping[&alias_a_ref];
let final_alias_b = source_remapping[&alias_b_ref];
assert_eq!(static_forest[final_alias_a].digest(), static_forest[final_alias_b].digest());
let mut exact_builder = MastForestBuilder::new([&static_forest]).unwrap();
let exact_alias_b_ref = {
let source_forest = Arc::clone(&exact_builder.statically_linked_mast);
let node = source_forest[final_alias_b].clone();
let node_refs_by_source_id = BTreeMap::new();
let child_refs = exact_builder
.pending_refs_for_statically_linked_source(&node, &node_refs_by_source_id);
exact_builder
.ensure_node_from_statically_linked_source_ref(node, child_refs, None)
.unwrap()
};
record_test_root(&mut exact_builder, exact_alias_b_ref);
let (_exact_forest, exact_remapping, exact_source_graph, _) =
exact_builder.build().unwrap().into_parts_with_source_graph();
let final_exact_alias_b = exact_remapping[&exact_alias_b_ref];
assert!(source_asm_contexts(&exact_source_graph, final_exact_alias_b).is_empty());
}
#[test]
fn test_source_graph_distinguishes_same_digest_alias_roots() {
let mut builder = MastForestBuilder::new(&[]).unwrap();
let alias_a_asm_op =
add_test_asm_op(&mut builder, AssemblyOp::new(None, "alias_a".into(), 1, "add".into()));
let alias_b_asm_op =
add_test_asm_op(&mut builder, AssemblyOp::new(None, "alias_b".into(), 1, "add".into()));
let alias_a_ref = builder
.ensure_block_ref(vec![Operation::Add], vec![(0, alias_a_asm_op)], vec![])
.unwrap();
let alias_b_ref = builder
.ensure_block_ref(vec![Operation::Add], vec![(0, alias_b_asm_op)], vec![])
.unwrap();
record_test_root(&mut builder, alias_a_ref);
record_test_root(&mut builder, alias_b_ref);
let (forest, remapping, source_graph, _) =
builder.build().unwrap().into_parts_with_source_graph();
let final_alias_a = remapping[&alias_a_ref];
let final_alias_b = remapping[&alias_b_ref];
let root_contexts = source_graph
.roots()
.iter()
.flat_map(|source_root| source_graph.nodes()[*source_root].asm_ops())
.map(|(_, asm_op)| asm_op.context_name().to_string())
.collect::<BTreeSet<_>>();
assert_eq!(final_alias_a, final_alias_b);
assert_eq!(forest.num_nodes(), 1);
assert_eq!(source_graph.roots().len(), 2);
assert_eq!(root_contexts, BTreeSet::from(["alias_a".to_string(), "alias_b".to_string()]));
}
#[test]
fn test_static_link_by_digest_imports_only_selected_alias() {
let mut source_builder = MastForestBuilder::new(&[]).unwrap();
let alias_a_asm_op = add_test_asm_op(
&mut source_builder,
AssemblyOp::new(None, "alias_a".into(), 1, "add".into()),
);
let alias_b_asm_op = add_test_asm_op(
&mut source_builder,
AssemblyOp::new(None, "alias_b".into(), 1, "add".into()),
);
let alias_a_ref = source_builder
.ensure_block_ref(vec![Operation::Add], vec![(0, alias_a_asm_op)], vec![])
.unwrap();
let alias_b_ref = source_builder
.ensure_block_ref(vec![Operation::Add], vec![(0, alias_b_asm_op)], vec![])
.unwrap();
record_test_root(&mut source_builder, alias_a_ref);
record_test_root(&mut source_builder, alias_b_ref);
let (static_forest, source_remapping) = source_builder.build().unwrap().into_parts();
let final_alias_a = source_remapping[&alias_a_ref];
let mut builder = MastForestBuilder::new([&static_forest]).unwrap();
let linked_ref = builder
.ensure_external_link_with_source_ref(
static_forest[final_alias_a].digest(),
None,
None,
None,
)
.unwrap();
record_test_root(&mut builder, linked_ref);
let (forest, remapping) = builder.build().unwrap().into_parts();
let final_linked = remapping[&linked_ref];
assert_eq!(forest.num_nodes(), 1, "only the selected alias should be imported");
assert_eq!(final_linked, MastNodeId::new_unchecked(0));
}
#[test]
fn test_static_link_ambiguous_same_commitment_source_root_drops_source_metadata() {
let mut source_a_builder = MastForestBuilder::new(&[]).unwrap();
let source_a_asm_op = add_test_asm_op(
&mut source_a_builder,
AssemblyOp::new(None, "source_a".into(), 1, "add".into()),
);
let source_a_ref = source_a_builder
.ensure_block_ref(vec![Operation::Add], vec![(0, source_a_asm_op)], vec![])
.unwrap();
record_test_root(&mut source_a_builder, source_a_ref);
let (source_a_forest, source_a_remapping) = source_a_builder.build().unwrap().into_parts();
let source_a_root = source_a_remapping[&source_a_ref];
let mut source_b_builder = MastForestBuilder::new(&[]).unwrap();
let source_b_asm_op = add_test_asm_op(
&mut source_b_builder,
AssemblyOp::new(None, "source_b".into(), 1, "add".into()),
);
let source_b_ref = source_b_builder
.ensure_block_ref(vec![Operation::Add], vec![(0, source_b_asm_op)], vec![])
.unwrap();
record_test_root(&mut source_b_builder, source_b_ref);
let (source_b_forest, source_b_remapping) = source_b_builder.build().unwrap().into_parts();
let source_b_root = source_b_remapping[&source_b_ref];
assert_eq!(source_a_root, source_b_root);
assert_eq!(source_a_forest.commitment(), source_b_forest.commitment());
assert_eq!(
source_a_forest[source_a_root].digest(),
source_b_forest[source_b_root].digest()
);
let mut builder = MastForestBuilder::new([&source_a_forest, &source_b_forest]).unwrap();
let linked_ref = builder
.ensure_external_link_with_source_ref(
source_a_forest[source_a_root].digest(),
Some(source_a_forest.commitment()),
Some(source_a_root),
None,
)
.unwrap();
assert!(
!builder.nodes[linked_ref].kind.is_external(),
"ambiguous same-commitment provenance should not force an external node"
);
record_test_root(&mut builder, linked_ref);
let (_forest, remapping, source_graph, _) =
builder.build().unwrap().into_parts_with_source_graph();
let final_linked = remapping[&linked_ref];
assert!(
source_asm_contexts(&source_graph, final_linked).is_empty(),
"ambiguous same-commitment provenance should import execution without source metadata"
);
}
#[test]
fn test_static_link_with_source_root_preserves_selected_alias_metadata() {
let mut source_builder = MastForestBuilder::new(&[]).unwrap();
let alias_a_asm_op = add_test_asm_op(
&mut source_builder,
AssemblyOp::new(None, "alias_a".into(), 1, "add".into()),
);
let alias_b_asm_op = add_test_asm_op(
&mut source_builder,
AssemblyOp::new(None, "alias_b".into(), 1, "add".into()),
);
let alias_a_ref = source_builder
.ensure_block_ref(vec![Operation::Add], vec![(0, alias_a_asm_op)], vec![])
.unwrap();
let alias_b_ref = source_builder
.ensure_block_ref(vec![Operation::Add], vec![(0, alias_b_asm_op)], vec![])
.unwrap();
record_test_root(&mut source_builder, alias_a_ref);
record_test_root(&mut source_builder, alias_b_ref);
let (static_forest, source_remapping, static_source_graph, _) =
source_builder.build().unwrap().into_parts_with_source_graph();
let final_alias_a = source_remapping[&alias_a_ref];
let final_alias_b = source_remapping[&alias_b_ref];
assert_eq!(static_forest[final_alias_a].digest(), static_forest[final_alias_b].digest());
let alias_b_source_root = static_source_graph.roots()[1];
let package_debug_info = package_debug_info_from_source_graph(&static_source_graph);
let mut provenance_builder =
MastForestBuilder::new_with_static_libraries([StaticLibrary::new(
&static_forest,
Some(package_debug_info),
)])
.unwrap();
let linked_alias_b_ref = provenance_builder
.ensure_external_link_with_source_ref(
static_forest[final_alias_b].digest(),
Some(static_forest.commitment()),
Some(final_alias_b),
Some(DebugSourceNodeId::from(u32::from(alias_b_source_root))),
)
.unwrap();
record_test_root(&mut provenance_builder, linked_alias_b_ref);
let (_linked_forest, linked_remapping, linked_source_graph, _) =
provenance_builder.build().unwrap().into_parts_with_source_graph();
let final_linked_alias_b = linked_remapping[&linked_alias_b_ref];
let linked_source_root = linked_source_graph.roots()[0];
let linked_source_node = &linked_source_graph.nodes()[linked_source_root];
assert_eq!(linked_source_node.exec_node(), final_linked_alias_b);
assert_eq!(
linked_source_node.asm_ops().first().unwrap().1.context_name(),
"alias_b",
"exact static provenance should select the hinted package source occurrence",
);
}
}