Much work to produce better errors during name analysis.

This commit is contained in:
Jesse Brault 2025-05-16 15:58:42 -05:00
parent dda86f75e7
commit f5a82c414c
13 changed files with 668 additions and 275 deletions

7
Cargo.lock generated
View File

@ -148,6 +148,7 @@ version = "0.1.0"
dependencies = [
"clap",
"codespan-reporting",
"log",
"pest",
"pest_derive",
]
@ -190,6 +191,12 @@ version = "0.2.167"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc"
[[package]]
name = "log"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "memchr"
version = "2.7.4"

View File

@ -16,3 +16,4 @@ pest = "2.7.14"
clap = { version = "4.5.23", features = ["derive"] }
pest_derive = "2.7.14"
codespan-reporting = "0.12.0"
log = "0.4.27"

View File

@ -1,13 +1,13 @@
ns greeter
fn main() {
fn main(x: String) {
let x = 'Hello';
let y = 'World';
{
let x = 'Hello';
let test = 'Test';
let test = 'Again';
let test = 'test';
let x = 'x';
let y = 'y';
let z = 'z';
let test = 'oops.';
};
x = y;
let x = 'Hello!';
}

File diff suppressed because it is too large Load Diff

View File

@ -55,15 +55,17 @@ pub enum SuffixUnaryOperator {
#[derive(Debug, Clone)]
pub struct Identifier {
pub name: String,
pub file_id: usize,
pub range: Range<usize>,
scope_id: Option<usize>,
symbol: Option<Symbol>,
}
impl Identifier {
pub fn new(name: &str, range: Range<usize>) -> Self {
pub fn new(name: &str, file_id: usize, range: Range<usize>) -> Self {
Identifier {
name: name.to_string(),
file_id,
range,
scope_id: None,
symbol: None,
@ -90,16 +92,18 @@ impl Identifier {
#[derive(Debug)]
pub struct FullyQualifiedName {
pub identifiers: Vec<Identifier>,
pub file_id: usize,
pub range: Range<usize>,
scope_id: Option<usize>,
symbol: Option<Symbol>,
}
impl FullyQualifiedName {
pub fn new(identifiers: Vec<Identifier>, range: Range<usize>) -> Self {
pub fn new(identifiers: Vec<Identifier>, file_id: usize, range: Range<usize>) -> Self {
FullyQualifiedName {
identifiers,
range,
file_id,
scope_id: None,
symbol: None,
}

View File

@ -17,7 +17,7 @@ pub fn name_analysis(path: &Path) -> Result<(), Box<dyn std::error::Error>> {
match parse_result {
Ok(mut pairs) => {
let compilation_unit_pair = pairs.next().unwrap();
let mut compilation_unit = build_ast(compilation_unit_pair);
let mut compilation_unit = build_ast(file_id, compilation_unit_pair);
let mut symbol_table = SymbolTable::new();
let diagnostics = analyze_names(file_id, &mut compilation_unit, &mut symbol_table);
if diagnostics.is_empty() {

View File

@ -11,7 +11,7 @@ pub fn pretty_print_parse(path: &PathBuf) {
match parse_result {
Ok(mut pairs) => {
let compilation_unit_pair = pairs.next().unwrap();
let compilation_unit = build_ast(compilation_unit_pair);
let compilation_unit = build_ast(0, compilation_unit_pair);
let mut indent_writer = IndentWriter::new(0, " ", Box::new(std::io::stdout()));
compilation_unit
.pretty_print(&mut indent_writer)

View File

@ -11,7 +11,7 @@ pub fn unparse(path: &PathBuf) {
match parse_result {
Ok(mut pairs) => {
let compilation_unit_pair = pairs.next().unwrap();
let compilation_unit = build_ast(compilation_unit_pair);
let compilation_unit = build_ast(0, compilation_unit_pair);
let mut writer = IndentWriter::new(0, " ", Box::new(std::io::stdout()));
compilation_unit
.unparse(&mut writer)

View File

@ -1,10 +1,45 @@
use crate::ast::named::Named;
use crate::ast::*;
use crate::name_analysis::fqn_context::FqnContext;
use crate::name_analysis::symbol::{FunctionSymbol, Symbol, VariableSymbol};
use crate::name_analysis::symbol_table::SymbolTable;
use crate::name_analysis::symbol::{FunctionSymbol, ModuleSymbol, SourceDefinition, Symbol, VariableSymbol};
use crate::name_analysis::symbol_table::{SymbolInsertError, SymbolTable};
use crate::name_analysis::DiagnosticsContainer;
use codespan_reporting::diagnostic::{Diagnostic, Label};
use std::range::Range;
fn handle_insert_error(
err: SymbolInsertError,
error_symbol_name: &str,
error_file_id: usize,
error_range: Range<usize>,
symbol_types: &str,
diagnostics: &mut DiagnosticsContainer,
) {
match err {
SymbolInsertError::SymbolAlreadyDefined(s) => {
let already_defined_definition = s.definition();
diagnostics.add(
Diagnostic::error()
.with_message(format!(
"{} symbol '{}' already defined in the current scope.",
symbol_types, error_symbol_name,
))
.with_label(
Label::primary(error_file_id, error_range)
.with_message("Symbol duplicated here."),
)
.with_label(
Label::secondary(
already_defined_definition.file_id(),
already_defined_definition.range(),
)
.with_message("Symbol defined here."),
),
);
}
}
}
pub(super) fn gather_module_level_declaration(
declaration: &mut ModuleLevelDeclaration,
@ -14,6 +49,9 @@ pub(super) fn gather_module_level_declaration(
) {
use ModuleLevelDeclaration::*;
match declaration {
Module(module_declaration) => {
gather_module_declaration(module_declaration, symbol_table, fqn_context, diagnostics);
}
Function(function_definition) => {
gather_function_definition(function_definition, symbol_table, fqn_context, diagnostics)
}
@ -21,28 +59,78 @@ pub(super) fn gather_module_level_declaration(
}
}
fn gather_module_declaration(
declaration: &mut ModuleDeclaration,
symbol_table: &mut SymbolTable,
fqn_context: &mut FqnContext,
diagnostics: &mut DiagnosticsContainer,
) {
// 1. Add mod identifier symbol
// 2. Update fqn context
// 3. Push scope
// 4. Process declarations
// 5. Pop scope
let module_name = declaration.identifier.name();
let insert_result = symbol_table.insert(
&module_name,
Symbol::Module(ModuleSymbol::new(
&fqn_context.resolve(&module_name),
&module_name,
declaration.is_public,
&declaration.identifier,
)),
);
if let Err(err) = insert_result {
handle_insert_error(
err,
&module_name,
declaration.identifier.file_id,
declaration.identifier.range,
"module/type",
diagnostics,
)
}
fqn_context.push(module_name.to_string());
symbol_table.push_scope(&format!("Module '{}' Scope", module_name));
for inner_declaration in &mut declaration.declarations {
gather_module_level_declaration(inner_declaration, symbol_table, fqn_context, diagnostics);
}
symbol_table.pop_scope()
}
fn gather_function_definition(
function: &mut FunctionDefinition,
symbol_table: &mut SymbolTable,
fqn_context: &mut FqnContext,
diagnostics: &mut DiagnosticsContainer,
) {
let declared_name = function.identifier.name().to_string();
let declared_name = function.identifier.name();
let resolved_name = fqn_context.resolve(&declared_name);
let insert_result = symbol_table.insert(
declared_name.clone(),
Symbol::Function(FunctionSymbol {
fqn: resolved_name.clone(),
declared_name,
is_public: function.is_public,
}),
&declared_name,
Symbol::Function(FunctionSymbol::new(
&resolved_name,
&declared_name,
function.is_public,
&function.identifier,
)),
);
if let Err(msg) = insert_result {
diagnostics.add(|file_id| {
Diagnostic::error().with_label(Label::primary(file_id, function.identifier.range))
});
if let Err(err) = insert_result {
handle_insert_error(
err,
&declared_name,
function.identifier.file_id,
function.identifier.range,
"function/variable",
diagnostics,
)
}
function
@ -80,18 +168,28 @@ fn gather_parameter(
let parameter_name = parameter.identifier.name();
let insert_result = symbol_table.insert(
parameter_name.to_string(),
Symbol::Variable(VariableSymbol {
name: parameter_name.to_string(),
is_mutable: false,
}),
&parameter_name,
Symbol::Variable(VariableSymbol::new(
&parameter_name,
false,
SourceDefinition::from_identifier(&parameter.identifier),
)),
);
if let Err(msg) = insert_result {
diagnostics.add_error_at_identifier(&msg, &parameter.identifier);
if let Err(err) = insert_result {
handle_insert_error(
err,
&parameter_name,
parameter.identifier.file_id,
parameter.identifier.range,
"function/variable",
diagnostics,
)
}
parameter.identifier.set_scope_id(symbol_table.current_scope_id());
parameter
.identifier
.set_scope_id(symbol_table.current_scope_id());
}
fn gather_function_body(
@ -114,7 +212,7 @@ fn gather_block_statement(
diagnostics: &mut DiagnosticsContainer,
) {
symbol_table.push_scope("BlockStatementScope");
gather_block_statement(block, symbol_table, fqn_context, diagnostics);
gather_block_statement_inner(block, symbol_table, fqn_context, diagnostics);
symbol_table.pop_scope();
}
@ -158,18 +256,26 @@ fn gather_variable_declaration(
symbol_table: &mut SymbolTable,
diagnostics: &mut DiagnosticsContainer,
) {
let variable_name = variable_declaration.identifier.name().to_string();
let variable_name = variable_declaration.identifier.name();
let insert_result = symbol_table.insert(
variable_name.clone(),
Symbol::Variable(VariableSymbol {
name: variable_name,
is_mutable: variable_declaration.is_mutable,
}),
&variable_name,
Symbol::Variable(VariableSymbol::new(
&variable_name,
variable_declaration.is_mutable,
SourceDefinition::from_identifier(&variable_declaration.identifier),
)),
);
if let Err(msg) = insert_result {
diagnostics.add_error_at_identifier(&msg, &variable_declaration.identifier);
if let Err(err) = insert_result {
handle_insert_error(
err,
&variable_name,
variable_declaration.identifier.file_id,
variable_declaration.identifier.range,
"function/variable",
diagnostics,
)
}
variable_declaration

View File

@ -6,6 +6,7 @@ use crate::name_analysis::gather::gather_module_level_declaration;
use crate::name_analysis::resolve::resolve_module_level_declaration;
use crate::name_analysis::symbol_table::SymbolTable;
use codespan_reporting::diagnostic::{Diagnostic, Label};
use log::debug;
mod fqn_context;
mod gather;
@ -26,8 +27,8 @@ impl DiagnosticsContainer {
}
}
pub fn add(&mut self, f: impl FnOnce(usize) -> DmDiagnostic) {
self.diagnostics.push(f(self.file_id));
pub fn add(&mut self, diagnostic: DmDiagnostic) {
self.diagnostics.push(diagnostic);
}
pub fn add_error_at_identifier(&mut self, msg: &str, identifier: &Identifier) {
@ -86,15 +87,16 @@ mod tests {
use pest::Parser;
fn assert_no_diagnostics(src: &str) {
let mut files = SimpleFiles::new();
let test_file_id = files.add("test.dm", src);
let parse_result = DeimosParser::parse(Rule::CompilationUnit, src);
if let Err(err) = &parse_result {
panic!("{:?}", err);
}
let compilation_unit_pair = parse_result.unwrap().next().unwrap();
let mut ast = build_ast(compilation_unit_pair);
let mut files = SimpleFiles::new();
let test_file_id = files.add("test.dm", src);
let compilation_unit_pair = parse_result.unwrap().next().unwrap();
let mut ast = build_ast(test_file_id, compilation_unit_pair);
let mut symbol_table = SymbolTable::new();

View File

@ -128,10 +128,16 @@ fn resolve_fully_qualified_name(
Ok(symbol) => {
fully_qualified_name.set_symbol(symbol.clone());
}
Err(e) => diagnostics.add(|file_id| {
Err(e) => diagnostics.add(
Diagnostic::error()
.with_message(e)
.with_label(Label::primary(file_id, fully_qualified_name.range))
}),
.with_message(format!(
"No symbol with name '{}' found in current scope.",
fully_qualified_name.name()
))
.with_label(Label::primary(
fully_qualified_name.file_id,
fully_qualified_name.range,
)),
),
}
}

View File

@ -1,9 +1,35 @@
use crate::ast::Identifier;
use std::fmt::Display;
use std::range::Range;
#[derive(Clone, Debug)]
pub struct SourceDefinition {
file_id: usize,
range: Range<usize>,
}
impl SourceDefinition {
pub fn from_identifier(identifier: &Identifier) -> Self {
SourceDefinition {
file_id: identifier.file_id,
range: identifier.range,
}
}
pub fn file_id(&self) -> usize {
self.file_id
}
pub fn range(&self) -> Range<usize> {
self.range.clone()
}
}
#[derive(Debug, Clone)]
pub enum Symbol {
Function(FunctionSymbol),
Variable(VariableSymbol),
Module(ModuleSymbol),
}
impl Symbol {
@ -11,6 +37,15 @@ impl Symbol {
match self {
Symbol::Function(s) => s.declared_name.as_str(),
Symbol::Variable(s) => s.name.as_str(),
Symbol::Module(s) => s.declared_name.as_str(),
}
}
pub fn definition(&self) -> &SourceDefinition {
match self {
Symbol::Function(s) => s.definition(),
Symbol::Module(s) => s.definition(),
Symbol::Variable(s) => s.definition(),
}
}
}
@ -21,6 +56,7 @@ impl Display for Symbol {
match self {
Function(function_symbol) => write!(f, "{}", function_symbol),
Variable(variable_symbol) => write!(f, "{}", variable_symbol),
Module(module_symbol) => write!(f, "{}", module_symbol),
}
}
}
@ -30,6 +66,27 @@ pub struct FunctionSymbol {
pub fqn: String,
pub declared_name: String,
pub is_public: bool,
definition: SourceDefinition,
}
impl FunctionSymbol {
pub fn new(
fqn: &str,
declared_name: &str,
is_public: bool,
identifier: &Identifier,
) -> FunctionSymbol {
FunctionSymbol {
fqn: fqn.to_string(),
declared_name: declared_name.to_string(),
is_public,
definition: SourceDefinition::from_identifier(identifier),
}
}
fn definition(&self) -> &SourceDefinition {
&self.definition
}
}
impl Display for FunctionSymbol {
@ -46,6 +103,21 @@ impl Display for FunctionSymbol {
pub struct VariableSymbol {
pub name: String,
pub is_mutable: bool,
definition: SourceDefinition,
}
impl VariableSymbol {
pub fn new(name: &str, is_mutable: bool, definition: SourceDefinition) -> Self {
VariableSymbol {
name: name.to_string(),
is_mutable,
definition,
}
}
pub fn definition(&self) -> &SourceDefinition {
&self.definition
}
}
impl Display for VariableSymbol {
@ -57,3 +129,41 @@ impl Display for VariableSymbol {
)
}
}
#[derive(Debug, Clone)]
pub struct ModuleSymbol {
pub fqn: String,
pub declared_name: String,
pub is_public: bool,
definition: SourceDefinition,
}
impl ModuleSymbol {
pub fn new(
fqn: &str,
declared_name: &str,
is_public: bool,
identifier: &Identifier,
) -> ModuleSymbol {
ModuleSymbol {
fqn: fqn.to_string(),
declared_name: declared_name.to_string(),
is_public,
definition: SourceDefinition::from_identifier(identifier),
}
}
pub fn definition(&self) -> &SourceDefinition {
&self.definition
}
}
impl Display for ModuleSymbol {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"ModuleSymbol(name = {}, is_public = {})",
self.fqn, self.is_public
)
}
}

View File

@ -1,14 +1,35 @@
use crate::name_analysis::symbol::Symbol;
use std::collections::HashMap;
use std::fmt::Display;
use crate::name_analysis::symbol_table::SymbolInsertError::SymbolAlreadyDefined;
use crate::name_analysis::symbol_table::SymbolLookupError::NoDefinition;
pub struct Scope {
parent: Option<usize>,
symbols: HashMap<String, Symbol>,
function_and_variable_symbols: HashMap<String, Symbol>,
type_and_module_symbols: HashMap<String, Symbol>,
debug_name: String,
}
#[derive(Default)]
impl Scope {
pub fn new(parent: Option<usize>, debug_name: String) -> Scope {
Scope {
parent,
function_and_variable_symbols: HashMap::new(),
type_and_module_symbols: HashMap::new(),
debug_name,
}
}
}
pub enum SymbolInsertError {
SymbolAlreadyDefined(Symbol),
}
pub enum SymbolLookupError {
NoDefinition
}
pub struct SymbolTable {
scopes: Vec<Scope>,
current_scope_id: usize,
@ -17,13 +38,10 @@ pub struct SymbolTable {
/// Contains a vec of scopes, like a flattened tree
impl SymbolTable {
pub fn new() -> Self {
let mut t = SymbolTable::default();
t.scopes.push(Scope {
parent: None,
symbols: HashMap::new(),
debug_name: String::from("Global Scope"),
});
t.current_scope_id = 0;
let mut t = SymbolTable {
scopes: vec![Scope::new(None, String::from("GlobalScope"))],
current_scope_id: 0,
};
t
}
@ -33,11 +51,8 @@ impl SymbolTable {
pub fn push_scope(&mut self, debug_name: &str) {
let id = self.scopes.len();
self.scopes.push(Scope {
parent: Some(self.current_scope_id),
symbols: HashMap::new(),
debug_name: debug_name.to_string(),
});
self.scopes
.push(Scope::new(Some(self.current_scope_id), debug_name.to_string()));
self.current_scope_id = id;
}
@ -47,24 +62,47 @@ impl SymbolTable {
}
}
pub fn insert(&mut self, name: String, symbol: Symbol) -> Result<(), String> {
if let Some(current_symbol) = self.scopes[self.current_scope_id].symbols.get(&name) {
Err(format!(
"Symbol '{}' already defined in current scope.",
current_symbol.name()
))
pub fn insert(&mut self, name: &str, symbol: Symbol) -> Result<(), SymbolInsertError> {
match symbol {
Symbol::Function(_) | Symbol::Variable(_) => {
self.insert_function_or_variable(name, symbol)
}
Symbol::Module(_) => self.insert_module_or_type(name, symbol),
}
}
fn insert_function_or_variable(&mut self, name: &str, symbol: Symbol) -> Result<(), SymbolInsertError> {
if let Some(defined_symbol) = self.scopes[self.current_scope_id]
.function_and_variable_symbols
.get(name)
{
Err(SymbolAlreadyDefined(defined_symbol.clone()))
} else {
self.scopes[self.current_scope_id]
.symbols
.insert(name, symbol);
.function_and_variable_symbols
.insert(name.to_string(), symbol);
Ok(())
}
}
pub fn lookup(&self, name: &str, scope_id: usize) -> Result<&Symbol, String> {
fn insert_module_or_type(&mut self, name: &str, symbol: Symbol) -> Result<(), SymbolInsertError> {
if let Some(defined_symbol) = self.scopes[self.current_scope_id]
.type_and_module_symbols
.get(name)
{
Err(SymbolAlreadyDefined(defined_symbol.clone()))
} else {
self.scopes[self.current_scope_id]
.type_and_module_symbols
.insert(name.to_string(), symbol);
Ok(())
}
}
pub fn lookup(&self, name: &str, scope_id: usize) -> Result<&Symbol, SymbolLookupError> {
let mut scope_opt = Some(&self.scopes[scope_id]);
while let Some(scope) = scope_opt {
if let Some(symbol) = scope.symbols.get(name) {
if let Some(symbol) = scope.function_and_variable_symbols.get(name) {
return Ok(symbol);
}
scope_opt = if let Some(parent_id) = scope.parent {
@ -73,15 +111,16 @@ impl SymbolTable {
None
};
}
Err(format!("Symbol '{}' not found in current scope.", name))
Err(NoDefinition)
}
}
impl Display for SymbolTable {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
writeln!(f, "SymbolTable(current_scope = {})", self.current_scope_id)?;
for (i, scope) in self.scopes.iter().enumerate() {
writeln!(f, "Scope {} {}", i, scope.debug_name)?;
for (name, symbol) in &scope.symbols {
for (name, symbol) in &scope.function_and_variable_symbols {
writeln!(f, " {}({})", name, symbol)?;
}
}