1use core::cell::{Cell, RefCell};
18
19use alloc::collections::VecDeque;
20use alloc::format;
21use alloc::string::String;
22use alloc::vec::Vec;
23
24use crate::code::{ChunkId, Node, NodeId, PatId, StaticChunk};
25use crate::source::{report, CtxSpan, DocSource, Location};
26use crate::sym::Sym;
27use crate::{Ctx, Text, Value};
28
29#[derive(Debug, Clone)]
44pub enum Raised {
45 Error(Value),
47 NoMatch(Value),
49}
50
51impl Raised {
52 pub(crate) fn no_match(tag: Sym) -> Raised {
55 Raised::NoMatch(Value::Symbol(tag))
56 }
57
58 pub(crate) fn symbol(tag: Sym) -> Raised {
61 Raised::Error(Value::Symbol(tag))
62 }
63
64 pub fn value(&self) -> &Value {
66 match self {
67 Raised::Error(v) | Raised::NoMatch(v) => v,
68 }
69 }
70
71 pub fn into_value(self) -> Value {
73 match self {
74 Raised::Error(v) | Raised::NoMatch(v) => v,
75 }
76 }
77}
78
79const FRAMES_CAP: usize = 128;
86
87#[derive(Default)]
89pub(crate) struct Unwind {
90 dirty: Cell<bool>,
92 state: RefCell<State>,
93}
94
95#[derive(Default)]
96struct State {
97 frames: Frames,
98 refuted: Option<Refuted>,
101}
102
103#[derive(Default)]
106struct Frames {
107 head: Vec<Frame>,
108 tail: VecDeque<Frame>,
109 omitted: u32,
111}
112
113#[derive(Clone, Copy)]
116struct Frame {
117 chunk: ChunkId,
118 call: NodeId,
119 repeat: u32,
120}
121
122struct Refuted {
124 chunk: ChunkId,
125 site: Site,
126 value: Value,
127}
128
129#[derive(Clone, Copy)]
133enum Site {
134 Pat(PatId),
135 Case(NodeId),
136 Match,
137}
138
139impl Frames {
140 fn push(&mut self, chunk: ChunkId, call: NodeId) {
141 let prev = match self.tail.back_mut() {
142 Some(prev) => Some(prev),
143 None => self.head.last_mut(),
144 };
145 if let Some(prev) = prev {
146 if prev.chunk == chunk && prev.call == call {
147 prev.repeat += 1;
148 return;
149 }
150 }
151 let frame = Frame {
152 chunk,
153 call,
154 repeat: 0,
155 };
156 if self.head.len() < FRAMES_CAP / 2 {
157 self.head.push(frame);
158 return;
159 }
160 if self.tail.len() == FRAMES_CAP / 2 {
161 let dropped = self.tail.pop_front().expect("the tail is full");
162 self.omitted = self
163 .omitted
164 .saturating_add(dropped.repeat.saturating_add(1));
165 }
166 self.tail.push_back(frame);
167 }
168
169 fn clear(&mut self) {
170 if !self.head.is_empty() {
171 self.head.clear();
172 self.tail.clear();
173 self.omitted = 0;
174 }
175 }
176}
177
178impl Ctx {
183 #[cold]
186 #[inline(never)]
187 pub(crate) fn unwound(&self, at: ChunkId, call: NodeId) {
188 if call == NodeId::NONE {
189 return;
190 }
191 self.unwind.dirty.set(true);
192 self.unwind.state.borrow_mut().frames.push(at, call);
193 }
194
195 #[cold]
199 #[inline(never)]
200 pub(crate) fn escaped(&self, raised: Raised, at: ChunkId, pat: PatId, value: &Value) -> Raised {
201 if let Raised::NoMatch(_) = raised {
202 let mut state = self.unwind.state.borrow_mut();
203 if state.refuted.is_none() {
204 state.refuted = Some(Refuted {
205 chunk: at,
206 site: Site::Pat(pat),
207 value: value.clone(),
208 });
209 self.unwind.dirty.set(true);
210 }
211 }
212 raised
213 }
214
215 #[cold]
218 #[inline(never)]
219 pub(crate) fn no_arm_matched(&self, at: ChunkId, subject: NodeId, value: &Value) -> Raised {
220 self.refuted(at, Site::Case(subject), value)
221 }
222
223 #[cold]
225 #[inline(never)]
226 pub(crate) fn no_clause_matched(&self, value: &Value) -> Raised {
227 self.refuted(ChunkId::NONE, Site::Match, value)
228 }
229
230 fn refuted(&self, chunk: ChunkId, site: Site, value: &Value) -> Raised {
231 self.unwind.state.borrow_mut().refuted = Some(Refuted {
232 chunk,
233 site,
234 value: value.clone(),
235 });
236 self.unwind.dirty.set(true);
237 Raised::no_match(Sym::NO_MATCH)
238 }
239
240 #[inline(never)]
243 pub(crate) fn caught(&self) {
244 if self.unwind.dirty.get() {
245 self.clear_state();
246 }
247 }
248
249 #[cold]
250 #[inline(never)]
251 fn clear_state(&self) {
252 self.unwind.dirty.set(false);
253 let mut state = self.unwind.state.borrow_mut();
254 state.frames.clear();
255 state.refuted = None;
256 }
257
258 pub(crate) fn clear_unwind(&self) {
260 self.caught();
261 }
262}
263
264#[derive(Debug, Clone, Default, PartialEq)]
271pub struct Trace {
272 pub frames: Vec<TraceFrame>,
274 pub omitted: Option<(usize, u32)>,
277 pub refuted: Option<Refutation>,
279}
280
281#[derive(Debug, Clone, PartialEq, Eq)]
283pub struct TraceFrame {
284 pub call: Option<CtxSpan>,
286 pub repeat: u32,
288 pub within: FrameName,
290}
291
292#[derive(Debug, Clone, PartialEq, Eq)]
294pub enum FrameName {
295 Member(Text),
297 Main,
299 Closure(Option<CtxSpan>),
301 Top,
303}
304
305#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
307pub enum ReportStyle {
308 #[default]
311 Snippets,
312 Lines,
314}
315
316pub type DocText = (String, Option<String>);
319
320#[derive(Debug, Clone, PartialEq)]
324pub struct Refutation {
325 pub by: RefutedBy,
326 pub at: Option<CtxSpan>,
329 pub value: Value,
331}
332
333#[derive(Debug, Clone, Copy, PartialEq, Eq)]
335pub enum RefutedBy {
336 Pattern,
337 Case,
338 Match,
339}
340
341impl Ctx {
342 pub fn take_trace(&self, raised: &Raised) -> Trace {
346 self.unwind.dirty.set(false);
347 let State { frames, refuted } = core::mem::take(&mut *self.unwind.state.borrow_mut());
348 let frame = |f: &Frame| TraceFrame {
349 call: self.node_span(f.chunk, f.call),
350 repeat: f.repeat,
351 within: self.frame_name(f.chunk, f.call),
352 };
353 let mut trace = Trace {
354 frames: frames.head.iter().map(frame).collect(),
355 ..Trace::default()
356 };
357 if frames.omitted > 0 {
358 trace.omitted = Some((trace.frames.len(), frames.omitted));
359 }
360 trace.frames.extend(frames.tail.iter().map(frame));
361 if let (Raised::NoMatch(_), Some(r)) = (raised, refuted) {
362 let (by, at) = match r.site {
363 Site::Pat(pat) => (RefutedBy::Pattern, self.pat_span(r.chunk, pat)),
364 Site::Case(subject) => (RefutedBy::Case, self.node_span(r.chunk, subject)),
365 Site::Match => (RefutedBy::Match, None),
366 };
367 trace.refuted = Some(Refutation {
368 by,
369 at,
370 value: r.value,
371 });
372 }
373 trace
374 }
375
376 fn frame_name(&self, at: ChunkId, call: NodeId) -> FrameName {
380 let code = self.chunk(at);
381 let chunk: &StaticChunk = &code;
382 let Some(span) = chunk.node_span(call) else {
383 return FrameName::Top;
384 };
385 let expression = chunk.name().as_str().starts_with("__eval_");
387 let lambda = chunk
389 .lambda_ids()
390 .filter_map(|l| Some((l, chunk.lambda_span(l)?)))
391 .filter(|(_, s)| s.contains(span))
392 .min_by_key(|(_, s)| s.1);
393 let members = chunk.module_ids().flat_map(|m| {
394 (0..chunk.member_len(m)).filter_map(move |i| Some((m, i, chunk.member(m, i)?)))
395 });
396 if let Some((l, lspan)) = lambda {
397 let named = members
398 .clone()
399 .find(|(_, _, member)| {
400 member.node.get().is_some() && chunk.node(member.node) == Node::Abs(l)
401 })
402 .map(|(_, _, member)| member.name);
403 let main = || {
404 let body = chunk.module(chunk.top()).body;
405 body.get().is_some() && chunk.node(body) == Node::Abs(l) && !expression
406 };
407 return match named {
408 Some(name) => FrameName::Member(self.text(name)),
409 None if main() => FrameName::Main,
410 None => FrameName::Closure(self.ctx_span(at, lspan)),
411 };
412 }
413 let member = members
414 .filter_map(|(m, i, member)| Some((member, chunk.member_span(m, i)?)))
415 .filter(|(_, s)| s.contains(span))
416 .min_by_key(|(_, s)| s.1);
417 match member {
418 Some((member, _)) => FrameName::Member(self.text(member.name)),
419 None if expression => FrameName::Top,
420 None => FrameName::Main,
421 }
422 }
423
424 pub fn report_raised(
430 &self,
431 raised: &Raised,
432 trace: &Trace,
433 doc: &dyn Fn(&DocSource, &str) -> DocText,
434 style: ReportStyle,
435 ) -> String {
436 let indent = match style {
437 ReportStyle::Snippets => "",
438 ReportStyle::Lines => " ",
439 };
440 let mut docs: Vec<(ChunkId, u32, String, Option<String>)> = Vec::new();
441 let mut at = |span: Option<CtxSpan>, message: &str| -> String {
442 let Some((location, span)) = span.and_then(|s| Some((self.locate(s.0)?, s))) else {
443 return format!("{indent}<unknown>: {message}");
444 };
445 let known = docs
446 .iter()
447 .position(|d| (d.0, d.1) == (location.chunk, location.doc));
448 let i = match known {
449 Some(i) => i,
450 None => {
451 let chunk = self.chunk(location.chunk);
452 let source = chunk.documents()[location.doc as usize].source();
453 let (name, text) = doc(source, chunk.name().as_str());
454 docs.push((location.chunk, location.doc, name, text));
455 docs.len() - 1
456 }
457 };
458 let (_, _, name, text) = &docs[i];
459 match style {
460 ReportStyle::Snippets => shown(name, text.as_deref(), &location, span, message),
461 ReportStyle::Lines => {
462 let line = location.line_num;
463 match text.as_deref().and_then(|t| location.column(t)) {
464 Some(column) => format!("{indent}{name}:{line}:{column}: {message}"),
465 None => format!("{indent}{name}:{line}: {message}"),
466 }
467 }
468 }
469 };
470 let mut out = format!("uncaught {}", raised.value().to_display(self));
471 if let Some(r) = &trace.refuted {
472 let what = match r.by {
473 RefutedBy::Pattern => "refuted",
474 RefutedBy::Case => "no arm matched",
475 RefutedBy::Match => "no clause matched",
476 };
477 let span = r.at.or_else(|| trace.frames.first()?.call);
478 out.push('\n');
479 out.push_str(&at(span, &format!("{what} {}", r.value.to_display(self))));
480 }
481 for (i, frame) in trace.frames.iter().enumerate() {
482 if let Some((j, n)) = trace.omitted {
483 if i == j {
484 out.push_str(&format!("\n{indent}[{n} frames omitted]"));
485 }
486 }
487 let message = match &frame.within {
488 FrameName::Member(name) => format!("in {name}"),
489 FrameName::Main => String::from("in __main"),
490 FrameName::Closure(span) => match span.and_then(|s| self.locate(s.0)) {
491 Some(l) => format!("in the closure at line {}", l.line_num),
492 None => String::from("in a closure"),
493 },
494 FrameName::Top => String::from("in the expression"),
495 };
496 out.push('\n');
497 out.push_str(&at(frame.call, &message));
498 if frame.repeat > 0 {
499 out.push_str(&format!(
500 "\n{indent}[the call above repeated {} more times]",
501 frame.repeat
502 ));
503 }
504 }
505 out
506 }
507}
508
509fn shown(
512 name: &str,
513 text: Option<&str>,
514 location: &Location,
515 span: CtxSpan,
516 message: &str,
517) -> String {
518 match text {
519 Some(text) => report(
520 name,
521 text,
522 muon::Span {
523 offset: location.pos,
524 length: span.1,
525 },
526 message,
527 ),
528 None => format!("{name}:{}: {message}", location.line_num),
529 }
530}