pr 12
This commit is contained in:
parent
f37c15f3d6
commit
9515bddce8
@ -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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
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<String>,
|
||||
pub message: String,
|
||||
pub span: Option<Span>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct DiagnosticBundle {
|
||||
pub diagnostics: Vec<Diagnostic>,
|
||||
}
|
||||
@ -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<CanonicalSpan>,
|
||||
}
|
||||
|
||||
let canonical_diags: Vec<CanonicalDiag> = 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<Diagnostic> for DiagnosticBundle {
|
||||
|
||||
@ -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<Token, DiagnosticBundle> {
|
||||
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<String, DiagnosticBundle> {
|
||||
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),
|
||||
};
|
||||
|
||||
64
crates/prometeu-compiler/tests/pbs_diagnostics_tests.rs
Normal file
64
crates/prometeu-compiler/tests/pbs_diagnostics_tests.rs
Normal file
@ -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"));
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user