Skip to main content

miden_core/mast/node/
loop_node.rs

1use alloc::{boxed::Box, vec::Vec};
2use core::fmt;
3
4#[cfg(feature = "serde")]
5use serde::{Deserialize, Serialize};
6
7use super::{MastForestContributor, MastNodeExt, fingerprint_with_child_fingerprints};
8use crate::{
9    Felt, Word,
10    chiplets::hasher,
11    mast::{MastForest, MastForestError, MastNodeId},
12    operations::opcodes,
13    prettier::PrettyPrint,
14    utils::{Idx, LookupByIdx},
15};
16
17// LOOP NODE
18// ================================================================================================
19
20/// A Loop node defines condition-controlled iterative execution. When the VM encounters a Loop
21/// node, it will keep executing the body of the loop as long as the top of the stack is `1``,
22/// except for the encounter which it executes unconditionally.
23///
24/// The loop is exited when at the end of executing the loop body the top of the stack is `0``.
25/// If the top of the stack is neither `0` nor `1` when the condition is checked, the execution
26/// fails.
27#[derive(Debug, Clone, PartialEq, Eq)]
28#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
29#[cfg_attr(all(feature = "arbitrary", test), miden_test_serde_macros::serde_test)]
30pub struct LoopNode {
31    body: MastNodeId,
32    digest: Word,
33}
34
35/// Constants
36impl LoopNode {
37    /// The domain of the loop node (used for control block hashing).
38    pub const DOMAIN: Felt = Felt::new_unchecked(opcodes::LOOP as u64);
39}
40
41impl LoopNode {
42    /// Returns the ID of the node presenting the body of the loop.
43    pub fn body(&self) -> MastNodeId {
44        self.body
45    }
46}
47
48// PRETTY PRINTING
49// ================================================================================================
50
51impl LoopNode {
52    pub(super) fn to_display<'a>(&'a self, mast_forest: &'a MastForest) -> impl fmt::Display + 'a {
53        LoopNodePrettyPrint { loop_node: self, mast_forest }
54    }
55
56    pub(super) fn to_pretty_print<'a>(
57        &'a self,
58        mast_forest: &'a MastForest,
59    ) -> impl PrettyPrint + 'a {
60        LoopNodePrettyPrint { loop_node: self, mast_forest }
61    }
62}
63
64struct LoopNodePrettyPrint<'a> {
65    loop_node: &'a LoopNode,
66    mast_forest: &'a MastForest,
67}
68
69impl PrettyPrint for LoopNodePrettyPrint<'_> {
70    fn render(&self) -> crate::prettier::Document {
71        use crate::prettier::*;
72
73        let loop_body = self.mast_forest[self.loop_node.body].to_pretty_print(self.mast_forest);
74
75        indent(4, const_text("loop") + nl() + loop_body.render()) + nl() + const_text("end")
76    }
77}
78
79impl fmt::Display for LoopNodePrettyPrint<'_> {
80    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        use crate::prettier::PrettyPrint;
82        self.pretty_print(f)
83    }
84}
85
86// MAST NODE TRAIT IMPLEMENTATION
87// ================================================================================================
88
89impl MastNodeExt for LoopNode {
90    /// Returns a commitment to this Loop node.
91    ///
92    /// The commitment is computed as a hash of the loop body and an empty word ([ZERO; 4]) in
93    /// the domain defined by [Self::DOMAIN] - i..e,:
94    /// ```
95    /// # use miden_core::mast::LoopNode;
96    /// # use miden_crypto::{Word, hash::poseidon2::Poseidon2 as Hasher};
97    /// # let body_digest = Word::default();
98    /// Hasher::merge_in_domain(&[body_digest, Word::default()], LoopNode::DOMAIN);
99    /// ```
100    fn digest(&self) -> Word {
101        self.digest
102    }
103
104    fn to_display<'a>(&'a self, mast_forest: &'a MastForest) -> Box<dyn fmt::Display + 'a> {
105        Box::new(LoopNode::to_display(self, mast_forest))
106    }
107
108    fn to_pretty_print<'a>(&'a self, mast_forest: &'a MastForest) -> Box<dyn PrettyPrint + 'a> {
109        Box::new(LoopNode::to_pretty_print(self, mast_forest))
110    }
111
112    fn has_children(&self) -> bool {
113        true
114    }
115
116    fn append_children_to(&self, target: &mut Vec<MastNodeId>) {
117        target.push(self.body());
118    }
119
120    fn for_each_child<F>(&self, mut f: F)
121    where
122        F: FnMut(MastNodeId),
123    {
124        f(self.body());
125    }
126
127    fn domain(&self) -> Felt {
128        Self::DOMAIN
129    }
130
131    type Builder = LoopNodeBuilder;
132
133    fn to_builder(self, _forest: &MastForest) -> Self::Builder {
134        LoopNodeBuilder::new(self.body).with_digest(self.digest)
135    }
136}
137
138// ARBITRARY IMPLEMENTATION
139// ================================================================================================
140
141#[cfg(all(feature = "arbitrary", test))]
142impl proptest::prelude::Arbitrary for LoopNode {
143    type Parameters = ();
144
145    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
146        use proptest::prelude::*;
147
148        use crate::Felt;
149
150        // Generate one MastNodeId value and digest for the body
151        (any::<MastNodeId>(), any::<[u64; 4]>())
152            .prop_map(|(body, digest_array)| {
153                // Generate a random digest
154                let digest = Word::from(digest_array.map(Felt::new_unchecked));
155                // Construct directly to avoid MastForest validation for arbitrary data
156                LoopNode {
157                    body,
158                    digest,
159                }
160            })
161            .no_shrink()  // Pure random values, no meaningful shrinking pattern
162            .boxed()
163    }
164
165    type Strategy = proptest::prelude::BoxedStrategy<Self>;
166}
167
168// ------------------------------------------------------------------------------------------------
169/// Builder for creating [`LoopNode`] instances.
170#[derive(Debug)]
171pub struct LoopNodeBuilder {
172    body: MastNodeId,
173    digest: Option<Word>,
174}
175
176impl LoopNodeBuilder {
177    /// Creates a new builder for a LoopNode with the specified body.
178    pub fn new(body: MastNodeId) -> Self {
179        Self { body, digest: None }
180    }
181
182    /// Builds the LoopNode.
183    pub fn build(self, mast_forest: &MastForest) -> Result<LoopNode, MastForestError> {
184        if self.body.to_usize() >= mast_forest.nodes.len() {
185            return Err(MastForestError::NodeIdOverflow(self.body, mast_forest.nodes.len()));
186        }
187
188        // Use the forced digest if provided, otherwise compute the digest
189        let digest = if let Some(forced_digest) = self.digest {
190            forced_digest
191        } else {
192            let body_hash = mast_forest[self.body].digest();
193
194            hasher::merge_in_domain(&[body_hash, Word::default()], LoopNode::DOMAIN)
195        };
196
197        Ok(LoopNode { body: self.body, digest })
198    }
199
200    pub(in crate::mast) fn build_linked(self) -> Result<LoopNode, MastForestError> {
201        Ok(LoopNode {
202            body: self.body,
203            digest: self.digest.ok_or(MastForestError::DigestRequiredForDeserialization)?,
204        })
205    }
206}
207
208impl MastForestContributor for LoopNodeBuilder {
209    fn add_to_forest(self, forest: &mut MastForest) -> Result<MastNodeId, MastForestError> {
210        let node = self.build(forest)?;
211
212        // Create the node in the forest with Linked variant from the start
213        // Move the data directly without intermediate cloning
214        let node_id = forest.nodes.push(node.into()).map_err(|_| MastForestError::TooManyNodes)?;
215
216        Ok(node_id)
217    }
218
219    fn fingerprint_for_node(
220        &self,
221        forest: &MastForest,
222        hash_by_node_id: &impl LookupByIdx<MastNodeId, Word>,
223    ) -> Result<Word, MastForestError> {
224        let node_digest = if let Some(forced_digest) = self.digest {
225            forced_digest
226        } else {
227            let body_hash = forest[self.body].digest();
228
229            hasher::merge_in_domain(&[body_hash, Word::default()], LoopNode::DOMAIN)
230        };
231
232        fingerprint_with_child_fingerprints(node_digest, &[self.body], forest, hash_by_node_id)
233    }
234
235    fn remap_children(self, remapping: &impl LookupByIdx<MastNodeId, MastNodeId>) -> Self {
236        LoopNodeBuilder {
237            body: *remapping.get(self.body).unwrap_or(&self.body),
238            digest: self.digest,
239        }
240    }
241
242    fn with_digest(mut self, digest: Word) -> Self {
243        self.digest = Some(digest);
244        self
245    }
246}
247
248impl LoopNodeBuilder {
249    /// Add this node to a forest using relaxed validation.
250    ///
251    /// This method is used during deserialization where nodes may reference child nodes
252    /// that haven't been added to the forest yet. The child node IDs have already been
253    /// validated against the expected final node count during the `try_into_mast_node_builder`
254    /// step, so we can safely skip validation here.
255    ///
256    /// Note: This is not part of the `MastForestContributor` trait because it's only
257    /// intended for internal use during deserialization.
258    pub(in crate::mast) fn add_to_forest_relaxed(
259        self,
260        forest: &mut MastForest,
261    ) -> Result<MastNodeId, MastForestError> {
262        // Use the forced digest if provided, otherwise use a default digest
263        // The actual digest computation will be handled when the forest is complete
264        let Some(digest) = self.digest else {
265            return Err(MastForestError::DigestRequiredForDeserialization);
266        };
267
268        // Create the node in the forest with Linked variant from the start
269        // Move the data directly without intermediate cloning
270        let node_id = forest
271            .nodes
272            .push(LoopNode { body: self.body, digest }.into())
273            .map_err(|_| MastForestError::TooManyNodes)?;
274
275        Ok(node_id)
276    }
277}
278
279#[cfg(any(test, feature = "arbitrary"))]
280impl proptest::prelude::Arbitrary for LoopNodeBuilder {
281    type Parameters = LoopNodeBuilderParams;
282    type Strategy = proptest::strategy::BoxedStrategy<Self>;
283
284    fn arbitrary_with(params: Self::Parameters) -> Self::Strategy {
285        use proptest::prelude::*;
286
287        let _ = params;
288        any::<MastNodeId>().prop_map(Self::new).boxed()
289    }
290}
291
292/// Parameters for generating LoopNodeBuilder instances
293#[cfg(any(test, feature = "arbitrary"))]
294#[derive(Clone, Debug, Default)]
295pub struct LoopNodeBuilderParams {}