use std::fmt;
#[derive(Debug, Clone)]
pub struct SourceLocation {
pub file: String,
pub line: usize,
pub column: usize,
pub line_content: String,
}
impl SourceLocation {
pub fn new(file: &str, line: usize, column: usize, line_content: &str) -> Self {
SourceLocation {
file: file.to_string(),
line,
column,
line_content: line_content.to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct CompileError {
pub message: String,
pub location: Option<SourceLocation>,
pub hint: Option<String>,
pub hint_location: Option<(usize, usize)>, pub suggestion: Option<String>,
pub error_code: Option<String>,
pub underline_note: Option<(usize, String)>,
pub help_line: Option<String>,
pub note_line: Option<String>,
pub is_warning: bool,
}
impl CompileError {
pub fn new(message: &str) -> Self {
CompileError {
message: message.to_string(),
location: None,
hint: None,
hint_location: None,
suggestion: None,
error_code: None,
underline_note: None,
help_line: None,
note_line: None,
is_warning: false,
}
}
pub fn with_location(mut self, loc: SourceLocation) -> Self {
self.location = Some(loc);
self
}
#[allow(dead_code)]
pub fn with_hint(mut self, hint: &str) -> Self {
self.hint = Some(hint.to_string());
self
}
#[allow(dead_code)]
pub fn with_hint_location(mut self, column: usize, length: usize) -> Self {
self.hint_location = Some((column, length));
self
}
pub fn with_suggestion(mut self, suggestion: &str) -> Self {
self.suggestion = Some(suggestion.to_string());
self
}
#[allow(dead_code)]
pub fn with_code(mut self, code: &str) -> Self {
self.error_code = Some(code.to_string());
self
}
pub fn with_underline_note(mut self, length: usize, note: &str) -> Self {
self.underline_note = Some((length, note.to_string()));
self
}
pub fn with_note_line(mut self, note: &str) -> Self {
self.note_line = Some(note.to_string());
self
}
pub fn with_help_line(mut self, help: &str) -> Self {
self.help_line = Some(help.to_string());
self
}
pub fn as_warning(mut self) -> Self {
self.is_warning = true;
self
}
}
impl From<String> for CompileError {
fn from(s: String) -> Self {
CompileError::new(&s)
}
}
impl From<&str> for CompileError {
fn from(s: &str) -> Self {
CompileError::new(s)
}
}
impl fmt::Display for CompileError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
const RED: &str = "\x1b[1;31m";
const BLUE: &str = "\x1b[1;34m";
const CYAN: &str = "\x1b[1;36m";
const YELLOW: &str = "\x1b[1;33m";
const GREEN: &str = "\x1b[1;32m";
const RESET: &str = "\x1b[0m";
const BOLD: &str = "\x1b[1m";
if self.is_warning {
write!(f, "{}warning{}: {}{}\n", YELLOW, RESET, BOLD, self.message)?;
} else if let Some(ref code) = self.error_code {
write!(f, "{}error[{}]{}: {}{}\n", RED, code, RESET, BOLD, self.message)?;
} else {
write!(f, "{}error{}: {}{}\n", RED, RESET, BOLD, self.message)?;
}
write!(f, "{}", RESET)?;
if let Some(ref loc) = self.location {
write!(f, " {}-->{} {}:{}:{}\n", BLUE, RESET, loc.file, loc.line, loc.column)?;
let line_num_width = loc.line.to_string().len();
write!(f, " {:width$} {}|\n", "", BLUE, width = line_num_width)?;
let separator = format!("{}|{}", BLUE, RESET);
write!(f, " {}{}{} {} {}{}\n",
BLUE, loc.line, RESET,
separator,
loc.line_content.trim_end(),
RESET)?;
let pointer_offset = if loc.column > 0 { loc.column - 1 } else { 0 };
let spaces = " ".repeat(pointer_offset);
if let Some((len, ref note)) = self.underline_note {
let carets = "^".repeat(len);
write!(f, " {:width$} {}| {}{}{} {}\n",
"", BLUE, spaces, RED, carets, note, width = line_num_width)?;
} else {
write!(f, " {:width$} {}| {}{}^--- here{}\n",
"", BLUE, spaces, RED, RESET, width = line_num_width)?;
}
if let (Some(ref hint), Some((hint_col, hint_len))) = (&self.hint, self.hint_location) {
let hint_offset = if hint_col > 0 { hint_col - 1 } else { 0 };
write!(f, " {:width$} {} ", "", BLUE, width = line_num_width)?;
write!(f, "{}{}|{}\n", " ".repeat(hint_offset), BLUE, RESET)?;
write!(f, " {:width$} {} ", "", BLUE, width = line_num_width)?;
let underline = "─".repeat(hint_len);
write!(f, "{}{}┴{}─── {}hint{}: {}\n",
" ".repeat(hint_offset), BLUE, underline, CYAN, RESET, hint)?;
return Ok(()); }
if self.note_line.is_some() || self.help_line.is_some() {
write!(f, " {:width$} {}|\n", "", BLUE, width = line_num_width)?;
}
if let Some(ref note) = self.note_line {
write!(f, " note: {}\n", note)?;
}
if let Some(ref help) = self.help_line {
write!(f, " help: {}\n", help)?;
}
}
if let Some(ref hint) = self.hint {
write!(f, "\n {}hint{}: {}\n", CYAN, RESET, hint)?;
}
if let Some(ref suggestion) = self.suggestion {
write!(f, " {}help{}: did you mean `{}{}{}`?\n", GREEN, RESET, YELLOW, suggestion, RESET)?;
}
Ok(())
}
}
pub fn levenshtein_distance(a: &str, b: &str) -> usize {
let a_lower = a.to_lowercase();
let b_lower = b.to_lowercase();
let a_chars: Vec<char> = a_lower.chars().collect();
let b_chars: Vec<char> = b_lower.chars().collect();
let m = a_chars.len();
let n = b_chars.len();
if m == 0 {
return n;
}
if n == 0 {
return m;
}
let (long, short) = if m >= n {
(&a_chars, &b_chars)
} else {
(&b_chars, &a_chars)
};
let long_len = long.len();
let short_len = short.len();
let mut prev_row: Vec<usize> = (0..=short_len).collect();
let mut curr_row: Vec<usize> = vec![0; short_len + 1];
for i in 1..=long_len {
curr_row[0] = i;
let long_ch = long[i - 1];
for j in 1..=short_len {
let cost = if long_ch == short[j - 1] { 0 } else { 1 };
let deletion = prev_row[j] + 1;
let insertion = curr_row[j - 1] + 1;
let substitution = prev_row[j - 1] + cost;
curr_row[j] = deletion.min(insertion).min(substitution);
}
std::mem::swap(&mut prev_row, &mut curr_row);
}
prev_row[short_len]
}
pub fn find_similar_keyword(word: &str, keywords: &[&str]) -> Option<String> {
let word_lower = word.to_lowercase();
let mut best_match: Option<(String, usize)> = None;
if word.len() <= 2 {
return None;
}
for &keyword in keywords {
let len_diff = word.len().abs_diff(keyword.len());
if len_diff > 2 {
continue;
}
let distance = levenshtein_distance(&word_lower, keyword);
if distance == 0 {
return None;
}
let max_distance = if word.len() >= 4 { 2 } else { 1 };
if distance <= max_distance {
if let Some((_, best_dist)) = &best_match {
if distance < *best_dist {
best_match = Some((keyword.to_string(), distance));
}
} else {
best_match = Some((keyword.to_string(), distance));
}
}
}
best_match.map(|(s, _)| s)
}
pub const ENGLISH_KEYWORDS: &[&str] = &[
"print", "set", "create", "add", "subtract", "multiply", "divide",
"increment", "decrement", "call", "allocate", "free",
"append", "copy", "clear",
"open", "read", "write", "close", "delete", "exists", "resize", "seek",
"if", "when", "then", "else", "but", "otherwise", "while", "until",
"for", "each", "every", "loop", "repeat", "times", "break", "continue",
"return", "exit", "with", "called", "modulo",
"is", "are", "equals", "equal", "greater", "less", "than", "not", "and", "or",
"from", "to", "between", "through", "in", "of", "on", "the", "a", "an", "all", "by",
"treating", "as",
"number", "text", "boolean", "list", "true", "false",
"buffer", "file", "bytes", "size", "into", "reading", "writing", "appending",
"standard", "input", "output",
"even", "odd", "positive", "negative", "zero", "empty",
"capacity", "length", "first", "last", "count",
"error", "stderr", "auto", "catching", "enable", "disable",
"see", "library", "version",
"argument", "arguments", "environment", "variable",
"define", "function", "end", "returning", "taking",
];
pub struct SourceFile {
pub filename: String,
#[allow(dead_code)]
pub content: String,
lines: Vec<String>,
}
impl SourceFile {
pub fn new(filename: &str, content: &str) -> Self {
let lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
SourceFile {
filename: filename.to_string(),
content: content.to_string(),
lines,
}
}
pub fn get_line(&self, line_num: usize) -> Option<&str> {
if line_num > 0 && line_num <= self.lines.len() {
Some(&self.lines[line_num - 1])
} else {
None
}
}
pub fn make_location(&self, line: usize, column: usize) -> SourceLocation {
let line_content = self.get_line(line).unwrap_or("").to_string();
SourceLocation::new(&self.filename, line, column, &line_content)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_levenshtein() {
assert_eq!(levenshtein_distance("print", "print"), 0);
assert_eq!(levenshtein_distance("print", "pront"), 1);
assert_eq!(levenshtein_distance("print", "prnt"), 1);
assert_eq!(levenshtein_distance("create", "crate"), 1);
}
#[test]
fn test_find_similar() {
assert_eq!(find_similar_keyword("pirnt", ENGLISH_KEYWORDS), Some("print".to_string()));
assert_eq!(find_similar_keyword("crate", ENGLISH_KEYWORDS), Some("create".to_string()));
assert_eq!(find_similar_keyword("bufer", ENGLISH_KEYWORDS), Some("buffer".to_string()));
}
}