pr 12
This commit is contained in:
parent
f37c15f3d6
commit
9515bddce8
@ -1,20 +1,35 @@
|
|||||||
use crate::common::spans::Span;
|
use crate::common::spans::Span;
|
||||||
|
use serde::{Serialize, Serializer};
|
||||||
|
use crate::common::files::FileManager;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum DiagnosticLevel {
|
pub enum DiagnosticLevel {
|
||||||
Error,
|
Error,
|
||||||
Warning,
|
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 {
|
pub struct Diagnostic {
|
||||||
|
#[serde(rename = "severity")]
|
||||||
pub level: DiagnosticLevel,
|
pub level: DiagnosticLevel,
|
||||||
pub code: Option<String>,
|
pub code: Option<String>,
|
||||||
pub message: String,
|
pub message: String,
|
||||||
pub span: Option<Span>,
|
pub span: Option<Span>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct DiagnosticBundle {
|
pub struct DiagnosticBundle {
|
||||||
pub diagnostics: Vec<Diagnostic>,
|
pub diagnostics: Vec<Diagnostic>,
|
||||||
}
|
}
|
||||||
@ -46,6 +61,45 @@ impl DiagnosticBundle {
|
|||||||
.iter()
|
.iter()
|
||||||
.any(|d| matches!(d.level, DiagnosticLevel::Error))
|
.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 {
|
impl From<Diagnostic> for DiagnosticBundle {
|
||||||
|
|||||||
@ -91,7 +91,7 @@ impl Parser {
|
|||||||
let span = self.advance().span;
|
let span = self.advance().span;
|
||||||
(s, 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 {
|
if self.peek().kind == TokenKind::Semicolon {
|
||||||
@ -133,7 +133,16 @@ impl Parser {
|
|||||||
match self.peek().kind {
|
match self.peek().kind {
|
||||||
TokenKind::Fn => self.parse_fn_decl(),
|
TokenKind::Fn => self.parse_fn_decl(),
|
||||||
TokenKind::Pub | TokenKind::Mod | TokenKind::Declare | TokenKind::Service => self.parse_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::Struct => { self.advance(); "struct".to_string() }
|
||||||
TokenKind::Contract => { self.advance(); "contract".to_string() }
|
TokenKind::Contract => { self.advance(); "contract".to_string() }
|
||||||
TokenKind::Error => { self.advance(); "error".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()?;
|
let name = self.expect_identifier()?;
|
||||||
|
|
||||||
@ -443,7 +452,7 @@ impl Parser {
|
|||||||
match *cast.ty {
|
match *cast.ty {
|
||||||
Node::Ident(id) => (*cast.expr, id.name),
|
Node::Ident(id) => (*cast.expr, id.name),
|
||||||
Node::TypeName(tn) => (*cast.expr, tn.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),
|
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> {
|
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())
|
Ok(self.advance())
|
||||||
} else {
|
} 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> {
|
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) => {
|
TokenKind::Identifier(name) => {
|
||||||
let name = name.clone();
|
|
||||||
self.advance();
|
self.advance();
|
||||||
Ok(name)
|
Ok(name)
|
||||||
}
|
}
|
||||||
@ -734,14 +762,26 @@ impl Parser {
|
|||||||
self.advance();
|
self.advance();
|
||||||
Ok("err".to_string())
|
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 {
|
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 {
|
let diag = Diagnostic {
|
||||||
level: DiagnosticLevel::Error,
|
level: DiagnosticLevel::Error,
|
||||||
code: None,
|
code: code.map(|c| c.to_string()),
|
||||||
message: message.to_string(),
|
message: message.to_string(),
|
||||||
span: Some(self.peek().span),
|
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