dev/pbs #8

Merged
bquarkz merged 74 commits from dev/pbs into master 2026-02-03 15:28:31 +00:00
3 changed files with 172 additions and 14 deletions
Showing only changes of commit 9515bddce8 - Show all commits

View File

@ -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 {

View File

@ -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),
};

View 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"));
}