Skip to main content

newter_compiler/
error.rs

1//! Error types and source span handling for the Newt compiler.
2
3use thiserror::Error;
4
5/// A span of source code (byte range + line/column for display).
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7pub struct Span {
8    pub start: usize,
9    pub end: usize,
10    pub line: u32,
11    pub column: u32,
12}
13
14impl Span {
15    pub fn new(start: usize, end: usize, line: u32, column: u32) -> Self {
16        Self { start, end, line, column }
17    }
18
19    pub fn merge(self, other: Span) -> Span {
20        Span {
21            start: self.start.min(other.start),
22            end: self.end.max(other.end),
23            line: self.line,
24            column: self.column,
25        }
26    }
27}
28
29/// Holds the full source and can slice by span.
30#[derive(Clone)]
31pub struct Source {
32    pub code: String,
33    pub path: Option<String>,
34}
35
36impl Source {
37    pub fn new(code: String, path: Option<String>) -> Self {
38        Self { code, path }
39    }
40
41    pub fn slice(&self, span: Span) -> &str {
42        &self.code[span.start..span.end.min(self.code.len())]
43    }
44
45    pub fn line_at(&self, line: u32) -> &str {
46        self.code
47            .lines()
48            .nth((line as usize).saturating_sub(1))
49            .unwrap_or("")
50    }
51}
52
53/// Compiler / runtime errors with source location.
54#[derive(Error, Debug)]
55pub enum NewtError {
56    #[error("lexer: {message}")]
57    Lexer {
58        span: Span,
59        message: String,
60        suggestion: Option<String>,
61    },
62
63    #[error("parse: {message}")]
64    Parse {
65        span: Span,
66        message: String,
67        suggestion: Option<String>,
68    },
69
70    #[error("semantic: {message}")]
71    Semantic {
72        span: Span,
73        message: String,
74        suggestion: Option<String>,
75    },
76
77    #[error("io error: {0}")]
78    Io(#[from] std::io::Error),
79
80    #[error("{0}")]
81    Other(String),
82}
83
84impl NewtError {
85    pub fn lexer(span: Span, message: impl Into<String>) -> Self {
86        Self::Lexer {
87            span,
88            message: message.into(),
89            suggestion: None,
90        }
91    }
92
93    pub fn lexer_with_suggestion(span: Span, message: impl Into<String>, suggestion: impl Into<String>) -> Self {
94        Self::Lexer {
95            span,
96            message: message.into(),
97            suggestion: Some(suggestion.into()),
98        }
99    }
100
101    pub fn parse(span: Span, message: impl Into<String>) -> Self {
102        Self::Parse {
103            span,
104            message: message.into(),
105            suggestion: None,
106        }
107    }
108
109    pub fn parse_with_suggestion(span: Span, message: impl Into<String>, suggestion: impl Into<String>) -> Self {
110        Self::Parse {
111            span,
112            message: message.into(),
113            suggestion: Some(suggestion.into()),
114        }
115    }
116
117    pub fn semantic(span: Span, message: impl Into<String>) -> Self {
118        Self::Semantic {
119            span,
120            message: message.into(),
121            suggestion: None,
122        }
123    }
124
125    pub fn span(&self) -> Option<Span> {
126        match self {
127            Self::Lexer { span, .. } | Self::Parse { span, .. } | Self::Semantic { span, .. } => Some(*span),
128            _ => None,
129        }
130    }
131
132    pub fn suggestion(&self) -> Option<&str> {
133        match self {
134            Self::Lexer { suggestion: s, .. } | Self::Parse { suggestion: s, .. } | Self::Semantic { suggestion: s, .. } => s.as_deref(),
135            _ => None,
136        }
137    }
138}
139
140/// Pretty-print an error with source context.
141pub fn format_error(source: &Source, err: &NewtError) -> String {
142    let mut out = String::new();
143    if let Some(span) = err.span() {
144        let path = source.path.as_deref().unwrap_or("<input>");
145        let line_content = source.line_at(span.line);
146        out.push_str(&format!("  --> {}:{}:{}\n", path, span.line, span.column));
147        out.push_str("   |\n");
148        out.push_str(&format!("{:>4} | {}\n", span.line, line_content));
149        let pad = span.column.saturating_sub(1) as usize;
150        out.push_str(&format!("   | {}^\n", " ".repeat(pad)));
151    }
152    out.push_str(&format!("error: {}\n", err));
153    if let Some(s) = err.suggestion() {
154        out.push_str(&format!("  hint: {}\n", s));
155    }
156    out
157}