Skip to main content

qanglang_core/
error.rs

1use std::{ops::Range, sync::Arc};
2
3use crate::{SourceMap, Vm, backend::chunk::SourceLocation, nodes::SourceSpan};
4
5#[derive(Debug, Clone, Copy, PartialEq)]
6pub enum ErrorMessageFormat {
7    Verbose,
8    Compact,
9    Minimal,
10}
11
12#[derive(Debug, Clone, PartialEq)]
13pub struct QangPipelineError(Vec<QangCompilerError>);
14
15impl QangPipelineError {
16    pub fn new(errors: Vec<QangCompilerError>) -> Self {
17        Self(errors)
18    }
19
20    pub fn all(&self) -> &[QangCompilerError] {
21        &self.0
22    }
23
24    pub fn into_errors(self) -> Vec<QangCompilerError> {
25        self.0
26    }
27}
28
29#[derive(Debug, Clone, PartialEq, Copy)]
30pub enum QangErrorKind {
31    Syntax,
32    Analysis,
33    Assembler,
34}
35
36impl std::fmt::Display for QangErrorKind {
37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        match self {
39            Self::Syntax => write!(f, "Syntax Error"),
40            Self::Analysis => write!(f, "Analysis Error"),
41            Self::Assembler => write!(f, "Assembler Error"),
42        }
43    }
44}
45
46#[derive(Debug, Clone, PartialEq)]
47pub struct QangCompilerError {
48    pub message: String,
49    pub span: SourceSpan,
50    pub kind: QangErrorKind,
51    pub source_map: Arc<SourceMap>,
52}
53
54impl QangCompilerError {
55    pub fn new_syntax_error(message: String, span: SourceSpan, source_map: Arc<SourceMap>) -> Self {
56        Self {
57            message,
58            span,
59            kind: QangErrorKind::Syntax,
60            source_map,
61        }
62    }
63
64    pub fn new_analysis_error(
65        message: String,
66        span: SourceSpan,
67        source_map: Arc<SourceMap>,
68    ) -> Self {
69        Self {
70            message,
71            span,
72            kind: QangErrorKind::Analysis,
73            source_map,
74        }
75    }
76
77    pub fn new_assembler_error(
78        message: String,
79        span: SourceSpan,
80        source_map: Arc<SourceMap>,
81    ) -> Self {
82        Self {
83            message,
84            span,
85            kind: QangErrorKind::Assembler,
86            source_map,
87        }
88    }
89
90    pub fn into_formatted(self) -> Self {
91        let message = pretty_print_error(&self.source_map, self.kind, &self.message, self.span);
92
93        Self {
94            message,
95            span: self.span,
96            kind: self.kind,
97            source_map: self.source_map,
98        }
99    }
100
101    pub fn into_short_formatted(self) -> Self {
102        let line_num = self.source_map.get_line_number(self.span.start);
103        let col_num = self.source_map.get_column_number(self.span.start);
104
105        let message = format!(
106            "{} at line {}, column {}: {}\n",
107            self.kind, line_num, col_num, self.message
108        );
109
110        Self {
111            message,
112            span: self.span,
113            kind: self.kind,
114            source_map: self.source_map,
115        }
116    }
117}
118
119impl std::fmt::Display for QangCompilerError {
120    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121        write!(f, "{}", self.message)
122    }
123}
124
125impl std::error::Error for QangCompilerError {}
126
127#[derive(Debug, Clone)]
128pub struct NativeFunctionError(pub String);
129
130impl NativeFunctionError {
131    pub fn new(message: &str) -> Self {
132        Self(message.to_string())
133    }
134
135    pub fn into_qang_error(self, loc: SourceLocation) -> QangRuntimeError {
136        QangRuntimeError::new(self.0, loc)
137    }
138}
139
140impl From<&'static str> for NativeFunctionError {
141    fn from(value: &'static str) -> Self {
142        NativeFunctionError::new(value)
143    }
144}
145
146#[derive(Debug, Clone, PartialEq)]
147pub struct Trace {
148    callee: String,
149    module: Option<String>,
150    loc: SourceLocation,
151}
152
153impl Trace {
154    pub fn new(callee: &str, loc: SourceLocation) -> Self {
155        Self {
156            callee: callee.to_string(),
157            module: None,
158            loc,
159        }
160    }
161
162    pub fn new_with_module(callee: &str, module: &str, loc: SourceLocation) -> Self {
163        Self {
164            callee: callee.to_string(),
165            module: Some(module.to_string()),
166            loc,
167        }
168    }
169}
170
171impl std::fmt::Display for Trace {
172    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173        if let Some(module) = &self.module {
174            write!(
175                f,
176                "  at {} in {} (line {}, column {})",
177                self.callee, module, self.loc.line, self.loc.col
178            )
179        } else {
180            write!(
181                f,
182                "  at {} (line {}, column {})",
183                self.callee, self.loc.line, self.loc.col
184            )
185        }
186    }
187}
188
189#[derive(Debug, Clone, PartialEq)]
190pub struct QangRuntimeError {
191    pub message: String,
192    pub stack_trace: Vec<Trace>,
193}
194
195impl QangRuntimeError {
196    pub fn new(message: String, loc: SourceLocation) -> Self {
197        Self {
198            message: format!(
199                "Runtime Error at line {}, column {}: {}",
200                loc.line, loc.col, message
201            )
202            .to_string(),
203            stack_trace: Vec::new(),
204        }
205    }
206
207    pub fn with_stack_trace(mut self, stack_trace: Vec<Trace>) -> Self {
208        self.stack_trace = stack_trace;
209        self
210    }
211}
212
213impl std::fmt::Display for QangRuntimeError {
214    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215        write!(f, "{}", self.message)?;
216        if !self.stack_trace.is_empty() {
217            writeln!(f)?;
218            writeln!(f, "Stack trace:")?;
219            for trace in &self.stack_trace {
220                writeln!(f, "{}", trace)?;
221            }
222        }
223        Ok(())
224    }
225}
226
227impl std::error::Error for QangRuntimeError {}
228
229#[derive(Debug, Clone, Default)]
230pub struct ErrorReporter {
231    errors: Vec<QangCompilerError>,
232}
233
234impl ErrorReporter {
235    pub fn new() -> Self {
236        Self { errors: Vec::new() }
237    }
238
239    pub fn report_error(&mut self, error: QangCompilerError) {
240        self.errors.push(error);
241    }
242
243    pub fn has_errors(&self) -> bool {
244        !self.errors.is_empty()
245    }
246
247    pub fn errors(&self) -> &[QangCompilerError] {
248        &self.errors
249    }
250
251    pub fn take_errors(&mut self) -> Vec<QangCompilerError> {
252        std::mem::take(&mut self.errors)
253    }
254
255    pub fn merge(&mut self, mut other: Self) {
256        self.errors.extend(other.take_errors());
257    }
258}
259
260pub fn pretty_print_error(
261    source_map: &SourceMap,
262    kind: QangErrorKind,
263    message: &str,
264    span: SourceSpan,
265) -> String {
266    let mut output = String::new();
267
268    let line_num = source_map.get_line_number(span.start);
269    let col_num = source_map.get_column_number(span.start);
270
271    // Error header with kind
272    output.push_str(&format!(
273        "{} at line {}, column {}: {}\n",
274        kind, line_num, col_num, message
275    ));
276
277    // Get the problematic line
278    let line_chars = source_map.get_line(line_num);
279    let line_str: String = line_chars.iter().collect();
280
281    // Line number padding for alignment
282    let line_num_str = line_num.to_string();
283    let padding = " ".repeat(line_num_str.len());
284
285    // Show the line with line number
286    output.push_str(&format!(" {} | {}\n", line_num, line_str));
287
288    // Create the error pointer
289    let error_pointer = create_error_pointer(source_map, span, &line_str, col_num);
290    output.push_str(&format!(" {} | {}\n", padding, error_pointer));
291
292    output
293}
294
295fn create_error_pointer(
296    source_map: &SourceMap,
297    span: SourceSpan,
298    line_str: &str,
299    col_num: u32,
300) -> String {
301    let mut pointer = String::new();
302
303    // Add spaces up to the error column
304    for i in 1..col_num {
305        if i <= line_str.len() as u32 {
306            let ch = line_str.chars().nth((i - 1) as usize).unwrap_or(' ');
307            if ch == '\t' {
308                pointer.push('\t');
309            } else {
310                pointer.push(' ');
311            }
312        } else {
313            pointer.push(' ');
314        }
315    }
316
317    // Calculate the span length within the line
318    let start_col = source_map.get_column_number(span.start) as usize;
319    let end_col = source_map.get_column_number(span.end.saturating_sub(1)) as usize;
320
321    // Add the error indicators
322    if start_col == end_col || span.start == span.end {
323        pointer.push('^');
324    } else {
325        // Multi-character span
326        let span_length = (end_col - start_col + 1).max(1);
327        for i in 0..span_length {
328            if i == 0 {
329                pointer.push('^');
330            } else {
331                pointer.push('~');
332            }
333        }
334    }
335
336    pointer
337}
338
339impl Vm {
340    pub fn get_stack_trace(&self) -> Vec<Trace> {
341        self.get_stack_trace_from_frames(0..self.state.frame_count)
342    }
343
344    fn get_stack_trace_from_frames(&self, frame_id_range: Range<usize>) -> Vec<Trace> {
345        let mut traces = Vec::new();
346        let mut previous_module: Option<String> = None;
347
348        for frame_idx in frame_id_range {
349            let frame = &self.state.frames[frame_idx];
350            let closure = self.alloc.closures.get_closure(frame.closure);
351            let function = self.alloc.get_function(closure.function);
352
353            // this will clean the canonical path if it has a matching prefix,otherwise return the input value.
354            // Because functions syntatically cannot start with a `/` or `.`, this will not modify declared function names.
355            let name = &clean_canonicalized_path(self.alloc.strings.get(function.name));
356
357            let loc = if frame.ip > 0 {
358                function
359                    .chunk
360                    .locs
361                    .get(frame.ip - 1)
362                    .copied()
363                    .unwrap_or_default()
364            } else {
365                SourceLocation::default()
366            };
367
368            // Get current module name if closure has module context
369            let current_module = closure.module_context.map(|module_handle| {
370                let module_string_handle = self.state.modules.get_module_id(module_handle);
371                let raw_path = self.alloc.strings.get(module_string_handle);
372                clean_canonicalized_path(raw_path)
373            });
374
375            // Only include module name if it's different from the previous frame
376            let should_show_module = match (&previous_module, &current_module) {
377                (None, Some(_)) => true,                  // First module encountered
378                (Some(prev), Some(curr)) => prev != curr, // Module changed
379                _ => false,                               // No module or same module
380            };
381
382            if should_show_module {
383                if let Some(module_name) = &current_module {
384                    traces.push(Trace::new_with_module(name, module_name, loc));
385                } else {
386                    traces.push(Trace::new(name, loc));
387                }
388            } else {
389                traces.push(Trace::new(name, loc));
390            }
391
392            previous_module = current_module;
393        }
394
395        traces
396    }
397}
398
399fn clean_canonicalized_path(path: &str) -> String {
400    // Remove Windows UNC prefix
401    #[cfg(target_os = "windows")]
402    if let Some(stripped) = path.strip_prefix(r"\\?\") {
403        return stripped.to_string();
404    }
405
406    // Remove leading "./" on Unix systems
407    #[cfg(not(target_os = "windows"))]
408    if let Some(stripped) = path.strip_prefix("./") {
409        return stripped.to_string();
410    }
411
412    path.to_string()
413}
414
415#[cfg(test)]
416mod tests {
417    use super::*;
418    use crate::SourceMap;
419
420    fn error_summary(errors: &[QangCompilerError]) -> String {
421        if errors.is_empty() {
422            "No errors found.".to_string()
423        } else if errors.len() == 1 {
424            "Found 1 error.".to_string()
425        } else {
426            format!("Found {} errors.", errors.len())
427        }
428    }
429
430    #[test]
431    fn test_error_reporter_basic_functionality() {
432        let source = "var x = 5 +\nvar y = 10;";
433        let source_map = SourceMap::from_source(source.to_string());
434        let mut reporter = ErrorReporter::new();
435
436        // Report an error
437        reporter.report_error(
438            QangCompilerError::new_syntax_error(
439                "Expected expression after '+'".to_string(),
440                SourceSpan::new(11, 11),
441                Arc::new(source_map),
442            )
443            .into_formatted(),
444        );
445
446        assert!(reporter.has_errors());
447        assert_eq!(reporter.errors().len(), 1);
448    }
449
450    #[test]
451    fn test_multiple_errors() {
452        let source = "var x = 5 +\nvar y = 10 *\nvar z;";
453        let source_map = SourceMap::from_source(source.to_string());
454        let source_map = Arc::new(source_map);
455        let mut reporter = ErrorReporter::new();
456
457        // Report multiple errors
458        reporter.report_error(
459            QangCompilerError::new_syntax_error(
460                "Missing operand after '+'".to_string(),
461                SourceSpan::new(11, 11),
462                source_map.clone(),
463            )
464            .into_formatted(),
465        );
466
467        reporter.report_error(
468            QangCompilerError::new_syntax_error(
469                "Missing operand after '*'".to_string(),
470                SourceSpan::new(23, 23),
471                source_map,
472            )
473            .into_formatted(),
474        );
475
476        assert_eq!(reporter.errors().len(), 2);
477
478        let summary = error_summary(reporter.errors());
479        assert_eq!(summary, "Found 2 errors.");
480    }
481
482    #[test]
483    fn test_error_summary_no_errors() {
484        let reporter = ErrorReporter::new();
485
486        let summary = error_summary(reporter.errors());
487        assert!(summary.contains("No errors found."));
488    }
489
490    #[test]
491    fn test_error_summary_single_errors() {
492        let source = "var x = 5;";
493        let source_map = SourceMap::from_source(source.to_string());
494        let mut reporter = ErrorReporter::new();
495
496        reporter.report_error(
497            QangCompilerError::new_syntax_error(
498                "Syntax error 1".to_string(),
499                SourceSpan::new(0, 3),
500                Arc::new(source_map),
501            )
502            .into_formatted(),
503        );
504
505        let summary = error_summary(reporter.errors());
506        assert!(summary.contains("Found 1 error."));
507    }
508
509    #[test]
510    fn test_error_summary_multiple_errors() {
511        let source = "var x = 5;";
512        let source_map = SourceMap::from_source(source.to_string());
513        let source_map = Arc::new(source_map);
514        let mut reporter = ErrorReporter::new();
515
516        reporter.report_error(
517            QangCompilerError::new_syntax_error(
518                "Syntax error 1".to_string(),
519                SourceSpan::new(0, 3),
520                source_map.clone(),
521            )
522            .into_formatted(),
523        );
524        reporter.report_error(
525            QangCompilerError::new_syntax_error(
526                "Syntax error 2".to_string(),
527                SourceSpan::new(4, 5),
528                source_map,
529            )
530            .into_formatted(),
531        );
532
533        let summary = error_summary(reporter.errors());
534        assert!(summary.contains("Found 2 errors."));
535    }
536}