use std::collections::HashMap;
use std::sync::{Arc, Mutex, PoisonError};
use std::time::{Duration, Instant};
use tatara_lisp::{Span, Spanned};
use tatara_lisp_eval::{EvalError, Value};
pub use tatara_lisp_eval::observe::{Call, CallObserver, Exit, Trace, TraceFrame};
use crate::alloc::Allocations;
use crate::uses::ResolvedProgram;
#[derive(Clone, Debug)]
pub struct Definition {
pub key: Arc<str>,
pub name: String,
pub top_level: usize,
pub span: Span,
pub entry: bool,
pub place: Place,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Place {
pub file: Option<String>,
pub line: Option<usize>,
pub column: Option<usize>,
}
impl Place {
fn of(program: &ResolvedProgram, top_level: usize, span: Span) -> Self {
let at = program.locate(top_level, span, "");
Self {
file: at.file_name(),
line: at.line_col().map(|(l, _)| l),
column: at.line_col().map(|(_, c)| c),
}
}
fn json(&self) -> serde_json::Value {
serde_json::json!({ "file": self.file, "line": self.line, "column": self.column })
}
}
impl std::fmt::Display for Place {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.file.as_deref().unwrap_or("<unknown file>"))?;
if let Some(line) = self.line {
write!(f, ":{line}")?;
if let Some(col) = self.column {
write!(f, ":{col}")?;
}
}
Ok(())
}
}
#[derive(Debug, Default)]
pub struct Definitions {
defs: Vec<Definition>,
by_key: HashMap<Arc<str>, usize>,
by_lambda: HashMap<Span, Option<usize>>,
}
impl Definitions {
#[must_use]
pub fn of(program: &ResolvedProgram, erased: &[Spanned]) -> Self {
let mut out = Self::default();
for (i, form) in erased.iter().enumerate() {
let Some(items) = form.as_list() else {
continue;
};
if items.first().and_then(Spanned::as_symbol) != Some("define") || items.len() < 3 {
continue;
}
let (key, lambda) = match items[1].as_list() {
Some(head) => match head.first().and_then(Spanned::as_symbol) {
Some(k) => (k, None),
None => continue,
},
None => match (items[1].as_symbol(), items[2].as_list()) {
(Some(k), Some(value))
if value.first().and_then(Spanned::as_symbol) == Some("lambda") =>
{
(k, Some(items[2].span))
}
_ => continue,
},
};
let key: Arc<str> = Arc::from(key);
let index = out.defs.len();
out.defs.push(Definition {
name: display_key(&key),
key: Arc::clone(&key),
top_level: i,
span: form.span,
entry: program.owner_of(i) == Some(ResolvedProgram::ENTRY),
place: Place::of(program, i, form.span),
});
out.by_key.insert(key, index);
if let Some(span) = lambda {
out.by_lambda
.entry(span)
.and_modify(|slot| *slot = None)
.or_insert(Some(index));
}
}
out
}
pub fn iter(&self) -> impl Iterator<Item = &Definition> {
self.defs.iter()
}
#[must_use]
pub fn len(&self) -> usize {
self.defs.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.defs.is_empty()
}
#[must_use]
pub fn get(&self, index: usize) -> Option<&Definition> {
self.defs.get(index)
}
#[must_use]
pub fn named(&self, name: &str) -> Vec<usize> {
let exact: Vec<usize> = (0..self.defs.len())
.filter(|&i| self.defs[i].name == name || &*self.defs[i].key == name)
.collect();
if !exact.is_empty() {
return exact;
}
(0..self.defs.len())
.filter(|&i| self.defs[i].name.rsplit("::").next() == Some(name))
.collect()
}
fn of_call(&self, name: Option<&Arc<str>>, source: Span) -> Option<usize> {
match name {
Some(n) => self.by_key.get(n).copied(),
None => self.by_lambda.get(&source).copied().flatten(),
}
}
fn of_frame(&self, frame: &TraceFrame) -> Option<usize> {
self.of_call(frame.name.as_ref(), frame.source)
}
fn is_builtin(&self, frame: &TraceFrame) -> bool {
frame.name.as_ref().is_some_and(|n| {
!self.by_key.contains_key(n) && blue_lang_syntax::qualified(n).is_none()
})
}
fn first_own(&self, trace: &Trace, error: Span) -> (Option<usize>, Span) {
let frames = &trace.frames;
let own = frames.iter().position(|f| !self.is_builtin(f));
match own {
Some(0) | None => (own, error),
Some(k) => (Some(k), frames[k - 1].at),
}
}
}
#[must_use]
pub fn display_key(key: &str) -> String {
let root = format!(
"{}{}",
blue_lang_check::names::ROOT_QUALIFIER,
blue_lang_syntax::QUALIFIER
);
if let Some(rest) = key.strip_prefix(&root) {
rest.to_string()
} else if let Some((pkg, name)) = blue_lang_syntax::qualified(key) {
format!("{pkg}::{name}")
} else {
key.to_string()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StackFrame {
pub function: String,
pub place: Option<Place>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Stack {
pub frames: Vec<StackFrame>,
pub depth: usize,
}
pub const STACK_LINES: usize = 24;
impl Stack {
#[must_use]
pub fn is_empty(&self) -> bool {
self.frames.is_empty()
}
#[must_use]
pub fn of(
program: &ResolvedProgram,
defs: &Definitions,
trace: &Trace,
error: Span,
top_level: usize,
) -> Self {
let frames = &trace.frames;
let mut out = Vec::with_capacity(frames.len() + 1);
for (k, frame) in frames.iter().enumerate() {
let def = defs.of_frame(frame);
let outer = frames[k + 1..].iter().find_map(|f| defs.of_frame(f));
let site = def
.or(outer)
.and_then(|d| defs.get(d))
.map_or(top_level, |d| d.top_level);
if defs.is_builtin(frame) {
out.push(StackFrame {
function: frame.name.as_deref().map_or_else(String::new, display_key),
place: None,
});
continue;
}
let at = if k == 0 { error } else { frames[k - 1].at };
let function = match (def.and_then(|d| defs.get(d)), &frame.name) {
(Some(d), _) => d.name.clone(),
(None, Some(n)) => display_key(n),
(None, None) => match outer.and_then(|d| defs.get(d)) {
Some(d) => format!("fn in {}", d.name),
None => "fn".to_string(),
},
};
out.push(StackFrame {
function,
place: Some(Place::of(program, site, at)),
});
}
if let Some(last) = frames.last() {
if trace.depth == frames.len() {
out.push(StackFrame {
function: "<top level>".to_string(),
place: Some(Place::of(program, top_level, last.at)),
});
}
}
Self {
frames: out,
depth: trace.depth,
}
}
#[must_use]
pub fn json(&self) -> serde_json::Value {
serde_json::Value::Array(
self.frames
.iter()
.map(|f| {
let mut v = f
.place
.as_ref()
.map_or_else(|| serde_json::json!({ "builtin": true }), Place::json);
v["function"] = serde_json::Value::String(f.function.clone());
v
})
.collect(),
)
}
}
impl std::fmt::Display for Stack {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut runs: Vec<(&StackFrame, usize)> = Vec::new();
for frame in &self.frames {
match runs.last_mut() {
Some((last, n)) if *last == frame => *n += 1,
_ => runs.push((frame, 1)),
}
}
let mut first = true;
let mut line = |f: &mut std::fmt::Formatter<'_>, text: String| {
let sep = if first { "" } else { "\n" };
first = false;
write!(f, "{sep} {text}")
};
for (shown, (frame, n)) in runs.iter().enumerate() {
if shown == STACK_LINES {
let rest: usize = runs[shown..].iter().map(|(_, n)| n).sum();
line(f, format!("… {rest} more frame(s)"))?;
break;
}
match &frame.place {
Some(place) => line(f, format!("at {} ({place})", frame.function))?,
None => line(f, format!("at {} (builtin)", frame.function))?,
}
if *n > 1 {
line(f, format!("… repeated {} more time(s)", n - 1))?;
}
}
let unrecorded = self.depth.saturating_sub(self.frames.len());
if unrecorded > 0 {
line(f, format!("… {unrecorded} outer frame(s) not recorded"))?;
}
Ok(())
}
}
#[derive(Clone, Debug, Default)]
pub struct Fault {
pub message: String,
pub place: Place,
pub located: String,
pub stack: Stack,
}
impl Fault {
#[must_use]
pub fn of(
program: &ResolvedProgram,
erased: &[Spanned],
top_level: usize,
e: &EvalError,
trace: &Trace,
) -> Self {
let defs = Definitions::of(program, erased);
let message = crate::pipeline::display_keys(&crate::messages::describe(e, erased));
let raised = e.span().unwrap_or_else(Span::synthetic);
let (own, at) = defs.first_own(trace, raised);
let site = own
.and_then(|k| trace.frames[k..].iter().find_map(|f| defs.of_frame(f)))
.and_then(|d| defs.get(d))
.map_or(top_level, |d| d.top_level);
Self {
located: program.locate(site, at, &message).to_string(),
place: Place::of(program, site, at),
stack: Stack::of(program, &defs, trace, raised, top_level),
message,
}
}
#[must_use]
pub fn json(&self) -> serde_json::Value {
let mut v = self.place.json();
v["stage"] = "eval".into();
v["severity"] = "error".into();
v["message"] = self.message.clone().into();
v["stack"] = self.stack.json();
v["stack_depth"] = self.stack.depth.into();
v
}
}
impl From<String> for Fault {
fn from(message: String) -> Self {
Self {
located: message.clone(),
message,
..Self::default()
}
}
}
impl std::fmt::Display for Fault {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.located)?;
if !self.stack.is_empty() {
write!(f, "\n{}", self.stack)?;
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Counts {
pub calls: u64,
pub inclusive: Duration,
pub exclusive: Duration,
pub allocated: Allocations,
pub allocated_exclusive: Allocations,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
enum Slot {
Def(usize),
Other {
name: Option<Arc<str>>,
within: Option<usize>,
source: Span,
},
}
struct Active {
slot: Slot,
start: Option<Instant>,
children: Duration,
allocated_at: Option<Allocations>,
allocated_children: Allocations,
traced: Option<Vec<Value>>,
}
#[derive(Default)]
struct State {
stack: Vec<Active>,
counts: HashMap<Slot, Counts>,
live: HashMap<Slot, usize>,
trace_depth: usize,
}
pub enum TraceEvent<'a> {
Call {
depth: usize,
name: &'a str,
args: &'a [Value],
},
Exit {
depth: usize,
name: &'a str,
args: &'a [Value],
exit: Exit<'a>,
},
}
pub struct Tracer {
pub targets: Vec<usize>,
pub sink: Box<dyn Fn(&TraceEvent<'_>) + Send + Sync>,
}
pub struct Recorder {
defs: Arc<Definitions>,
timed: bool,
tracer: Option<Tracer>,
state: Mutex<State>,
}
impl Recorder {
#[must_use]
pub fn new(defs: Arc<Definitions>, timed: bool, tracer: Option<Tracer>) -> Self {
Self {
defs,
timed,
tracer,
state: Mutex::new(State::default()),
}
}
fn state(&self) -> std::sync::MutexGuard<'_, State> {
self.state.lock().unwrap_or_else(PoisonError::into_inner)
}
fn slot(&self, call: &Call<'_>, stack: &[Active]) -> Slot {
if let Some(d) = self.defs.of_call(call.name, call.source) {
return Slot::Def(d);
}
let within = stack.iter().rev().find_map(|a| match a.slot {
Slot::Def(d) => Some(d),
Slot::Other { within, .. } => within,
});
Slot::Other {
name: call.name.cloned(),
within,
source: call.source,
}
}
fn label(&self, slot: &Slot) -> String {
match slot {
Slot::Def(d) => self
.defs
.get(*d)
.map_or_else(String::new, |d| d.name.clone()),
Slot::Other { name: Some(n), .. } => display_key(n),
Slot::Other {
name: None, within, ..
} => match within.and_then(|d| self.defs.get(d)) {
Some(d) => format!("fn in {}", d.name),
None => "fn".to_string(),
},
}
}
#[must_use]
pub fn report(&self) -> Vec<Measured> {
let state = self.state();
let mut out: Vec<Measured> = self
.defs
.iter()
.enumerate()
.map(|(i, d)| Measured {
name: d.name.clone(),
definition: Some(i),
place: d.place.clone(),
entry: d.entry,
counts: state.counts.get(&Slot::Def(i)).copied().unwrap_or_default(),
})
.collect();
for (slot, counts) in &state.counts {
let Slot::Other { within, .. } = slot else {
continue;
};
let place = within
.and_then(|d| self.defs.get(d))
.map(|d| d.place.clone())
.unwrap_or_default();
out.push(Measured {
name: self.label(slot),
definition: None,
place,
entry: within
.and_then(|d| self.defs.get(d))
.is_some_and(|d| d.entry),
counts: *counts,
});
}
out
}
#[must_use]
pub fn definitions(&self) -> &Definitions {
&self.defs
}
#[must_use]
pub fn counts_allocations(&self) -> bool {
crate::alloc::allocations().is_some()
}
}
#[derive(Clone, Debug)]
pub struct Measured {
pub name: String,
pub definition: Option<usize>,
pub place: Place,
pub entry: bool,
pub counts: Counts,
}
impl CallObserver for Recorder {
fn enter(&self, call: &Call<'_>, args: &[Value]) -> Option<Value> {
let mut state = self.state();
let slot = self.slot(call, &state.stack);
state.counts.entry(slot.clone()).or_default().calls += 1;
*state.live.entry(slot.clone()).or_default() += 1;
let traced = match (&self.tracer, &slot) {
(Some(t), Slot::Def(d)) if t.targets.contains(d) => {
let depth = state.trace_depth;
state.trace_depth += 1;
(t.sink)(&TraceEvent::Call {
depth,
name: &self.label(&slot),
args,
});
Some(args.to_vec())
}
_ => None,
};
state.stack.push(Active {
slot,
start: self.timed.then(Instant::now),
children: Duration::ZERO,
allocated_at: None,
allocated_children: Allocations::default(),
traced,
});
if let Some(active) = state.stack.last_mut() {
active.allocated_at = crate::alloc::allocations();
}
None
}
fn leave(&self, _call: &Call<'_>, exit: Exit<'_>) {
let allocated_now = crate::alloc::allocations();
let mut state = self.state();
let Some(active) = state.stack.pop() else {
return;
};
let elapsed = active.start.map_or(Duration::ZERO, |s| s.elapsed());
let allocated = match (allocated_now, active.allocated_at) {
(Some(now), Some(at)) => now.since(at),
_ => Allocations::default(),
};
if let Some(parent) = state.stack.last_mut() {
parent.children += elapsed;
parent.allocated_children += allocated;
}
let live = state.live.entry(active.slot.clone()).or_default();
*live = live.saturating_sub(1);
let outermost = *live == 0;
let counts = state.counts.entry(active.slot.clone()).or_default();
counts.exclusive += elapsed.saturating_sub(active.children);
counts.allocated_exclusive += allocated.since(active.allocated_children);
if outermost {
counts.inclusive += elapsed;
counts.allocated += allocated;
}
if let (Some(t), Some(args)) = (&self.tracer, &active.traced) {
state.trace_depth = state.trace_depth.saturating_sub(1);
(t.sink)(&TraceEvent::Exit {
depth: state.trace_depth,
name: &self.label(&active.slot),
args,
exit,
});
}
}
}