Skip to main content

clash_brush_core/
callstack.rs

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