1use std::collections::HashMap;
23use std::sync::{Arc, Mutex, PoisonError};
24use std::time::{Duration, Instant};
25
26use tatara_lisp::{Span, Spanned};
27use tatara_lisp_eval::{EvalError, Value};
28
29pub use tatara_lisp_eval::observe::{Call, CallObserver, Exit, Trace, TraceFrame};
30
31use crate::alloc::Allocations;
32use crate::uses::ResolvedProgram;
33
34#[derive(Clone, Debug)]
36pub struct Definition {
37 pub key: Arc<str>,
39 pub name: String,
41 pub top_level: usize,
43 pub span: Span,
45 pub entry: bool,
47 pub place: Place,
49}
50
51#[derive(Clone, Debug, Default, PartialEq, Eq)]
54pub struct Place {
55 pub file: Option<String>,
56 pub line: Option<usize>,
57 pub column: Option<usize>,
58}
59
60impl Place {
61 fn of(program: &ResolvedProgram, top_level: usize, span: Span) -> Self {
62 let at = program.locate(top_level, span, "");
63 Self {
64 file: at.file_name(),
65 line: at.line_col().map(|(l, _)| l),
66 column: at.line_col().map(|(_, c)| c),
67 }
68 }
69
70 fn json(&self) -> serde_json::Value {
71 serde_json::json!({ "file": self.file, "line": self.line, "column": self.column })
72 }
73}
74
75impl std::fmt::Display for Place {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 f.write_str(self.file.as_deref().unwrap_or("<unknown file>"))?;
78 if let Some(line) = self.line {
79 write!(f, ":{line}")?;
80 if let Some(col) = self.column {
81 write!(f, ":{col}")?;
82 }
83 }
84 Ok(())
85 }
86}
87
88#[derive(Debug, Default)]
92pub struct Definitions {
93 defs: Vec<Definition>,
94 by_key: HashMap<Arc<str>, usize>,
95 by_lambda: HashMap<Span, Option<usize>>,
99}
100
101impl Definitions {
102 #[must_use]
105 pub fn of(program: &ResolvedProgram, erased: &[Spanned]) -> Self {
106 let mut out = Self::default();
107 for (i, form) in erased.iter().enumerate() {
108 let Some(items) = form.as_list() else {
109 continue;
110 };
111 if items.first().and_then(Spanned::as_symbol) != Some("define") || items.len() < 3 {
112 continue;
113 }
114 let (key, lambda) = match items[1].as_list() {
115 Some(head) => match head.first().and_then(Spanned::as_symbol) {
116 Some(k) => (k, None),
117 None => continue,
118 },
119 None => match (items[1].as_symbol(), items[2].as_list()) {
120 (Some(k), Some(value))
121 if value.first().and_then(Spanned::as_symbol) == Some("lambda") =>
122 {
123 (k, Some(items[2].span))
124 }
125 _ => continue,
126 },
127 };
128 let key: Arc<str> = Arc::from(key);
129 let index = out.defs.len();
130 out.defs.push(Definition {
131 name: display_key(&key),
132 key: Arc::clone(&key),
133 top_level: i,
134 span: form.span,
135 entry: program.owner_of(i) == Some(ResolvedProgram::ENTRY),
136 place: Place::of(program, i, form.span),
137 });
138 out.by_key.insert(key, index);
139 if let Some(span) = lambda {
140 out.by_lambda
141 .entry(span)
142 .and_modify(|slot| *slot = None)
143 .or_insert(Some(index));
144 }
145 }
146 out
147 }
148
149 pub fn iter(&self) -> impl Iterator<Item = &Definition> {
150 self.defs.iter()
151 }
152
153 #[must_use]
154 pub fn len(&self) -> usize {
155 self.defs.len()
156 }
157
158 #[must_use]
159 pub fn is_empty(&self) -> bool {
160 self.defs.is_empty()
161 }
162
163 #[must_use]
164 pub fn get(&self, index: usize) -> Option<&Definition> {
165 self.defs.get(index)
166 }
167
168 #[must_use]
171 pub fn named(&self, name: &str) -> Vec<usize> {
172 let exact: Vec<usize> = (0..self.defs.len())
173 .filter(|&i| self.defs[i].name == name || &*self.defs[i].key == name)
174 .collect();
175 if !exact.is_empty() {
176 return exact;
177 }
178 (0..self.defs.len())
179 .filter(|&i| self.defs[i].name.rsplit("::").next() == Some(name))
180 .collect()
181 }
182
183 fn of_call(&self, name: Option<&Arc<str>>, source: Span) -> Option<usize> {
186 match name {
187 Some(n) => self.by_key.get(n).copied(),
188 None => self.by_lambda.get(&source).copied().flatten(),
189 }
190 }
191
192 fn of_frame(&self, frame: &TraceFrame) -> Option<usize> {
193 self.of_call(frame.name.as_ref(), frame.source)
194 }
195
196 fn is_builtin(&self, frame: &TraceFrame) -> bool {
200 frame.name.as_ref().is_some_and(|n| {
201 !self.by_key.contains_key(n) && blue_lang_syntax::qualified(n).is_none()
202 })
203 }
204
205 fn first_own(&self, trace: &Trace, error: Span) -> (Option<usize>, Span) {
209 let frames = &trace.frames;
210 let own = frames.iter().position(|f| !self.is_builtin(f));
211 match own {
212 Some(0) | None => (own, error),
213 Some(k) => (Some(k), frames[k - 1].at),
214 }
215 }
216}
217
218#[must_use]
221pub fn display_key(key: &str) -> String {
222 let root = format!(
223 "{}{}",
224 blue_lang_check::names::ROOT_QUALIFIER,
225 blue_lang_syntax::QUALIFIER
226 );
227 if let Some(rest) = key.strip_prefix(&root) {
228 rest.to_string()
229 } else if let Some((pkg, name)) = blue_lang_syntax::qualified(key) {
230 format!("{pkg}::{name}")
231 } else {
232 key.to_string()
233 }
234}
235
236#[derive(Clone, Debug, PartialEq, Eq)]
239pub struct StackFrame {
240 pub function: String,
241 pub place: Option<Place>,
244}
245
246#[derive(Clone, Debug, Default, PartialEq, Eq)]
249pub struct Stack {
250 pub frames: Vec<StackFrame>,
251 pub depth: usize,
254}
255
256pub const STACK_LINES: usize = 24;
258
259impl Stack {
260 #[must_use]
261 pub fn is_empty(&self) -> bool {
262 self.frames.is_empty()
263 }
264
265 #[must_use]
268 pub fn of(
269 program: &ResolvedProgram,
270 defs: &Definitions,
271 trace: &Trace,
272 error: Span,
273 top_level: usize,
274 ) -> Self {
275 let frames = &trace.frames;
276 let mut out = Vec::with_capacity(frames.len() + 1);
277 for (k, frame) in frames.iter().enumerate() {
278 let def = defs.of_frame(frame);
279 let outer = frames[k + 1..].iter().find_map(|f| defs.of_frame(f));
282 let site = def
283 .or(outer)
284 .and_then(|d| defs.get(d))
285 .map_or(top_level, |d| d.top_level);
286 if defs.is_builtin(frame) {
287 out.push(StackFrame {
288 function: frame.name.as_deref().map_or_else(String::new, display_key),
289 place: None,
290 });
291 continue;
292 }
293 let at = if k == 0 { error } else { frames[k - 1].at };
294 let function = match (def.and_then(|d| defs.get(d)), &frame.name) {
295 (Some(d), _) => d.name.clone(),
296 (None, Some(n)) => display_key(n),
297 (None, None) => match outer.and_then(|d| defs.get(d)) {
298 Some(d) => format!("fn in {}", d.name),
299 None => "fn".to_string(),
300 },
301 };
302 out.push(StackFrame {
303 function,
304 place: Some(Place::of(program, site, at)),
305 });
306 }
307 if let Some(last) = frames.last() {
308 if trace.depth == frames.len() {
309 out.push(StackFrame {
310 function: "<top level>".to_string(),
311 place: Some(Place::of(program, top_level, last.at)),
312 });
313 }
314 }
315 Self {
316 frames: out,
317 depth: trace.depth,
318 }
319 }
320
321 #[must_use]
322 pub fn json(&self) -> serde_json::Value {
323 serde_json::Value::Array(
324 self.frames
325 .iter()
326 .map(|f| {
327 let mut v = f
328 .place
329 .as_ref()
330 .map_or_else(|| serde_json::json!({ "builtin": true }), Place::json);
331 v["function"] = serde_json::Value::String(f.function.clone());
332 v
333 })
334 .collect(),
335 )
336 }
337}
338
339impl std::fmt::Display for Stack {
340 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
343 let mut runs: Vec<(&StackFrame, usize)> = Vec::new();
344 for frame in &self.frames {
345 match runs.last_mut() {
346 Some((last, n)) if *last == frame => *n += 1,
347 _ => runs.push((frame, 1)),
348 }
349 }
350 let mut first = true;
351 let mut line = |f: &mut std::fmt::Formatter<'_>, text: String| {
352 let sep = if first { "" } else { "\n" };
353 first = false;
354 write!(f, "{sep} {text}")
355 };
356 for (shown, (frame, n)) in runs.iter().enumerate() {
357 if shown == STACK_LINES {
358 let rest: usize = runs[shown..].iter().map(|(_, n)| n).sum();
359 line(f, format!("… {rest} more frame(s)"))?;
360 break;
361 }
362 match &frame.place {
363 Some(place) => line(f, format!("at {} ({place})", frame.function))?,
364 None => line(f, format!("at {} (builtin)", frame.function))?,
365 }
366 if *n > 1 {
367 line(f, format!("… repeated {} more time(s)", n - 1))?;
368 }
369 }
370 let unrecorded = self.depth.saturating_sub(self.frames.len());
371 if unrecorded > 0 {
372 line(f, format!("… {unrecorded} outer frame(s) not recorded"))?;
373 }
374 Ok(())
375 }
376}
377
378#[derive(Clone, Debug, Default)]
381pub struct Fault {
382 pub message: String,
384 pub place: Place,
386 pub located: String,
388 pub stack: Stack,
389 pub breach: bool,
391}
392
393impl Fault {
394 #[must_use]
396 pub fn of(
397 program: &ResolvedProgram,
398 erased: &[Spanned],
399 top_level: usize,
400 e: &EvalError,
401 trace: &Trace,
402 ) -> Self {
403 let defs = Definitions::of(program, erased);
404 let message = crate::pipeline::display_keys(&crate::messages::describe(e, erased));
405 let raised = e.span().unwrap_or_else(Span::synthetic);
409 let (own, at) = defs.first_own(trace, raised);
413 let site = own
414 .and_then(|k| trace.frames[k..].iter().find_map(|f| defs.of_frame(f)))
415 .and_then(|d| defs.get(d))
416 .map_or(top_level, |d| d.top_level);
417 Self {
418 located: program.locate(site, at, &message).to_string(),
419 place: Place::of(program, site, at),
420 stack: Stack::of(program, &defs, trace, raised, top_level),
421 message,
422 breach: crate::is_breach(e),
423 }
424 }
425
426 #[must_use]
429 pub fn unplaced(e: &EvalError, erased: &[Spanned]) -> Self {
430 let message = crate::pipeline::display_keys(&crate::messages::describe(e, erased));
431 Self {
432 located: message.clone(),
433 message,
434 breach: crate::is_breach(e),
435 ..Self::default()
436 }
437 }
438
439 #[must_use]
441 pub fn json(&self) -> serde_json::Value {
442 let mut v = self.place.json();
443 v["stage"] = "eval".into();
444 v["severity"] = "error".into();
445 v["message"] = self.message.clone().into();
446 v["stack"] = self.stack.json();
447 v["stack_depth"] = self.stack.depth.into();
448 if self.breach {
449 v["kind"] = crate::BREACH.into();
450 }
451 v
452 }
453}
454
455impl From<String> for Fault {
456 fn from(message: String) -> Self {
457 Self {
458 located: message.clone(),
459 message,
460 ..Self::default()
461 }
462 }
463}
464
465impl std::fmt::Display for Fault {
466 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
467 f.write_str(&self.located)?;
468 if !self.stack.is_empty() {
469 write!(f, "\n{}", self.stack)?;
470 }
471 Ok(())
472 }
473}
474
475#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
477pub struct Counts {
478 pub calls: u64,
480 pub inclusive: Duration,
482 pub exclusive: Duration,
484 pub allocated: Allocations,
488 pub allocated_exclusive: Allocations,
490}
491
492#[derive(Clone, Debug, PartialEq, Eq, Hash)]
494enum Slot {
495 Def(usize),
496 Other {
499 name: Option<Arc<str>>,
500 within: Option<usize>,
501 source: Span,
502 },
503}
504
505struct Active {
507 slot: Slot,
508 start: Option<Instant>,
509 children: Duration,
510 allocated_at: Option<Allocations>,
513 allocated_children: Allocations,
514 traced: Option<Vec<Value>>,
516}
517
518#[derive(Default)]
519struct State {
520 stack: Vec<Active>,
521 counts: HashMap<Slot, Counts>,
522 live: HashMap<Slot, usize>,
523 trace_depth: usize,
524}
525
526pub enum TraceEvent<'a> {
528 Call {
529 depth: usize,
530 name: &'a str,
531 args: &'a [Value],
532 },
533 Exit {
534 depth: usize,
535 name: &'a str,
536 args: &'a [Value],
537 exit: Exit<'a>,
538 },
539}
540
541pub struct Tracer {
543 pub targets: Vec<usize>,
544 pub sink: Box<dyn Fn(&TraceEvent<'_>) + Send + Sync>,
545}
546
547pub struct Recorder {
550 defs: Arc<Definitions>,
551 timed: bool,
552 tracer: Option<Tracer>,
553 state: Mutex<State>,
554}
555
556impl Recorder {
557 #[must_use]
559 pub fn new(defs: Arc<Definitions>, timed: bool, tracer: Option<Tracer>) -> Self {
560 Self {
561 defs,
562 timed,
563 tracer,
564 state: Mutex::new(State::default()),
565 }
566 }
567
568 fn state(&self) -> std::sync::MutexGuard<'_, State> {
569 self.state.lock().unwrap_or_else(PoisonError::into_inner)
570 }
571
572 fn slot(&self, call: &Call<'_>, stack: &[Active]) -> Slot {
573 if let Some(d) = self.defs.of_call(call.name, call.source) {
574 return Slot::Def(d);
575 }
576 let within = stack.iter().rev().find_map(|a| match a.slot {
577 Slot::Def(d) => Some(d),
578 Slot::Other { within, .. } => within,
579 });
580 Slot::Other {
581 name: call.name.cloned(),
582 within,
583 source: call.source,
584 }
585 }
586
587 fn label(&self, slot: &Slot) -> String {
588 match slot {
589 Slot::Def(d) => self
590 .defs
591 .get(*d)
592 .map_or_else(String::new, |d| d.name.clone()),
593 Slot::Other { name: Some(n), .. } => display_key(n),
594 Slot::Other {
595 name: None, within, ..
596 } => match within.and_then(|d| self.defs.get(d)) {
597 Some(d) => format!("fn in {}", d.name),
598 None => "fn".to_string(),
599 },
600 }
601 }
602
603 #[must_use]
606 pub fn report(&self) -> Vec<Measured> {
607 let state = self.state();
608 let mut out: Vec<Measured> = self
609 .defs
610 .iter()
611 .enumerate()
612 .map(|(i, d)| Measured {
613 name: d.name.clone(),
614 definition: Some(i),
615 place: d.place.clone(),
616 entry: d.entry,
617 counts: state.counts.get(&Slot::Def(i)).copied().unwrap_or_default(),
618 })
619 .collect();
620 for (slot, counts) in &state.counts {
621 let Slot::Other { within, .. } = slot else {
622 continue;
623 };
624 let place = within
625 .and_then(|d| self.defs.get(d))
626 .map(|d| d.place.clone())
627 .unwrap_or_default();
628 out.push(Measured {
629 name: self.label(slot),
630 definition: None,
631 place,
632 entry: within
633 .and_then(|d| self.defs.get(d))
634 .is_some_and(|d| d.entry),
635 counts: *counts,
636 });
637 }
638 out
639 }
640
641 #[must_use]
642 pub fn definitions(&self) -> &Definitions {
643 &self.defs
644 }
645
646 #[must_use]
650 pub fn counts_allocations(&self) -> bool {
651 crate::alloc::allocations().is_some()
652 }
653}
654
655#[derive(Clone, Debug)]
657pub struct Measured {
658 pub name: String,
659 pub definition: Option<usize>,
661 pub place: Place,
662 pub entry: bool,
663 pub counts: Counts,
664}
665
666impl CallObserver for Recorder {
667 fn enter(&self, call: &Call<'_>, args: &[Value]) -> Option<Value> {
668 let mut state = self.state();
669 let slot = self.slot(call, &state.stack);
670 state.counts.entry(slot.clone()).or_default().calls += 1;
671 *state.live.entry(slot.clone()).or_default() += 1;
672 let traced = match (&self.tracer, &slot) {
673 (Some(t), Slot::Def(d)) if t.targets.contains(d) => {
674 let depth = state.trace_depth;
675 state.trace_depth += 1;
676 (t.sink)(&TraceEvent::Call {
677 depth,
678 name: &self.label(&slot),
679 args,
680 });
681 Some(args.to_vec())
682 }
683 _ => None,
684 };
685 state.stack.push(Active {
686 slot,
687 start: self.timed.then(Instant::now),
688 children: Duration::ZERO,
689 allocated_at: None,
690 allocated_children: Allocations::default(),
691 traced,
692 });
693 if let Some(active) = state.stack.last_mut() {
696 active.allocated_at = crate::alloc::allocations();
697 }
698 None
699 }
700
701 fn leave(&self, _call: &Call<'_>, exit: Exit<'_>) {
702 let allocated_now = crate::alloc::allocations();
704 let mut state = self.state();
705 let Some(active) = state.stack.pop() else {
706 return;
707 };
708 let elapsed = active.start.map_or(Duration::ZERO, |s| s.elapsed());
709 let allocated = match (allocated_now, active.allocated_at) {
710 (Some(now), Some(at)) => now.since(at),
711 _ => Allocations::default(),
712 };
713 if let Some(parent) = state.stack.last_mut() {
714 parent.children += elapsed;
715 parent.allocated_children += allocated;
716 }
717 let live = state.live.entry(active.slot.clone()).or_default();
718 *live = live.saturating_sub(1);
719 let outermost = *live == 0;
720 let counts = state.counts.entry(active.slot.clone()).or_default();
721 counts.exclusive += elapsed.saturating_sub(active.children);
722 counts.allocated_exclusive += allocated.since(active.allocated_children);
723 if outermost {
724 counts.inclusive += elapsed;
725 counts.allocated += allocated;
726 }
727 if let (Some(t), Some(args)) = (&self.tracer, &active.traced) {
728 state.trace_depth = state.trace_depth.saturating_sub(1);
729 (t.sink)(&TraceEvent::Exit {
730 depth: state.trace_depth,
731 name: &self.label(&active.slot),
732 args,
733 exit,
734 });
735 }
736 }
737}