Skip to main content

miden_core/mast/
sparse.rs

1use alloc::{
2    collections::{BTreeMap, BTreeSet},
3    string::ToString,
4    sync::Arc,
5    vec::Vec,
6};
7
8use miden_utils_indexing::newtype_id;
9
10use crate::{
11    Word,
12    advice::AdviceMap,
13    mast::{ExecutableMastForest, MastForest, MastNode, MastNodeExt, MastNodeId},
14    serde::DeserializationError,
15    utils::Idx,
16};
17
18// MAST FOREST ID
19// ================================================================================================
20
21// `MastForestId` is an opaque handle to a [`MastForest`] in some forest store such as an execution
22// trace replay's `mast_forest_store`. It is not a content-derived or stable identity for a forest,
23// and must not be compared or reused across stores or trace contexts. It is analogous to
24// `MastNodeId`, which is meaningful only within one forest's node store.
25newtype_id!(
26    #[cfg_attr(
27        all(feature = "arbitrary", test),
28        miden_test_serialization_macros::serialization_test
29    )]
30    pub struct MastForestId;
31);
32
33#[cfg(all(feature = "arbitrary", test))]
34use crate::serde::{Deserializable, Serializable};
35
36#[cfg(any(test, feature = "arbitrary"))]
37mod mast_forest_id_arbitrary {
38    use proptest::prelude::*;
39
40    use super::MastForestId;
41
42    impl Arbitrary for MastForestId {
43        type Parameters = ();
44        type Strategy = BoxedStrategy<Self>;
45
46        fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
47            // Opaque u32 handle; edge-biased to the 0 and u32::MAX bounds.
48            prop_oneof![Just(0u32), Just(u32::MAX), any::<u32>()]
49                .prop_map(Self::from)
50                .boxed()
51        }
52    }
53}
54
55// SPARSE MAST FOREST
56// ================================================================================================
57
58/// A sparse replay view over a single source [`MastForest`]'s [`MastNodeId`] space, retaining only
59/// the nodes visited during execution.
60///
61/// Unlike [`MastForest`], which stores nodes contiguously in an `IndexVec`, a [`SparseMastForest`]
62/// uses a `BTreeMap` so that it can preserve the original [`MastNodeId`]s of the source forest
63/// while omitting nodes that were not visited. A [`SparseMastForest`] is not an independent forest
64/// shape: it shares its source forest's ID space, and is intended to back re-execution of the same
65/// program. Lookups by the original [`MastNodeId`] continue to resolve to the correct
66/// [`MastNode`]s.
67///
68/// In addition to the visited nodes, a [`SparseMastForest`] may also carry digest-only entries for
69/// nodes that were referenced during execution but never actually entered (e.g. the not-taken
70/// branch of a split, or the children of a join that only need to contribute their digests to the
71/// parent's trace row). These entries let trace generation read the digest without having to copy
72/// the full child node, and they make accidental entry into a pruned node a clean
73/// `get_node_by_id` miss rather than a partially-populated node.
74///
75/// Its serialized form is a trusted, version-local trace-replay format. It has no stable wire
76/// version and must not be persisted across releases or accepted as an untrusted program format;
77/// use the validated [`MastForest`] entry points for those cases.
78#[derive(Debug)]
79pub struct SparseMastForest {
80    /// Subset of the original forest's nodes, keyed by their original [`MastNodeId`].
81    nodes: BTreeMap<MastNodeId, MastNode>,
82
83    /// Digests of nodes that were referenced (but not entered) during execution, keyed by their
84    /// original [`MastNodeId`]. Nodes present in [`Self::nodes`] are excluded from this map: a
85    /// full-node entry implicitly carries its own digest via [`MastNodeExt::digest`].
86    digests: BTreeMap<MastNodeId, Word>,
87
88    /// Roots of procedures defined within the original MAST forest.
89    roots: Vec<MastNodeId>,
90
91    /// Advice map to be loaded into the VM prior to executing procedures from this MAST forest.
92    advice_map: AdviceMap,
93}
94
95impl SparseMastForest {
96    /// Returns the underlying nodes of this sparse forest, keyed by their original
97    /// [`MastNodeId`].
98    pub fn nodes(&self) -> &BTreeMap<MastNodeId, MastNode> {
99        &self.nodes
100    }
101
102    /// Returns the minimum node count needed to cover all IDs retained in this sparse replay view.
103    ///
104    /// This is *not* the number of visited nodes and may be smaller than the source
105    /// [`MastForest`]'s node count when high source IDs were not needed during replay.
106    pub fn num_nodes(&self) -> usize {
107        self.nodes
108            .keys()
109            .chain(self.digests.keys())
110            .chain(self.roots.iter())
111            .map(|id| id.to_usize() + 1)
112            .max()
113            .unwrap_or(0)
114    }
115
116    /// Returns the roots of procedures defined within this sparse forest.
117    pub fn procedure_roots(&self) -> &[MastNodeId] {
118        &self.roots
119    }
120
121    /// Returns the empty advice map associated with this sparse forest.
122    ///
123    /// Sparse replay uses `AdviceReplay` for advice reads; this map remains empty to satisfy the
124    /// shared [`ExecutableMastForest`] interface.
125    pub fn advice_map(&self) -> &AdviceMap {
126        &self.advice_map
127    }
128
129    /// Returns the digest-only entries associated with this sparse forest.
130    pub(in crate::mast) fn digest_entries(&self) -> &BTreeMap<MastNodeId, Word> {
131        &self.digests
132    }
133
134    /// Builds a sparse forest from trusted replay parts.
135    pub(in crate::mast) fn from_serialized_parts(
136        nodes: Vec<(MastNodeId, MastNode)>,
137        digests: Vec<(MastNodeId, Word)>,
138        roots: Vec<MastNodeId>,
139        advice_map: AdviceMap,
140    ) -> Result<Self, DeserializationError> {
141        if !advice_map.is_empty() {
142            return Err(DeserializationError::InvalidValue(
143                "sparse MAST replay payload must not carry advice map entries".to_string(),
144            ));
145        }
146
147        let nodes = collect_unique_nodes(nodes)?;
148        let digests = collect_unique_digests(digests)?;
149
150        for &root in &roots {
151            validate_sparse_id(root, "procedure root")?;
152        }
153
154        for node_id in nodes.keys() {
155            if digests.contains_key(node_id) {
156                return Err(DeserializationError::InvalidValue(format!(
157                    "sparse full-node id {} overlaps a digest-only entry",
158                    node_id.0
159                )));
160            }
161        }
162
163        validate_full_node_child_digests(&nodes, &digests)?;
164
165        Ok(Self {
166            nodes,
167            digests,
168            roots,
169            advice_map: AdviceMap::default(),
170        })
171    }
172}
173
174fn validate_sparse_id(id: MastNodeId, label: &str) -> Result<(), DeserializationError> {
175    if id.to_usize() >= MastForest::MAX_NODES {
176        return Err(DeserializationError::InvalidValue(format!(
177            "{label} id {} exceeds maximum sparse MAST node id {}",
178            id.0,
179            MastForest::MAX_NODES - 1
180        )));
181    }
182    Ok(())
183}
184
185fn collect_unique_nodes(
186    nodes: Vec<(MastNodeId, MastNode)>,
187) -> Result<BTreeMap<MastNodeId, MastNode>, DeserializationError> {
188    let mut result = BTreeMap::new();
189    for (id, node) in nodes {
190        validate_sparse_id(id, "full node")?;
191        if result.insert(id, node).is_some() {
192            return Err(DeserializationError::InvalidValue(format!(
193                "duplicate sparse full-node id {}",
194                id.0
195            )));
196        }
197    }
198    Ok(result)
199}
200
201fn collect_unique_digests(
202    digests: Vec<(MastNodeId, Word)>,
203) -> Result<BTreeMap<MastNodeId, Word>, DeserializationError> {
204    let mut result = BTreeMap::new();
205    for (id, digest) in digests {
206        validate_sparse_id(id, "digest-only node")?;
207        if result.insert(id, digest).is_some() {
208            return Err(DeserializationError::InvalidValue(format!(
209                "duplicate sparse digest-only id {}",
210                id.0
211            )));
212        }
213    }
214    Ok(result)
215}
216
217/// Checks that every child of a retained full node is available as either a full node or a
218/// digest-only entry.
219fn validate_full_node_child_digests(
220    nodes: &BTreeMap<MastNodeId, MastNode>,
221    digests: &BTreeMap<MastNodeId, Word>,
222) -> Result<(), DeserializationError> {
223    for (&node_id, node) in nodes {
224        validate_sparse_id(node_id, "full node")?;
225
226        match node {
227            MastNode::Block(block) => {
228                block.validate_batch_invariants().map_err(|error_msg| {
229                    DeserializationError::InvalidValue(format!(
230                        "invalid sparse basic block {}: {error_msg}",
231                        node_id.0
232                    ))
233                })?;
234            },
235            MastNode::External(_) | MastNode::Dyn(_) => {},
236            MastNode::Join(join) => {
237                require_child_digest(node_id, join.first(), nodes, digests)?;
238                require_child_digest(node_id, join.second(), nodes, digests)?;
239            },
240            MastNode::Split(split) => {
241                require_child_digest(node_id, split.on_true(), nodes, digests)?;
242                require_child_digest(node_id, split.on_false(), nodes, digests)?;
243            },
244            MastNode::Loop(loop_node) => {
245                require_child_digest(node_id, loop_node.body(), nodes, digests)?;
246            },
247            MastNode::Call(call) => {
248                require_child_digest(node_id, call.callee(), nodes, digests)?;
249            },
250        }
251    }
252    Ok(())
253}
254
255fn require_child_digest(
256    parent_id: MastNodeId,
257    child_id: MastNodeId,
258    nodes: &BTreeMap<MastNodeId, MastNode>,
259    digests: &BTreeMap<MastNodeId, Word>,
260) -> Result<(), DeserializationError> {
261    validate_sparse_id(child_id, "child")?;
262    if !nodes.contains_key(&child_id) && !digests.contains_key(&child_id) {
263        return Err(DeserializationError::InvalidValue(format!(
264            "sparse full node {} references child {} without a full node or digest-only entry",
265            parent_id.0, child_id.0
266        )));
267    }
268    Ok(())
269}
270
271impl ExecutableMastForest for SparseMastForest {
272    #[inline(always)]
273    fn get_node_by_id(&self, node_id: MastNodeId) -> Option<&MastNode> {
274        self.nodes.get(&node_id)
275    }
276
277    #[inline(always)]
278    fn get_digest_by_id(&self, node_id: MastNodeId) -> Option<Word> {
279        if let Some(node) = self.nodes.get(&node_id) {
280            return Some(node.digest());
281        }
282        self.digests.get(&node_id).copied()
283    }
284
285    #[inline(always)]
286    fn find_procedure_root(&self, digest: Word) -> Option<MastNodeId> {
287        // The `roots` list is copied wholesale from the source forest and may include roots that
288        // were never visited (and thus aren't present in `nodes`). Skip those gracefully rather
289        // than panicking via the `Index` impl.
290        self.roots.iter().find_map(|&root_id| {
291            let node = self.nodes.get(&root_id)?;
292            (node.digest() == digest).then_some(root_id)
293        })
294    }
295
296    #[inline(always)]
297    fn advice_map(&self) -> &AdviceMap {
298        &self.advice_map
299    }
300}
301
302// SPARSE MAST FOREST BUILDER
303// ================================================================================================
304
305/// Describes how a node referenced during execution should be represented in the resulting
306/// [`SparseMastForest`].
307#[derive(Debug, Clone, Copy, PartialEq, Eq)]
308pub enum VisitKind {
309    /// The node was actually entered (or otherwise needs to be available in full at replay time).
310    /// The full [`MastNode`] is copied into [`SparseMastForest::nodes`].
311    FullVisit,
312    /// Only the node's digest is required at replay time (e.g. a child of a control-flow node
313    /// whose digest contributes to the parent's trace row, but which is itself never entered).
314    /// The digest is copied into the digest-only map; the full node is omitted.
315    DigestOnly,
316}
317
318/// Incrementally builds a [`SparseMastForest`] by collecting the [`MastNodeId`]s of nodes visited
319/// during execution of a single source [`MastForest`].
320///
321/// The builder retains a strong reference to the source forest so that it can copy out the visited
322/// nodes (and the source's roots, advice map, and debug info) at finalization time.
323///
324/// Each recorded id carries a [`VisitKind`] that controls whether the full node is copied or only
325/// its digest. If the same id is recorded as both a [`VisitKind::FullVisit`] and a
326/// [`VisitKind::DigestOnly`], the full-visit representation wins (the digest is recoverable from
327/// the full node).
328#[derive(Debug)]
329pub struct SparseMastForestBuilder {
330    /// The source forest whose nodes are being collected.
331    source: Arc<MastForest>,
332
333    /// IDs of nodes that were entered during execution. Their full [`MastNode`] is copied into the
334    /// finalized forest's `nodes` map.
335    full_visits: BTreeSet<MastNodeId>,
336
337    /// IDs of nodes that were only referenced (not entered) during execution. At finalization,
338    /// any id that also appears in [`Self::full_visits`] is excluded; the remainder contributes a
339    /// digest-only entry to the finalized forest.
340    digest_only_visits: BTreeSet<MastNodeId>,
341}
342
343impl SparseMastForestBuilder {
344    /// Creates a new builder for the given source forest.
345    pub fn new(source: Arc<MastForest>) -> Self {
346        Self {
347            source,
348            full_visits: BTreeSet::new(),
349            digest_only_visits: BTreeSet::new(),
350        }
351    }
352
353    /// Records a visit to the node with the provided id. Idempotent.
354    ///
355    /// If the same id is recorded both as [`VisitKind::FullVisit`] and as
356    /// [`VisitKind::DigestOnly`], the full-visit representation wins.
357    pub fn record_visit(&mut self, node_id: MastNodeId, kind: VisitKind) {
358        match kind {
359            VisitKind::FullVisit => {
360                self.full_visits.insert(node_id);
361            },
362            VisitKind::DigestOnly => {
363                self.digest_only_visits.insert(node_id);
364            },
365        }
366    }
367
368    /// Returns a strong reference to the source forest backing this builder.
369    pub fn source(&self) -> &Arc<MastForest> {
370        &self.source
371    }
372
373    /// Consumes the builder and produces a [`SparseMastForest`] containing only the visited nodes
374    /// from the source forest. The roots are cloned from the source in full. Advice data is not
375    /// copied because sparse replay uses `AdviceReplay`.
376    pub fn finalize(self) -> SparseMastForest {
377        let SparseMastForestBuilder { source, full_visits, digest_only_visits } = self;
378
379        let mut nodes = BTreeMap::new();
380        for node_id in &full_visits {
381            let node = source
382                .get_node_by_id(*node_id)
383                .expect("recorded full-visit id must exist in source forest");
384            nodes.insert(*node_id, node.clone());
385        }
386
387        let mut digests = BTreeMap::new();
388        for node_id in digest_only_visits {
389            if full_visits.contains(&node_id) {
390                continue;
391            }
392            let node = source
393                .get_node_by_id(node_id)
394                .expect("recorded digest-only id must exist in source forest");
395            digests.insert(node_id, node.digest());
396        }
397
398        SparseMastForest {
399            nodes,
400            digests,
401            roots: source.procedure_roots().to_vec(),
402            advice_map: AdviceMap::default(),
403        }
404    }
405}