1use crate::{functions, traps};
4use std::{
5 borrow::Cow,
6 collections::{HashSet, VecDeque},
7 sync::Arc,
8};
9
10use brush_parser::ast::SourceLocation;
11
12#[derive(Clone, Debug)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15pub struct ScriptCall {
16 pub call_type: ScriptCallType,
18 pub source_info: crate::SourceInfo,
20}
21
22impl ScriptCall {
23 pub fn name(&self) -> Cow<'_, str> {
25 self.source_info.source.as_str().into()
26 }
27}
28
29#[derive(Clone, Copy, Debug)]
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32pub enum ScriptCallType {
33 Source,
35 Run,
37}
38
39impl std::fmt::Display for ScriptCall {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 match self.call_type {
42 ScriptCallType::Source => write!(f, "source({})", self.source_info),
43 ScriptCallType::Run => write!(f, "script({})", self.source_info),
44 }
45 }
46}
47
48#[derive(Clone, Debug)]
51#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
52pub enum FrameType {
53 Script(ScriptCall),
55 Function(FunctionCall),
57 TrapHandler(traps::TrapSignal),
59 Eval,
61 CommandString,
63 InteractiveSession,
65}
66
67impl FrameType {
68 pub fn name(&self) -> Cow<'_, str> {
70 match self {
71 Self::Script(call) => call.name(),
72 Self::Function(call) => call.name(),
73 Self::TrapHandler(_) => "trap".into(),
74 Self::Eval => "eval".into(),
75 Self::CommandString => "-c".into(),
76 Self::InteractiveSession => "interactive".into(),
77 }
78 }
79
80 pub const fn is_function(&self) -> bool {
82 matches!(self, Self::Function(..))
83 }
84
85 pub const fn is_script(&self) -> bool {
87 matches!(self, Self::Script(..))
88 }
89
90 pub const fn is_trap_handler(&self) -> bool {
92 matches!(self, Self::TrapHandler(_))
93 }
94
95 pub const fn is_interactive_session(&self) -> bool {
97 matches!(self, Self::InteractiveSession)
98 }
99
100 pub const fn is_command_string(&self) -> bool {
102 matches!(self, Self::CommandString)
103 }
104
105 pub const fn is_sourced_script(&self) -> bool {
107 matches!(self, Self::Script(call) if matches!(call.call_type, ScriptCallType::Source))
108 }
109
110 pub const fn is_run_script(&self) -> bool {
112 matches!(self, Self::Script(call) if matches!(call.call_type, ScriptCallType::Run))
113 }
114}
115
116impl std::fmt::Display for FrameType {
117 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 match self {
119 Self::Script(call) => call.fmt(f),
120 Self::Function(call) => call.fmt(f),
121 Self::TrapHandler(_) => write!(f, "trap"),
122 Self::Eval => write!(f, "eval"),
123 Self::CommandString => write!(f, "-c"),
124 Self::InteractiveSession => write!(f, "interactive"),
125 }
126 }
127}
128
129#[derive(Clone, Debug)]
131#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
132pub struct FunctionCall {
133 pub function_name: String,
135 pub function: functions::Registration,
137}
138
139impl FunctionCall {
140 pub fn name(&self) -> Cow<'_, str> {
142 self.function_name.as_str().into()
143 }
144}
145
146impl std::fmt::Display for FunctionCall {
147 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148 write!(f, "func({})", self.function_name)
149 }
150}
151
152#[derive(Clone, Debug)]
154#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
155pub struct Frame {
156 pub frame_type: FrameType,
158 pub source_info: crate::SourceInfo,
162 pub entry: Option<Arc<crate::SourcePosition>>,
165 pub current: Option<Arc<crate::SourcePosition>>,
171 pub args: Vec<String>,
173 pub current_line_offset: usize,
175}
176
177impl Frame {
178 pub fn adjusted_source_info(&self) -> crate::SourceInfo {
181 self.pos_as_source_info(None)
182 }
183
184 pub fn current_pos_as_source_info(&self) -> crate::SourceInfo {
187 self.pos_as_source_info(self.current.as_ref())
188 }
189
190 fn pos_as_source_info(&self, pos: Option<&Arc<crate::SourcePosition>>) -> crate::SourceInfo {
191 let mut new_start = if let Some(existing_start) = &self.source_info.start {
192 if let Some(current) = pos {
193 Some(Arc::new(crate::SourcePosition {
194 index: existing_start.index + current.index,
195 line: existing_start.line + (current.line - 1),
196 column: if current.line <= 1 {
197 existing_start.column + (current.column - 1)
198 } else {
199 current.column
200 },
201 }))
202 } else {
203 Some(existing_start.clone())
204 }
205 } else {
206 pos.cloned()
207 };
208
209 if self.current_line_offset > 0 {
210 new_start = if let Some(new_start) = new_start {
211 let mut pos = (*new_start).clone();
212 pos.line += self.current_line_offset;
213
214 Some(Arc::new(pos))
215 } else {
216 Some(Arc::new(crate::SourcePosition {
217 index: 0,
218 line: self.current_line_offset + 1,
219 column: 1,
220 }))
221 };
222 }
223
224 crate::SourceInfo {
225 source: self.source_info.source.clone(),
226 start: new_start,
227 }
228 }
229
230 pub fn current_line(&self) -> Option<usize> {
232 let start_line = self.source_info.start.as_ref().map_or(1, |pos| pos.line);
233 let current_line = self.current.as_ref().map(|pos| pos.line)?;
234
235 Some(start_line.saturating_sub(1) + current_line + self.current_line_offset)
236 }
237
238 pub fn current_frame_relative_line(&self) -> Option<usize> {
240 let current_line = self.current.as_ref().map(|pos| pos.line)?;
241 let entry_line = self.entry.as_ref().map_or(1, |pos| pos.line);
242
243 Some(current_line.saturating_sub(entry_line) + self.current_line_offset + 1)
244 }
245}
246
247#[derive(Default)]
249pub struct FormatOptions {
250 pub show_args: bool,
252 pub show_entry_points: bool,
254}
255
256pub struct FormatCallStack<'a> {
261 stack: &'a CallStack,
262 options: &'a FormatOptions,
263}
264
265impl<'a> FormatCallStack<'a> {
266 pub const fn new(stack: &'a CallStack, options: &'a FormatOptions) -> Self {
273 Self { stack, options }
274 }
275}
276
277impl std::fmt::Display for FormatCallStack<'_> {
278 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
279 self.stack.fmt_with_options(f, self.options)
280 }
281}
282
283#[derive(Clone, Debug, Default)]
285#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
286pub struct CallStack {
287 frames: VecDeque<Frame>,
288 func_call_depth: usize,
289 script_source_depth: usize,
290 active_trap_signals: HashSet<traps::TrapSignal>,
291 trap_delivery_suppress_count: usize,
292}
293
294impl CallStack {
295 pub const fn format<'a>(&'a self, options: &'a FormatOptions) -> FormatCallStack<'a> {
301 FormatCallStack::new(self, options)
302 }
303
304 fn fmt_with_options(
311 &self,
312 f: &mut std::fmt::Formatter<'_>,
313 options: &FormatOptions,
314 ) -> std::fmt::Result {
315 if self.is_empty() {
316 return Ok(());
317 }
318
319 color_print::cwriteln!(f, "<underline>Call stack (most recent first):</underline>")?;
320
321 for (index, frame) in self.iter().enumerate() {
322 let si = frame.current_pos_as_source_info();
323
324 color_print::cwrite!(
325 f,
326 " <dim>#{index}</dim><yellow>|</yellow> <strong>{}</strong>",
327 si.source
328 )?;
329
330 if let Some(pos) = &si.start {
331 color_print::cwrite!(f, ":<cyan>{}</cyan>,<cyan>{}</cyan>", pos.line, pos.column)?;
332 }
333
334 color_print::cwrite!(f, " (<dim>{}</dim>", frame.frame_type)?;
335
336 if options.show_entry_points {
337 if let Some(entry) = &frame.entry {
338 let entry_si = frame.pos_as_source_info(Some(entry));
339 if let Some(entry_start) = &entry_si.start {
340 color_print::cwrite!(
341 f,
342 " <dim>entered at {}:{}</dim>",
343 entry_si.source,
344 entry_start
345 )?;
346 }
347 }
348 }
349
350 color_print::cwriteln!(f, ")")?;
351
352 if !frame.args.is_empty() && options.show_args {
353 for (i, arg) in frame.args.iter().enumerate() {
354 color_print::cwriteln!(
355 f,
356 " <yellow>${}</yellow>: <blue>{}</blue>",
357 i + 1,
358 arg
359 )?;
360 }
361 }
362 }
363
364 Ok(())
365 }
366}
367
368impl std::fmt::Display for CallStack {
369 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
370 self.fmt_with_options(f, &FormatOptions::default())
371 }
372}
373
374impl std::ops::Index<usize> for CallStack {
375 type Output = Frame;
376
377 fn index(&self, index: usize) -> &Self::Output {
378 &self.frames[index]
379 }
380}
381
382impl CallStack {
383 pub fn new() -> Self {
385 Self::default()
386 }
387
388 pub fn pop(&mut self) -> Option<Frame> {
391 let frame = self.frames.pop_front()?;
392
393 if frame.frame_type.is_function() {
394 self.func_call_depth = self.func_call_depth.saturating_sub(1);
395 }
396
397 if frame.frame_type.is_sourced_script() {
398 self.script_source_depth = self.script_source_depth.saturating_sub(1);
399 }
400
401 if let FrameType::TrapHandler(signal) = &frame.frame_type {
402 self.active_trap_signals.remove(signal);
403 }
404
405 Some(frame)
406 }
407
408 pub fn current_frame(&self) -> Option<&Frame> {
411 self.frames.front()
412 }
413
414 pub fn current_pos_as_source_info(&self) -> crate::SourceInfo {
419 let Some(frame) = self.frames.front() else {
420 return crate::SourceInfo::default();
421 };
422
423 frame.current_pos_as_source_info()
424 }
425
426 pub fn set_current_pos(&mut self, position: Option<Arc<crate::SourcePosition>>) {
428 if let Some(frame) = self.frames.front_mut() {
429 frame.current = position;
430 }
431 }
432
433 pub(crate) fn increment_current_line_offset(&mut self, delta: usize) {
439 let Some(frame) = self.frames.front_mut() else {
440 return;
441 };
442
443 frame.current_line_offset += delta;
444 }
445
446 pub fn push_script(
454 &mut self,
455 call_type: ScriptCallType,
456 source_info: &crate::SourceInfo,
457 args: impl IntoIterator<Item = String>,
458 ) {
459 self.frames.push_front(Frame {
460 frame_type: FrameType::Script(ScriptCall {
461 call_type,
462 source_info: source_info.to_owned(),
463 }),
464 args: args.into_iter().collect(),
465 source_info: source_info.to_owned(),
466 current_line_offset: 0,
467 current: None, entry: None, });
470
471 if matches!(call_type, ScriptCallType::Source) {
472 self.script_source_depth += 1;
473 }
474 }
475
476 pub fn push_trap_handler(
483 &mut self,
484 signal: traps::TrapSignal,
485 handler: Option<&traps::TrapHandler>,
486 ) {
487 let source_info =
488 handler.map_or_else(crate::SourceInfo::default, |h| h.source_info.clone());
489
490 self.frames.push_front(Frame {
491 frame_type: FrameType::TrapHandler(signal),
492 args: vec![],
493 source_info,
494 current_line_offset: 0,
495 current: None, entry: None, });
498
499 self.active_trap_signals.insert(signal);
500 }
501
502 pub fn push_eval(&mut self) {
504 self.frames.push_front(Frame {
505 frame_type: FrameType::Eval,
506 args: vec![],
507 source_info: crate::SourceInfo::from("eval"), current_line_offset: 0,
509 current: None, entry: None, });
512 }
513
514 pub fn push_command_string(&mut self) {
516 self.frames.push_front(Frame {
517 frame_type: FrameType::CommandString,
518 args: vec![],
519 source_info: crate::SourceInfo::from("environment"),
520 current_line_offset: 0,
521 current: None, entry: None, });
524 }
525
526 pub fn push_interactive_session(&mut self) {
528 self.frames.push_front(Frame {
529 frame_type: FrameType::InteractiveSession,
530 args: vec![],
531 current_line_offset: 0,
532 source_info: crate::SourceInfo::from("main"),
533 current: None, entry: None, });
536 }
537
538 pub fn push_function(
546 &mut self,
547 name: impl Into<String>,
548 function: &functions::Registration,
549 args: impl IntoIterator<Item = String>,
550 ) {
551 self.frames.push_front(Frame {
552 frame_type: FrameType::Function(FunctionCall {
553 function_name: name.into(),
554 function: function.to_owned(),
555 }),
556 args: args.into_iter().collect(),
557 source_info: function.source().clone(),
558 entry: function.definition().location().map(|span| span.start),
559 current: None, current_line_offset: 0,
561 });
562
563 self.func_call_depth += 1;
564 }
565
566 pub fn iter_function_calls(&self) -> impl Iterator<Item = &FunctionCall> {
568 self.iter().filter_map(|frame| {
569 if let FrameType::Function(call) = &frame.frame_type {
570 Some(call)
571 } else {
572 None
573 }
574 })
575 }
576
577 pub fn iter_script_calls(&self) -> impl Iterator<Item = &ScriptCall> {
579 self.iter().filter_map(|frame| {
580 if let FrameType::Script(call) = &frame.frame_type {
581 Some(call)
582 } else {
583 None
584 }
585 })
586 }
587
588 pub fn in_sourced_script(&self) -> bool {
590 self.iter_script_calls()
591 .next()
592 .is_some_and(|call| matches!(call.call_type, ScriptCallType::Source))
593 }
594
595 pub const fn function_call_depth(&self) -> usize {
597 self.func_call_depth
598 }
599
600 pub const fn script_source_depth(&self) -> usize {
602 self.script_source_depth
603 }
604
605 pub fn is_trap_signal_active(&self, signal: traps::TrapSignal) -> bool {
608 self.active_trap_signals.contains(&signal)
609 }
610
611 pub fn clear_active_trap_signals(&mut self) {
615 self.active_trap_signals.clear();
616 }
617
618 pub const fn is_trap_delivery_suppressed(&self) -> bool {
620 self.trap_delivery_suppress_count > 0
621 }
622
623 pub const fn acquire_trap_delivery_block(&mut self) {
627 self.trap_delivery_suppress_count += 1;
628 }
629
630 pub const fn release_trap_delivery_block(&mut self) {
633 self.trap_delivery_suppress_count = self.trap_delivery_suppress_count.saturating_sub(1);
634 }
635
636 pub fn in_function(&self) -> bool {
638 self.iter_function_calls().next().is_some()
639 }
640
641 pub fn depth(&self) -> usize {
643 self.frames.len()
644 }
645
646 pub fn is_empty(&self) -> bool {
648 self.frames.is_empty()
649 }
650
651 pub fn iter(&self) -> impl Iterator<Item = &Frame> {
654 self.frames.iter()
655 }
656
657 pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Frame> {
660 self.frames.iter_mut()
661 }
662}
663
664#[cfg(test)]
665mod tests {
666 use std::path::PathBuf;
667
668 use super::*;
669 use crate::SourceInfo;
670 use pretty_assertions::assert_matches;
671
672 #[test]
673 fn test_call_stack_new() {
674 let stack = CallStack::new();
675 assert!(stack.is_empty());
676 assert_eq!(stack.depth(), 0);
677 }
678
679 #[test]
680 fn test_call_stack_default() {
681 let stack = CallStack::default();
682 assert!(stack.is_empty());
683 assert_eq!(stack.depth(), 0);
684 }
685
686 #[test]
687 fn test_call_stack_push_pop() {
688 let mut stack = CallStack::new();
689
690 stack.push_script(
691 ScriptCallType::Source,
692 &SourceInfo::from(PathBuf::from("script1.sh")),
693 vec![],
694 );
695 assert!(!stack.is_empty());
696 assert_eq!(stack.depth(), 1);
697
698 stack.push_script(
699 ScriptCallType::Run,
700 &SourceInfo::from(PathBuf::from("script2.sh")),
701 vec![],
702 );
703 assert_eq!(stack.depth(), 2);
704
705 let frame = stack.pop().unwrap();
706 assert_matches!(
707 frame.frame_type,
708 FrameType::Script(ScriptCall {
709 call_type: ScriptCallType::Run,
710 source_info: SourceInfo {
711 source: file_path,
712 ..
713 },
714 }) if &file_path == "script2.sh"
715 );
716 assert_eq!(stack.depth(), 1);
717
718 let frame = stack.pop().unwrap();
719 assert_matches!(
720 frame.frame_type,
721 FrameType::Script(ScriptCall {
722 call_type: ScriptCallType::Source,
723 source_info: SourceInfo {
724 source: file_path,
725 ..
726 },
727 }) if &file_path == "script1.sh"
728 );
729 assert_eq!(stack.depth(), 0);
730 assert!(stack.is_empty());
731 }
732
733 #[test]
734 fn test_call_stack_pop_empty() {
735 let mut stack = CallStack::new();
736 assert!(stack.pop().is_none());
737 }
738
739 #[test]
740 fn test_in_sourced_script() {
741 let mut stack = CallStack::new();
742 assert!(!stack.in_sourced_script());
743
744 stack.push_script(
745 ScriptCallType::Run,
746 &SourceInfo::from(PathBuf::from("script1.sh")),
747 vec![],
748 );
749 assert!(!stack.in_sourced_script());
750
751 stack.push_script(
752 ScriptCallType::Source,
753 &SourceInfo::from(PathBuf::from("script2.sh")),
754 vec![],
755 );
756 assert!(stack.in_sourced_script());
757
758 stack.pop();
759 assert!(!stack.in_sourced_script());
760 }
761
762 #[test]
763 fn test_call_stack_iter() {
764 let mut stack = CallStack::new();
765 stack.push_script(
766 ScriptCallType::Source,
767 &SourceInfo::from(PathBuf::from("script1.sh")),
768 vec![],
769 );
770 stack.push_script(
771 ScriptCallType::Run,
772 &SourceInfo::from(PathBuf::from("script2.sh")),
773 vec![],
774 );
775 stack.push_script(
776 ScriptCallType::Source,
777 &SourceInfo::from(PathBuf::from("script3.sh")),
778 vec![],
779 );
780
781 let frames: Vec<_> = stack.iter().collect();
782 assert_eq!(frames.len(), 3);
783 assert_matches!(&frames[0].frame_type, FrameType::Script(ScriptCall { source_info: SourceInfo { source: file_path, .. }, .. }) if file_path == "script3.sh");
784 assert_matches!(&frames[1].frame_type, FrameType::Script(ScriptCall { source_info: SourceInfo { source: file_path, .. }, .. }) if file_path == "script2.sh");
785 assert_matches!(&frames[2].frame_type, FrameType::Script(ScriptCall { source_info: SourceInfo { source: file_path, .. }, .. }) if file_path == "script1.sh");
786 }
787}