use std::path::PathBuf;
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SourceLocation {
pub file_id: usize,
pub line: usize,
pub column: usize,
pub offset: usize,
pub length: usize,
}
impl SourceLocation {
pub fn new(file_id: usize, line: usize, column: usize, offset: usize, length: usize) -> Self {
Self {
file_id,
line,
column,
offset,
length,
}
}
pub fn format(&self) -> String {
format!("{}:{}:{}", self.line, self.column, self.length)
}
pub fn is_valid(&self) -> bool {
self.line > 0 && self.column > 0
}
}
impl std::fmt::Display for SourceLocation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.format())
}
}
pub struct SourceManager {
files: Vec<PathBuf>,
file_contents: Vec<String>,
}
impl SourceManager {
pub fn new() -> Self {
Self {
files: Vec::new(),
file_contents: Vec::new(),
}
}
pub fn add_file(&mut self, path: PathBuf, content: String) -> usize {
let file_id = self.files.len();
self.files.push(path);
self.file_contents.push(content);
file_id
}
pub fn get_file_path(&self, file_id: usize) -> Option<&PathBuf> {
self.files.get(file_id)
}
pub fn get_file_content(&self, file_id: usize) -> Option<&str> {
self.file_contents.get(file_id).map(|s| s.as_str())
}
pub fn file_count(&self) -> usize {
self.files.len()
}
pub fn resolve_location(&self, file_id: usize, offset: usize) -> SourceLocation {
if let Some(content) = self.get_file_content(file_id) {
let mut line = 1;
let mut column = 1;
let mut current_offset = 0;
for (i, c) in content.chars().enumerate() {
if i >= offset {
break;
}
if c == '\n' {
line += 1;
column = 1;
} else {
column += 1;
}
current_offset = i + 1;
}
SourceLocation {
file_id,
line,
column,
offset: current_offset,
length: 1, }
} else {
SourceLocation {
file_id,
line: 1,
column: 1,
offset: 0,
length: 0,
}
}
}
}
#[derive(Debug, Clone)]
pub struct ErrorSuggestion {
pub message: String,
pub code_snippet: Option<String>,
pub replacement: Option<String>,
}
impl ErrorSuggestion {
pub fn new(message: String) -> Self {
Self {
message,
code_snippet: None,
replacement: None,
}
}
pub fn with_snippet(message: String, snippet: String) -> Self {
Self {
message,
code_snippet: Some(snippet),
replacement: None,
}
}
pub fn with_replacement(message: String, replacement: String) -> Self {
Self {
message,
code_snippet: None,
replacement: Some(replacement),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiagnosticSeverity {
Error,
Warning,
Info,
Hint,
}
impl DiagnosticSeverity {
pub fn as_str(&self) -> &'static str {
match self {
DiagnosticSeverity::Error => "error",
DiagnosticSeverity::Warning => "warning",
DiagnosticSeverity::Info => "info",
DiagnosticSeverity::Hint => "hint",
}
}
pub fn should_report(&self) -> bool {
matches!(self, DiagnosticSeverity::Error | DiagnosticSeverity::Warning)
}
}
#[derive(Debug, Clone)]
pub struct Diagnostic {
pub severity: DiagnosticSeverity,
pub location: SourceLocation,
pub message: String,
pub code: Option<String>,
pub suggestions: Vec<ErrorSuggestion>,
pub related_diagnostics: Vec<Diagnostic>,
}
impl Diagnostic {
pub fn new(severity: DiagnosticSeverity, location: SourceLocation, message: String) -> Self {
Self {
severity,
location,
message,
code: None,
suggestions: Vec::new(),
related_diagnostics: Vec::new(),
}
}
pub fn error(location: SourceLocation, message: String) -> Self {
Self::new(DiagnosticSeverity::Error, location, message)
}
pub fn warning(location: SourceLocation, message: String) -> Self {
Self::new(DiagnosticSeverity::Warning, location, message)
}
pub fn info(location: SourceLocation, message: String) -> Self {
Self::new(DiagnosticSeverity::Info, location, message)
}
pub fn hint(location: SourceLocation, message: String) -> Self {
Self::new(DiagnosticSeverity::Hint, location, message)
}
pub fn with_code(mut self, code: String) -> Self {
self.code = Some(code);
self
}
pub fn with_suggestion(mut self, suggestion: ErrorSuggestion) -> Self {
self.suggestions.push(suggestion);
self
}
pub fn with_suggestions(mut self, suggestions: Vec<ErrorSuggestion>) -> Self {
self.suggestions.extend(suggestions);
self
}
pub fn with_related(mut self, related: Diagnostic) -> Self {
self.related_diagnostics.push(related);
self
}
pub fn format(&self, source_manager: &SourceManager) -> String {
let file_path = if let Some(path) = source_manager.get_file_path(self.location.file_id) {
path.to_string_lossy().to_string()
} else {
format!("<file {}>", self.location.file_id)
};
let location_str = if self.location.is_valid() {
format!("{}:{}", file_path, self.location.format())
} else {
file_path
};
let mut output = format!("{}: {}: {}", location_str, self.severity.as_str(), self.message);
if let Some(code) = &self.code {
output.push_str(&format!(" (code: {})", code));
}
for suggestion in &self.suggestions {
output.push_str(&format!("\n = note: {}", suggestion.message));
}
for related in &self.related_diagnostics {
output.push_str(&format!("\n = {}: {}", related.severity.as_str(), related.message));
}
output
}
}
#[derive(Error, Debug)]
pub enum CompilerError {
#[error("IO error: {0}")]
IoError(String),
#[error("Lexical error at {location}: {message}")]
LexerError {
location: SourceLocation,
message: String,
},
#[error("Syntax error at {location}: {message}")]
SyntaxError {
location: SourceLocation,
message: String,
expected_tokens: Vec<String>,
},
#[error("Semantic error at {location}: {message}")]
SemanticError {
location: SourceLocation,
message: String,
suggestion: Option<String>,
},
#[error("Type error at {location}: {message}")]
TypeError {
location: SourceLocation,
message: String,
expected: String,
found: String,
},
#[error("Code generation error: {message}")]
CodegenError {
location: Option<SourceLocation>,
message: String,
},
#[error("Optimizer error: {message}")]
OptimizerError {
location: Option<SourceLocation>,
message: String,
},
#[error("Runtime error: {message}")]
RuntimeError {
location: Option<SourceLocation>,
message: String,
},
#[error("Internal compiler error: {message}")]
InternalError {
location: Option<SourceLocation>,
message: String,
},
#[error("Configuration error: {message}")]
ConfigError {
message: String,
},
}
impl CompilerError {
pub fn lexer_error(location: SourceLocation, message: String) -> Self {
Self::LexerError { location, message }
}
pub fn syntax_error(location: SourceLocation, message: String, expected_tokens: Vec<String>) -> Self {
Self::SyntaxError { location, message, expected_tokens }
}
pub fn semantic_error(location: SourceLocation, message: String, suggestion: Option<String>) -> Self {
Self::SemanticError { location, message, suggestion }
}
pub fn type_error(location: SourceLocation, message: String, expected: String, found: String) -> Self {
Self::TypeError { location, message, expected, found }
}
pub fn codegen_error(location: Option<SourceLocation>, message: String) -> Self {
Self::CodegenError { location, message }
}
pub fn runtime_error(location: Option<SourceLocation>, message: String) -> Self {
Self::RuntimeError { location, message }
}
pub fn internal_error(location: Option<SourceLocation>, message: String) -> Self {
Self::InternalError { location, message }
}
pub fn config_error(message: String) -> Self {
Self::ConfigError { message }
}
pub fn severity(&self) -> DiagnosticSeverity {
match self {
CompilerError::IoError(_) => DiagnosticSeverity::Error,
CompilerError::LexerError { .. } => DiagnosticSeverity::Error,
CompilerError::SyntaxError { .. } => DiagnosticSeverity::Error,
CompilerError::SemanticError { .. } => DiagnosticSeverity::Error,
CompilerError::TypeError { .. } => DiagnosticSeverity::Error,
CompilerError::CodegenError { .. } => DiagnosticSeverity::Error,
CompilerError::OptimizerError { .. } => DiagnosticSeverity::Error,
CompilerError::RuntimeError { .. } => DiagnosticSeverity::Error,
CompilerError::InternalError { .. } => DiagnosticSeverity::Error,
CompilerError::ConfigError { .. } => DiagnosticSeverity::Error,
}
}
pub fn location(&self) -> Option<&SourceLocation> {
match self {
CompilerError::LexerError { location, .. } => Some(location),
CompilerError::SyntaxError { location, .. } => Some(location),
CompilerError::SemanticError { location, .. } => Some(location),
CompilerError::TypeError { location, .. } => Some(location),
CompilerError::CodegenError { location, .. } => location.as_ref(),
CompilerError::OptimizerError { location, .. } => location.as_ref(),
CompilerError::RuntimeError { location, .. } => location.as_ref(),
CompilerError::InternalError { location, .. } => location.as_ref(),
CompilerError::IoError(_) | CompilerError::ConfigError { .. } => None,
}
}
pub fn to_diagnostic(&self, source_manager: &SourceManager) -> Diagnostic {
let severity = self.severity();
let location = self.location().copied().unwrap_or_else(|| SourceLocation::new(0, 0, 0, 0, 0));
let message = self.to_string();
match self {
CompilerError::SyntaxError { expected_tokens, .. } => {
let suggestions = expected_tokens
.iter()
.map(|token| {
let mut suggestion = ErrorSuggestion::new(format!("Did you mean '{}'?", token));
suggestion.code_snippet = Some(token.clone());
suggestion
})
.collect();
Diagnostic::error(location, message)
.with_suggestions(suggestions)
},
CompilerError::SemanticError { suggestion, .. } => {
let suggestions = if let Some(suggestion) = suggestion {
vec![ErrorSuggestion::new(suggestion.clone())]
} else {
Vec::new()
};
Diagnostic::error(location, message)
.with_suggestions(suggestions)
},
CompilerError::TypeError { expected, found, .. } => {
let suggestion = ErrorSuggestion::new(format!("Expected type '{}', found type '{}'", expected, found));
Diagnostic::error(location, message)
.with_suggestion(suggestion)
},
_ => Diagnostic::error(location, message),
}
}
}
pub struct ErrorReporter {
diagnostics: Vec<Diagnostic>,
source_manager: SourceManager,
}
impl ErrorReporter {
pub fn new() -> Self {
Self {
diagnostics: Vec::new(),
source_manager: SourceManager::new(),
}
}
pub fn add_diagnostic(&mut self, diagnostic: Diagnostic) {
self.diagnostics.push(diagnostic);
}
pub fn report_error(&mut self, error: CompilerError) {
let diagnostic = error.to_diagnostic(&self.source_manager);
self.add_diagnostic(diagnostic);
}
pub fn add_file(&mut self, path: PathBuf, content: String) -> usize {
self.source_manager.add_file(path, content)
}
pub fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
pub fn errors(&self) -> Vec<&Diagnostic> {
self.diagnostics
.iter()
.filter(|d| d.severity == DiagnosticSeverity::Error)
.collect()
}
pub fn warnings(&self) -> Vec<&Diagnostic> {
self.diagnostics
.iter()
.filter(|d| d.severity == DiagnosticSeverity::Warning)
.collect()
}
pub fn has_errors(&self) -> bool {
self.errors().is_empty()
}
pub fn has_warnings(&self) -> bool {
self.warnings().is_empty()
}
pub fn error_count(&self) -> usize {
self.errors().len()
}
pub fn warning_count(&self) -> usize {
self.warnings().len()
}
pub fn format_diagnostics(&self) -> String {
let mut output = String::new();
for diagnostic in &self.diagnostics {
if !output.is_empty() {
output.push('\n');
}
output.push_str(&diagnostic.format(&self.source_manager));
}
output
}
pub fn clear(&mut self) {
self.diagnostics.clear();
}
}
impl Default for ErrorReporter {
fn default() -> Self {
Self::new()
}
}
pub mod error_utils {
use super::*;
pub fn did_you_mean(identifier: &str, candidates: &[&str]) -> Option<String> {
if candidates.is_empty() {
return None;
}
let best_candidate = candidates.iter()
.map(|c| (c, strsim::jaro_winkler(identifier, c)))
.max_by(|(_, sim1), (_, sim2)| sim1.partial_cmp(sim2).unwrap_or(std::cmp::Ordering::Less))?;
if best_candidate.1 > 0.7 { Some(format!("Did you mean '{}'?", best_candidate.0))
} else {
None
}
}
pub fn suggest_semicolon() -> ErrorSuggestion {
ErrorSuggestion::with_replacement(
"Missing semicolon".to_string(),
";".to_string(),
)
}
pub fn suggest_closing_bracket(opening: char) -> ErrorSuggestion {
let closing = match opening {
'(' => ')',
'[' => ']',
'{' => '}',
_ => '?',
};
ErrorSuggestion::with_replacement(
format!("Missing closing '{}'", closing),
closing.to_string(),
)
}
pub fn suggest_begin_end() -> ErrorSuggestion {
ErrorSuggestion::new(
"Consider using 'begin' and 'end' to group statements".to_string(),
)
}
}
pub use error_utils::*;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_source_location() {
let location = SourceLocation::new(1, 10, 5, 100, 10);
assert_eq!(location.file_id, 1);
assert_eq!(location.line, 10);
assert_eq!(location.column, 5);
assert_eq!(location.offset, 100);
assert_eq!(location.length, 10);
assert!(location.is_valid());
}
#[test]
fn test_diagnostic_creation() {
let location = SourceLocation::new(0, 1, 1, 0, 1);
let diagnostic = Diagnostic::error(location, "Test error message".to_string());
assert_eq!(diagnostic.severity, DiagnosticSeverity::Error);
assert_eq!(diagnostic.location, location);
assert_eq!(diagnostic.message, "Test error message");
assert!(diagnostic.suggestions.is_empty());
}
#[test]
fn test_error_reporter() {
let mut reporter = ErrorReporter::new();
let location = SourceLocation::new(0, 1, 1, 0, 1);
let diagnostic = Diagnostic::error(location, "Test error".to_string());
reporter.add_diagnostic(diagnostic);
assert_eq!(reporter.error_count(), 1);
assert_eq!(reporter.warning_count(), 0);
assert!(!reporter.has_errors());
assert!(reporter.has_warnings());
}
#[test]
fn test_error_utils() {
let candidates = ["variable", "function", "procedure", "begin"];
let suggestion = error_utils::did_you_mean("varible", &candidates);
assert!(suggestion.is_some());
assert!(suggestion.unwrap().contains("variable"));
}
}