This commit is contained in:
bQUARKz 2026-02-05 01:23:23 +00:00
parent 8c02f505d0
commit 836b280998
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
10 changed files with 268 additions and 238 deletions

View File

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

View File

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

View File

@ -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<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"),
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<String>,
pub severity: Severity,
pub code: String,
pub message: String,
pub span: Option<Span>,
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<Span>) -> 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<CanonicalSpan>,
span: CanonicalSpan,
related: Vec<(String, 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,
}
})
});
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<CanonicalDiag> = diags
.iter()
.map(|d| {
let s = d.span;
let file = if s.file_id == usize::MAX {
"<virtual>".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 {
"<virtual>".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<Diagnostic> 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"));
}
}

View File

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

View File

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

View File

@ -40,7 +40,11 @@ impl Frontend for PbsFrontend {
file_manager: &mut FileManager,
) -> Result<ir_vm::Module, DiagnosticBundle> {
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),
)
})
}
}

View File

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

View File

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

View File

@ -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<T> 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<T, E> 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<T> or array<T>[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]

View File

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