Skip to main content

miden_core/mast/node/
dyn_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};
8#[cfg(debug_assertions)]
9use crate::mast::MastNode;
10use crate::{
11    Felt, Word,
12    mast::{
13        DecoratorId, DecoratorStore, MastForest, MastForestError, MastNodeFingerprint, MastNodeId,
14    },
15    operations::opcodes,
16    prettier::{Document, PrettyPrint, const_text, nl},
17    utils::LookupByIdx,
18};
19
20// DYN NODE
21// ================================================================================================
22
23/// A Dyn node specifies that the node to be executed next is defined dynamically via the stack.
24#[derive(Debug, Clone, PartialEq, Eq)]
25#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
26#[cfg_attr(all(feature = "arbitrary", test), miden_test_serde_macros::serde_test)]
27pub struct DynNode {
28    is_dyncall: bool,
29    digest: Word,
30    decorator_store: DecoratorStore,
31}
32
33/// Constants
34impl DynNode {
35    /// The domain of the Dyn block (used for control block hashing).
36    pub const DYN_DOMAIN: Felt = Felt::new_unchecked(opcodes::DYN as u64);
37
38    /// The domain of the Dyncall block (used for control block hashing).
39    pub const DYNCALL_DOMAIN: Felt = Felt::new_unchecked(opcodes::DYNCALL as u64);
40}
41
42/// Default digest constants
43impl DynNode {
44    /// The default digest for a DynNode representing a dyncall operation.
45    pub const DYNCALL_DEFAULT_DIGEST: Word = Word::new([
46        Felt::new_unchecked(16830415514927835337),
47        Felt::new_unchecked(12164645914672292987),
48        Felt::new_unchecked(13192574193032437705),
49        Felt::new_unchecked(4604554596675732269),
50    ]);
51
52    /// The default digest for a DynNode representing a dynexec operation.
53    pub const DYN_DEFAULT_DIGEST: Word = Word::new([
54        Felt::new_unchecked(16952228088962355159),
55        Felt::new_unchecked(5793482471479538911),
56        Felt::new_unchecked(14446299416172848527),
57        Felt::new_unchecked(13522295374716441620),
58    ]);
59}
60
61/// Public accessors
62impl DynNode {
63    /// Returns true if the [`DynNode`] represents a dyncall operation, and false for dynexec.
64    pub fn is_dyncall(&self) -> bool {
65        self.is_dyncall
66    }
67
68    /// Returns the domain of this dyn node.
69    pub fn domain(&self) -> Felt {
70        if self.is_dyncall() {
71            Self::DYNCALL_DOMAIN
72        } else {
73            Self::DYN_DOMAIN
74        }
75    }
76}
77
78// PRETTY PRINTING
79// ================================================================================================
80
81impl DynNode {
82    pub(super) fn to_display<'a>(&'a self, mast_forest: &'a MastForest) -> impl fmt::Display + 'a {
83        DynNodePrettyPrint { node: self, mast_forest }
84    }
85
86    pub(super) fn to_pretty_print<'a>(
87        &'a self,
88        mast_forest: &'a MastForest,
89    ) -> impl PrettyPrint + 'a {
90        DynNodePrettyPrint { node: self, mast_forest }
91    }
92}
93
94struct DynNodePrettyPrint<'a> {
95    node: &'a DynNode,
96    mast_forest: &'a MastForest,
97}
98
99impl DynNodePrettyPrint<'_> {
100    /// Concatenates the provided decorators in a single line. If the list of decorators is not
101    /// empty, prepends `prepend` and appends `append` to the decorator document.
102    fn concatenate_decorators(
103        &self,
104        decorator_ids: &[DecoratorId],
105        prepend: Document,
106        append: Document,
107    ) -> Document {
108        let decorators = decorator_ids
109            .iter()
110            .map(|&decorator_id| self.mast_forest[decorator_id].render())
111            .reduce(|acc, doc| acc + const_text(" ") + doc)
112            .unwrap_or_default();
113
114        if decorators.is_empty() {
115            decorators
116        } else {
117            prepend + decorators + append
118        }
119    }
120
121    fn single_line_pre_decorators(&self) -> Document {
122        self.concatenate_decorators(
123            self.node.before_enter(self.mast_forest),
124            Document::Empty,
125            const_text(" "),
126        )
127    }
128
129    fn single_line_post_decorators(&self) -> Document {
130        self.concatenate_decorators(
131            self.node.after_exit(self.mast_forest),
132            const_text(" "),
133            Document::Empty,
134        )
135    }
136
137    fn multi_line_pre_decorators(&self) -> Document {
138        self.concatenate_decorators(self.node.before_enter(self.mast_forest), Document::Empty, nl())
139    }
140
141    fn multi_line_post_decorators(&self) -> Document {
142        self.concatenate_decorators(self.node.after_exit(self.mast_forest), nl(), Document::Empty)
143    }
144}
145
146impl PrettyPrint for DynNodePrettyPrint<'_> {
147    fn render(&self) -> Document {
148        let dyn_text = if self.node.is_dyncall() {
149            const_text("dyncall")
150        } else {
151            const_text("dyn")
152        };
153
154        let single_line = self.single_line_pre_decorators()
155            + dyn_text.clone()
156            + self.single_line_post_decorators();
157        let multi_line =
158            self.multi_line_pre_decorators() + dyn_text + self.multi_line_post_decorators();
159
160        single_line | multi_line
161    }
162}
163
164impl fmt::Display for DynNodePrettyPrint<'_> {
165    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166        self.pretty_print(f)
167    }
168}
169
170// MAST NODE TRAIT IMPLEMENTATION
171// ================================================================================================
172
173impl MastNodeExt for DynNode {
174    /// Returns a commitment to a Dyn node.
175    fn digest(&self) -> Word {
176        self.digest
177    }
178
179    /// Returns the decorators to be executed before this node is executed.
180    fn before_enter<'a>(&'a self, forest: &'a MastForest) -> &'a [DecoratorId] {
181        #[cfg(debug_assertions)]
182        self.verify_node_in_forest(forest);
183        self.decorator_store.before_enter(forest)
184    }
185
186    /// Returns the decorators to be executed after this node is executed.
187    fn after_exit<'a>(&'a self, forest: &'a MastForest) -> &'a [DecoratorId] {
188        #[cfg(debug_assertions)]
189        self.verify_node_in_forest(forest);
190        self.decorator_store.after_exit(forest)
191    }
192
193    fn to_display<'a>(&'a self, mast_forest: &'a MastForest) -> Box<dyn fmt::Display + 'a> {
194        Box::new(DynNode::to_display(self, mast_forest))
195    }
196
197    fn to_pretty_print<'a>(&'a self, mast_forest: &'a MastForest) -> Box<dyn PrettyPrint + 'a> {
198        Box::new(DynNode::to_pretty_print(self, mast_forest))
199    }
200
201    fn has_children(&self) -> bool {
202        false
203    }
204
205    fn append_children_to(&self, _target: &mut Vec<MastNodeId>) {
206        // No children for dyn nodes
207    }
208
209    fn for_each_child<F>(&self, _f: F)
210    where
211        F: FnMut(MastNodeId),
212    {
213        // DynNode has no children
214    }
215
216    fn domain(&self) -> Felt {
217        self.domain()
218    }
219
220    type Builder = DynNodeBuilder;
221
222    fn to_builder(self, forest: &MastForest) -> Self::Builder {
223        // Extract decorators from decorator_store if in Owned state
224        match self.decorator_store {
225            DecoratorStore::Owned { before_enter, after_exit, .. } => {
226                let mut builder = if self.is_dyncall {
227                    DynNodeBuilder::new_dyncall()
228                } else {
229                    DynNodeBuilder::new_dyn()
230                };
231                builder = builder.with_before_enter(before_enter).with_after_exit(after_exit);
232                builder
233            },
234            DecoratorStore::Linked { id } => {
235                // Extract decorators from forest storage when in Linked state
236                let before_enter = forest.before_enter_decorators(id).to_vec();
237                let after_exit = forest.after_exit_decorators(id).to_vec();
238                let mut builder = if self.is_dyncall {
239                    DynNodeBuilder::new_dyncall()
240                } else {
241                    DynNodeBuilder::new_dyn()
242                };
243                builder = builder.with_before_enter(before_enter).with_after_exit(after_exit);
244                builder
245            },
246        }
247    }
248
249    #[cfg(debug_assertions)]
250    fn verify_node_in_forest(&self, forest: &MastForest) {
251        if let Some(id) = self.decorator_store.linked_id() {
252            // Verify that this node is the one stored at the given ID in the forest
253            let self_ptr = self as *const Self;
254            let forest_node = &forest.nodes[id];
255            let forest_node_ptr = match forest_node {
256                MastNode::Dyn(dyn_node) => dyn_node as *const DynNode as *const (),
257                _ => panic!("Node type mismatch at {id:?}"),
258            };
259            let self_as_void = self_ptr as *const ();
260            debug_assert_eq!(
261                self_as_void, forest_node_ptr,
262                "Node pointer mismatch: expected node at {id:?} to be self"
263            );
264        }
265    }
266}
267
268// ARBITRARY IMPLEMENTATION
269// ================================================================================================
270
271#[cfg(all(feature = "arbitrary", test))]
272impl proptest::prelude::Arbitrary for DynNode {
273    type Parameters = ();
274
275    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
276        use proptest::prelude::*;
277
278        // Generate whether it's a dyncall or dynexec
279        any::<bool>()
280            .prop_map(|is_dyncall| {
281                if is_dyncall {
282                    DynNodeBuilder::new_dyncall().build()
283                } else {
284                    DynNodeBuilder::new_dyn().build()
285                }
286            })
287            .no_shrink()  // Pure random values, no meaningful shrinking pattern
288            .boxed()
289    }
290
291    type Strategy = proptest::prelude::BoxedStrategy<Self>;
292}
293
294// ------------------------------------------------------------------------------------------------
295/// Builder for creating [`DynNode`] instances with decorators.
296#[derive(Debug)]
297pub struct DynNodeBuilder {
298    is_dyncall: bool,
299    before_enter: Vec<DecoratorId>,
300    after_exit: Vec<DecoratorId>,
301    digest: Option<Word>,
302}
303
304impl DynNodeBuilder {
305    /// Creates a new builder for a DynNode representing a dynexec operation.
306    pub fn new_dyn() -> Self {
307        Self {
308            is_dyncall: false,
309            before_enter: Vec::new(),
310            after_exit: Vec::new(),
311            digest: None,
312        }
313    }
314
315    /// Creates a new builder for a DynNode representing a dyncall operation.
316    pub fn new_dyncall() -> Self {
317        Self {
318            is_dyncall: true,
319            before_enter: Vec::new(),
320            after_exit: Vec::new(),
321            digest: None,
322        }
323    }
324
325    /// Builds the DynNode with the specified decorators.
326    pub fn build(self) -> DynNode {
327        // Use the forced digest if provided, otherwise use the default digest
328        let digest = if let Some(forced_digest) = self.digest {
329            forced_digest
330        } else if self.is_dyncall {
331            DynNode::DYNCALL_DEFAULT_DIGEST
332        } else {
333            DynNode::DYN_DEFAULT_DIGEST
334        };
335
336        DynNode {
337            is_dyncall: self.is_dyncall,
338            digest,
339            decorator_store: DecoratorStore::new_owned_with_decorators(
340                self.before_enter,
341                self.after_exit,
342            ),
343        }
344    }
345}
346
347impl MastForestContributor for DynNodeBuilder {
348    fn add_to_forest(self, forest: &mut MastForest) -> Result<MastNodeId, MastForestError> {
349        // Use the forced digest if provided, otherwise use the default digest
350        let digest = if let Some(forced_digest) = self.digest {
351            forced_digest
352        } else if self.is_dyncall {
353            DynNode::DYNCALL_DEFAULT_DIGEST
354        } else {
355            DynNode::DYN_DEFAULT_DIGEST
356        };
357
358        // Determine the node ID that will be assigned
359        let future_node_id = MastNodeId::new_unchecked(forest.nodes.len() as u32);
360
361        // Store node-level decorators in the centralized NodeToDecoratorIds for efficient access
362        forest.register_node_decorators(future_node_id, &self.before_enter, &self.after_exit);
363
364        // Create the node in the forest with Linked variant from the start
365        // Move the data directly without intermediate cloning
366        let node_id = forest
367            .nodes
368            .push(
369                DynNode {
370                    is_dyncall: self.is_dyncall,
371                    digest,
372                    decorator_store: DecoratorStore::Linked { id: future_node_id },
373                }
374                .into(),
375            )
376            .map_err(|_| MastForestError::TooManyNodes)?;
377
378        Ok(node_id)
379    }
380
381    fn fingerprint_for_node(
382        &self,
383        forest: &MastForest,
384        _hash_by_node_id: &impl LookupByIdx<MastNodeId, MastNodeFingerprint>,
385    ) -> Result<MastNodeFingerprint, MastForestError> {
386        // DynNode has no children, so we don't need hash_by_node_id
387        // Use the fingerprint_from_parts helper function with empty children array
388        crate::mast::node_fingerprint::fingerprint_from_parts(
389            forest,
390            _hash_by_node_id,
391            &self.before_enter,
392            &self.after_exit,
393            &[], // DynNode has no children
394            // Use the forced digest if available, otherwise use the default digest values
395            if let Some(forced_digest) = self.digest {
396                forced_digest
397            } else if self.is_dyncall {
398                DynNode::DYNCALL_DEFAULT_DIGEST
399            } else {
400                DynNode::DYN_DEFAULT_DIGEST
401            },
402        )
403    }
404
405    fn remap_children(self, _remapping: &impl LookupByIdx<MastNodeId, MastNodeId>) -> Self {
406        // DynNode has no children to remap, but preserve the digest
407        self
408    }
409
410    fn with_before_enter(mut self, decorators: impl Into<Vec<DecoratorId>>) -> Self {
411        self.before_enter = decorators.into();
412        self
413    }
414
415    fn with_after_exit(mut self, decorators: impl Into<Vec<DecoratorId>>) -> Self {
416        self.after_exit = decorators.into();
417        self
418    }
419
420    fn append_before_enter(&mut self, decorators: impl IntoIterator<Item = DecoratorId>) {
421        self.before_enter.extend(decorators);
422    }
423
424    fn append_after_exit(&mut self, decorators: impl IntoIterator<Item = DecoratorId>) {
425        self.after_exit.extend(decorators);
426    }
427
428    fn with_digest(mut self, digest: Word) -> Self {
429        self.digest = Some(digest);
430        self
431    }
432}
433
434impl DynNodeBuilder {
435    /// Add this node to a forest using relaxed validation.
436    ///
437    /// This method is used during deserialization where nodes may reference child nodes
438    /// that haven't been added to the forest yet. The child node IDs have already been
439    /// validated against the expected final node count during the `try_into_mast_node_builder`
440    /// step, so we can safely skip validation here.
441    ///
442    /// Note: This is not part of the `MastForestContributor` trait because it's only
443    /// intended for internal use during deserialization.
444    pub(in crate::mast) fn add_to_forest_relaxed(
445        self,
446        forest: &mut MastForest,
447    ) -> Result<MastNodeId, MastForestError> {
448        // Use the forced digest if provided, otherwise use the default digest
449        let digest = if let Some(forced_digest) = self.digest {
450            forced_digest
451        } else if self.is_dyncall {
452            DynNode::DYNCALL_DEFAULT_DIGEST
453        } else {
454            DynNode::DYN_DEFAULT_DIGEST
455        };
456
457        // Determine the node ID that will be assigned
458        let future_node_id = MastNodeId::new_unchecked(forest.nodes.len() as u32);
459
460        // Create the node in the forest with Linked variant from the start
461        // Note: Decorators are already in forest.debug_info from deserialization
462        // Move the data directly without intermediate cloning
463        let node_id = forest
464            .nodes
465            .push(
466                DynNode {
467                    is_dyncall: self.is_dyncall,
468                    digest,
469                    decorator_store: DecoratorStore::Linked { id: future_node_id },
470                }
471                .into(),
472            )
473            .map_err(|_| MastForestError::TooManyNodes)?;
474
475        Ok(node_id)
476    }
477}
478
479#[cfg(any(test, feature = "arbitrary"))]
480impl proptest::prelude::Arbitrary for DynNodeBuilder {
481    type Parameters = DynNodeBuilderParams;
482    type Strategy = proptest::strategy::BoxedStrategy<Self>;
483
484    fn arbitrary_with(params: Self::Parameters) -> Self::Strategy {
485        use proptest::prelude::*;
486
487        (
488            any::<bool>(),
489            proptest::collection::vec(
490                super::arbitrary::decorator_id_strategy(params.max_decorator_id_u32),
491                0..=params.max_decorators,
492            ),
493            proptest::collection::vec(
494                super::arbitrary::decorator_id_strategy(params.max_decorator_id_u32),
495                0..=params.max_decorators,
496            ),
497        )
498            .prop_map(|(is_dyncall, before_enter, after_exit)| {
499                let builder = if is_dyncall {
500                    Self::new_dyncall()
501                } else {
502                    Self::new_dyn()
503                };
504                builder.with_before_enter(before_enter).with_after_exit(after_exit)
505            })
506            .boxed()
507    }
508}
509
510/// Parameters for generating DynNodeBuilder instances
511#[cfg(any(test, feature = "arbitrary"))]
512#[derive(Clone, Debug)]
513pub struct DynNodeBuilderParams {
514    pub max_decorators: usize,
515    pub max_decorator_id_u32: u32,
516}
517
518#[cfg(any(test, feature = "arbitrary"))]
519impl Default for DynNodeBuilderParams {
520    fn default() -> Self {
521        Self {
522            max_decorators: 4,
523            max_decorator_id_u32: 10,
524        }
525    }
526}
527
528#[cfg(test)]
529mod tests {
530    use miden_crypto::hash::poseidon2::Poseidon2;
531
532    use super::*;
533
534    /// Ensures that the hash of `DynNode` is indeed the hash of 2 empty words, in the `DynNode`
535    /// domain.
536    #[test]
537    pub fn test_dyn_node_digest() {
538        let mut forest = MastForest::new();
539        let dyn_node_id = DynNodeBuilder::new_dyn().add_to_forest(&mut forest).unwrap();
540        let dyn_node = forest.get_node_by_id(dyn_node_id).unwrap().unwrap_dyn();
541        assert_eq!(
542            dyn_node.digest(),
543            Poseidon2::merge_in_domain(&[Word::default(), Word::default()], DynNode::DYN_DOMAIN)
544        );
545
546        let dyncall_node_id = DynNodeBuilder::new_dyncall().add_to_forest(&mut forest).unwrap();
547        let dyncall_node = forest.get_node_by_id(dyncall_node_id).unwrap().unwrap_dyn();
548        assert_eq!(
549            dyncall_node.digest(),
550            Poseidon2::merge_in_domain(
551                &[Word::default(), Word::default()],
552                DynNode::DYNCALL_DOMAIN
553            )
554        );
555    }
556}