Skip to main content

miden_processor/
continuation_stack.rs

1use alloc::{sync::Arc, vec::Vec};
2
3use miden_core::{
4    mast::{MastForestId, MastNodeId},
5    program::Program,
6    serde::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable},
7};
8use miden_mast_package::debug_info::{DebugSourceInlineCall, DebugSourceNodeId, PackageDebugInfo};
9
10/// A hint for the initial size of the continuation stack.
11const CONTINUATION_STACK_SIZE_HINT: usize = 64;
12
13/// Package-owned source context whose inline-call rows remain active across a dynamic target.
14#[derive(Debug, Clone)]
15pub struct SourceInlineCallContext {
16    package_debug_info: Arc<PackageDebugInfo>,
17    source_node_id: DebugSourceNodeId,
18    op_idx: u32,
19}
20
21impl SourceInlineCallContext {
22    pub(crate) fn new(
23        package_debug_info: Arc<PackageDebugInfo>,
24        source_node_id: DebugSourceNodeId,
25        op_idx: u32,
26    ) -> Self {
27        Self {
28            package_debug_info,
29            source_node_id,
30            op_idx,
31        }
32    }
33
34    pub(crate) fn for_source_boundary(
35        package_debug_info: Arc<PackageDebugInfo>,
36        source_node_id: Option<DebugSourceNodeId>,
37    ) -> Option<Self> {
38        let source_node_id = source_node_id?;
39        let op_idx = package_debug_info.source_node(source_node_id)?.op_start;
40        package_debug_info.inline_calls_for_operation(source_node_id, op_idx).next()?;
41        Some(Self::new(package_debug_info, source_node_id, op_idx))
42    }
43
44    /// Returns the package debug information which owns this source occurrence.
45    pub fn debug_info(&self) -> &Arc<PackageDebugInfo> {
46        &self.package_debug_info
47    }
48
49    /// Returns the source occurrence whose boundary inline rows form this context.
50    pub fn source_node_id(&self) -> DebugSourceNodeId {
51        self.source_node_id
52    }
53
54    /// Returns the inline calls inherited at this source boundary.
55    pub fn inline_calls(&self) -> impl Iterator<Item = &DebugSourceInlineCall> {
56        self.package_debug_info
57            .inline_calls_for_operation(self.source_node_id, self.op_idx)
58    }
59}
60
61// CONTINUATION
62// ================================================================================================
63
64/// Represents a unit of work in the continuation stack.
65///
66/// This enum defines the different types of continuations that can be performed on MAST nodes
67/// during program execution.
68///
69/// The type parameter `F` is the representation of a MAST forest carried by the
70/// [`Continuation::EnterForest`] variant. For live execution this is `Arc<MastForest>`; for the
71/// snapshotted continuation stack inside a trace fragment it is a `usize` index into the
72/// `mast_forest_store` of the trace generation context.
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub enum Continuation<F> {
75    /// Start processing a node in the MAST forest.
76    StartNode(MastNodeId),
77    /// Process the finish phase of a Join node.
78    FinishJoin(MastNodeId),
79    /// Process the finish phase of a Split node.
80    FinishSplit(MastNodeId),
81    /// Process the finish phase of a Loop node.
82    ///
83    /// Reached after the loop body has finished executing. Inspects the condition the body left on
84    /// top of the stack and either fires REPEAT (re-enter the body) or END (exit the loop). Loop
85    /// bodies are entered unconditionally — a `while.true` source construct is desugared into a
86    /// SPLIT that wraps the LOOP, so the LOOP itself sees a do-while body.
87    FinishLoop(MastNodeId),
88    /// Process the finish phase of a Call node.
89    FinishCall(MastNodeId),
90    /// Process the finish phase of a Dyn node.
91    FinishDyn(MastNodeId),
92    /// Resume execution at the specified operation of the specified batch in the given basic block
93    /// node.
94    ResumeBasicBlock {
95        node_id: MastNodeId,
96        batch_index: usize,
97        op_idx_in_batch: usize,
98    },
99    /// Resume execution at the RESPAN operation before the specific batch within a basic block
100    /// node.
101    Respan { node_id: MastNodeId, batch_index: usize },
102    /// Process the finish phase of a basic block node.
103    ///
104    /// This corresponds to incrementing the clock to account for the inserted END operation.
105    FinishBasicBlock(MastNodeId),
106    /// Enter a new MAST forest, where all subsequent `MastNodeId`s will be relative to this forest.
107    ///
108    /// When we encounter an `ExternalNode`, we enter the corresponding MAST forest directly, and
109    /// push an `EnterForest` continuation to restore the previous forest when done.
110    EnterForest {
111        forest: F,
112        package_debug_info: Option<Arc<PackageDebugInfo>>,
113        /// Inline-context stack depth to restore when returning to this forest.
114        inline_context_depth: usize,
115    },
116}
117
118impl<F> Continuation<F> {
119    /// Returns true if executing this continuation increments the processor clock, and false
120    /// otherwise.
121    pub fn increments_clk(&self) -> bool {
122        use Continuation::*;
123
124        // Note: we prefer naming all the variants over using a wildcard arm to ensure that if new
125        // variants are added in the future, we consciously decide whether they should increment the
126        // clock or not.
127        match self {
128            StartNode(_)
129            | FinishJoin(_)
130            | FinishSplit(_)
131            | FinishLoop(_)
132            | FinishCall(_)
133            | FinishDyn(_)
134            | ResumeBasicBlock {
135                node_id: _,
136                batch_index: _,
137                op_idx_in_batch: _,
138            }
139            | Respan { node_id: _, batch_index: _ }
140            | FinishBasicBlock(_) => true,
141
142            EnterForest { .. } => false,
143        }
144    }
145
146    pub fn exec_node(&self) -> Option<MastNodeId> {
147        match self {
148            Self::StartNode(node_id)
149            | Self::FinishJoin(node_id)
150            | Self::FinishSplit(node_id)
151            | Self::FinishLoop(node_id)
152            | Self::FinishCall(node_id)
153            | Self::FinishDyn(node_id)
154            | Self::ResumeBasicBlock { node_id, .. }
155            | Self::Respan { node_id, .. }
156            | Self::FinishBasicBlock(node_id) => Some(*node_id),
157            Self::EnterForest { .. } => None,
158        }
159    }
160}
161
162// CONTINUATION STACK
163// ================================================================================================
164
165/// [ContinuationStack] reifies the call stack used by the processor when executing a program made
166/// up of possibly multiple MAST forests.
167///
168/// This allows the processor to execute a program iteratively in a loop rather than recursively
169/// traversing the nodes. It also allows the processor to pass the state of execution to another
170/// processor for further processing, which is useful for parallel execution of MAST forests.
171///
172/// Note: the binary wire format for this type is deliberately lossy (`package_debug_info` and
173/// `source_node_ids` are not serialized), so the exact-equality round-trip test generated by
174/// `serde_test` does not apply; `continuation_stack_mast_forest_id_round_trip_omits_debug_metadata`
175/// covers the lossy round trip instead.
176#[derive(Debug, Clone, PartialEq, Eq)]
177pub struct ContinuationStack<F> {
178    stack: Vec<Continuation<F>>,
179    source_node_ids: Option<Vec<Option<DebugSourceNodeId>>>,
180}
181
182impl<F> Default for ContinuationStack<F> {
183    fn default() -> Self {
184        Self { stack: Vec::new(), source_node_ids: None }
185    }
186}
187
188impl<F> ContinuationStack<F> {
189    /// Creates a new continuation stack for a program.
190    ///
191    /// # Arguments
192    /// * `program` - The program whose execution will be managed by this continuation stack
193    pub fn new(program: &Program) -> Self {
194        let mut stack = Vec::with_capacity(CONTINUATION_STACK_SIZE_HINT);
195        stack.push(Continuation::StartNode(program.entrypoint()));
196
197        Self { stack, source_node_ids: None }
198    }
199
200    pub(crate) fn new_with_source_node_id(
201        program: &Program,
202        source_node_id: DebugSourceNodeId,
203    ) -> Self {
204        Self::new_with_optional_source_node_id(program, Some(source_node_id))
205    }
206
207    pub(crate) fn new_with_optional_source_node_id(
208        program: &Program,
209        source_node_id: Option<DebugSourceNodeId>,
210    ) -> Self {
211        let mut stack = Vec::with_capacity(CONTINUATION_STACK_SIZE_HINT);
212        stack.push(Continuation::StartNode(program.entrypoint()));
213
214        let mut source_node_ids = Vec::with_capacity(CONTINUATION_STACK_SIZE_HINT);
215        source_node_ids.push(source_node_id);
216
217        Self {
218            stack,
219            source_node_ids: Some(source_node_ids),
220        }
221    }
222
223    // STATE MUTATORS
224    // --------------------------------------------------------------------------------------------
225
226    /// Pushes a continuation onto the continuation stack.
227    pub fn push_continuation(&mut self, continuation: Continuation<F>) {
228        self.stack.push(continuation);
229        self.push_source_node_id(None);
230    }
231
232    pub(crate) fn push_with_source_node_id(
233        &mut self,
234        continuation: Continuation<F>,
235        source_node_id: Option<DebugSourceNodeId>,
236    ) {
237        self.stack.push(continuation);
238        self.push_source_node_id(source_node_id);
239    }
240
241    /// Pushes a continuation to enter the given MAST forest on the continuation stack.
242    ///
243    /// # Arguments
244    /// * `forest` - The MAST forest to enter
245    pub fn push_enter_forest(&mut self, forest: F) {
246        self.push_enter_forest_with_package_debug_info(forest, None, 0);
247    }
248
249    pub(crate) fn push_enter_forest_with_package_debug_info(
250        &mut self,
251        forest: F,
252        package_debug_info: Option<Arc<PackageDebugInfo>>,
253        inline_context_depth: usize,
254    ) {
255        self.stack.push(Continuation::EnterForest {
256            forest,
257            package_debug_info,
258            inline_context_depth,
259        });
260        self.push_source_node_id(None);
261    }
262
263    /// Pushes a join finish continuation onto the stack.
264    pub fn push_finish_join(&mut self, node_id: MastNodeId) {
265        self.stack.push(Continuation::FinishJoin(node_id));
266        self.push_source_node_id(None);
267    }
268
269    /// Pushes a split finish continuation onto the stack.
270    pub fn push_finish_split(&mut self, node_id: MastNodeId) {
271        self.stack.push(Continuation::FinishSplit(node_id));
272        self.push_source_node_id(None);
273    }
274
275    /// Pushes a loop finish continuation onto the stack.
276    pub fn push_finish_loop(&mut self, node_id: MastNodeId) {
277        self.stack.push(Continuation::FinishLoop(node_id));
278        self.push_source_node_id(None);
279    }
280
281    /// Pushes a call finish continuation onto the stack.
282    pub fn push_finish_call(&mut self, node_id: MastNodeId) {
283        self.stack.push(Continuation::FinishCall(node_id));
284        self.push_source_node_id(None);
285    }
286
287    /// Pushes a dyn finish continuation onto the stack.
288    pub fn push_finish_dyn(&mut self, node_id: MastNodeId) {
289        self.stack.push(Continuation::FinishDyn(node_id));
290        self.push_source_node_id(None);
291    }
292
293    /// Pushes a continuation to start processing the given node.
294    ///
295    /// # Arguments
296    /// * `node_id` - The ID of the node to process
297    pub fn push_start_node(&mut self, node_id: MastNodeId) {
298        self.stack.push(Continuation::StartNode(node_id));
299        self.push_source_node_id(None);
300    }
301
302    /// Pops the next continuation from the continuation stack, and returns it along with its
303    /// associated MAST forest.
304    pub fn pop_continuation(&mut self) -> Option<Continuation<F>> {
305        let continuation = self.stack.pop()?;
306        if let Some(source_node_ids) = &mut self.source_node_ids {
307            source_node_ids.pop();
308        }
309        Some(continuation)
310    }
311
312    pub(crate) fn pop_continuation_with_source_node_id(
313        &mut self,
314    ) -> Option<(Continuation<F>, Option<DebugSourceNodeId>)> {
315        let continuation = self.stack.pop()?;
316        let source_node_id = self.source_node_ids.as_mut().and_then(Vec::pop).flatten();
317        Some((continuation, source_node_id))
318    }
319
320    /// Consumes this stack and returns its continuations in bottom-to-top order (i.e. the order in
321    /// which they were originally pushed).
322    pub fn into_inner(self) -> Vec<Continuation<F>> {
323        self.stack
324    }
325
326    fn push_source_node_id(&mut self, source_node_id: Option<DebugSourceNodeId>) {
327        if let Some(source_node_ids) = &mut self.source_node_ids {
328            source_node_ids.push(source_node_id);
329        }
330    }
331
332    pub(crate) fn start_tracking_source_nodes(
333        &mut self,
334        next_source_node_id: Option<DebugSourceNodeId>,
335    ) {
336        let mut source_node_ids = Vec::with_capacity(self.stack.len());
337        source_node_ids.resize(self.stack.len(), None);
338        if let Some(source_node_id) = source_node_ids.last_mut() {
339            *source_node_id = next_source_node_id;
340        }
341        self.source_node_ids = Some(source_node_ids);
342    }
343
344    // PUBLIC ACCESSORS
345    // --------------------------------------------------------------------------------------------
346
347    /// Returns the number of continuations on the stack.
348    pub fn len(&self) -> usize {
349        self.stack.len()
350    }
351
352    /// Peeks at the next continuation to execute without removing it.
353    ///
354    /// Note that more than one continuation may execute in the same clock cycle. To get all
355    /// continuations that will execute in the next clock cycle, use
356    /// [`Self::iter_continuations_for_next_clock`].
357    pub fn peek_continuation(&self) -> Option<&Continuation<F>> {
358        self.stack.last()
359    }
360
361    pub(crate) fn peek_continuation_with_source_node_id(
362        &self,
363    ) -> Option<(&Continuation<F>, Option<DebugSourceNodeId>)> {
364        let continuation = self.stack.last()?;
365        let source_node_id = self
366            .source_node_ids
367            .as_ref()
368            .and_then(|source_node_ids| source_node_ids.last().copied().flatten());
369        Some((continuation, source_node_id))
370    }
371
372    pub(crate) fn tracks_source_nodes(&self) -> bool {
373        self.source_node_ids.is_some()
374    }
375
376    /// Returns an iterator over the continuations on the stack that will execute in the next clock
377    /// cycle.
378    ///
379    /// This includes all coming continuations up to and including the first continuation that
380    /// increments the clock.
381    ///
382    /// Note: for this iterator to function correctly, it must be the case that executing a
383    /// continuation that doesn't increment the clock *does not* push new continuations on the
384    /// stack. This is currently the case, and is a reasonable invariant to maintain, as
385    /// continuations that don't increment the clock can be expected to be simple (e.g. enter a new
386    /// mast forest).
387    pub fn iter_continuations_for_next_clock(&self) -> impl Iterator<Item = &Continuation<F>> {
388        let mut found_incrementing_cont = false;
389
390        self.stack.iter().rev().take_while(move |continuation| {
391            if found_incrementing_cont {
392                // We have already found the first incrementing continuation, stop here.
393                false
394            } else if continuation.increments_clk() {
395                // This is the first incrementing continuation we have found.
396                found_incrementing_cont = true;
397                true
398            } else {
399                // This continuation does not increment the clock, continue.
400                true
401            }
402        })
403    }
404
405    /// Same as [`Self::iter_continuations_for_next_clock`], but provides the set of source node
406    /// ids for each continuation.
407    pub fn iter_continuations_for_next_clock_with_source_node_ids(
408        &self,
409    ) -> impl Iterator<Item = (&Continuation<F>, Option<DebugSourceNodeId>)> {
410        let mut stack_index = self.stack.len().saturating_sub(1);
411
412        self.iter_continuations_for_next_clock().map(move |cont| {
413            let source_node_id = self
414                .source_node_ids
415                .as_deref()
416                .and_then(|ids| ids.get(stack_index).copied())
417                .flatten();
418            stack_index = stack_index.saturating_sub(1);
419            (cont, source_node_id)
420        })
421    }
422}
423
424impl ContinuationStack<MastForestId> {
425    pub(crate) fn iter_enter_forest_ids(&self) -> impl Iterator<Item = MastForestId> + '_ {
426        self.stack.iter().filter_map(|continuation| match continuation {
427            Continuation::EnterForest { forest, .. } => Some(*forest),
428            _ => None,
429        })
430    }
431}
432
433// SERIALIZATION
434// ================================================================================================
435
436const TAG_START_NODE: u8 = 0;
437const TAG_FINISH_JOIN: u8 = 1;
438const TAG_FINISH_SPLIT: u8 = 2;
439const TAG_FINISH_LOOP: u8 = 3;
440const TAG_FINISH_CALL: u8 = 4;
441const TAG_FINISH_DYN: u8 = 5;
442const TAG_RESUME_BASIC_BLOCK: u8 = 6;
443const TAG_RESPAN: u8 = 7;
444const TAG_FINISH_BASIC_BLOCK: u8 = 8;
445const TAG_ENTER_FOREST: u8 = 9;
446
447// NOTE: `package_debug_info` is deliberately *not* serialized: it is reconstructable from the
448// package a witness was produced from and would bloat the wire with data irrelevant to proving.
449// Round-tripping a `Continuation` (or any struct containing one, such as `ContinuationStack`)
450// therefore does not restore debug info exactly: deserialized continuations always carry
451// `package_debug_info: None`.
452impl Serializable for Continuation<MastForestId> {
453    fn write_into<W: ByteWriter>(&self, target: &mut W) {
454        match self {
455            Self::StartNode(node_id) => {
456                TAG_START_NODE.write_into(target);
457                node_id.write_into(target);
458            },
459            Self::FinishJoin(node_id) => {
460                TAG_FINISH_JOIN.write_into(target);
461                node_id.write_into(target);
462            },
463            Self::FinishSplit(node_id) => {
464                TAG_FINISH_SPLIT.write_into(target);
465                node_id.write_into(target);
466            },
467            Self::FinishLoop(node_id) => {
468                TAG_FINISH_LOOP.write_into(target);
469                node_id.write_into(target);
470            },
471            Self::FinishCall(node_id) => {
472                TAG_FINISH_CALL.write_into(target);
473                node_id.write_into(target);
474            },
475            Self::FinishDyn(node_id) => {
476                TAG_FINISH_DYN.write_into(target);
477                node_id.write_into(target);
478            },
479            Self::ResumeBasicBlock { node_id, batch_index, op_idx_in_batch } => {
480                TAG_RESUME_BASIC_BLOCK.write_into(target);
481                node_id.write_into(target);
482                batch_index.write_into(target);
483                op_idx_in_batch.write_into(target);
484            },
485            Self::Respan { node_id, batch_index } => {
486                TAG_RESPAN.write_into(target);
487                node_id.write_into(target);
488                batch_index.write_into(target);
489            },
490            Self::FinishBasicBlock(node_id) => {
491                TAG_FINISH_BASIC_BLOCK.write_into(target);
492                node_id.write_into(target);
493            },
494            Self::EnterForest {
495                forest,
496                package_debug_info: _,
497                inline_context_depth,
498            } => {
499                TAG_ENTER_FOREST.write_into(target);
500                forest.write_into(target);
501                inline_context_depth.write_into(target);
502            },
503        }
504    }
505}
506
507impl Deserializable for Continuation<MastForestId> {
508    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
509        match u8::read_from(source)? {
510            TAG_START_NODE => Ok(Self::StartNode(MastNodeId::read_from(source)?)),
511            TAG_FINISH_JOIN => Ok(Self::FinishJoin(MastNodeId::read_from(source)?)),
512            TAG_FINISH_SPLIT => Ok(Self::FinishSplit(MastNodeId::read_from(source)?)),
513            TAG_FINISH_LOOP => Ok(Self::FinishLoop(MastNodeId::read_from(source)?)),
514            TAG_FINISH_CALL => Ok(Self::FinishCall(MastNodeId::read_from(source)?)),
515            TAG_FINISH_DYN => Ok(Self::FinishDyn(MastNodeId::read_from(source)?)),
516            TAG_RESUME_BASIC_BLOCK => Ok(Self::ResumeBasicBlock {
517                node_id: MastNodeId::read_from(source)?,
518                batch_index: usize::read_from(source)?,
519                op_idx_in_batch: usize::read_from(source)?,
520            }),
521            TAG_RESPAN => Ok(Self::Respan {
522                node_id: MastNodeId::read_from(source)?,
523                batch_index: usize::read_from(source)?,
524            }),
525            TAG_FINISH_BASIC_BLOCK => Ok(Self::FinishBasicBlock(MastNodeId::read_from(source)?)),
526            TAG_ENTER_FOREST => Ok(Self::EnterForest {
527                forest: MastForestId::read_from(source)?,
528                package_debug_info: None,
529                inline_context_depth: usize::read_from(source)?,
530            }),
531            tag => {
532                Err(DeserializationError::InvalidValue(format!("invalid continuation tag {tag}")))
533            },
534        }
535    }
536}
537
538// `source_node_ids` is deliberately *not* serialized: those indices point into a package's
539// `PackageDebugInfo`, which is itself absent from the wire (see the note above), so they would be
540// dangling references on the deserialized side. A restored stack simply does not track source
541// nodes, matching a non-debug-aware execution, which is all proving requires.
542impl Serializable for ContinuationStack<MastForestId> {
543    fn write_into<W: ByteWriter>(&self, target: &mut W) {
544        self.stack.write_into(target);
545    }
546}
547
548impl Deserializable for ContinuationStack<MastForestId> {
549    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
550        let stack = Vec::<Continuation<MastForestId>>::read_from(source)?;
551        Ok(Self { stack, source_node_ids: None })
552    }
553}
554
555// TESTS
556// ================================================================================================
557
558#[cfg(test)]
559mod tests {
560    use alloc::sync::Arc;
561
562    use miden_core::mast::MastForest;
563    use miden_mast_package::debug_info::{
564        DebugFunctionIdx, DebugLocIdx, DebugSourceInlineCall, DebugSourceNode,
565        PackageDebugInfoBuilder,
566    };
567
568    use super::*;
569
570    #[test]
571    fn get_next_clock_cycle_increment_empty_stack() {
572        let stack: ContinuationStack<Arc<MastForest>> = ContinuationStack::default();
573        assert!(stack.iter_continuations_for_next_clock().next().is_none());
574    }
575
576    #[test]
577    fn get_next_clock_cycle_increment_ends_with_incrementing() {
578        let mut stack: ContinuationStack<Arc<MastForest>> = ContinuationStack::default();
579        // Push a continuation that increments the clock
580        stack.push_continuation(Continuation::StartNode(MastNodeId::new_unchecked(0)));
581
582        let result: Vec<_> = stack.iter_continuations_for_next_clock().collect();
583        assert_eq!(result.len(), 1);
584        assert!(matches!(result[0], Continuation::StartNode(_)));
585    }
586
587    #[test]
588    fn get_next_clock_cycle_increment_enter_forest_after_incrementing() {
589        let mut stack: ContinuationStack<Arc<MastForest>> = ContinuationStack::default();
590        // Push an incrementing continuation first (bottom of stack)
591        stack.push_continuation(Continuation::StartNode(MastNodeId::new_unchecked(0)));
592        // Push a non-incrementing continuation on top
593        stack.push_continuation(Continuation::EnterForest {
594            forest: Arc::new(MastForest::new()),
595            package_debug_info: None,
596            inline_context_depth: 0,
597        });
598
599        let result: Vec<_> = stack.iter_continuations_for_next_clock().collect();
600        // Should return: EnterForest (non-incrementing), then StartNode (first incrementing)
601        assert_eq!(result.len(), 2);
602        assert!(matches!(result[0], Continuation::EnterForest { .. }));
603        assert!(matches!(result[1], Continuation::StartNode(_)));
604    }
605
606    #[test]
607    fn get_next_clock_cycle_increment_multiple_enter_forest_after_incrementing() {
608        let mut stack: ContinuationStack<Arc<MastForest>> = ContinuationStack::default();
609        // Push an incrementing continuation first (bottom of stack)
610        stack.push_continuation(Continuation::StartNode(MastNodeId::new_unchecked(0)));
611        // Push two non-incrementing continuations on top
612        stack.push_continuation(Continuation::EnterForest {
613            forest: Arc::new(MastForest::new()),
614            package_debug_info: None,
615            inline_context_depth: 0,
616        });
617        stack.push_continuation(Continuation::EnterForest {
618            forest: Arc::new(MastForest::new()),
619            package_debug_info: None,
620            inline_context_depth: 0,
621        });
622
623        let result: Vec<_> = stack.iter_continuations_for_next_clock().collect();
624        // Should return: EnterForest, EnterForest, StartNode
625        assert_eq!(result.len(), 3);
626        assert!(matches!(result[0], Continuation::EnterForest { .. }));
627        assert!(matches!(result[1], Continuation::EnterForest { .. }));
628        assert!(matches!(result[2], Continuation::StartNode(_)));
629    }
630
631    #[test]
632    fn inline_call_context_uses_the_source_boundary_index() {
633        let mut builder = PackageDebugInfoBuilder::default();
634        let source_node_id = builder
635            .add_node(DebugSourceNode {
636                exec_node: MastNodeId::new_unchecked(0),
637                children: Vec::new(),
638                op_start: 7,
639                op_end: 7,
640                asm_ops: Vec::new(),
641                debug_vars: Vec::new(),
642                inline_calls: vec![DebugSourceInlineCall {
643                    op_idx: 7,
644                    callee_idx: DebugFunctionIdx::from(0),
645                    loc_idx: DebugLocIdx::from(0),
646                }],
647            })
648            .unwrap();
649        let debug_info = Arc::from(builder.build());
650
651        let context =
652            SourceInlineCallContext::for_source_boundary(debug_info, Some(source_node_id))
653                .expect("boundary row should create inherited inline context");
654
655        assert_eq!(context.inline_calls().map(|row| row.op_idx).collect::<Vec<_>>(), [7]);
656    }
657
658    #[test]
659    fn continuation_stack_mast_forest_id_round_trip_omits_debug_metadata() {
660        let mut stack: ContinuationStack<MastForestId> = ContinuationStack::default();
661        stack.push_continuation(Continuation::StartNode(MastNodeId::from(1)));
662        stack.push_continuation(Continuation::EnterForest {
663            forest: MastForestId::from(2),
664            package_debug_info: None,
665            inline_context_depth: 0,
666        });
667        stack.push_continuation(Continuation::ResumeBasicBlock {
668            node_id: MastNodeId::from(3),
669            batch_index: 4,
670            op_idx_in_batch: 5,
671        });
672        stack.source_node_ids =
673            Some(vec![Some(DebugSourceNodeId::from(10)), None, Some(DebugSourceNodeId::from(11))]);
674
675        let bytes = stack.to_bytes();
676        let restored = ContinuationStack::<MastForestId>::read_from_bytes(&bytes).unwrap();
677
678        assert_eq!(restored.stack.len(), 3);
679        assert!(matches!(
680            restored.stack[0],
681            Continuation::StartNode(node_id) if node_id == MastNodeId::from(1)
682        ));
683        assert!(matches!(
684            restored.stack[1],
685            Continuation::EnterForest {
686                forest,
687                package_debug_info: None,
688                inline_context_depth: 0,
689            } if forest == MastForestId::from(2)
690        ));
691        assert!(matches!(
692            restored.stack[2],
693            Continuation::ResumeBasicBlock {
694                node_id,
695                batch_index: 4,
696                op_idx_in_batch: 5,
697            } if node_id == MastNodeId::from(3)
698        ));
699        // source_node_ids is not serialized: indices without their owning package debug info
700        // would be dangling on the restored side.
701        assert_eq!(restored.source_node_ids, None);
702    }
703}