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