1use 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#[derive(Clone, Debug)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14pub struct ScriptCall {
15 pub call_type: ScriptCallType,
17 pub source_info: crate::SourceInfo,
19}
20
21impl ScriptCall {
22 pub fn name(&self) -> Cow<'_, str> {
24 self.source_info.source.as_str().into()
25 }
26}
27
28#[derive(Clone, Copy, Debug)]
30#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
31pub enum ScriptCallType {
32 Source,
34 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#[derive(Clone, Debug)]
50#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
51pub enum FrameType {
52 Script(ScriptCall),
54 Function(FunctionCall),
56 TrapHandler(traps::TrapSignal),
58 Eval,
60 CommandString,
62 InteractiveSession,
64}
65
66impl FrameType {
67 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 pub const fn is_function(&self) -> bool {
81 matches!(self, Self::Function(..))
82 }
83
84 pub const fn is_script(&self) -> bool {
86 matches!(self, Self::Script(..))
87 }
88
89 pub const fn is_trap_handler(&self) -> bool {
91 matches!(self, Self::TrapHandler(_))
92 }
93
94 pub const fn is_interactive_session(&self) -> bool {
96 matches!(self, Self::InteractiveSession)
97 }
98
99 pub const fn is_command_string(&self) -> bool {
101 matches!(self, Self::CommandString)
102 }
103
104 pub const fn is_sourced_script(&self) -> bool {
106 matches!(self, Self::Script(call) if matches!(call.call_type, ScriptCallType::Source))
107 }
108
109 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#[derive(Clone, Debug)]
130#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
131pub struct FunctionCall {
132 pub function_name: String,
134 pub function: functions::Registration,
136}
137
138impl FunctionCall {
139 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#[derive(Clone, Debug)]
153#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
154pub struct Frame {
155 pub frame_type: FrameType,
157 pub source_info: crate::SourceInfo,
161 pub entry: Option<Arc<crate::SourcePosition>>,
165 pub current: Option<Arc<crate::SourcePosition>>,
172 pub args: Vec<String>,
175 pub current_line_offset: usize,
178}
179
180impl Frame {
181 pub fn adjusted_source_info(&self) -> crate::SourceInfo {
184 self.pos_as_source_info(None)
185 }
186
187 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 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 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#[derive(Default)]
252pub struct FormatOptions {
253 pub show_args: bool,
255 pub show_entry_points: bool,
257}
258
259pub struct FormatCallStack<'a> {
264 stack: &'a CallStack,
265 options: &'a FormatOptions,
266}
267
268impl<'a> FormatCallStack<'a> {
269 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#[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 pub const fn format<'a>(&'a self, options: &'a FormatOptions) -> FormatCallStack<'a> {
305 FormatCallStack::new(self, options)
306 }
307
308 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 pub fn new() -> Self {
389 Self::default()
390 }
391
392 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 pub fn current_frame(&self) -> Option<&Frame> {
415 self.frames.front()
416 }
417
418 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 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 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 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, entry: None, });
475
476 if matches!(call_type, ScriptCallType::Source) {
477 self.script_source_depth += 1;
478 }
479 }
480
481 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, entry: None, });
503
504 self.active_trap_signals.insert(signal);
505 }
506
507 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"), current_line_offset: 0,
514 current: None, entry: None, });
517 }
518
519 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, entry: None, });
529 }
530
531 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, entry: None, });
541 }
542
543 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, current_line_offset: 0,
566 });
567
568 self.func_call_depth += 1;
569 }
570
571 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 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 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 pub const fn function_call_depth(&self) -> usize {
603 self.func_call_depth
604 }
605
606 pub const fn script_source_depth(&self) -> usize {
608 self.script_source_depth
609 }
610
611 pub fn is_trap_signal_active(&self, signal: traps::TrapSignal) -> bool {
614 self.active_trap_signals.contains(&signal)
615 }
616
617 pub fn clear_active_trap_signals(&mut self) {
621 self.active_trap_signals.clear();
622 }
623
624 pub const fn is_trap_delivery_suppressed(&self) -> bool {
626 self.trap_delivery_suppress_count > 0
627 }
628
629 pub const fn acquire_trap_delivery_block(&mut self) {
634 self.trap_delivery_suppress_count += 1;
635 }
636
637 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 pub fn in_function(&self) -> bool {
646 self.iter_function_calls().next().is_some()
647 }
648
649 pub fn depth(&self) -> usize {
651 self.frames.len()
652 }
653
654 pub fn is_empty(&self) -> bool {
656 self.frames.is_empty()
657 }
658
659 pub fn iter(&self) -> impl Iterator<Item = &Frame> {
662 self.frames.iter()
663 }
664
665 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}