Skip to main content

miden_core/mast/node/
join_node.rs

1use alloc::{boxed::Box, vec::Vec};
2use core::fmt;
3
4#[cfg(feature = "serde")]
5use serde::{Deserialize, Serialize};
6
7use super::{
8    MastForestContributor, MastNodeContext, MastNodeExt, fingerprint_with_child_fingerprints,
9};
10use crate::{
11    Felt, Word,
12    chiplets::hasher,
13    mast::{MastForest, MastForestError, MastNodeId},
14    operations::opcodes,
15    prettier::PrettyPrint,
16    utils::LookupByIdx,
17};
18
19// JOIN NODE
20// ================================================================================================
21
22/// A Join node describe sequential execution. When the VM encounters a Join node, it executes the
23/// first child first and the second child second.
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 JoinNode {
28    children: [MastNodeId; 2],
29    digest: Word,
30}
31
32/// Constants
33impl JoinNode {
34    /// The domain of the join block (used for control block hashing).
35    pub const DOMAIN: Felt = Felt::new_unchecked(opcodes::JOIN as u64);
36}
37
38/// Public accessors
39impl JoinNode {
40    /// Returns the ID of the node that is to be executed first.
41    pub fn first(&self) -> MastNodeId {
42        self.children[0]
43    }
44
45    /// Returns the ID of the node that is to be executed after the execution of the program
46    /// defined by the first node completes.
47    pub fn second(&self) -> MastNodeId {
48        self.children[1]
49    }
50}
51
52// PRETTY PRINTING
53// ================================================================================================
54
55impl JoinNode {
56    pub(super) fn to_display<'a>(&'a self, mast_forest: &'a MastForest) -> impl fmt::Display + 'a {
57        JoinNodePrettyPrint { join_node: self, mast_forest }
58    }
59
60    pub(super) fn to_pretty_print<'a>(
61        &'a self,
62        mast_forest: &'a MastForest,
63    ) -> impl PrettyPrint + 'a {
64        JoinNodePrettyPrint { join_node: self, mast_forest }
65    }
66}
67
68struct JoinNodePrettyPrint<'a> {
69    join_node: &'a JoinNode,
70    mast_forest: &'a MastForest,
71}
72
73impl PrettyPrint for JoinNodePrettyPrint<'_> {
74    #[rustfmt::skip]
75    fn render(&self) -> crate::prettier::Document {
76        use crate::prettier::*;
77
78        let first_child =
79            self.mast_forest[self.join_node.first()].to_pretty_print(self.mast_forest);
80        let second_child =
81            self.mast_forest[self.join_node.second()].to_pretty_print(self.mast_forest);
82
83        indent(
84            4,
85            const_text("join")
86            + nl()
87            + first_child.render()
88            + nl()
89            + second_child.render(),
90        ) + nl() + const_text("end")
91    }
92}
93
94impl fmt::Display for JoinNodePrettyPrint<'_> {
95    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96        use crate::prettier::PrettyPrint;
97        self.pretty_print(f)
98    }
99}
100
101// MAST NODE TRAIT IMPLEMENTATION
102// ================================================================================================
103
104impl MastNodeExt for JoinNode {
105    /// Returns a commitment to this Join node.
106    ///
107    /// The commitment is computed as a hash of the `first` and `second` child node in the domain
108    /// defined by [Self::DOMAIN] - i.e.,:
109    /// ```
110    /// # use miden_core::mast::JoinNode;
111    /// # use miden_crypto::{Word, hash::poseidon2::Poseidon2 as Hasher};
112    /// # let first_child_digest = Word::default();
113    /// # let second_child_digest = Word::default();
114    /// Hasher::merge_in_domain(&[first_child_digest, second_child_digest], JoinNode::DOMAIN);
115    /// ```
116    fn digest(&self) -> Word {
117        self.digest
118    }
119
120    fn to_display<'a>(&'a self, mast_forest: &'a MastForest) -> Box<dyn fmt::Display + 'a> {
121        Box::new(JoinNode::to_display(self, mast_forest))
122    }
123
124    fn to_pretty_print<'a>(&'a self, mast_forest: &'a MastForest) -> Box<dyn PrettyPrint + 'a> {
125        Box::new(JoinNode::to_pretty_print(self, mast_forest))
126    }
127
128    fn has_children(&self) -> bool {
129        true
130    }
131
132    fn append_children_to(&self, target: &mut Vec<MastNodeId>) {
133        target.push(self.first());
134        target.push(self.second());
135    }
136
137    fn for_each_child<F>(&self, mut f: F)
138    where
139        F: FnMut(MastNodeId),
140    {
141        f(self.first());
142        f(self.second());
143    }
144
145    fn domain(&self) -> Felt {
146        Self::DOMAIN
147    }
148
149    type Builder = JoinNodeBuilder;
150
151    fn to_builder(self, _forest: &MastForest) -> Self::Builder {
152        JoinNodeBuilder::new(self.children).with_digest(self.digest)
153    }
154}
155
156// ARBITRARY IMPLEMENTATION
157// ================================================================================================
158
159#[cfg(all(feature = "arbitrary", test))]
160impl proptest::prelude::Arbitrary for JoinNode {
161    type Parameters = ();
162
163    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
164        use proptest::prelude::*;
165
166        use crate::Felt;
167
168        // Generate two MastNodeId values and digest for the children
169        (any::<MastNodeId>(), any::<MastNodeId>(), any::<[u64; 4]>())
170            .prop_map(|(first_child, second_child, digest_array)| {
171                // Generate a random digest
172                let digest = Word::from(digest_array.map(Felt::new_unchecked));
173                // Construct directly to avoid MastForest validation for arbitrary data
174                JoinNode {
175                    children: [first_child, second_child],
176                    digest,
177                }
178            })
179            .no_shrink()  // Pure random values, no meaningful shrinking pattern
180            .boxed()
181    }
182
183    type Strategy = proptest::prelude::BoxedStrategy<Self>;
184}
185
186// ------------------------------------------------------------------------------------------------
187/// Builder for creating [`JoinNode`] instances.
188#[derive(Debug)]
189pub struct JoinNodeBuilder {
190    children: [MastNodeId; 2],
191    digest: Option<Word>,
192}
193
194impl JoinNodeBuilder {
195    /// Creates a new builder for a JoinNode with the specified children.
196    pub fn new(children: [MastNodeId; 2]) -> Self {
197        Self { children, digest: None }
198    }
199
200    /// Builds the JoinNode.
201    pub fn build(self, context: &impl MastNodeContext) -> Result<JoinNode, MastForestError> {
202        let left_child = context.get_node_by_id(self.children[0]).ok_or_else(|| {
203            MastForestError::NodeIdOverflow(self.children[0], context.node_count())
204        })?;
205        let right_child = context.get_node_by_id(self.children[1]).ok_or_else(|| {
206            MastForestError::NodeIdOverflow(self.children[1], context.node_count())
207        })?;
208
209        // Use the forced digest if provided, otherwise compute the digest
210        let digest = if let Some(forced_digest) = self.digest {
211            forced_digest
212        } else {
213            let left_child_hash = left_child.digest();
214            let right_child_hash = right_child.digest();
215
216            hasher::merge_in_domain(&[left_child_hash, right_child_hash], JoinNode::DOMAIN)
217        };
218
219        Ok(JoinNode { children: self.children, digest })
220    }
221
222    pub(in crate::mast) fn build_linked(self) -> Result<JoinNode, MastForestError> {
223        Ok(JoinNode {
224            children: self.children,
225            digest: self.digest.ok_or(MastForestError::DigestRequiredForDeserialization)?,
226        })
227    }
228}
229
230#[cfg(any(test, feature = "arbitrary"))]
231impl JoinNodeBuilder {
232    /// Adds this builder to a mutable forest for test and arbitrary data construction.
233    pub fn add_to_forest(self, forest: &mut MastForest) -> Result<MastNodeId, MastForestError> {
234        let node = self.build(forest)?;
235        forest.nodes.push(node.into()).map_err(|_| MastForestError::TooManyNodes)
236    }
237}
238
239impl MastForestContributor for JoinNodeBuilder {
240    fn fingerprint_for_node(
241        &self,
242        context: &impl MastNodeContext,
243        hash_by_node_id: &impl LookupByIdx<MastNodeId, Word>,
244    ) -> Result<Word, MastForestError> {
245        let node_digest = if let Some(forced_digest) = self.digest {
246            forced_digest
247        } else {
248            let left_child_hash = context
249                .get_node_by_id(self.children[0])
250                .ok_or_else(|| {
251                    MastForestError::NodeIdOverflow(self.children[0], context.node_count())
252                })?
253                .digest();
254            let right_child_hash = context
255                .get_node_by_id(self.children[1])
256                .ok_or_else(|| {
257                    MastForestError::NodeIdOverflow(self.children[1], context.node_count())
258                })?
259                .digest();
260
261            hasher::merge_in_domain(&[left_child_hash, right_child_hash], JoinNode::DOMAIN)
262        };
263
264        fingerprint_with_child_fingerprints(node_digest, &self.children, context, hash_by_node_id)
265    }
266
267    fn remap_children(self, remapping: &impl LookupByIdx<MastNodeId, MastNodeId>) -> Self {
268        JoinNodeBuilder {
269            children: [
270                *remapping.get(self.children[0]).unwrap_or(&self.children[0]),
271                *remapping.get(self.children[1]).unwrap_or(&self.children[1]),
272            ],
273            digest: self.digest,
274        }
275    }
276
277    fn with_digest(mut self, digest: Word) -> Self {
278        self.digest = Some(digest);
279        self
280    }
281}
282
283#[cfg(any(test, feature = "arbitrary"))]
284impl proptest::prelude::Arbitrary for JoinNodeBuilder {
285    type Parameters = ();
286    type Strategy = proptest::strategy::BoxedStrategy<Self>;
287
288    fn arbitrary_with(_params: Self::Parameters) -> Self::Strategy {
289        use proptest::prelude::*;
290
291        any::<[MastNodeId; 2]>().prop_map(Self::new).boxed()
292    }
293}