use crate::compiler::parser::SourceLocation;
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LexErrorKind {
UnterminatedString,
UnterminatedTemplateLiteral,
UnterminatedInterpolation,
UnterminatedControlBlock,
UnterminatedDirective,
UnterminatedIdentBlock,
InvalidEscapeSequence,
InvalidCharacter,
InvalidUnicodeEscape,
UnexpectedEof,
UnbalancedBraces,
InvalidControlBlock,
InvalidDirective,
InvalidTemplateExpression,
InterpolationTooDeep,
}
impl LexErrorKind {
pub fn description(&self) -> &'static str {
match self {
Self::UnterminatedString => "unterminated string literal",
Self::UnterminatedTemplateLiteral => "unterminated template literal",
Self::UnterminatedInterpolation => "unterminated interpolation",
Self::UnterminatedControlBlock => "unterminated control block",
Self::UnterminatedDirective => "unterminated directive",
Self::UnterminatedIdentBlock => "unterminated ident block",
Self::InvalidEscapeSequence => "invalid escape sequence",
Self::InvalidCharacter => "invalid character",
Self::InvalidUnicodeEscape => "invalid unicode escape sequence",
Self::UnexpectedEof => "unexpected end of input",
Self::UnbalancedBraces => "unbalanced braces in interpolation",
Self::InvalidControlBlock => "invalid control block syntax",
Self::InvalidDirective => "invalid directive syntax",
Self::InvalidTemplateExpression => "invalid template expression",
Self::InterpolationTooDeep => "interpolation nesting too deep",
}
}
pub fn suggestion(&self) -> Option<&'static str> {
match self {
Self::UnterminatedString => Some("add a closing quote \" or '"),
Self::UnterminatedTemplateLiteral => Some("add a closing backtick `"),
Self::UnterminatedInterpolation => Some("add a closing brace } to @{...}"),
Self::UnterminatedControlBlock => Some("add a closing brace } to control block"),
Self::UnterminatedDirective => Some("add a closing brace } to directive {$...}"),
Self::UnterminatedIdentBlock => Some("add closing |} to ident block {|...|}"),
Self::InvalidEscapeSequence => Some("use valid escape: \\n, \\t, \\r, \\\\, \\\", \\'"),
Self::UnbalancedBraces => Some("ensure all { have matching }"),
Self::InvalidControlBlock => Some("use {#if}, {#for}, {#while}, or {#match}"),
Self::InvalidDirective => Some("use {$directive_name}"),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct LexError {
pub kind: LexErrorKind,
pub position: usize,
pub mode: String,
pub context: String,
pub snippet: String,
pub expected: Vec<String>,
pub found: Option<String>,
pub help: Option<String>,
pub opened_at: Option<usize>,
}
impl LexError {
pub fn new(kind: LexErrorKind, position: usize) -> Self {
Self {
kind,
position,
mode: String::new(),
context: String::new(),
snippet: String::new(),
expected: Vec::new(),
found: None,
help: None,
opened_at: None,
}
}
pub fn unterminated_string(position: usize, opened_at: usize, quote_char: char) -> Self {
Self {
kind: LexErrorKind::UnterminatedString,
position,
mode: "StringLiteral".to_string(),
context: format!("string starting with {}", quote_char),
snippet: String::new(),
expected: vec![format!("closing {}", quote_char)],
found: Some("end of input".to_string()),
help: Some(format!("add {} to close the string", quote_char)),
opened_at: Some(opened_at),
}
}
pub fn unterminated_template(position: usize, opened_at: usize) -> Self {
Self {
kind: LexErrorKind::UnterminatedTemplateLiteral,
position,
mode: "TemplateLiteral".to_string(),
context: "template literal".to_string(),
snippet: String::new(),
expected: vec!["`".to_string()],
found: Some("end of input".to_string()),
help: Some("add ` to close the template literal".to_string()),
opened_at: Some(opened_at),
}
}
pub fn unterminated_interpolation(position: usize, opened_at: usize, depth: usize) -> Self {
Self {
kind: LexErrorKind::UnterminatedInterpolation,
position,
mode: "Interpolation".to_string(),
context: format!("interpolation at depth {}", depth),
snippet: String::new(),
expected: vec!["}".to_string()],
found: Some("end of input".to_string()),
help: Some("add } to close the @{...} interpolation".to_string()),
opened_at: Some(opened_at),
}
}
pub fn unterminated_control_block(position: usize, opened_at: usize, block_type: &str) -> Self {
Self {
kind: LexErrorKind::UnterminatedControlBlock,
position,
mode: "ControlBlock".to_string(),
context: format!("{} control block", block_type),
snippet: String::new(),
expected: vec!["}".to_string()],
found: Some("end of input".to_string()),
help: Some(format!("add }} to close the {{#{} block", block_type)),
opened_at: Some(opened_at),
}
}
pub fn unexpected_eof(position: usize, context: &str) -> Self {
Self {
kind: LexErrorKind::UnexpectedEof,
position,
mode: String::new(),
context: context.to_string(),
snippet: String::new(),
expected: Vec::new(),
found: None,
help: None,
opened_at: None,
}
}
pub fn invalid_character(position: usize, char: char, context: &str) -> Self {
Self {
kind: LexErrorKind::InvalidCharacter,
position,
mode: String::new(),
context: context.to_string(),
snippet: String::new(),
expected: Vec::new(),
found: Some(format!("'{}'", char)),
help: None,
opened_at: None,
}
}
pub fn invalid_escape(position: usize, sequence: &str) -> Self {
Self {
kind: LexErrorKind::InvalidEscapeSequence,
position,
mode: "StringLiteral".to_string(),
context: "escape sequence".to_string(),
snippet: String::new(),
expected: vec![
"\\n".to_string(),
"\\t".to_string(),
"\\r".to_string(),
"\\\\".to_string(),
],
found: Some(format!("\\{}", sequence)),
help: Some("use a valid escape sequence".to_string()),
opened_at: None,
}
}
pub fn in_mode(mut self, mode: &str) -> Self {
self.mode = mode.to_string();
self
}
pub fn with_context(mut self, context: &str) -> Self {
self.context = context.to_string();
self
}
pub fn with_snippet(mut self, snippet: &str) -> Self {
self.snippet = snippet.to_string();
self
}
pub fn with_expected(mut self, expected: &[&str]) -> Self {
self.expected = expected.iter().map(|s| (*s).to_string()).collect();
self
}
pub fn with_found(mut self, found: &str) -> Self {
self.found = Some(found.to_string());
self
}
pub fn with_help(mut self, help: &str) -> Self {
self.help = Some(help.to_string());
self
}
pub fn opened_at(mut self, pos: usize) -> Self {
self.opened_at = Some(pos);
self
}
pub fn format_with_source(&self, source: &str) -> String {
self.format_with_source_and_file(source, "input", 0)
}
pub fn format_with_source_and_file(
&self,
source: &str,
filename: &str,
line_offset: usize,
) -> String {
let loc = SourceLocation::from_offset(source, self.position);
let absolute_line = loc.line + line_offset;
let mut msg = format!("error: {}\n", self.kind.description());
msg.push_str(&format!(
" --> {}:{}:{}\n",
filename, absolute_line, loc.column
));
let lines: Vec<&str> = source.lines().collect();
if loc.line > 0 && loc.line <= lines.len() {
let line_content = lines[loc.line - 1];
let expanded_content = line_content.replace('\t', " ").trim_end().to_string();
let line_start_offset = source[..self.position]
.rfind('\n')
.map(|pos| pos + 1)
.unwrap_or(0);
let byte_offset_in_line = self.position.saturating_sub(line_start_offset);
let visual_column: usize = line_content
.char_indices()
.take_while(|(i, _)| *i < byte_offset_in_line)
.map(|(_, c)| if c == '\t' { 4 } else { 1 })
.sum();
let annotation = if let Some(ref found) = self.found {
format!("found: {}", found)
} else if !self.expected.is_empty() {
format!("expected: {}", self.expected.join(" or "))
} else {
String::new()
};
const MAX_LINE_LEN: usize = 80;
const CONTEXT_CHARS: usize = 30;
let (display_content, display_caret_col) = if expanded_content.len() > MAX_LINE_LEN {
let start = visual_column.saturating_sub(CONTEXT_CHARS);
let end = (visual_column + CONTEXT_CHARS).min(expanded_content.len());
let content_chars: Vec<char> = expanded_content.chars().collect();
let start = start.min(content_chars.len());
let end = end.min(content_chars.len());
let prefix = if start > 0 { "..." } else { "" };
let suffix = if end < content_chars.len() { "..." } else { "" };
let snippet: String = content_chars[start..end].iter().collect();
let new_caret_col = visual_column - start + prefix.len();
(format!("{}{}{}", prefix, snippet, suffix), new_caret_col)
} else {
(expanded_content.clone(), visual_column)
};
let line_num_width = absolute_line.to_string().len();
let source_line = format!(
"{:>width$} | {}",
absolute_line,
display_content,
width = line_num_width
);
let caret_content = format!("{:>col$}^ {}", "", annotation, col = display_caret_col);
let caret_line = format!("{:>width$} | {}", "", caret_content, width = line_num_width);
let max_len = source_line.len().max(caret_line.len());
let padded_source = format!("{:<width$}", source_line, width = max_len);
let padded_caret = format!("{:<width$}", caret_line, width = max_len);
msg.push_str(&format!("`{}`\n", padded_source));
msg.push_str(&format!("`{}`\n", padded_caret));
}
if let Some(opened_pos) = self.opened_at {
let opened_loc = SourceLocation::from_offset(source, opened_pos);
msg.push_str(&format!(
"opened at: {}:{}:{}\n",
filename,
opened_loc.line + line_offset,
opened_loc.column
));
}
if let Some(ref help) = self.help {
msg.push_str(&format!("help: {}\n", help));
} else if let Some(suggestion) = self.kind.suggestion() {
msg.push_str(&format!("help: {}\n", suggestion));
}
msg
}
pub fn to_message(&self) -> String {
let mut msg = format!("Lex error at position {}: ", self.position);
msg.push_str(self.kind.description());
if !self.mode.is_empty() {
msg.push_str(&format!(" (in {} mode)", self.mode));
}
if let Some(ref found) = self.found {
msg.push_str(&format!(", found {}", found));
}
if !self.expected.is_empty() {
if self.expected.len() == 1 {
msg.push_str(&format!(", expected {}", self.expected[0]));
} else {
msg.push_str(&format!(", expected one of: {}", self.expected.join(", ")));
}
}
if !self.context.is_empty() {
msg.push_str(&format!(" while lexing {}", self.context));
}
if let Some(opened) = self.opened_at {
msg.push_str(&format!(" (opened at position {})", opened));
}
if !self.snippet.is_empty() {
msg.push_str(&format!("\n --> {}", self.snippet));
}
if let Some(ref help) = self.help {
msg.push_str(&format!("\n help: {}", help));
} else if let Some(suggestion) = self.kind.suggestion() {
msg.push_str(&format!("\n help: {}", suggestion));
}
msg
}
}
impl fmt::Display for LexError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_message())
}
}
impl std::error::Error for LexError {}
pub type LexResult<T> = Result<T, LexError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_unterminated_string_error() {
let err = LexError::unterminated_string(50, 10, '"');
let msg = err.to_message();
assert!(msg.contains("position 50"));
assert!(msg.contains("unterminated string"));
assert!(msg.contains("opened at position 10"));
}
#[test]
fn test_unterminated_template_error() {
let err = LexError::unterminated_template(100, 20);
let msg = err.to_message();
assert!(msg.contains("position 100"));
assert!(msg.contains("unterminated template literal"));
}
#[test]
fn test_unterminated_interpolation_error() {
let err = LexError::unterminated_interpolation(75, 30, 2);
let msg = err.to_message();
assert!(msg.contains("position 75"));
assert!(msg.contains("depth 2"));
}
#[test]
fn test_invalid_escape_error() {
let err = LexError::invalid_escape(42, "q");
let msg = err.to_message();
assert!(msg.contains("position 42"));
assert!(msg.contains("invalid escape"));
assert!(msg.contains("\\q"));
}
#[test]
fn test_error_with_snippet() {
let err = LexError::new(LexErrorKind::InvalidCharacter, 10)
.with_snippet("const x = @invalid")
.with_found("@")
.with_context("identifier");
let msg = err.to_message();
assert!(msg.contains("const x = @invalid"));
}
#[test]
fn test_all_error_kinds_have_descriptions() {
let kinds = [
LexErrorKind::UnterminatedString,
LexErrorKind::UnterminatedTemplateLiteral,
LexErrorKind::UnterminatedInterpolation,
LexErrorKind::UnterminatedControlBlock,
LexErrorKind::UnterminatedDirective,
LexErrorKind::UnterminatedIdentBlock,
LexErrorKind::InvalidEscapeSequence,
LexErrorKind::InvalidCharacter,
LexErrorKind::InvalidUnicodeEscape,
LexErrorKind::UnexpectedEof,
LexErrorKind::UnbalancedBraces,
LexErrorKind::InvalidControlBlock,
LexErrorKind::InvalidDirective,
LexErrorKind::InvalidTemplateExpression,
LexErrorKind::InterpolationTooDeep,
];
for kind in kinds {
let desc = kind.description();
assert!(!desc.is_empty(), "{:?} has empty description", kind);
}
}
}