Skip to main content

tl_errors/
lib.rs

1// ThinkingLanguage — Error types and diagnostics
2// Licensed under MIT OR Apache-2.0
3
4use ariadne::{Color, Label, Report, ReportKind, Source};
5use std::fmt;
6use std::ops::Range;
7
8/// A source location span (byte offsets into source text)
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct Span {
11    pub start: usize,
12    pub end: usize,
13}
14
15impl Span {
16    pub fn new(start: usize, end: usize) -> Self {
17        Self { start, end }
18    }
19
20    pub fn range(&self) -> Range<usize> {
21        self.start..self.end
22    }
23}
24
25impl From<Range<usize>> for Span {
26    fn from(range: Range<usize>) -> Self {
27        Self {
28            start: range.start,
29            end: range.end,
30        }
31    }
32}
33
34// logos::Span is just Range<usize>, covered by the From<Range<usize>> impl above
35
36/// All error types in the ThinkingLanguage compiler
37#[derive(Debug, Clone)]
38pub enum TlError {
39    Lexer(LexerError),
40    Parser(ParserError),
41    Runtime(RuntimeError),
42    Type(TypeError),
43}
44
45#[derive(Debug, Clone)]
46pub struct TypeError {
47    pub message: String,
48    pub span: Span,
49    pub expected: Option<String>,
50    pub found: Option<String>,
51    pub hint: Option<String>,
52}
53
54#[derive(Debug, Clone)]
55pub struct LexerError {
56    pub message: String,
57    pub span: Span,
58}
59
60#[derive(Debug, Clone)]
61pub struct ParserError {
62    pub message: String,
63    pub span: Span,
64    pub hint: Option<String>,
65}
66
67#[derive(Debug, Clone)]
68pub struct RuntimeError {
69    pub message: String,
70    pub span: Option<Span>,
71    pub stack_trace: Vec<StackFrame>,
72}
73
74/// A frame in a stack trace.
75#[derive(Debug, Clone)]
76pub struct StackFrame {
77    pub function: String,
78    pub line: u32,
79}
80
81impl fmt::Display for TlError {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        match self {
84            TlError::Lexer(e) => write!(f, "Lexer error: {}", e.message),
85            TlError::Parser(e) => write!(f, "Parse error: {}", e.message),
86            TlError::Runtime(e) => write!(f, "Runtime error: {}", e.message),
87            TlError::Type(e) => {
88                write!(f, "Type error: {}", e.message)?;
89                if let (Some(expected), Some(found)) = (&e.expected, &e.found) {
90                    write!(f, " (expected `{expected}`, found `{found}`)")?;
91                }
92                Ok(())
93            }
94        }
95    }
96}
97
98impl std::error::Error for TlError {}
99
100/// Pretty-print a parser error with source context using ariadne
101pub fn report_parser_error(source: &str, filename: &str, error: &ParserError) {
102    let mut builder = Report::build(ReportKind::Error, filename, error.span.start)
103        .with_message(&error.message)
104        .with_label(
105            Label::new((filename, error.span.range()))
106                .with_message(&error.message)
107                .with_color(Color::Red),
108        );
109
110    if let Some(hint) = &error.hint {
111        builder = builder.with_help(hint);
112    }
113
114    builder
115        .finish()
116        .eprint((filename, Source::from(source)))
117        .unwrap();
118}
119
120/// Pretty-print a type error with source context using ariadne
121pub fn report_type_error(source: &str, filename: &str, error: &TypeError) {
122    let mut builder =
123        Report::build(ReportKind::Error, filename, error.span.start).with_message(&error.message);
124
125    let mut label_msg = error.message.clone();
126    if let (Some(expected), Some(found)) = (&error.expected, &error.found) {
127        label_msg = format!("expected `{expected}`, found `{found}`");
128    }
129
130    builder = builder.with_label(
131        Label::new((filename, error.span.range()))
132            .with_message(&label_msg)
133            .with_color(Color::Red),
134    );
135
136    if let Some(hint) = &error.hint {
137        builder = builder.with_help(hint);
138    }
139
140    builder
141        .finish()
142        .eprint((filename, Source::from(source)))
143        .unwrap();
144}
145
146/// Pretty-print a type warning with source context using ariadne
147pub fn report_type_warning(source: &str, filename: &str, error: &TypeError) {
148    let mut builder =
149        Report::build(ReportKind::Warning, filename, error.span.start).with_message(&error.message);
150
151    let mut label_msg = error.message.clone();
152    if let (Some(expected), Some(found)) = (&error.expected, &error.found) {
153        label_msg = format!("expected `{expected}`, found `{found}`");
154    }
155
156    builder = builder.with_label(
157        Label::new((filename, error.span.range()))
158            .with_message(&label_msg)
159            .with_color(Color::Yellow),
160    );
161
162    if let Some(hint) = &error.hint {
163        builder = builder.with_help(hint);
164    }
165
166    builder
167        .finish()
168        .eprint((filename, Source::from(source)))
169        .unwrap();
170}
171
172/// Pretty-print a runtime error
173pub fn report_runtime_error(source: &str, filename: &str, error: &RuntimeError) {
174    // If we have a span, use ariadne for pretty source display
175    if let Some(span) = &error.span {
176        Report::build(ReportKind::Error, filename, span.start)
177            .with_message(&error.message)
178            .with_label(
179                Label::new((filename, span.range()))
180                    .with_message(&error.message)
181                    .with_color(Color::Red),
182            )
183            .finish()
184            .eprint((filename, Source::from(source)))
185            .unwrap();
186    } else if !error.stack_trace.is_empty() && error.stack_trace[0].line > 0 {
187        // No span but we have a line number from the stack trace — build a span from it
188        let line = error.stack_trace[0].line as usize;
189        let lines: Vec<&str> = source.lines().collect();
190        if line > 0 && line <= lines.len() {
191            let mut offset = 0;
192            for l in &lines[..line - 1] {
193                offset += l.len() + 1; // +1 for newline
194            }
195            let line_len = lines[line - 1].len().max(1);
196            let span = Span::new(offset, offset + line_len);
197            Report::build(ReportKind::Error, filename, span.start)
198                .with_message(&error.message)
199                .with_label(
200                    Label::new((filename, span.range()))
201                        .with_message(&error.message)
202                        .with_color(Color::Red),
203                )
204                .finish()
205                .eprint((filename, Source::from(source)))
206                .unwrap();
207        } else {
208            eprintln!("Runtime error (line {}): {}", line, error.message);
209        }
210    } else {
211        eprintln!("Runtime error: {}", error.message);
212    }
213    // Print stack trace if available (skip if only one frame at top-level)
214    if error.stack_trace.len() > 1
215        || (error.stack_trace.len() == 1 && error.stack_trace[0].function != "<main>")
216    {
217        eprintln!("Stack trace:");
218        for frame in &error.stack_trace {
219            if frame.line > 0 {
220                eprintln!("  at {} (line {})", frame.function, frame.line);
221            } else {
222                eprintln!("  at {}", frame.function);
223            }
224        }
225    }
226}