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}
390
391impl Fault {
392 #[must_use]
394 pub fn of(
395 program: &ResolvedProgram,
396 erased: &[Spanned],
397 top_level: usize,
398 e: &EvalError,
399 trace: &Trace,
400 ) -> Self {
401 let defs = Definitions::of(program, erased);
402 let message = crate::pipeline::display_keys(&crate::messages::describe(e, erased));
403 let raised = e.span().unwrap_or_else(Span::synthetic);
407 let (own, at) = defs.first_own(trace, raised);
411 let site = own
412 .and_then(|k| trace.frames[k..].iter().find_map(|f| defs.of_frame(f)))
413 .and_then(|d| defs.get(d))
414 .map_or(top_level, |d| d.top_level);
415 Self {
416 located: program.locate(site, at, &message).to_string(),
417 place: Place::of(program, site, at),
418 stack: Stack::of(program, &defs, trace, raised, top_level),
419 message,
420 }
421 }
422
423 #[must_use]
425 pub fn json(&self) -> serde_json::Value {
426 let mut v = self.place.json();
427 v["stage"] = "eval".into();
428 v["severity"] = "error".into();
429 v["message"] = self.message.clone().into();
430 v["stack"] = self.stack.json();
431 v["stack_depth"] = self.stack.depth.into();
432 v
433 }
434}
435
436impl From<String> for Fault {
437 fn from(message: String) -> Self {
438 Self {
439 located: message.clone(),
440 message,
441 ..Self::default()
442 }
443 }
444}
445
446impl std::fmt::Display for Fault {
447 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
448 f.write_str(&self.located)?;
449 if !self.stack.is_empty() {
450 write!(f, "\n{}", self.stack)?;
451 }
452 Ok(())
453 }
454}
455
456#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
458pub struct Counts {
459 pub calls: u64,
461 pub inclusive: Duration,
463 pub exclusive: Duration,
465 pub allocated: Allocations,
469 pub allocated_exclusive: Allocations,
471}
472
473#[derive(Clone, Debug, PartialEq, Eq, Hash)]
475enum Slot {
476 Def(usize),
477 Other {
480 name: Option<Arc<str>>,
481 within: Option<usize>,
482 source: Span,
483 },
484}
485
486struct Active {
488 slot: Slot,
489 start: Option<Instant>,
490 children: Duration,
491 allocated_at: Option<Allocations>,
494 allocated_children: Allocations,
495 traced: Option<Vec<Value>>,
497}
498
499#[derive(Default)]
500struct State {
501 stack: Vec<Active>,
502 counts: HashMap<Slot, Counts>,
503 live: HashMap<Slot, usize>,
504 trace_depth: usize,
505}
506
507pub enum TraceEvent<'a> {
509 Call {
510 depth: usize,
511 name: &'a str,
512 args: &'a [Value],
513 },
514 Exit {
515 depth: usize,
516 name: &'a str,
517 args: &'a [Value],
518 exit: Exit<'a>,
519 },
520}
521
522pub struct Tracer {
524 pub targets: Vec<usize>,
525 pub sink: Box<dyn Fn(&TraceEvent<'_>) + Send + Sync>,
526}
527
528pub struct Recorder {
531 defs: Arc<Definitions>,
532 timed: bool,
533 tracer: Option<Tracer>,
534 state: Mutex<State>,
535}
536
537impl Recorder {
538 #[must_use]
540 pub fn new(defs: Arc<Definitions>, timed: bool, tracer: Option<Tracer>) -> Self {
541 Self {
542 defs,
543 timed,
544 tracer,
545 state: Mutex::new(State::default()),
546 }
547 }
548
549 fn state(&self) -> std::sync::MutexGuard<'_, State> {
550 self.state.lock().unwrap_or_else(PoisonError::into_inner)
551 }
552
553 fn slot(&self, call: &Call<'_>, stack: &[Active]) -> Slot {
554 if let Some(d) = self.defs.of_call(call.name, call.source) {
555 return Slot::Def(d);
556 }
557 let within = stack.iter().rev().find_map(|a| match a.slot {
558 Slot::Def(d) => Some(d),
559 Slot::Other { within, .. } => within,
560 });
561 Slot::Other {
562 name: call.name.cloned(),
563 within,
564 source: call.source,
565 }
566 }
567
568 fn label(&self, slot: &Slot) -> String {
569 match slot {
570 Slot::Def(d) => self
571 .defs
572 .get(*d)
573 .map_or_else(String::new, |d| d.name.clone()),
574 Slot::Other { name: Some(n), .. } => display_key(n),
575 Slot::Other {
576 name: None, within, ..
577 } => match within.and_then(|d| self.defs.get(d)) {
578 Some(d) => format!("fn in {}", d.name),
579 None => "fn".to_string(),
580 },
581 }
582 }
583
584 #[must_use]
587 pub fn report(&self) -> Vec<Measured> {
588 let state = self.state();
589 let mut out: Vec<Measured> = self
590 .defs
591 .iter()
592 .enumerate()
593 .map(|(i, d)| Measured {
594 name: d.name.clone(),
595 definition: Some(i),
596 place: d.place.clone(),
597 entry: d.entry,
598 counts: state.counts.get(&Slot::Def(i)).copied().unwrap_or_default(),
599 })
600 .collect();
601 for (slot, counts) in &state.counts {
602 let Slot::Other { within, .. } = slot else {
603 continue;
604 };
605 let place = within
606 .and_then(|d| self.defs.get(d))
607 .map(|d| d.place.clone())
608 .unwrap_or_default();
609 out.push(Measured {
610 name: self.label(slot),
611 definition: None,
612 place,
613 entry: within
614 .and_then(|d| self.defs.get(d))
615 .is_some_and(|d| d.entry),
616 counts: *counts,
617 });
618 }
619 out
620 }
621
622 #[must_use]
623 pub fn definitions(&self) -> &Definitions {
624 &self.defs
625 }
626
627 #[must_use]
631 pub fn counts_allocations(&self) -> bool {
632 crate::alloc::allocations().is_some()
633 }
634}
635
636#[derive(Clone, Debug)]
638pub struct Measured {
639 pub name: String,
640 pub definition: Option<usize>,
642 pub place: Place,
643 pub entry: bool,
644 pub counts: Counts,
645}
646
647impl CallObserver for Recorder {
648 fn enter(&self, call: &Call<'_>, args: &[Value]) -> Option<Value> {
649 let mut state = self.state();
650 let slot = self.slot(call, &state.stack);
651 state.counts.entry(slot.clone()).or_default().calls += 1;
652 *state.live.entry(slot.clone()).or_default() += 1;
653 let traced = match (&self.tracer, &slot) {
654 (Some(t), Slot::Def(d)) if t.targets.contains(d) => {
655 let depth = state.trace_depth;
656 state.trace_depth += 1;
657 (t.sink)(&TraceEvent::Call {
658 depth,
659 name: &self.label(&slot),
660 args,
661 });
662 Some(args.to_vec())
663 }
664 _ => None,
665 };
666 state.stack.push(Active {
667 slot,
668 start: self.timed.then(Instant::now),
669 children: Duration::ZERO,
670 allocated_at: None,
671 allocated_children: Allocations::default(),
672 traced,
673 });
674 if let Some(active) = state.stack.last_mut() {
677 active.allocated_at = crate::alloc::allocations();
678 }
679 None
680 }
681
682 fn leave(&self, _call: &Call<'_>, exit: Exit<'_>) {
683 let allocated_now = crate::alloc::allocations();
685 let mut state = self.state();
686 let Some(active) = state.stack.pop() else {
687 return;
688 };
689 let elapsed = active.start.map_or(Duration::ZERO, |s| s.elapsed());
690 let allocated = match (allocated_now, active.allocated_at) {
691 (Some(now), Some(at)) => now.since(at),
692 _ => Allocations::default(),
693 };
694 if let Some(parent) = state.stack.last_mut() {
695 parent.children += elapsed;
696 parent.allocated_children += allocated;
697 }
698 let live = state.live.entry(active.slot.clone()).or_default();
699 *live = live.saturating_sub(1);
700 let outermost = *live == 0;
701 let counts = state.counts.entry(active.slot.clone()).or_default();
702 counts.exclusive += elapsed.saturating_sub(active.children);
703 counts.allocated_exclusive += allocated.since(active.allocated_children);
704 if outermost {
705 counts.inclusive += elapsed;
706 counts.allocated += allocated;
707 }
708 if let (Some(t), Some(args)) = (&self.tracer, &active.traced) {
709 state.trace_depth = state.trace_depth.saturating_sub(1);
710 (t.sink)(&TraceEvent::Exit {
711 depth: state.trace_depth,
712 name: &self.label(&active.slot),
713 args,
714 exit,
715 });
716 }
717 }
718}