1use crate::{functions, traps};
4use std::{borrow::Cow, collections::VecDeque, sync::Arc};
5
6use brush_parser::ast::SourceLocation;
7
8#[derive(Clone, Debug)]
10#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
11pub struct ScriptCall {
12 pub call_type: ScriptCallType,
14 pub source_info: crate::SourceInfo,
16}
17
18impl ScriptCall {
19 pub fn name(&self) -> Cow<'_, str> {
21 self.source_info.source.as_str().into()
22 }
23}
24
25#[derive(Clone, Copy, Debug)]
27#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
28pub enum ScriptCallType {
29 Source,
31 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#[derive(Clone, Debug)]
47#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
48pub enum FrameType {
49 Script(ScriptCall),
51 Function(FunctionCall),
53 TrapHandler,
55 Eval,
57 CommandString,
59 InteractiveSession,
61}
62
63impl FrameType {
64 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 pub const fn is_function(&self) -> bool {
78 matches!(self, Self::Function(..))
79 }
80
81 pub const fn is_script(&self) -> bool {
83 matches!(self, Self::Script(..))
84 }
85
86 pub const fn is_trap_handler(&self) -> bool {
88 matches!(self, Self::TrapHandler)
89 }
90
91 pub const fn is_interactive_session(&self) -> bool {
93 matches!(self, Self::InteractiveSession)
94 }
95
96 pub const fn is_command_string(&self) -> bool {
98 matches!(self, Self::CommandString)
99 }
100
101 pub const fn is_sourced_script(&self) -> bool {
103 matches!(self, Self::Script(call) if matches!(call.call_type, ScriptCallType::Source))
104 }
105
106 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#[derive(Clone, Debug)]
127#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
128pub struct FunctionCall {
129 pub function_name: String,
131 pub function: functions::Registration,
133}
134
135impl FunctionCall {
136 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#[derive(Clone, Debug)]
150#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
151pub struct Frame {
152 pub frame_type: FrameType,
154 pub source_info: crate::SourceInfo,
158 pub entry: Option<Arc<crate::SourcePosition>>,
161 pub current: Option<Arc<crate::SourcePosition>>,
167 pub args: Vec<String>,
169 pub current_line_offset: usize,
171}
172
173impl Frame {
174 pub fn adjusted_source_info(&self) -> crate::SourceInfo {
177 self.pos_as_source_info(None)
178 }
179
180 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 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 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#[derive(Default)]
245pub struct FormatOptions {
246 pub show_args: bool,
248 pub show_entry_points: bool,
250}
251
252pub struct FormatCallStack<'a> {
257 stack: &'a CallStack,
258 options: &'a FormatOptions,
259}
260
261impl<'a> FormatCallStack<'a> {
262 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#[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 pub const fn format<'a>(&'a self, options: &'a FormatOptions) -> FormatCallStack<'a> {
296 FormatCallStack::new(self, options)
297 }
298
299 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 pub fn new() -> Self {
380 Self::default()
381 }
382
383 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 pub fn current_frame(&self) -> Option<&Frame> {
406 self.frames.front()
407 }
408
409 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 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 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 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, entry: None, });
465
466 if matches!(call_type, ScriptCallType::Source) {
467 self.script_source_depth += 1;
468 }
469 }
470
471 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, entry: None, });
484
485 self.trap_handler_depth += 1;
486 }
487
488 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"), current_line_offset: 0,
495 current: None, entry: None, });
498 }
499
500 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, entry: None, });
510 }
511
512 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, entry: None, });
522 }
523
524 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, current_line_offset: 0,
547 });
548
549 self.func_call_depth += 1;
550 }
551
552 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 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 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 pub const fn function_call_depth(&self) -> usize {
583 self.func_call_depth
584 }
585
586 pub const fn script_source_depth(&self) -> usize {
588 self.script_source_depth
589 }
590
591 pub const fn trap_handler_depth(&self) -> usize {
593 self.trap_handler_depth
594 }
595
596 pub fn in_function(&self) -> bool {
598 self.iter_function_calls().next().is_some()
599 }
600
601 pub fn depth(&self) -> usize {
603 self.frames.len()
604 }
605
606 pub fn is_empty(&self) -> bool {
608 self.frames.is_empty()
609 }
610
611 pub fn iter(&self) -> impl Iterator<Item = &Frame> {
614 self.frames.iter()
615 }
616
617 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}