diff --git a/Cargo.lock b/Cargo.lock index 55f81929..a2f2af9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1137,6 +1137,7 @@ dependencies = [ name = "ndc_analyser" version = "0.3.0" dependencies = [ + "ahash", "itertools 0.15.0", "ndc_core", "ndc_lexer", diff --git a/ndc_analyser/Cargo.toml b/ndc_analyser/Cargo.toml index 48c56f64..d29d34fe 100644 --- a/ndc_analyser/Cargo.toml +++ b/ndc_analyser/Cargo.toml @@ -9,3 +9,4 @@ ndc_core.workspace = true ndc_lexer.workspace = true ndc_parser.workspace = true thiserror.workspace = true +ahash.workspace = true diff --git a/ndc_analyser/src/lib.rs b/ndc_analyser/src/lib.rs index 133ebf62..cd1a5e08 100644 --- a/ndc_analyser/src/lib.rs +++ b/ndc_analyser/src/lib.rs @@ -1,5 +1,7 @@ mod analyser; +mod name_resolution; mod scope; +mod symbols; pub use analyser::{Analyser, AnalysisError, AnalysisResult}; pub use scope::ScopeTree; diff --git a/ndc_analyser/src/name_resolution.rs b/ndc_analyser/src/name_resolution.rs new file mode 100644 index 00000000..b2031bdc --- /dev/null +++ b/ndc_analyser/src/name_resolution.rs @@ -0,0 +1,451 @@ +use crate::symbols::{SymbolId, SymbolTable}; +use ahash::AHashMap; +use ndc_lexer::Span; +use ndc_parser::{Expression, ExpressionLocation, ForBody, ForIteration, Lvalue, NodeId}; + +#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)] +pub(crate) struct ScopeId(usize); + +#[derive(Debug, Default)] +pub(crate) struct Scope { + parent: Option, + bindings: Vec, +} + +pub(crate) struct ScopeTree { + current: ScopeId, + scopes: Vec, +} + +pub(crate) struct NameResolver { + symbols: SymbolTable, + scopes: ScopeTree, + references: AHashMap, + // Lvalues have source spans but no NodeId. Keep their declaration/write + // bindings separately so later passes do not have to resolve them again. + lvalue_bindings: Vec<(Span, SymbolId)>, +} + +#[derive(Debug, thiserror::Error)] +pub(crate) enum NameResolutionError { + #[error("identifier '{name}' has not been declared")] + UnknownName { name: String, span: Span }, + #[error("index and member targets cannot declare names")] + InvalidDeclaration { span: Span }, + #[error("name resolution for {feature} is not implemented yet")] + Unsupported { feature: &'static str, span: Span }, +} + +impl Scope { + pub(crate) fn new_with_parent(parent: ScopeId) -> Self { + Self { + parent: Some(parent), + bindings: Vec::default(), + } + } + pub(crate) fn bind(&mut self, symbol_id: SymbolId) { + self.bindings.push(symbol_id); + } + + pub(crate) fn lookup(&self, name: &str, table: &SymbolTable) -> Option { + for id in self.bindings.iter().rev() { + if table.get(*id) == name { + return Some(*id); + } + } + None + } +} + +impl ScopeTree { + pub(crate) fn new() -> Self { + Self { + current: ScopeId(0), + scopes: vec![Scope::default()], + } + } + + pub(crate) fn bind(&mut self, symbol_id: SymbolId) { + self.scopes[self.current.0].bind(symbol_id); + } + + pub(crate) fn lookup(&self, name: &str, table: &SymbolTable) -> Option { + let mut current_scope = &self.scopes[self.current.0]; + loop { + if let Some(id) = current_scope.lookup(name, table) { + return Some(id); + } + + current_scope = if let Some(parent) = current_scope.parent { + &self.scopes[parent.0] + } else { + return None; + } + } + } + + pub(crate) fn enter_scope(&mut self) { + self.scopes.push(Scope::new_with_parent(self.current)); + self.current = ScopeId(self.scopes.len() - 1); + } + + pub(crate) fn leave_scope(&mut self) { + self.current = self.scopes[self.current.0] + .parent + .expect("internal error: scope stack is empty"); + } +} + +impl NameResolver { + pub(crate) fn new() -> Self { + Self { + symbols: Default::default(), + scopes: ScopeTree::new(), + references: Default::default(), + lvalue_bindings: Default::default(), + } + } + + /// Resolve lexical value names. Type annotations and member selection + /// belong to later passes; this does not validate types or control flow. + pub(crate) fn resolve(&mut self, loc: &ExpressionLocation) -> Result<(), NameResolutionError> { + match &loc.expression { + Expression::BoolLiteral(_) + | Expression::StringLiteral(_) + | Expression::NumericLiteral(_) + | Expression::Break + | Expression::Continue => {} + Expression::Identifier { name, .. } => { + let symbol = self.lookup(name, loc.span)?; + self.references.insert(loc.id, symbol); + } + Expression::Statement(inner) + | Expression::Grouping(inner) + | Expression::Cast { value: inner, .. } + | Expression::Return { value: inner } => self.resolve(inner)?, + Expression::Logical { left, right, .. } => { + self.resolve(left)?; + self.resolve(right)?; + } + Expression::VariableDeclaration { l_value, value, .. } => { + self.resolve(value)?; + self.declare_pattern(l_value, loc.span)?; + } + Expression::Assignment { l_value, r_value } => { + self.resolve_target(l_value)?; + self.resolve(r_value)?; + } + Expression::Block { statements } => { + self.in_scope(|resolver| { + for statement in statements { + resolver.resolve(statement)?; + } + Ok(()) + })?; + } + Expression::If { + condition, + on_true, + on_false, + } => { + self.resolve(condition)?; + self.resolve(on_true)?; + if let Some(on_false) = on_false { + self.resolve(on_false)?; + } + } + Expression::While { + expression, + loop_body, + } => { + self.resolve(expression)?; + self.resolve(loop_body)?; + } + Expression::For { iterations, body } => self.resolve_for(iterations, body, loc.span)?, + Expression::Call { + function, + arguments, + } + | Expression::OperatorCall { + function, + arguments, + } => { + self.resolve(function)?; + for argument in arguments { + self.resolve(argument)?; + } + } + Expression::MemberAccess { receiver, .. } => self.resolve(receiver)?, + Expression::Tuple { values } | Expression::List { values } => { + for value in values { + self.resolve(value)?; + } + } + Expression::Map { values, default } => { + for (key, value) in values { + self.resolve(key)?; + if let Some(value) = value { + self.resolve(value)?; + } + } + if let Some(default) = default { + self.resolve(default)?; + } + } + Expression::RangeInclusive { start, end } + | Expression::RangeExclusive { start, end } => { + if let Some(start) = start { + self.resolve(start)?; + } + if let Some(end) = end { + self.resolve(end)?; + } + } + Expression::FunctionDeclaration { name, body, .. } => { + s + return Err(NameResolutionError::Unsupported { + feature: "function declarations", + span: loc.span, + }); + } + Expression::StructDeclaration { .. } => { + return Err(NameResolutionError::Unsupported { + feature: "struct declarations", + span: loc.span, + }); + } + Expression::OpAssignment { .. } => { + return Err(NameResolutionError::Unsupported { + feature: "augmented assignment operator selection", + span: loc.span, + }); + } + } + Ok(()) + } + + fn lookup(&self, name: &str, span: Span) -> Result { + self.scopes + .lookup(name, &self.symbols) + .ok_or_else(|| NameResolutionError::UnknownName { + name: name.to_string(), + span, + }) + } + + fn declare_pattern(&mut self, pattern: &Lvalue, span: Span) -> Result<(), NameResolutionError> { + // Validate the whole pattern before publishing any of its names. + // The parser already rejects these targets in declarations, but a + // malformed AST should still produce an error rather than new names. + if pattern.non_binding_target().is_some() { + return Err(NameResolutionError::InvalidDeclaration { span }); + } + match pattern { + Lvalue::Identifier { + identifier, span, .. + } => { + let symbol = self.symbols.declare(identifier.clone()); + self.scopes.bind(symbol); + self.lvalue_bindings.push((*span, symbol)); + } + Lvalue::Sequence(items) => { + for item in items { + self.declare_pattern(item, span)?; + } + } + Lvalue::Index { .. } | Lvalue::Member { .. } => { + unreachable!("validated declaration pattern"); + } + } + Ok(()) + } + + fn resolve_target(&mut self, target: &Lvalue) -> Result<(), NameResolutionError> { + match target { + Lvalue::Identifier { + identifier, span, .. + } => { + let symbol = self.lookup(identifier, *span)?; + self.lvalue_bindings.push((*span, symbol)); + } + Lvalue::Sequence(items) => { + for item in items { + self.resolve_target(item)?; + } + } + Lvalue::Index { value, index, .. } => { + self.resolve(value)?; + self.resolve(index)?; + } + Lvalue::Member { receiver, .. } => self.resolve(receiver)?, + } + Ok(()) + } + + fn in_scope( + &mut self, + resolve: impl FnOnce(&mut Self) -> Result<(), NameResolutionError>, + ) -> Result<(), NameResolutionError> { + self.scopes.enter_scope(); + let result = resolve(self); + // Restore visibility on errors as well as successful traversal. + self.scopes.leave_scope(); + result + } + + fn resolve_for( + &mut self, + iterations: &[ForIteration], + body: &ForBody, + span: Span, + ) -> Result<(), NameResolutionError> { + match iterations.split_first() { + Some((ForIteration::Iteration { l_value, sequence }, tail)) => { + // A binder is not visible in its own iterable. Later + // iterables and guards can see the earlier binders. + self.resolve(sequence)?; + self.in_scope(|resolver| { + resolver.declare_pattern(l_value, span)?; + resolver.resolve_for(tail, body, span) + }) + } + Some((ForIteration::Guard(guard), tail)) => { + self.resolve(guard)?; + self.resolve_for(tail, body, span) + } + None => match body { + ForBody::Block(block) | ForBody::List { expr: block } => self.resolve(block), + ForBody::Map { + key, + value, + default, + } => { + self.resolve(key)?; + if let Some(value) = value { + self.resolve(value)?; + } + if let Some(default) = default { + self.resolve(default)?; + } + Ok(()) + } + }, + } + } +} + +#[cfg(test)] +mod resolver_tests; + +#[cfg(test)] +mod test { + use crate::name_resolution::Scope; + use crate::name_resolution::ScopeTree; + use crate::symbols::SymbolTable; + + #[test] + fn binding_follows_shadowing_rules() { + let mut table = SymbolTable::default(); + let first = table.declare("foo".to_string()); + let second = table.declare("foo".to_string()); + let other = table.declare("another".to_string()); + + let mut scope = Scope::default(); + scope.bind(first); + scope.bind(second); + scope.bind(other); + + // The second binding shadows the first + assert_eq!(scope.lookup("foo", &table), Some(second)); + + // Invalid bindings return None + assert_eq!(scope.lookup("nope", &table), None); + + // Bindings don't interfere + assert_eq!(scope.lookup("another", &table), Some(other)); + } + + #[test] + fn scope_tree_traverses() { + let mut table = SymbolTable::default(); + let first = table.declare("foo".to_string()); + let middle = table.declare("bar".to_string()); + + let mut tree = ScopeTree::new(); + tree.bind(first); + tree.enter_scope(); + tree.bind(middle); + tree.enter_scope(); + assert_eq!(tree.lookup("foo", &table), Some(first)); + assert_eq!(tree.lookup("bar", &table), Some(middle)); + assert_eq!(tree.lookup("missing", &table), None); + + tree.leave_scope(); + assert_eq!(tree.lookup("bar", &table), Some(middle)); + tree.leave_scope(); + assert_eq!(tree.lookup("foo", &table), Some(first)); + assert_eq!(tree.lookup("bar", &table), None); + } + + #[test] + fn scope_tree_shadows() { + let mut table = SymbolTable::default(); + let first = table.declare("foo".to_string()); + let second = table.declare("foo".to_string()); + + let mut tree = ScopeTree::new(); + tree.bind(first); + tree.enter_scope(); + tree.bind(second); + assert_eq!(tree.lookup("foo", &table), Some(second)); + + tree.leave_scope(); + assert_eq!(tree.lookup("foo", &table), Some(first)); + } + + #[test] + fn sibling_scopes_do_not_share_bindings() { + let mut table = SymbolTable::default(); + let first = table.declare("first".to_string()); + let second = table.declare("second".to_string()); + + let mut tree = ScopeTree::new(); + tree.enter_scope(); + tree.bind(first); + tree.leave_scope(); + assert_eq!(tree.lookup("first", &table), None); + + tree.enter_scope(); + assert_eq!(tree.lookup("first", &table), None); + tree.bind(second); + assert_eq!(tree.lookup("second", &table), Some(second)); + tree.leave_scope(); + assert_eq!(tree.lookup("first", &table), None); + assert_eq!(tree.lookup("second", &table), None); + } + + #[test] + fn lookup_does_not_change_where_new_bindings_are_created() { + let mut table = SymbolTable::default(); + let outer = table.declare("outer".to_string()); + let inner = table.declare("inner".to_string()); + + let mut tree = ScopeTree::new(); + tree.bind(outer); + tree.enter_scope(); + assert_eq!(tree.lookup("outer", &table), Some(outer)); + assert_eq!(tree.lookup("missing", &table), None); + + tree.bind(inner); + assert_eq!(tree.lookup("inner", &table), Some(inner)); + tree.leave_scope(); + assert_eq!(tree.lookup("inner", &table), None); + assert_eq!(tree.lookup("outer", &table), Some(outer)); + } + + #[test] + #[should_panic(expected = "internal error: scope stack is empty")] + fn leaving_root_scope_is_rejected() { + ScopeTree::new().leave_scope(); + } +} diff --git a/ndc_analyser/src/name_resolution/resolver_tests.rs b/ndc_analyser/src/name_resolution/resolver_tests.rs new file mode 100644 index 00000000..e27ad44e --- /dev/null +++ b/ndc_analyser/src/name_resolution/resolver_tests.rs @@ -0,0 +1,302 @@ +use super::{NameResolutionError, NameResolver}; +use ndc_lexer::{Lexer, SourceId}; +use ndc_parser::Parser; + +fn resolve_source(resolver: &mut NameResolver, source: &str) -> Result<(), NameResolutionError> { + let tokens = Lexer::new(source, SourceId::new(0)) + .collect::, _>>() + .expect("source lexes"); + let expressions = Parser::from_tokens(tokens).parse().expect("source parses"); + for expression in &expressions { + resolver.resolve(expression)?; + } + Ok(()) +} + +#[test] +fn initializer_reads_outer_binding_before_shadowing() { + let mut resolver = NameResolver::new(); + resolve_source(&mut resolver, "let x = 1; { let x = x; x; } x;").unwrap(); + + let outer = resolver.lvalue_bindings[0].1; + let inner = resolver.lvalue_bindings[1].1; + assert_ne!(outer, inner); + assert_eq!( + resolver + .references + .values() + .filter(|&&id| id == outer) + .count(), + 2 + ); + assert_eq!( + resolver + .references + .values() + .filter(|&&id| id == inner) + .count(), + 1 + ); + assert_eq!(resolver.scopes.lookup("x", &resolver.symbols), Some(outer)); +} + +#[test] +fn self_reference_reports_the_initializer_location() { + let mut resolver = NameResolver::new(); + let source = "let missing = missing;"; + let error = resolve_source(&mut resolver, source).unwrap_err(); + assert_eq!( + error.to_string(), + "identifier 'missing' has not been declared" + ); + let NameResolutionError::UnknownName { name, span } = error else { + panic!("expected an unknown name"); + }; + assert_eq!(name, "missing"); + assert_eq!(span.source_id(), SourceId::new(0)); + assert_eq!(&source[span.range()], "missing"); + assert_eq!(span.offset(), source.rfind("missing").unwrap()); + assert!(resolver.lvalue_bindings.is_empty()); +} + +#[test] +fn destructuring_resolves_all_initializers_before_binding_names() { + let mut resolver = NameResolver::new(); + resolve_source( + &mut resolver, + "let x = 1; let y = 2; let (x, (y, z)) = (y, (x, 3)); x; y; z;", + ) + .unwrap(); + + assert_eq!(resolver.lvalue_bindings.len(), 5); + for (_, symbol) in &resolver.lvalue_bindings { + assert_eq!( + resolver + .references + .values() + .filter(|&id| id == symbol) + .count(), + 1 + ); + } + assert_eq!( + resolver.scopes.lookup("x", &resolver.symbols), + Some(resolver.lvalue_bindings[2].1) + ); + assert_eq!( + resolver.scopes.lookup("y", &resolver.symbols), + Some(resolver.lvalue_bindings[3].1) + ); +} + +#[test] +fn assignment_reuses_declared_symbols() { + let mut resolver = NameResolver::new(); + resolve_source( + &mut resolver, + "let x = 1; let y = 2; (x, y) = (y, x); x = 3;", + ) + .unwrap(); + let bindings = &resolver.lvalue_bindings; + assert_eq!(bindings.len(), 5); + assert_eq!(bindings[0].1, bindings[2].1); + assert_eq!(bindings[1].1, bindings[3].1); + assert_eq!(bindings[0].1, bindings[4].1); +} + +#[test] +fn member_and_index_targets_resolve_receivers_without_declaring_names() { + let mut resolver = NameResolver::new(); + // This supplied name stands for a value in the surrounding environment. + let object = resolver.symbols.declare("object".to_string()); + resolver.scopes.bind(object); + resolve_source( + &mut resolver, + "let i = 0; object.field = i; object[i] = i; object.field;", + ) + .unwrap(); + assert_eq!(resolver.lvalue_bindings.len(), 1); + assert_eq!(resolver.scopes.lookup("field", &resolver.symbols), None); + assert_eq!( + resolver.scopes.lookup("object", &resolver.symbols), + Some(object) + ); + assert_eq!( + resolver + .references + .values() + .filter(|&&id| id == object) + .count(), + 3 + ); +} + +#[test] +fn traversal_reports_unknown_names_in_expression_children() { + let sources = [ + "(missing);", + "true and missing;", + "missing or false;", + "missing as Int;", + "missing(1);", + "callee(missing);", + "1 + missing;", + "missing.field;", + "[1, missing];", + "(1, missing);", + "%{missing: 1};", + "%{1: missing};", + "%{:missing};", + "%{missing};", + "if missing { 1; };", + "if true { missing; };", + "if true { 1; } else { missing; };", + "while missing { 1; };", + "while true { missing; };", + "return missing;", + "missing..;", + "let xs = []; xs[0..missing] = 1;", + "1..=missing;", + "missing = 1;", + "let x = 1; x = missing;", + "let xs = []; xs[missing] = 1;", + "missing[0] = 1;", + "missing.field = 1;", + ]; + for source in sources { + let mut resolver = NameResolver::new(); + for name in ["callee", "+"] { + let symbol = resolver.symbols.declare(name.to_string()); + resolver.scopes.bind(symbol); + } + let error = resolve_source(&mut resolver, source).expect_err(source); + let NameResolutionError::UnknownName { name, span } = error else { + panic!("expected an unknown name in {source:?}, got {error:?}"); + }; + assert_eq!(name, "missing", "{source}"); + assert!(source[span.range()].contains("missing"), "{source}"); + } +} + +#[test] +fn errors_restore_visibility_after_nested_blocks() { + let mut resolver = NameResolver::new(); + resolve_source( + &mut resolver, + "let x = 1; { let x = 2; { let local = 3; missing; } }", + ) + .unwrap_err(); + let outer = resolver.lvalue_bindings[0].1; + assert_eq!(resolver.scopes.lookup("x", &resolver.symbols), Some(outer)); + assert_eq!(resolver.scopes.lookup("local", &resolver.symbols), None); + resolve_source(&mut resolver, "x;").unwrap(); + assert_eq!( + resolver.references.values().copied().collect::>(), + vec![outer] + ); +} + +#[test] +fn later_comprehension_clauses_see_earlier_binders() { + let mut resolver = NameResolver::new(); + resolve_source( + &mut resolver, + "let xs = [[true]]; [y for x in xs, y in x, if y];", + ) + .unwrap(); + let bindings = &resolver.lvalue_bindings; + assert_eq!(bindings.len(), 3); + assert_eq!( + resolver + .references + .values() + .filter(|&&id| id == bindings[0].1) + .count(), + 1 + ); + assert_eq!( + resolver + .references + .values() + .filter(|&&id| id == bindings[1].1) + .count(), + 1 + ); + assert_eq!( + resolver + .references + .values() + .filter(|&&id| id == bindings[2].1) + .count(), + 2 + ); + assert_eq!(resolver.scopes.lookup("x", &resolver.symbols), None); + assert_eq!(resolver.scopes.lookup("y", &resolver.symbols), None); +} + +#[test] +fn iterable_uses_the_outer_binding_of_its_own_name() { + let mut resolver = NameResolver::new(); + resolve_source(&mut resolver, "let x = [1]; [x for x in x]; x;").unwrap(); + let outer = resolver.lvalue_bindings[0].1; + let inner = resolver.lvalue_bindings[1].1; + assert_eq!( + resolver + .references + .values() + .filter(|&&id| id == outer) + .count(), + 2 + ); + assert_eq!( + resolver + .references + .values() + .filter(|&&id| id == inner) + .count(), + 1 + ); +} + +#[test] +fn loop_body_forms_resolve_names_and_restore_scope_on_errors() { + for source in [ + "for x in [1] { missing; }", + "[missing for x in [1]];", + "[x for x in [1], if missing];", + "%{missing: x for x in [1]};", + "%{x: missing for x in [1]};", + "%{:missing, x: x for x in [1]};", + ] { + let mut resolver = NameResolver::new(); + let error = resolve_source(&mut resolver, source).unwrap_err(); + assert!( + matches!(error, NameResolutionError::UnknownName { ref name, .. } if name == "missing"), + "{source}: {error:?}" + ); + assert_eq!( + resolver.scopes.lookup("x", &resolver.symbols), + None, + "{source}" + ); + } +} + +#[test] +fn unfinished_language_features_report_errors_instead_of_panicking() { + for (source, feature) in [ + ("fn identity(x) => x;", "function declarations"), + ("struct Point { x: Int }", "struct declarations"), + ( + "let x = 1; x += 2;", + "augmented assignment operator selection", + ), + ] { + let mut resolver = NameResolver::new(); + let error = resolve_source(&mut resolver, source).unwrap_err(); + assert!( + matches!(error, NameResolutionError::Unsupported { feature: found, span } if found == feature && span.source_id() == SourceId::new(0)), + "{error:?}" + ); + } +} diff --git a/ndc_analyser/src/symbols.rs b/ndc_analyser/src/symbols.rs new file mode 100644 index 00000000..fc9bba09 --- /dev/null +++ b/ndc_analyser/src/symbols.rs @@ -0,0 +1,43 @@ +#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)] +pub(crate) struct SymbolId(usize); + +#[derive(Debug, Default)] +pub(crate) struct SymbolTable { + names: Vec, +} + +impl SymbolTable { + pub(crate) fn declare(&mut self, name: String) -> SymbolId { + self.names.push(name); + SymbolId(self.names.len() - 1) + } + + pub(crate) fn get(&self, id: SymbolId) -> &str { + &self.names[id.0] + } +} + +#[cfg(test)] +mod test { + use crate::symbols::SymbolTable; + + #[test] + fn it_tracks_multiple_identifiers() { + let mut table = SymbolTable::default(); + let a = table.declare("foo".to_string()); + let b = table.declare("bar".to_string()); + + assert_ne!(a, b); + assert_eq!(table.get(a), "foo"); + assert_eq!(table.get(b), "bar"); + } + + #[test] + fn identifiers_with_the_same_name_are_not_equal() { + let mut table = SymbolTable::default(); + let a = table.declare("foo".to_string()); + let b = table.declare("foo".to_string()); + + assert_ne!(a, b); + } +}