Skip to main content

shrub_core/
callstack.rs

1//! Call stack representations.
2
3use std::borrow::Cow;
4use std::collections::{HashSet, VecDeque};
5use std::sync::Arc;
6
7use shrub_parser::ast::SourceLocation;
8
9use crate::{functions, traps};
10
11/// Encapsulates info regarding a script call.
12#[derive(Clone, Debug)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14pub struct ScriptCall {
15    /// The type of script call.
16    pub call_type: ScriptCallType,
17    /// The source info for the script called.
18    pub source_info: crate::SourceInfo,
19}
20
21impl ScriptCall {
22    /// Returns the name of the script that was called.
23    pub fn name(&self) -> Cow<'_, str> {
24        self.source_info.source.as_str().into()
25    }
26}
27
28/// The type of script call.
29#[derive(Clone, Copy, Debug)]
30#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
31pub enum ScriptCallType {
32    /// A script was sourced.
33    Source,
34    /// A script was executed.
35    Run,
36}
37
38impl std::fmt::Display for ScriptCall {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        match self.call_type {
41            ScriptCallType::Source => write!(f, "source({})", self.source_info),
42            ScriptCallType::Run => write!(f, "script({})", self.source_info),
43        }
44    }
45}
46
47/// Represents the type of a frame, indicating how it was invoked from
48/// a different source context.
49#[derive(Clone, Debug)]
50#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
51pub enum FrameType {
52    /// A script was called (sourced or executed).
53    Script(ScriptCall),
54    /// A function was called.
55    Function(FunctionCall),
56    /// A trap handler was invoked.
57    TrapHandler(traps::TrapSignal),
58    /// A string was eval'd.
59    Eval,
60    /// A command-line string (i.e., -c) was executed.
61    CommandString,
62    /// An interactive command session was started.
63    InteractiveSession,
64}
65
66impl FrameType {
67    /// Returns a name for the frame (i.e., script path or function name).
68    pub fn name(&self) -> Cow<'_, str> {
69        match self {
70            Self::Script(call) => call.name(),
71            Self::Function(call) => call.name(),
72            Self::TrapHandler(_) => "trap".into(),
73            Self::Eval => "eval".into(),
74            Self::CommandString => "-c".into(),
75            Self::InteractiveSession => "interactive".into(),
76        }
77    }
78
79    /// Returns `true` if the frame is for a function call.
80    pub const fn is_function(&self) -> bool {
81        matches!(self, Self::Function(..))
82    }
83
84    /// Returns `true` if the frame is for a script call.
85    pub const fn is_script(&self) -> bool {
86        matches!(self, Self::Script(..))
87    }
88
89    /// Returns `true` if the frame is for a trap handler.
90    pub const fn is_trap_handler(&self) -> bool {
91        matches!(self, Self::TrapHandler(_))
92    }
93
94    /// Returns `true` if the frame is for an interactive session.
95    pub const fn is_interactive_session(&self) -> bool {
96        matches!(self, Self::InteractiveSession)
97    }
98
99    /// Returns `true` if the frame is for a command string being executed.
100    pub const fn is_command_string(&self) -> bool {
101        matches!(self, Self::CommandString)
102    }
103
104    /// Returns `true` if the frame is for a sourced script.
105    pub const fn is_sourced_script(&self) -> bool {
106        matches!(self, Self::Script(call) if matches!(call.call_type, ScriptCallType::Source))
107    }
108
109    /// Returns `true` if the frame is for a run script.
110    pub const fn is_run_script(&self) -> bool {
111        matches!(self, Self::Script(call) if matches!(call.call_type, ScriptCallType::Run))
112    }
113}
114
115impl std::fmt::Display for FrameType {
116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117        match self {
118            Self::Script(call) => call.fmt(f),
119            Self::Function(call) => call.fmt(f),
120            Self::TrapHandler(_) => write!(f, "trap"),
121            Self::Eval => write!(f, "eval"),
122            Self::CommandString => write!(f, "-c"),
123            Self::InteractiveSession => write!(f, "interactive"),
124        }
125    }
126}
127
128/// Describes the target of a function call.
129#[derive(Clone, Debug)]
130#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
131pub struct FunctionCall {
132    /// The name of the function invoked.
133    pub function_name: String,
134    /// The invoked function.
135    pub function: functions::Registration,
136}
137
138impl FunctionCall {
139    /// Returns the name of the function that was called.
140    pub fn name(&self) -> Cow<'_, str> {
141        self.function_name.as_str().into()
142    }
143}
144
145impl std::fmt::Display for FunctionCall {
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        write!(f, "func({})", self.function_name)
148    }
149}
150
151/// Represents a single frame in a `CallStack`.
152#[derive(Clone, Debug)]
153#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
154pub struct Frame {
155    /// The type of frame.
156    pub frame_type: FrameType,
157    /// The source information for the frame. The locations associated with AST
158    /// nodes executed in this frame should be interpreted as being relative
159    /// to this source info.
160    pub source_info: crate::SourceInfo,
161    /// The location of the entry point into this frame, within the frame of
162    /// reference of `source_info`. May be `None` if the entry point is not
163    /// known.
164    pub entry: Option<Arc<crate::SourcePosition>>,
165    /// Information about the currently executing location. For the topmost
166    /// frame on the stack, this represents the current execution location.
167    /// For older frames, this represents the site from which a control
168    /// transfer was made to the next younger frame. May be `None` if the
169    /// current location is not known. When present, it is relative to the
170    /// frame of reference of `source_info`.
171    pub current: Option<Arc<crate::SourcePosition>>,
172    /// Positional arguments (not including $0). May not be present for all
173    /// frames.
174    pub args: Vec<String>,
175    /// Optionally, indicates an additional line offset within the current
176    /// source context.
177    pub current_line_offset: usize,
178}
179
180impl Frame {
181    /// Returns the adjusted source info for this frame, combining the
182    /// frame's `source_info` and `current_line_offset`, if present.
183    pub fn adjusted_source_info(&self) -> crate::SourceInfo {
184        self.pos_as_source_info(None)
185    }
186
187    /// Returns the current position as a new `SourceInfo`, combining the
188    /// frame's `source_info` and `current` position.
189    pub fn current_pos_as_source_info(&self) -> crate::SourceInfo {
190        self.pos_as_source_info(self.current.as_ref())
191    }
192
193    fn pos_as_source_info(&self, pos: Option<&Arc<crate::SourcePosition>>) -> crate::SourceInfo {
194        let mut new_start = if let Some(existing_start) = &self.source_info.start {
195            if let Some(current) = pos {
196                Some(Arc::new(crate::SourcePosition {
197                    index: existing_start.index + current.index,
198                    line: existing_start.line + (current.line - 1),
199                    column: if current.line <= 1 {
200                        existing_start.column + (current.column - 1)
201                    } else {
202                        current.column
203                    },
204                }))
205            } else {
206                Some(existing_start.clone())
207            }
208        } else {
209            pos.cloned()
210        };
211
212        if self.current_line_offset > 0 {
213            new_start = if let Some(new_start) = new_start {
214                let mut pos = (*new_start).clone();
215                pos.line += self.current_line_offset;
216
217                Some(Arc::new(pos))
218            } else {
219                Some(Arc::new(crate::SourcePosition {
220                    index: 0,
221                    line: self.current_line_offset + 1,
222                    column: 1,
223                }))
224            };
225        }
226
227        crate::SourceInfo {
228            source: self.source_info.source.clone(),
229            start: new_start,
230        }
231    }
232
233    /// Returns the current line number.
234    pub fn current_line(&self) -> Option<usize> {
235        let start_line = self.source_info.start.as_ref().map_or(1, |pos| pos.line);
236        let current_line = self.current.as_ref().map(|pos| pos.line)?;
237
238        Some(start_line.saturating_sub(1) + current_line + self.current_line_offset)
239    }
240
241    /// Returns the current line number, relative to the frame's entry.
242    pub fn current_frame_relative_line(&self) -> Option<usize> {
243        let current_line = self.current.as_ref().map(|pos| pos.line)?;
244        let entry_line = self.entry.as_ref().map_or(1, |pos| pos.line);
245
246        Some(current_line.saturating_sub(entry_line) + self.current_line_offset + 1)
247    }
248}
249
250/// Options for formatting a call stack.
251#[derive(Default)]
252pub struct FormatOptions {
253    /// Whether or not to show args.
254    pub show_args: bool,
255    /// Whether or not to show frame entry points.
256    pub show_entry_points: bool,
257}
258
259/// Helper struct for formatting a call stack with custom options.
260///
261/// This struct implements `Display` and can be used to write a formatted
262/// call stack to any type that implements `io::Write`.
263pub struct FormatCallStack<'a> {
264    stack: &'a CallStack,
265    options: &'a FormatOptions,
266}
267
268impl<'a> FormatCallStack<'a> {
269    /// Creates a new formatter for the given call stack with the specified
270    /// options.
271    ///
272    /// # Arguments
273    ///
274    /// * `stack` - The call stack to format.
275    /// * `options` - The formatting options to use.
276    pub const fn new(stack: &'a CallStack, options: &'a FormatOptions) -> Self {
277        Self { stack, options }
278    }
279}
280
281impl std::fmt::Display for FormatCallStack<'_> {
282    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283        self.stack.fmt_with_options(f, self.options)
284    }
285}
286
287/// Encapsulates a script call stack.
288#[derive(Clone, Debug, Default)]
289#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
290pub struct CallStack {
291    frames: VecDeque<Frame>,
292    func_call_depth: usize,
293    script_source_depth: usize,
294    active_trap_signals: HashSet<traps::TrapSignal>,
295    trap_delivery_suppress_count: usize,
296}
297
298impl CallStack {
299    /// Creates a formatter for this call stack with the given options.
300    ///
301    /// # Arguments
302    ///
303    /// * `options` - The formatting options to use.
304    pub const fn format<'a>(&'a self, options: &'a FormatOptions) -> FormatCallStack<'a> {
305        FormatCallStack::new(self, options)
306    }
307
308    /// Formats the call stack with the given options.
309    ///
310    /// # Arguments
311    ///
312    /// * `f` - The formatter to write to.
313    /// * `options` - The formatting options.
314    fn fmt_with_options(
315        &self,
316        f: &mut std::fmt::Formatter<'_>,
317        options: &FormatOptions,
318    ) -> std::fmt::Result {
319        if self.is_empty() {
320            return Ok(());
321        }
322
323        color_print::cwriteln!(f, "<underline>Call stack (most recent first):</underline>")?;
324
325        for (index, frame) in self.iter().enumerate() {
326            let si = frame.current_pos_as_source_info();
327
328            color_print::cwrite!(
329                f,
330                "   <dim>#{index}</dim><yellow>|</yellow> <strong>{}</strong>",
331                si.source
332            )?;
333
334            if let Some(pos) = &si.start {
335                color_print::cwrite!(f, ":<cyan>{}</cyan>,<cyan>{}</cyan>", pos.line, pos.column)?;
336            }
337
338            color_print::cwrite!(f, " (<dim>{}</dim>", frame.frame_type)?;
339
340            if options.show_entry_points {
341                if let Some(entry) = &frame.entry {
342                    let entry_si = frame.pos_as_source_info(Some(entry));
343                    if let Some(entry_start) = &entry_si.start {
344                        color_print::cwrite!(
345                            f,
346                            " <dim>entered at {}:{}</dim>",
347                            entry_si.source,
348                            entry_start
349                        )?;
350                    }
351                }
352            }
353
354            color_print::cwriteln!(f, ")")?;
355
356            if !frame.args.is_empty() && options.show_args {
357                for (i, arg) in frame.args.iter().enumerate() {
358                    color_print::cwriteln!(
359                        f,
360                        "     <yellow>${}</yellow>: <blue>{}</blue>",
361                        i + 1,
362                        arg
363                    )?;
364                }
365            }
366        }
367
368        Ok(())
369    }
370}
371
372impl std::fmt::Display for CallStack {
373    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
374        self.fmt_with_options(f, &FormatOptions::default())
375    }
376}
377
378impl std::ops::Index<usize> for CallStack {
379    type Output = Frame;
380
381    fn index(&self, index: usize) -> &Self::Output {
382        &self.frames[index]
383    }
384}
385
386impl CallStack {
387    /// Creates a new empty script call stack.
388    pub fn new() -> Self {
389        Self::default()
390    }
391
392    /// Removes the top from from the stack. If the stack is empty, does nothing
393    /// and returns `None`; otherwise, returns the removed call frame.
394    pub fn pop(&mut self) -> Option<Frame> {
395        let frame = self.frames.pop_front()?;
396
397        if frame.frame_type.is_function() {
398            self.func_call_depth = self.func_call_depth.saturating_sub(1);
399        }
400
401        if frame.frame_type.is_sourced_script() {
402            self.script_source_depth = self.script_source_depth.saturating_sub(1);
403        }
404
405        if let FrameType::TrapHandler(signal) = &frame.frame_type {
406            self.active_trap_signals.remove(signal);
407        }
408
409        Some(frame)
410    }
411
412    /// Returns a reference to the current (topmost) call frame in the stack.
413    /// Returns `None` if the stack is empty.
414    pub fn current_frame(&self) -> Option<&Frame> {
415        self.frames.front()
416    }
417
418    /// Returns the position in the current (topmost) call frame in the stack,
419    /// expressed as a new `SourceInfo`. Note that this may not be identical
420    /// to that frame's `SourceInfo` since it may include an offset representing
421    /// the current execution position within that source.
422    pub fn current_pos_as_source_info(&self) -> crate::SourceInfo {
423        let Some(frame) = self.frames.front() else {
424            return crate::SourceInfo::default();
425        };
426
427        frame.current_pos_as_source_info()
428    }
429
430    /// Updates the currently executing position in the top stack frame.
431    pub fn set_current_pos(&mut self, position: Option<Arc<crate::SourcePosition>>) {
432        if let Some(frame) = self.frames.front_mut() {
433            frame.current = position;
434        }
435    }
436
437    /// Increments the current line offset in the top stack frame by the given
438    /// delta.
439    ///
440    /// # Arguments
441    ///
442    /// * `delta` - The number of lines to increment the current line offset by.
443    pub(crate) fn increment_current_line_offset(&mut self, delta: usize) {
444        let Some(frame) = self.frames.front_mut() else {
445            return;
446        };
447
448        frame.current_line_offset += delta;
449    }
450
451    /// Pushes a new script call frame onto the stack.
452    ///
453    /// # Arguments
454    ///
455    /// * `call_type` - The type of script call (sourced or executed).
456    /// * `source_info` - The source of the script.
457    /// * `args` - The positional arguments for the script call.
458    pub fn push_script(
459        &mut self,
460        call_type: ScriptCallType,
461        source_info: &crate::SourceInfo,
462        args: impl IntoIterator<Item = String>,
463    ) {
464        self.frames.push_front(Frame {
465            frame_type: FrameType::Script(ScriptCall {
466                call_type,
467                source_info: source_info.to_owned(),
468            }),
469            args: args.into_iter().collect(),
470            source_info: source_info.to_owned(),
471            current_line_offset: 0,
472            current: None, // TODO(source-info): fill this out
473            entry: None,   // TODO(source-info): fill this out
474        });
475
476        if matches!(call_type, ScriptCallType::Source) {
477            self.script_source_depth += 1;
478        }
479    }
480
481    /// Pushes a new trap handler frame onto the stack.
482    ///
483    /// # Arguments
484    ///
485    /// * `signal` - The signal being handled.
486    /// * `handler` - The trap handler being invoked, if any.
487    pub fn push_trap_handler(
488        &mut self,
489        signal: traps::TrapSignal,
490        handler: Option<&traps::TrapHandler>,
491    ) {
492        let source_info =
493            handler.map_or_else(crate::SourceInfo::default, |h| h.source_info.clone());
494
495        self.frames.push_front(Frame {
496            frame_type: FrameType::TrapHandler(signal),
497            args: vec![],
498            source_info,
499            current_line_offset: 0,
500            current: None, // TODO(source-info): fill this out
501            entry: None,   // TODO(source-info): fill this out
502        });
503
504        self.active_trap_signals.insert(signal);
505    }
506
507    /// Pushes a new eval frame onto the stack.
508    pub fn push_eval(&mut self) {
509        self.frames.push_front(Frame {
510            frame_type: FrameType::Eval,
511            args: vec![],
512            source_info: crate::SourceInfo::from("eval"), // TODO(source-info): fill this out
513            current_line_offset: 0,
514            current: None, // TODO(source-info): fill this out
515            entry: None,   // TODO(source-info): fill this out
516        });
517    }
518
519    /// Pushes a new command string frame onto the stack.
520    pub fn push_command_string(&mut self) {
521        self.frames.push_front(Frame {
522            frame_type: FrameType::CommandString,
523            args: vec![],
524            source_info: crate::SourceInfo::from("environment"),
525            current_line_offset: 0,
526            current: None, // TODO(source-info): fill this out
527            entry: None,   // TODO(source-info): fill this out
528        });
529    }
530
531    /// Pushes a new interactive session frame onto the stack.
532    pub fn push_interactive_session(&mut self) {
533        self.frames.push_front(Frame {
534            frame_type: FrameType::InteractiveSession,
535            args: vec![],
536            current_line_offset: 0,
537            source_info: crate::SourceInfo::from("main"),
538            current: None, // TODO(source-info): fill this out
539            entry: None,   // TODO(source-info): fill this out
540        });
541    }
542
543    /// Pushes a new function call frame onto the stack.
544    ///
545    /// # Arguments
546    ///
547    /// * `name` - The name of the function being called.
548    /// * `function` - The function being called.
549    /// * `args` - The positional arguments for the function call.
550    pub fn push_function(
551        &mut self,
552        name: impl Into<String>,
553        function: &functions::Registration,
554        args: impl IntoIterator<Item = String>,
555    ) {
556        self.frames.push_front(Frame {
557            frame_type: FrameType::Function(FunctionCall {
558                function_name: name.into(),
559                function: function.to_owned(),
560            }),
561            args: args.into_iter().collect(),
562            source_info: function.source().clone(),
563            entry: function.definition().location().map(|span| span.start),
564            current: None, // TODO(source-info): fill this out
565            current_line_offset: 0,
566        });
567
568        self.func_call_depth += 1;
569    }
570
571    /// Iterates through the function calls on the stack.
572    pub fn iter_function_calls(&self) -> impl Iterator<Item = &FunctionCall> {
573        self.iter().filter_map(|frame| {
574            if let FrameType::Function(call) = &frame.frame_type {
575                Some(call)
576            } else {
577                None
578            }
579        })
580    }
581
582    /// Iterates through the script calls on the stack.
583    pub fn iter_script_calls(&self) -> impl Iterator<Item = &ScriptCall> {
584        self.iter().filter_map(|frame| {
585            if let FrameType::Script(call) = &frame.frame_type {
586                Some(call)
587            } else {
588                None
589            }
590        })
591    }
592
593    /// Returns whether or not the current script stack frame is a sourced
594    /// script.
595    pub fn in_sourced_script(&self) -> bool {
596        self.iter_script_calls()
597            .next()
598            .is_some_and(|call| matches!(call.call_type, ScriptCallType::Source))
599    }
600
601    /// Returns the current depth of function calls in the call stack.
602    pub const fn function_call_depth(&self) -> usize {
603        self.func_call_depth
604    }
605
606    /// Returns the current depth of sourced script calls in the call stack.
607    pub const fn script_source_depth(&self) -> usize {
608        self.script_source_depth
609    }
610
611    /// Returns whether the given trap signal is currently being handled
612    /// (i.e., there is an active frame on the stack for this signal).
613    pub fn is_trap_signal_active(&self, signal: traps::TrapSignal) -> bool {
614        self.active_trap_signals.contains(&signal)
615    }
616
617    /// Clears the set of active trap signals. This should be called when
618    /// creating subshells so they start with fresh trap execution state
619    /// independent of the parent shell's currently-executing traps.
620    pub fn clear_active_trap_signals(&mut self) {
621        self.active_trap_signals.clear();
622    }
623
624    /// Returns whether the given trap signal is currently suppressed.
625    pub const fn is_trap_delivery_suppressed(&self) -> bool {
626        self.trap_delivery_suppress_count > 0
627    }
628
629    /// Acquires a block on trap delivery, preventing traps from being delivered
630    /// until the block is released. Multiple blocks may be acquired, and
631    /// trap delivery will remain suppressed until all blocks have been
632    /// released.
633    pub const fn acquire_trap_delivery_block(&mut self) {
634        self.trap_delivery_suppress_count += 1;
635    }
636
637    /// Releases a block on trap delivery; note that trap delivery will remain
638    /// suppressed until all blocks have been released.
639    pub const fn release_trap_delivery_block(&mut self) {
640        self.trap_delivery_suppress_count = self.trap_delivery_suppress_count.saturating_sub(1);
641    }
642
643    /// Returns whether or not the shell is actively executing in a shell
644    /// function.
645    pub fn in_function(&self) -> bool {
646        self.iter_function_calls().next().is_some()
647    }
648
649    /// Returns the current depth of the call stack.
650    pub fn depth(&self) -> usize {
651        self.frames.len()
652    }
653
654    /// Returns whether or not the call stack is empty.
655    pub fn is_empty(&self) -> bool {
656        self.frames.is_empty()
657    }
658
659    /// Returns an iterator over the call frames, starting from the most
660    /// recent.
661    pub fn iter(&self) -> impl Iterator<Item = &Frame> {
662        self.frames.iter()
663    }
664
665    /// Returns a mutable iterator over the call frames, starting from the most
666    /// recent.
667    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Frame> {
668        self.frames.iter_mut()
669    }
670}
671
672#[cfg(test)]
673mod tests {
674    use std::path::PathBuf;
675
676    use pretty_assertions::assert_matches;
677
678    use super::*;
679    use crate::SourceInfo;
680
681    #[test]
682    fn test_call_stack_new() {
683        let stack = CallStack::new();
684        assert!(stack.is_empty());
685        assert_eq!(stack.depth(), 0);
686    }
687
688    #[test]
689    fn test_call_stack_default() {
690        let stack = CallStack::default();
691        assert!(stack.is_empty());
692        assert_eq!(stack.depth(), 0);
693    }
694
695    #[test]
696    fn test_call_stack_push_pop() {
697        let mut stack = CallStack::new();
698
699        stack.push_script(
700            ScriptCallType::Source,
701            &SourceInfo::from(PathBuf::from("script1.sh")),
702            vec![],
703        );
704        assert!(!stack.is_empty());
705        assert_eq!(stack.depth(), 1);
706
707        stack.push_script(
708            ScriptCallType::Run,
709            &SourceInfo::from(PathBuf::from("script2.sh")),
710            vec![],
711        );
712        assert_eq!(stack.depth(), 2);
713
714        let frame = stack.pop().unwrap();
715        assert_matches!(
716            frame.frame_type,
717            FrameType::Script(ScriptCall {
718                call_type: ScriptCallType::Run,
719                source_info: SourceInfo {
720                    source: file_path,
721                    ..
722                },
723            }) if &file_path == "script2.sh"
724        );
725        assert_eq!(stack.depth(), 1);
726
727        let frame = stack.pop().unwrap();
728        assert_matches!(
729            frame.frame_type,
730            FrameType::Script(ScriptCall {
731                call_type: ScriptCallType::Source,
732                source_info: SourceInfo {
733                    source: file_path,
734                    ..
735                },
736            }) if &file_path == "script1.sh"
737        );
738        assert_eq!(stack.depth(), 0);
739        assert!(stack.is_empty());
740    }
741
742    #[test]
743    fn test_call_stack_pop_empty() {
744        let mut stack = CallStack::new();
745        assert!(stack.pop().is_none());
746    }
747
748    #[test]
749    fn test_in_sourced_script() {
750        let mut stack = CallStack::new();
751        assert!(!stack.in_sourced_script());
752
753        stack.push_script(
754            ScriptCallType::Run,
755            &SourceInfo::from(PathBuf::from("script1.sh")),
756            vec![],
757        );
758        assert!(!stack.in_sourced_script());
759
760        stack.push_script(
761            ScriptCallType::Source,
762            &SourceInfo::from(PathBuf::from("script2.sh")),
763            vec![],
764        );
765        assert!(stack.in_sourced_script());
766
767        stack.pop();
768        assert!(!stack.in_sourced_script());
769    }
770
771    #[test]
772    fn test_call_stack_iter() {
773        let mut stack = CallStack::new();
774        stack.push_script(
775            ScriptCallType::Source,
776            &SourceInfo::from(PathBuf::from("script1.sh")),
777            vec![],
778        );
779        stack.push_script(
780            ScriptCallType::Run,
781            &SourceInfo::from(PathBuf::from("script2.sh")),
782            vec![],
783        );
784        stack.push_script(
785            ScriptCallType::Source,
786            &SourceInfo::from(PathBuf::from("script3.sh")),
787            vec![],
788        );
789
790        let frames: Vec<_> = stack.iter().collect();
791        assert_eq!(frames.len(), 3);
792        assert_matches!(&frames[0].frame_type, FrameType::Script(ScriptCall { source_info: SourceInfo { source: file_path, .. }, .. }) if file_path == "script3.sh");
793        assert_matches!(&frames[1].frame_type, FrameType::Script(ScriptCall { source_info: SourceInfo { source: file_path, .. }, .. }) if file_path == "script2.sh");
794        assert_matches!(&frames[2].frame_type, FrameType::Script(ScriptCall { source_info: SourceInfo { source: file_path, .. }, .. }) if file_path == "script1.sh");
795    }
796}