From 836b280998329dcb91b7d2f1cae6b658dd574096 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Thu, 5 Feb 2026 01:23:23 +0000 Subject: [PATCH] pr 06 --- .../src/analysis/symbols/mod.rs | 12 +- .../prometeu-compiler/src/building/output.rs | 6 +- .../src/common/diagnostics.rs | 164 ++++++++---------- .../src/frontends/pbs/collector.rs | 26 +-- .../src/frontends/pbs/lowering.rs | 22 +-- .../src/frontends/pbs/mod.rs | 18 +- .../src/frontends/pbs/parser.rs | 9 +- .../src/frontends/pbs/resolve.rs | 41 +++-- .../src/frontends/pbs/resolver.rs | 152 +++++++++------- .../src/frontends/pbs/typecheck.rs | 56 +++--- 10 files changed, 268 insertions(+), 238 deletions(-) diff --git a/crates/prometeu-compiler/src/analysis/symbols/mod.rs b/crates/prometeu-compiler/src/analysis/symbols/mod.rs index 2a1629fc..93f39ae2 100644 --- a/crates/prometeu-compiler/src/analysis/symbols/mod.rs +++ b/crates/prometeu-compiler/src/analysis/symbols/mod.rs @@ -1,4 +1,4 @@ -use crate::common::diagnostics::{Diagnostic, DiagnosticLevel}; +use crate::common::diagnostics::{Diagnostic, Severity}; use crate::common::spans::Span; use crate::frontends::pbs::ast::{AstArena, NodeId}; use prometeu_analysis::NameId; @@ -89,10 +89,12 @@ impl DefIndex { pub fn insert_symbol(&mut self, key: DefKey, symbol_id: SymbolId) -> Result<(), Diagnostic> { if self.symbols.contains_key(&key) { return Err(Diagnostic { - level: DiagnosticLevel::Error, - code: Some("E_RESOLVE_DUPLICATE_SYMBOL".to_string()), + severity: Severity::Error, + code: "E_RESOLVE_DUPLICATE_SYMBOL".to_string(), message: "Duplicate symbol in the same module and namespace".to_string(), - span: None, + // Placeholder span; callers should overwrite with accurate span when known. + span: Span::new(0, 0, 0), + related: Vec::new(), }); } @@ -215,7 +217,7 @@ mod tests { assert!(index.insert_symbol(key, SymbolId(0)).is_ok()); let err = index.insert_symbol(key, SymbolId(1)).unwrap_err(); - assert_eq!(err.code, Some("E_RESOLVE_DUPLICATE_SYMBOL".to_string())); + assert_eq!(err.code, "E_RESOLVE_DUPLICATE_SYMBOL"); } #[test] diff --git a/crates/prometeu-compiler/src/building/output.rs b/crates/prometeu-compiler/src/building/output.rs index d6d2b46b..ed71dac6 100644 --- a/crates/prometeu-compiler/src/building/output.rs +++ b/crates/prometeu-compiler/src/building/output.rs @@ -149,12 +149,13 @@ pub fn compile_project( for sym in ts.symbols.into_values() { if let Some(existing) = ms.type_symbols.get(sym.name) { return Err(DiagnosticBundle::error( + "E_RESOLVE_DUPLICATE_SYMBOL", format!( "Duplicate type symbol '{}' in module '{}'", interner.resolve(existing.name), module_path ), - Some(existing.span) + existing.span, ).into()); } let _ = ms.type_symbols.insert(sym); @@ -162,12 +163,13 @@ pub fn compile_project( for sym in vs.symbols.into_values() { if let Some(existing) = ms.value_symbols.get(sym.name) { return Err(DiagnosticBundle::error( + "E_RESOLVE_DUPLICATE_SYMBOL", format!( "Duplicate value symbol '{}' in module '{}'", interner.resolve(existing.name), module_path ), - Some(existing.span) + existing.span, ).into()); } let _ = ms.value_symbols.insert(sym); diff --git a/crates/prometeu-compiler/src/common/diagnostics.rs b/crates/prometeu-compiler/src/common/diagnostics.rs index b1b6cbc6..f209a389 100644 --- a/crates/prometeu-compiler/src/common/diagnostics.rs +++ b/crates/prometeu-compiler/src/common/diagnostics.rs @@ -3,30 +3,30 @@ use crate::common::spans::Span; use serde::{Serialize, Serializer}; #[derive(Debug, Clone, PartialEq)] -pub enum DiagnosticLevel { +pub enum Severity { Error, Warning, } -impl Serialize for DiagnosticLevel { +impl Serialize for Severity { fn serialize(&self, serializer: S) -> Result where S: Serializer, { match self { - DiagnosticLevel::Error => serializer.serialize_str("error"), - DiagnosticLevel::Warning => serializer.serialize_str("warning"), + Severity::Error => serializer.serialize_str("error"), + Severity::Warning => serializer.serialize_str("warning"), } } } #[derive(Debug, Clone, Serialize)] pub struct Diagnostic { - #[serde(rename = "severity")] - pub level: DiagnosticLevel, - pub code: Option, + pub severity: Severity, + pub code: String, pub message: String, - pub span: Option, + pub span: Span, + pub related: Vec<(String, Span)>, } #[derive(Debug, Clone, Serialize)] @@ -45,13 +45,14 @@ impl DiagnosticBundle { self.diagnostics.push(diagnostic); } - pub fn error(message: String, span: Option) -> Self { + pub fn error(code: &str, message: String, span: Span) -> Self { let mut bundle = Self::new(); bundle.push(Diagnostic { - level: DiagnosticLevel::Error, - code: None, + severity: Severity::Error, + code: code.to_string(), message, span, + related: Vec::new(), }); bundle } @@ -59,10 +60,11 @@ impl DiagnosticBundle { pub fn has_errors(&self) -> bool { self.diagnostics .iter() - .any(|d| matches!(d.level, DiagnosticLevel::Error)) + .any(|d| matches!(d.severity, Severity::Error)) } /// Serializes the diagnostic bundle to canonical JSON, resolving file IDs via FileManager. + /// The output is deterministic: diagnostics are sorted by (file_id, start, end, code). pub fn to_json(&self, file_manager: &FileManager) -> String { #[derive(Serialize)] struct CanonicalSpan { @@ -73,30 +75,67 @@ impl DiagnosticBundle { #[derive(Serialize)] struct CanonicalDiag { - severity: DiagnosticLevel, + severity: Severity, code: String, message: String, - span: Option, + span: CanonicalSpan, + related: Vec<(String, CanonicalSpan)>, } - 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, - } - }) - }); + let mut diags = self.diagnostics.clone(); + diags.sort_by(|a, b| { + (a.span.file_id, a.span.start, a.span.end, &a.code) + .cmp(&(b.span.file_id, b.span.start, b.span.end, &b.code)) + }); - CanonicalDiag { - severity: d.level.clone(), - code: d.code.clone().unwrap_or_else(|| "E_UNKNOWN".to_string()), - message: d.message.clone(), - span: canonical_span, - } - }).collect(); + let canonical_diags: Vec = diags + .iter() + .map(|d| { + let s = d.span; + let file = if s.file_id == usize::MAX { + "".to_string() + } else { + file_manager + .get_path(s.file_id) + .and_then(|p| p.file_name().map(|n| n.to_string_lossy().to_string())) + .unwrap_or_else(|| format!("file_{}", s.file_id)) + }; + let canonical_span = CanonicalSpan { + file, + start: s.start, + end: s.end, + }; + + let related = d + .related + .iter() + .map(|(msg, sp)| { + let file = if sp.file_id == usize::MAX { + "".to_string() + } else { + file_manager + .get_path(sp.file_id) + .and_then(|p| p.file_name().map(|n| n.to_string_lossy().to_string())) + .unwrap_or_else(|| format!("file_{}", sp.file_id)) + }; + let rsp = CanonicalSpan { + file, + start: sp.start, + end: sp.end, + }; + (msg.clone(), rsp) + }) + .collect(); + + CanonicalDiag { + severity: d.severity.clone(), + code: d.code.clone(), + message: d.message.clone(), + span: canonical_span, + related, + } + }) + .collect(); serde_json::to_string_pretty(&canonical_diags).unwrap() } @@ -109,66 +148,3 @@ impl From for DiagnosticBundle { bundle } } - -#[cfg(test)] -mod tests { - use crate::common::files::FileManager; - use crate::frontends::pbs::PbsFrontend; - use crate::frontends::Frontend; - 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); - 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); - 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); - 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); - 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); - assert!(json.contains("E_RESOLVE_NAMESPACE_COLLISION")); - } -} diff --git a/crates/prometeu-compiler/src/frontends/pbs/collector.rs b/crates/prometeu-compiler/src/frontends/pbs/collector.rs index b514f40a..9dc34b2b 100644 --- a/crates/prometeu-compiler/src/frontends/pbs/collector.rs +++ b/crates/prometeu-compiler/src/frontends/pbs/collector.rs @@ -1,4 +1,4 @@ -use crate::common::diagnostics::{Diagnostic, DiagnosticBundle, DiagnosticLevel}; +use crate::common::diagnostics::{Diagnostic, DiagnosticBundle, Severity}; use crate::frontends::pbs::ast::*; use crate::frontends::pbs::symbols::*; use crate::semantics::export_surface::ExportSurfaceKind; @@ -30,8 +30,9 @@ impl<'a> SymbolCollector<'a> { NodeKind::File(file) => file, _ => { return Err(DiagnosticBundle::error( + "E_COLLECT_INVALID_ROOT", "Expected File node as root".to_string(), - None, + arena.span(root), )) } }; @@ -165,38 +166,41 @@ impl<'a> SymbolCollector<'a> { fn error_duplicate(&mut self, symbol: &Symbol, existing: &Symbol) { self.diagnostics.push(Diagnostic { - level: DiagnosticLevel::Error, - code: Some("E_RESOLVE_DUPLICATE_SYMBOL".to_string()), + severity: Severity::Error, + code: "E_RESOLVE_DUPLICATE_SYMBOL".to_string(), message: format!( "Duplicate symbol '{}' already defined at {:?}", self.interner.resolve(symbol.name), existing.span ), - span: Some(symbol.span), + span: symbol.span, + related: Vec::new(), }); } fn error_collision(&mut self, symbol: &Symbol, existing: &Symbol) { self.diagnostics.push(Diagnostic { - level: DiagnosticLevel::Error, - code: Some("E_RESOLVE_NAMESPACE_COLLISION".to_string()), + severity: Severity::Error, + code: "E_RESOLVE_NAMESPACE_COLLISION".to_string(), message: format!( "DebugSymbol '{}' collides with another symbol in the {:?} namespace defined at {:?}", self.interner.resolve(symbol.name), existing.namespace, existing.span ), - span: Some(symbol.span), + span: symbol.span, + related: Vec::new(), }); } fn check_export_eligibility(&mut self, kind: SymbolKind, vis: Visibility, span: crate::common::spans::Span) { if let Err(msg) = ExportSurfaceKind::validate_visibility(kind, vis) { self.diagnostics.push(Diagnostic { - level: DiagnosticLevel::Error, - code: Some("E_SEMANTIC_EXPORT_RESTRICTION".to_string()), + severity: Severity::Error, + code: "E_SEMANTIC_EXPORT_RESTRICTION".to_string(), message: msg, - span: Some(span), + span, + related: Vec::new(), }); } } diff --git a/crates/prometeu-compiler/src/frontends/pbs/lowering.rs b/crates/prometeu-compiler/src/frontends/pbs/lowering.rs index 4f32069f..5294d155 100644 --- a/crates/prometeu-compiler/src/frontends/pbs/lowering.rs +++ b/crates/prometeu-compiler/src/frontends/pbs/lowering.rs @@ -1,4 +1,4 @@ -use crate::common::diagnostics::{Diagnostic, DiagnosticBundle, DiagnosticLevel}; +use crate::common::diagnostics::{Diagnostic, DiagnosticBundle, Severity}; use crate::common::spans::Span; use crate::frontends::pbs::ast::*; use crate::frontends::pbs::contracts::ContractRegistry; @@ -88,10 +88,11 @@ impl<'a> Lowerer<'a> { fn error(&mut self, code: &str, message: String, span: crate::common::spans::Span) { self.diagnostics.push(Diagnostic { - level: DiagnosticLevel::Error, - code: Some(code.to_string()), + severity: Severity::Error, + code: code.to_string(), message, - span: Some(span), + span, + related: Vec::new(), }); } @@ -100,8 +101,9 @@ impl<'a> Lowerer<'a> { NodeKind::File(file) => file, _ => { return Err(DiagnosticBundle::error( + "E_LOWER_INVALID_ROOT", "Expected File node as root".to_string(), - None, + self.arena.span(root), )) } }; @@ -1854,7 +1856,7 @@ mod tests { assert!(result.is_err()); let bundle = result.err().unwrap(); - assert!(bundle.diagnostics.iter().any(|d| d.code == Some("E_LOWER_UNSUPPORTED".to_string()))); + assert!(bundle.diagnostics.iter().any(|d| d.code == "E_LOWER_UNSUPPORTED")); } #[test] @@ -1882,7 +1884,7 @@ mod tests { assert!(result.is_err()); let bundle = result.err().unwrap(); - assert!(bundle.diagnostics.iter().any(|d| d.code == Some("E_LOWER_UNSUPPORTED".to_string()))); + assert!(bundle.diagnostics.iter().any(|d| d.code == "E_LOWER_UNSUPPORTED")); } #[test] @@ -1909,7 +1911,7 @@ mod tests { assert!(result.is_err()); let bundle = result.err().unwrap(); - assert!(bundle.diagnostics.iter().any(|d| d.code == Some("E_RESOLVE_UNDEFINED".to_string()))); + assert!(bundle.diagnostics.iter().any(|d| d.code == "E_RESOLVE_UNDEFINED")); } #[test] @@ -2044,7 +2046,7 @@ mod tests { assert!(result.is_err()); let bundle = result.err().unwrap(); - assert!(bundle.diagnostics.iter().any(|d| d.code == Some("E_RESOLVE_UNDEFINED".to_string()))); + assert!(bundle.diagnostics.iter().any(|d| d.code == "E_RESOLVE_UNDEFINED")); assert!(bundle.diagnostics.iter().any(|d| d.message.contains("Undefined function 'missing_func'"))); } @@ -2071,7 +2073,7 @@ mod tests { assert!(result.is_err()); let bundle = result.err().unwrap(); - assert!(bundle.diagnostics.iter().any(|d| d.code == Some("E_RESOLVE_UNDEFINED".to_string()))); + assert!(bundle.diagnostics.iter().any(|d| d.code == "E_RESOLVE_UNDEFINED")); assert!(bundle.diagnostics.iter().any(|d| d.message.contains("Undefined identifier 'undefined_var'"))); } } diff --git a/crates/prometeu-compiler/src/frontends/pbs/mod.rs b/crates/prometeu-compiler/src/frontends/pbs/mod.rs index df9f5ea4..a043a3e0 100644 --- a/crates/prometeu-compiler/src/frontends/pbs/mod.rs +++ b/crates/prometeu-compiler/src/frontends/pbs/mod.rs @@ -40,7 +40,11 @@ impl Frontend for PbsFrontend { file_manager: &mut FileManager, ) -> Result { let source = std::fs::read_to_string(entry).map_err(|e| { - DiagnosticBundle::error(format!("Failed to read file: {}", e), None) + crate::common::diagnostics::DiagnosticBundle::error( + "E_FRONTEND_IO", + format!("Failed to read file: {}", e), + crate::common::spans::Span::new(usize::MAX, 0, 0), + ) })?; let file_id = file_manager.add(entry.to_path_buf(), source.clone()); @@ -81,12 +85,20 @@ impl Frontend for PbsFrontend { // Validate Core IR Invariants crate::ir_core::validate_program(&core_program).map_err(|e| { - DiagnosticBundle::error(format!("Core IR Invariant Violation: {}", e), None) + crate::common::diagnostics::DiagnosticBundle::error( + "E_CORE_INVARIANT", + format!("Core IR Invariant Violation: {}", e), + crate::common::spans::Span::new(usize::MAX, 0, 0), + ) })?; // Lower Core IR to VM IR core_to_vm::lower_program(&core_program).map_err(|e| { - DiagnosticBundle::error(format!("Lowering error: {}", e), None) + crate::common::diagnostics::DiagnosticBundle::error( + "E_LOWERING", + format!("Lowering error: {}", e), + crate::common::spans::Span::new(usize::MAX, 0, 0), + ) }) } } diff --git a/crates/prometeu-compiler/src/frontends/pbs/parser.rs b/crates/prometeu-compiler/src/frontends/pbs/parser.rs index 7f13637e..00f689df 100644 --- a/crates/prometeu-compiler/src/frontends/pbs/parser.rs +++ b/crates/prometeu-compiler/src/frontends/pbs/parser.rs @@ -1,4 +1,4 @@ -use crate::common::diagnostics::{Diagnostic, DiagnosticBundle, DiagnosticLevel}; +use crate::common::diagnostics::{Diagnostic, DiagnosticBundle, Severity}; use crate::common::spans::Span; use crate::frontends::pbs::ast::*; use crate::frontends::pbs::lexer::Lexer; @@ -1040,10 +1040,11 @@ impl<'a> Parser<'a> { fn error_with_code(&mut self, message: &str, code: Option<&str>) -> DiagnosticBundle { let diag = Diagnostic { - level: DiagnosticLevel::Error, - code: code.map(|c| c.to_string()), + severity: Severity::Error, + code: code.unwrap_or("E_PARSE_ERROR").to_string(), message: message.to_string(), - span: Some(self.peek().span), + span: self.peek().span, + related: Vec::new(), }; self.errors.push(diag.clone()); DiagnosticBundle::from(diag) diff --git a/crates/prometeu-compiler/src/frontends/pbs/resolve.rs b/crates/prometeu-compiler/src/frontends/pbs/resolve.rs index 1fb612bf..11ba069d 100644 --- a/crates/prometeu-compiler/src/frontends/pbs/resolve.rs +++ b/crates/prometeu-compiler/src/frontends/pbs/resolve.rs @@ -1,4 +1,4 @@ -use crate::common::diagnostics::{Diagnostic, DiagnosticLevel}; +use crate::common::diagnostics::{Diagnostic, Severity}; use crate::frontends::pbs::ast::{AstArena, NodeKind, NodeId}; use crate::analysis::symbols::{Symbol, SymbolArena, SymbolKind, DefIndex, DefKey, Namespace, NodeToSymbol, RefIndex}; use prometeu_analysis::NameInterner; @@ -60,7 +60,7 @@ pub fn build_def_index( }; if let Err(mut diag) = index.insert_symbol(key, symbol_id) { - diag.span = Some(span); + diag.span = span; diagnostics.push(diag); } } @@ -132,28 +132,31 @@ fn walk_node( let symbol = import_arena.get(symbol_id); if !symbol.exported && symbol.module != module { diagnostics.push(Diagnostic { - level: DiagnosticLevel::Error, - code: Some("E_RESOLVE_VISIBILITY".to_string()), + severity: Severity::Error, + code: "E_RESOLVE_VISIBILITY".to_string(), message: format!("Symbol is not exported from module {}", symbol.module), - span: Some(span), + span, + related: Vec::new(), }); } ref_index.record_ref(symbol_id, span); node_to_symbol.bind_node(node_id, symbol_id); } else { diagnostics.push(Diagnostic { - level: DiagnosticLevel::Error, - code: Some("E_RESOLVE_UNDEFINED_IDENTIFIER".to_string()), - message: format!("Undefined identifier"), - span: Some(span), + severity: Severity::Error, + code: "E_RESOLVE_UNDEFINED_IDENTIFIER".to_string(), + message: "Undefined identifier".to_string(), + span, + related: Vec::new(), }); } } else { diagnostics.push(Diagnostic { - level: DiagnosticLevel::Error, - code: Some("E_RESOLVE_UNDEFINED_IDENTIFIER".to_string()), - message: format!("Undefined identifier"), - span: Some(span), + severity: Severity::Error, + code: "E_RESOLVE_UNDEFINED_IDENTIFIER".to_string(), + message: "Undefined identifier".to_string(), + span, + related: Vec::new(), }); } } @@ -330,8 +333,8 @@ mod tests { let (_symbols, _index, _ref_index, _node_to_symbol, diagnostics) = build_def_index(&arena, 1, &interner, None); assert_eq!(diagnostics.len(), 1); - assert_eq!(diagnostics[0].code, Some("E_RESOLVE_DUPLICATE_SYMBOL".to_string())); - assert_eq!(diagnostics[0].span, Some(Span::new(0, 30, 40))); + assert_eq!(diagnostics[0].code, "E_RESOLVE_DUPLICATE_SYMBOL"); + assert_eq!(diagnostics[0].span, Span::new(0, 30, 40)); } #[test] @@ -393,8 +396,8 @@ mod tests { let (_symbols, _index, _ref_index, _node_to_symbol, diagnostics) = build_def_index(&arena, 1, &interner, None); assert_eq!(diagnostics.len(), 1); - assert_eq!(diagnostics[0].code, Some("E_RESOLVE_UNDEFINED_IDENTIFIER".to_string())); - assert_eq!(diagnostics[0].span, Some(Span::new(0, 50, 60))); + assert_eq!(diagnostics[0].code, "E_RESOLVE_UNDEFINED_IDENTIFIER"); + assert_eq!(diagnostics[0].span, Span::new(0, 50, 60)); } #[test] @@ -496,8 +499,8 @@ mod tests { build_def_index(&arena2, 2, &interner, Some((&symbols1, &index1))); assert_eq!(diagnostics.len(), 1); - assert_eq!(diagnostics[0].code, Some("E_RESOLVE_VISIBILITY".to_string())); - assert_eq!(diagnostics[0].span, Some(Span::new(0, 50, 62))); + assert_eq!(diagnostics[0].code, "E_RESOLVE_VISIBILITY"); + assert_eq!(diagnostics[0].span, Span::new(0, 50, 62)); } #[test] diff --git a/crates/prometeu-compiler/src/frontends/pbs/resolver.rs b/crates/prometeu-compiler/src/frontends/pbs/resolver.rs index e9c54d1e..23015fc4 100644 --- a/crates/prometeu-compiler/src/frontends/pbs/resolver.rs +++ b/crates/prometeu-compiler/src/frontends/pbs/resolver.rs @@ -1,6 +1,6 @@ use crate::analysis::symbols::{SymbolArena, SymbolId, NodeToSymbol}; use crate::analysis::types::{TypeArena, TypeFacts, TypeId, TypeKind}; -use crate::common::diagnostics::{Diagnostic, DiagnosticBundle, DiagnosticLevel}; +use crate::common::diagnostics::{Diagnostic, DiagnosticBundle, Severity}; use crate::common::spans::Span; use crate::frontends::pbs::ast::*; use crate::frontends::pbs::symbols::*; @@ -60,8 +60,9 @@ impl<'a> Resolver<'a> { NodeKind::File(file) => file, _ => { return Err(DiagnosticBundle::error( + "E_RESOLVE_INVALID_ROOT", "Expected File node as root".to_string(), - None, + arena.span(root), )) } }; @@ -162,10 +163,11 @@ impl<'a> Resolver<'a> { NodeKind::ImportSpec(spec) => spec, _ => { self.diagnostics.push(Diagnostic { - level: DiagnosticLevel::Error, - code: Some("E_RESOLVE_INVALID_IMPORT".to_string()), + severity: Severity::Error, + code: "E_RESOLVE_INVALID_IMPORT".to_string(), message: "Invalid import spec".to_string(), - span: Some(arena.span(imp_id)), + span: arena.span(imp_id), + related: Vec::new(), }); return; } @@ -201,10 +203,11 @@ impl<'a> Resolver<'a> { } } else { self.diagnostics.push(Diagnostic { - level: DiagnosticLevel::Error, - code: Some("E_RESOLVE_INVALID_IMPORT".to_string()), + severity: Severity::Error, + code: "E_RESOLVE_INVALID_IMPORT".to_string(), message: format!("Module not found: {}", imp.from), - span: Some(arena.span(imp_id)), + span: arena.span(imp_id), + related: Vec::new(), }); } } @@ -255,10 +258,11 @@ impl<'a> Resolver<'a> { } } else { self.diagnostics.push(Diagnostic { - level: DiagnosticLevel::Error, - code: Some("E_TYPE_MISMATCH".to_string()), + severity: Severity::Error, + code: "E_TYPE_MISMATCH".to_string(), message: "Unary '-' operator expects 'int'".to_string(), - span: Some(arena.span(node)), + span: arena.span(node), + related: Vec::new(), }); } } @@ -272,10 +276,11 @@ impl<'a> Resolver<'a> { } } else { self.diagnostics.push(Diagnostic { - level: DiagnosticLevel::Error, - code: Some("E_TYPE_MISMATCH".to_string()), + severity: Severity::Error, + code: "E_TYPE_MISMATCH".to_string(), message: "Unary '!' operator expects 'bool'".to_string(), - span: Some(arena.span(node)), + span: arena.span(node), + related: Vec::new(), }); } } @@ -301,10 +306,11 @@ impl<'a> Resolver<'a> { } } else { self.diagnostics.push(Diagnostic { - level: DiagnosticLevel::Error, - code: Some("E_TYPE_MISMATCH".to_string()), + severity: Severity::Error, + code: "E_TYPE_MISMATCH".to_string(), message: format!("Binary '{}' operator expects 'int' operands", n.op), - span: Some(arena.span(node)), + span: arena.span(node), + related: Vec::new(), }); } } @@ -317,10 +323,11 @@ impl<'a> Resolver<'a> { } } else { self.diagnostics.push(Diagnostic { - level: DiagnosticLevel::Error, - code: Some("E_TYPE_MISMATCH".to_string()), + severity: Severity::Error, + code: "E_TYPE_MISMATCH".to_string(), message: format!("Binary '{}' operator expects operands of the same type", n.op), - span: Some(arena.span(node)), + span: arena.span(node), + related: Vec::new(), }); } } @@ -573,10 +580,11 @@ impl<'a> Resolver<'a> { // Actually, lookup_with_id checks imported_symbols too. self.diagnostics.push(Diagnostic { - level: DiagnosticLevel::Error, - code: Some("E_TYPE_UNKNOWN_TYPE".to_string()), + severity: Severity::Error, + code: "E_TYPE_UNKNOWN_TYPE".to_string(), message: format!("Unknown type: {}", name), - span: Some(arena.span(node)), + span: arena.span(node), + related: Vec::new(), }); None } @@ -586,10 +594,11 @@ impl<'a> Resolver<'a> { "optional" => { if n.args.len() != 1 { self.diagnostics.push(Diagnostic { - level: DiagnosticLevel::Error, - code: Some("E_TYPE_INVALID_ARGS".to_string()), + severity: Severity::Error, + code: "E_TYPE_INVALID_ARGS".to_string(), message: "optional expects exactly 1 argument".to_string(), - span: Some(arena.span(node)), + span: arena.span(node), + related: Vec::new(), }); return None; } @@ -599,10 +608,11 @@ impl<'a> Resolver<'a> { "result" => { if n.args.len() != 2 { self.diagnostics.push(Diagnostic { - level: DiagnosticLevel::Error, - code: Some("E_TYPE_INVALID_ARGS".to_string()), + severity: Severity::Error, + code: "E_TYPE_INVALID_ARGS".to_string(), message: "result expects exactly 2 arguments".to_string(), - span: Some(arena.span(node)), + span: arena.span(node), + related: Vec::new(), }); return None; } @@ -613,10 +623,11 @@ impl<'a> Resolver<'a> { "array" => { if n.args.len() < 1 || n.args.len() > 2 { self.diagnostics.push(Diagnostic { - level: DiagnosticLevel::Error, - code: Some("E_TYPE_INVALID_ARGS".to_string()), + severity: Severity::Error, + code: "E_TYPE_INVALID_ARGS".to_string(), message: "array or array[N] expects 1 or 2 arguments".to_string(), - span: Some(arena.span(node)), + span: arena.span(node), + related: Vec::new(), }); return None; } @@ -626,10 +637,11 @@ impl<'a> Resolver<'a> { NodeKind::IntLit(lit) => Some(lit.value as u32), _ => { self.diagnostics.push(Diagnostic { - level: DiagnosticLevel::Error, - code: Some("E_TYPE_INVALID_ARGS".to_string()), + severity: Severity::Error, + code: "E_TYPE_INVALID_ARGS".to_string(), message: "Array length must be an integer literal".to_string(), - span: Some(arena.span(n.args[1])), + span: arena.span(n.args[1]), + related: Vec::new(), }); None } @@ -641,10 +653,11 @@ impl<'a> Resolver<'a> { } _ => { self.diagnostics.push(Diagnostic { - level: DiagnosticLevel::Error, - code: Some("E_TYPE_UNKNOWN_TYPE".to_string()), + severity: Severity::Error, + code: "E_TYPE_UNKNOWN_TYPE".to_string(), message: format!("Unknown generic type: {}", base_name), - span: Some(arena.span(node)), + span: arena.span(node), + related: Vec::new(), }); None } @@ -652,10 +665,11 @@ impl<'a> Resolver<'a> { } _ => { self.diagnostics.push(Diagnostic { - level: DiagnosticLevel::Error, - code: Some("E_TYPE_NOT_A_TYPE".to_string()), + severity: Severity::Error, + code: "E_TYPE_NOT_A_TYPE".to_string(), message: "Expected a type node".to_string(), - span: Some(arena.span(node)), + span: arena.span(node), + related: Vec::new(), }); None } @@ -672,10 +686,11 @@ impl<'a> Resolver<'a> { } else { if namespace == Namespace::Type { self.diagnostics.push(Diagnostic { - level: DiagnosticLevel::Error, - code: Some("E_TYPE_UNKNOWN_TYPE".to_string()), + severity: Severity::Error, + code: "E_TYPE_UNKNOWN_TYPE".to_string(), message: format!("Unknown type: {}", self.interner.resolve(name)), - span: Some(span), + span, + related: Vec::new(), }); } else { self.error_undefined(name, span); @@ -780,25 +795,27 @@ impl<'a> Resolver<'a> { // Check for collision in Type namespace at top-level? // Actually, the spec says "A name may not exist in both namespaces". // If we want to be strict, we check current module's type symbols too. - if self.current_module.type_symbols.get(name).is_some() { + if let Some(existing) = self.current_module.type_symbols.get(name) { self.diagnostics.push(Diagnostic { - level: DiagnosticLevel::Error, - code: Some("E_RESOLVE_NAMESPACE_COLLISION".to_string()), + severity: Severity::Error, + code: "E_RESOLVE_NAMESPACE_COLLISION".to_string(), message: format!( "Local variable '{}' collides with a type name", self.interner.resolve(name) ), - span: Some(span), + span, + related: vec![("type defined here".to_string(), existing.span)], }); return None; } - if scope.contains_key(&name) { + if let Some((prev_sym, _)) = scope.get(&name) { self.diagnostics.push(Diagnostic { - level: DiagnosticLevel::Error, - code: Some("E_RESOLVE_DUPLICATE_SYMBOL".to_string()), + severity: Severity::Error, + code: "E_RESOLVE_DUPLICATE_SYMBOL".to_string(), message: format!("Duplicate local variable '{}'", self.interner.resolve(name)), - span: Some(span), + span, + related: vec![("previous definition here".to_string(), prev_sym.span)], }); None } else { @@ -840,31 +857,34 @@ impl<'a> Resolver<'a> { fn error_undefined(&mut self, name: NameId, span: Span) { self.diagnostics.push(Diagnostic { - level: DiagnosticLevel::Error, - code: Some("E_RESOLVE_UNDEFINED".to_string()), + severity: Severity::Error, + code: "E_RESOLVE_UNDEFINED".to_string(), message: format!("Undefined identifier: {}", self.interner.resolve(name)), - span: Some(span), + span, + related: Vec::new(), }); } fn error_duplicate_import(&mut self, name: NameId, span: Span) { self.diagnostics.push(Diagnostic { - level: DiagnosticLevel::Error, - code: Some("E_RESOLVE_DUPLICATE_SYMBOL".to_string()), + severity: Severity::Error, + code: "E_RESOLVE_DUPLICATE_SYMBOL".to_string(), message: format!("Duplicate import: {}", self.interner.resolve(name)), - span: Some(span), + span, + related: Vec::new(), }); } fn error_visibility(&mut self, sym: &Symbol, span: Span) { self.diagnostics.push(Diagnostic { - level: DiagnosticLevel::Error, - code: Some("E_RESOLVE_VISIBILITY".to_string()), + severity: Severity::Error, + code: "E_RESOLVE_VISIBILITY".to_string(), message: format!( "DebugSymbol '{}' is not visible here", self.interner.resolve(sym.name) ), - span: Some(span), + span, + related: vec![("symbol defined here".to_string(), sym.span)], }); } } @@ -897,7 +917,7 @@ mod tests { assert!(result.is_err()); let bundle = result.unwrap_err(); - assert!(bundle.diagnostics.iter().any(|d| d.code == Some("E_RESOLVE_DUPLICATE_SYMBOL".to_string()))); + assert!(bundle.diagnostics.iter().any(|d| d.code == "E_RESOLVE_DUPLICATE_SYMBOL")); } #[test] @@ -912,7 +932,7 @@ mod tests { assert!(result.is_err()); let bundle = result.unwrap_err(); - assert!(bundle.diagnostics.iter().any(|d| d.code == Some("E_RESOLVE_NAMESPACE_COLLISION".to_string()))); + assert!(bundle.diagnostics.iter().any(|d| d.code == "E_RESOLVE_NAMESPACE_COLLISION")); } #[test] @@ -939,7 +959,7 @@ mod tests { assert!(result.is_err()); let bundle = result.unwrap_err(); - assert!(bundle.diagnostics.iter().any(|d| d.code == Some("E_RESOLVE_UNDEFINED".to_string()))); + assert!(bundle.diagnostics.iter().any(|d| d.code == "E_RESOLVE_UNDEFINED")); } #[test] @@ -1011,7 +1031,7 @@ mod tests { assert!(result.is_err()); let bundle = result.unwrap_err(); - assert!(bundle.diagnostics.iter().any(|d| d.code == Some("E_RESOLVE_VISIBILITY".to_string()))); + assert!(bundle.diagnostics.iter().any(|d| d.code == "E_RESOLVE_VISIBILITY")); } #[test] @@ -1083,7 +1103,7 @@ mod tests { assert!(result.is_err()); let bundle = result.unwrap_err(); - assert!(bundle.diagnostics.iter().any(|d| d.code == Some("E_RESOLVE_INVALID_IMPORT".to_string()))); + assert!(bundle.diagnostics.iter().any(|d| d.code == "E_RESOLVE_INVALID_IMPORT")); } #[test] diff --git a/crates/prometeu-compiler/src/frontends/pbs/typecheck.rs b/crates/prometeu-compiler/src/frontends/pbs/typecheck.rs index 4f48dd5f..bcce125b 100644 --- a/crates/prometeu-compiler/src/frontends/pbs/typecheck.rs +++ b/crates/prometeu-compiler/src/frontends/pbs/typecheck.rs @@ -1,4 +1,4 @@ -use crate::common::diagnostics::{Diagnostic, DiagnosticBundle, DiagnosticLevel}; +use crate::common::diagnostics::{Diagnostic, DiagnosticBundle, Severity}; use crate::common::spans::Span; use crate::frontends::pbs::ast::*; use crate::frontends::pbs::contracts::ContractRegistry; @@ -51,8 +51,9 @@ impl<'a> TypeChecker<'a> { NodeKind::File(file) => file, _ => { return Err(DiagnosticBundle::error( + "E_TYPECHECK_INVALID_ROOT", "Expected File node as root".to_string(), - None, + arena.span(root), )) } }; @@ -261,10 +262,11 @@ impl<'a> TypeChecker<'a> { }; } else { self.diagnostics.push(Diagnostic { - level: DiagnosticLevel::Error, - code: Some("E_RESOLVE_UNDEFINED".to_string()), + severity: Severity::Error, + code: "E_RESOLVE_UNDEFINED".to_string(), message: format!("Method '{}' not found on host contract '{}'", member_str, name_str), - span: Some(arena.span(node)), + span: arena.span(node), + related: Vec::new(), }); } return PbsType::Void; @@ -372,10 +374,11 @@ impl<'a> TypeChecker<'a> { if obj_ty != PbsType::Void { let msg = format!("Member '{}' not found on type {:?}", member_str, obj_ty); self.diagnostics.push(Diagnostic { - level: DiagnosticLevel::Error, - code: Some("E_RESOLVE_UNDEFINED".to_string()), + severity: Severity::Error, + code: "E_RESOLVE_UNDEFINED".to_string(), message: msg, - span: Some(arena.span(node)), + span: arena.span(node), + related: Vec::new(), }); } PbsType::Void @@ -444,14 +447,15 @@ impl<'a> TypeChecker<'a> { // Void doesn't strictly need return } else { self.diagnostics.push(Diagnostic { - level: DiagnosticLevel::Error, - code: Some("E_TYPE_RETURN_PATH".to_string()), + severity: Severity::Error, + code: "E_TYPE_RETURN_PATH".to_string(), message: format!( "Function '{}' must return a value of type {}", self.interner.resolve(n.name), return_type ), - span: Some(arena.span(id)), + span: arena.span(id), + related: Vec::new(), }); } } @@ -570,10 +574,11 @@ impl<'a> TypeChecker<'a> { PbsType::Function { params, return_type } => { if n.args.len() != params.len() { self.diagnostics.push(Diagnostic { - level: DiagnosticLevel::Error, - code: Some("E_TYPE_MISMATCH".to_string()), + severity: Severity::Error, + code: "E_TYPE_MISMATCH".to_string(), message: format!("Expected {} arguments, found {}", params.len(), n.args.len()), - span: Some(arena.span(node)), + span: arena.span(node), + related: Vec::new(), }); } else { for (i, arg) in n.args.iter().enumerate() { @@ -588,10 +593,11 @@ impl<'a> TypeChecker<'a> { _ => { if callee_ty != PbsType::Void { self.diagnostics.push(Diagnostic { - level: DiagnosticLevel::Error, - code: Some("E_TYPE_MISMATCH".to_string()), + severity: Severity::Error, + code: "E_TYPE_MISMATCH".to_string(), message: format!("Type {} is not callable", callee_ty), - span: Some(arena.span(node)), + span: arena.span(node), + related: Vec::new(), }); } PbsType::Void @@ -789,10 +795,11 @@ impl<'a> TypeChecker<'a> { } } else { self.diagnostics.push(Diagnostic { - level: DiagnosticLevel::Error, - code: Some("E_TYPE_UNKNOWN_TYPE".to_string()), + severity: Severity::Error, + code: "E_TYPE_UNKNOWN_TYPE".to_string(), message: format!("Unknown type: {}", name_str), - span: Some(arena.span(node)), + span: arena.span(node), + related: Vec::new(), }); PbsType::Void } @@ -912,10 +919,11 @@ impl<'a> TypeChecker<'a> { fn error_type_mismatch(&mut self, expected: &PbsType, found: &PbsType, span: Span) { self.diagnostics.push(Diagnostic { - level: DiagnosticLevel::Error, - code: Some("E_TYPE_MISMATCH".to_string()), + severity: Severity::Error, + code: "E_TYPE_MISMATCH".to_string(), message: format!("Type mismatch: expected {}, found {}", expected, found), - span: Some(span), + span, + related: Vec::new(), }); } } @@ -953,7 +961,7 @@ mod tests { Err(bundle) => { let mut errors = Vec::new(); for diag in bundle.diagnostics { - let code = diag.code.unwrap_or_else(|| "NO_CODE".to_string()); + let code = diag.code; errors.push(format!("{}: {}", code, diag.message)); } let err_msg = errors.join(", ");