Skip to main content

tl_errors/
lib.rs

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