diff --git a/crates/prometeu-compiler/src/common/diagnostics.rs b/crates/prometeu-compiler/src/common/diagnostics.rs index 1dfed996..399f9628 100644 --- a/crates/prometeu-compiler/src/common/diagnostics.rs +++ b/crates/prometeu-compiler/src/common/diagnostics.rs @@ -1,20 +1,35 @@ use crate::common::spans::Span; +use serde::{Serialize, Serializer}; +use crate::common::files::FileManager; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum DiagnosticLevel { Error, Warning, } -#[derive(Debug, Clone)] +impl Serialize for DiagnosticLevel { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + DiagnosticLevel::Error => serializer.serialize_str("error"), + DiagnosticLevel::Warning => serializer.serialize_str("warning"), + } + } +} + +#[derive(Debug, Clone, Serialize)] pub struct Diagnostic { + #[serde(rename = "severity")] pub level: DiagnosticLevel, pub code: Option, pub message: String, pub span: Option, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct DiagnosticBundle { pub diagnostics: Vec, } @@ -46,6 +61,45 @@ impl DiagnosticBundle { .iter() .any(|d| matches!(d.level, DiagnosticLevel::Error)) } + + /// Serializes the diagnostic bundle to canonical JSON, resolving file IDs via FileManager. + pub fn to_json(&self, file_manager: &FileManager) -> String { + #[derive(Serialize)] + struct CanonicalSpan { + file: String, + start: u32, + end: u32, + } + + #[derive(Serialize)] + struct CanonicalDiag { + severity: DiagnosticLevel, + code: String, + message: String, + span: Option, + } + + let canonical_diags: Vec = self.diagnostics.iter().map(|d| { + let canonical_span = d.span.and_then(|s| { + file_manager.get_path(s.file_id).map(|p| { + CanonicalSpan { + file: p.file_name().unwrap().to_string_lossy().to_string(), + start: s.start, + end: s.end, + } + }) + }); + + CanonicalDiag { + severity: d.level.clone(), + code: d.code.clone().unwrap_or_else(|| "E_UNKNOWN".to_string()), + message: d.message.clone(), + span: canonical_span, + } + }).collect(); + + serde_json::to_string_pretty(&canonical_diags).unwrap() + } } impl From for DiagnosticBundle { diff --git a/crates/prometeu-compiler/src/frontends/pbs/parser.rs b/crates/prometeu-compiler/src/frontends/pbs/parser.rs index 96d33f2f..790d67c9 100644 --- a/crates/prometeu-compiler/src/frontends/pbs/parser.rs +++ b/crates/prometeu-compiler/src/frontends/pbs/parser.rs @@ -91,7 +91,7 @@ impl Parser { let span = self.advance().span; (s, span) } - _ => return Err(self.error("Expected string literal after 'from'")), + _ => return Err(self.error_with_code("Expected string literal after 'from'", Some("E_PARSE_EXPECTED_TOKEN"))), }; if self.peek().kind == TokenKind::Semicolon { @@ -133,7 +133,16 @@ impl Parser { match self.peek().kind { TokenKind::Fn => self.parse_fn_decl(), TokenKind::Pub | TokenKind::Mod | TokenKind::Declare | TokenKind::Service => self.parse_decl(), - _ => Err(self.error("Expected top-level declaration")), + TokenKind::Invalid(ref msg) => { + let code = if msg.contains("Unterminated string") { + "E_LEX_UNTERMINATED_STRING" + } else { + "E_LEX_INVALID_CHAR" + }; + let msg = msg.clone(); + Err(self.error_with_code(&msg, Some(code))) + } + _ => Err(self.error_with_code("Expected top-level declaration", Some("E_PARSE_UNEXPECTED_TOKEN"))), } } @@ -212,7 +221,7 @@ impl Parser { TokenKind::Struct => { self.advance(); "struct".to_string() } TokenKind::Contract => { self.advance(); "contract".to_string() } TokenKind::Error => { self.advance(); "error".to_string() } - _ => return Err(self.error("Expected 'struct', 'contract', or 'error'")), + _ => return Err(self.error_with_code("Expected 'struct', 'contract', or 'error'", Some("E_PARSE_EXPECTED_TOKEN"))), }; let name = self.expect_identifier()?; @@ -443,7 +452,7 @@ impl Parser { match *cast.ty { Node::Ident(id) => (*cast.expr, id.name), Node::TypeName(tn) => (*cast.expr, tn.name), - _ => return Err(self.error("Expected binding name after 'as'")), + _ => return Err(self.error_with_code("Expected binding name after 'as'", Some("E_PARSE_EXPECTED_TOKEN"))), } } _ => { @@ -572,7 +581,15 @@ impl Parser { expr: Box::new(expr), })) } - _ => Err(self.error("Expected expression")), + TokenKind::Invalid(msg) => { + let code = if msg.contains("Unterminated string") { + "E_LEX_UNTERMINATED_STRING" + } else { + "E_LEX_INVALID_CHAR" + }; + Err(self.error_with_code(&msg, Some(code))) + } + _ => Err(self.error_with_code("Expected expression", Some("E_PARSE_UNEXPECTED_TOKEN"))), } } @@ -696,17 +713,28 @@ impl Parser { } fn consume(&mut self, kind: TokenKind) -> Result { - if self.peek().kind == kind { + let peeked_kind = self.peek().kind.clone(); + if peeked_kind == kind { Ok(self.advance()) } else { - Err(self.error(&format!("Expected {:?}", kind))) + if let TokenKind::Invalid(ref msg) = peeked_kind { + let code = if msg.contains("Unterminated string") { + "E_LEX_UNTERMINATED_STRING" + } else { + "E_LEX_INVALID_CHAR" + }; + let msg = msg.clone(); + Err(self.error_with_code(&msg, Some(code))) + } else { + Err(self.error_with_code(&format!("Expected {:?}, found {:?}", kind, peeked_kind), Some("E_PARSE_EXPECTED_TOKEN"))) + } } } fn expect_identifier(&mut self) -> Result { - match &self.peek().kind { + let peeked_kind = self.peek().kind.clone(); + match peeked_kind { TokenKind::Identifier(name) => { - let name = name.clone(); self.advance(); Ok(name) } @@ -734,14 +762,26 @@ impl Parser { self.advance(); Ok("err".to_string()) } - _ => Err(self.error("Expected identifier")), + TokenKind::Invalid(msg) => { + let code = if msg.contains("Unterminated string") { + "E_LEX_UNTERMINATED_STRING" + } else { + "E_LEX_INVALID_CHAR" + }; + Err(self.error_with_code(&msg, Some(code))) + } + _ => Err(self.error_with_code("Expected identifier", Some("E_PARSE_EXPECTED_TOKEN"))), } } fn error(&mut self, message: &str) -> DiagnosticBundle { + self.error_with_code(message, None) + } + + fn error_with_code(&mut self, message: &str, code: Option<&str>) -> DiagnosticBundle { let diag = Diagnostic { level: DiagnosticLevel::Error, - code: None, + code: code.map(|c| c.to_string()), message: message.to_string(), span: Some(self.peek().span), }; diff --git a/crates/prometeu-compiler/tests/pbs_diagnostics_tests.rs b/crates/prometeu-compiler/tests/pbs_diagnostics_tests.rs new file mode 100644 index 00000000..13e7419d --- /dev/null +++ b/crates/prometeu-compiler/tests/pbs_diagnostics_tests.rs @@ -0,0 +1,64 @@ +use prometeu_compiler::frontends::pbs::PbsFrontend; +use prometeu_compiler::frontends::Frontend; +use prometeu_compiler::common::files::FileManager; +use std::fs; +use tempfile::tempdir; + +fn get_diagnostics(code: &str) -> String { + let mut file_manager = FileManager::new(); + let temp_dir = tempdir().unwrap(); + let file_path = temp_dir.path().join("main.pbs"); + fs::write(&file_path, code).unwrap(); + + let frontend = PbsFrontend; + match frontend.compile_to_ir(&file_path, &mut file_manager) { + Ok(_) => "[]".to_string(), + Err(bundle) => bundle.to_json(&file_manager), + } +} + +#[test] +fn test_golden_parse_error() { + let code = "fn main() { let x = ; }"; + let json = get_diagnostics(code); + println!("{}", json); + assert!(json.contains("E_PARSE_UNEXPECTED_TOKEN")); + assert!(json.contains("Expected expression")); +} + +#[test] +fn test_golden_lex_error() { + let code = "fn main() { let x = \"hello ; }"; + let json = get_diagnostics(code); + println!("{}", json); + assert!(json.contains("E_LEX_UNTERMINATED_STRING")); +} + +#[test] +fn test_golden_resolve_error() { + let code = "fn main() { let x = undefined_var; }"; + let json = get_diagnostics(code); + println!("{}", json); + assert!(json.contains("E_RESOLVE_UNDEFINED")); +} + +#[test] +fn test_golden_type_error() { + let code = "fn main() { let x: int = \"hello\"; }"; + let json = get_diagnostics(code); + println!("{}", json); + assert!(json.contains("E_TYPE_MISMATCH")); +} + +#[test] +fn test_golden_namespace_collision() { + let code = " + declare struct Foo {} + fn main() { + let Foo = 1; + } + "; + let json = get_diagnostics(code); + println!("{}", json); + assert!(json.contains("E_RESOLVE_NAMESPACE_COLLISION")); +}