259 lines
9.0 KiB
Rust
Executable File
259 lines
9.0 KiB
Rust
Executable File
//! Static variable resolution (Crafting Interpreters, chapter 11).
|
|
//!
|
|
//! Walks the AST with the shared [`Visitor`] traversal, tracking lexical scopes
|
|
//! in a [`ScopeStack`], and records for every variable *reference* how many
|
|
//! scopes up its declaration lives (`locals: NodeId -> distance`). It also
|
|
//! reports the resolution errors from chapter 11:
|
|
//!
|
|
//! * reading a local variable in its own initializer (`var a = a;`),
|
|
//! * declaring two variables with the same name in one local scope,
|
|
//! * `return` outside of any function.
|
|
//!
|
|
//! Diagnostics accumulate in an [`ErrorSink`] so a single pass surfaces them
|
|
//! all. The produced `locals` map is keyed by [`NodeId`] (stable identity),
|
|
//! ready for the interpreter to consume via distance-based lookup.
|
|
|
|
use std::collections::HashMap;
|
|
|
|
use crate::{
|
|
common::{
|
|
ast::{AstNode, Expr, NodeId},
|
|
base_value::{BaseValue, LoxFunction},
|
|
lox_result::{ErrorSink, LoxError, LoxResult},
|
|
},
|
|
middleend::{
|
|
scope_stack::ScopeStack,
|
|
visit_ast::{walk_expr, walk_function, Visitor},
|
|
},
|
|
};
|
|
|
|
/// Tracks whether resolution is currently inside a function body, so a
|
|
/// top-level `return` can be reported.
|
|
#[derive(Clone, Copy, PartialEq)]
|
|
enum FunctionType {
|
|
None,
|
|
Function,
|
|
}
|
|
|
|
pub struct Resolver {
|
|
scopes: ScopeStack<bool>,
|
|
locals: HashMap<NodeId, usize>,
|
|
errors: ErrorSink,
|
|
current_function: FunctionType,
|
|
}
|
|
|
|
impl Resolver {
|
|
pub fn new() -> Self {
|
|
Resolver {
|
|
scopes: ScopeStack::new(),
|
|
locals: HashMap::new(),
|
|
errors: ErrorSink::new(),
|
|
current_function: FunctionType::None,
|
|
}
|
|
}
|
|
|
|
/// Resolve a whole program, returning the per-reference scope distances or
|
|
/// the accumulated resolution errors.
|
|
pub fn resolve_program(
|
|
statements: &[AstNode],
|
|
) -> Result<HashMap<NodeId, usize>, Vec<LoxError>> {
|
|
let mut resolver = Resolver::new();
|
|
for statement in statements {
|
|
// Visiting only fails for fatal/internal errors, which this pass
|
|
// never produces; user-facing diagnostics go to `errors`.
|
|
let _ = resolver.visit_expr(statement);
|
|
}
|
|
if resolver.errors.has_errors() {
|
|
Err(resolver.errors.into_errors())
|
|
} else {
|
|
Ok(resolver.locals)
|
|
}
|
|
}
|
|
|
|
fn declare(&mut self, name: &str, slice: &crate::frontend::source_registry::SourceSlice) {
|
|
if self.scopes.is_empty() {
|
|
return; // global scope is untracked
|
|
}
|
|
if self.scopes.declared_in_current(name) {
|
|
self.errors.report(LoxError::ParseError {
|
|
source_slice: slice.clone(),
|
|
message: format!("Already a variable named '{}' in this scope.", name),
|
|
});
|
|
}
|
|
self.scopes.declare(name.to_string(), false);
|
|
}
|
|
|
|
fn define(&mut self, name: &str) {
|
|
if self.scopes.is_empty() {
|
|
return;
|
|
}
|
|
self.scopes.set_local(name, true);
|
|
}
|
|
|
|
fn resolve_local(&mut self, id: NodeId, name: &str) {
|
|
if let Some(distance) = self.scopes.resolve(name) {
|
|
self.locals.insert(id, distance);
|
|
}
|
|
// Not found locally: assume global, record nothing.
|
|
}
|
|
}
|
|
|
|
impl Default for Resolver {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl Visitor for Resolver {
|
|
fn visit_expr(&mut self, expr: &AstNode) -> LoxResult<()> {
|
|
match &expr.node {
|
|
Expr::Identifier { name } => {
|
|
if self.scopes.get_local(name) == Some(&false) {
|
|
self.errors.report(LoxError::ParseError {
|
|
source_slice: expr.source_slice.clone(),
|
|
message: "Can't read local variable in its own initializer.".to_string(),
|
|
});
|
|
}
|
|
self.resolve_local(expr.id, name);
|
|
Ok(())
|
|
}
|
|
Expr::Assign { name, .. } => {
|
|
walk_expr(self, expr)?; // resolve the assigned value first
|
|
self.resolve_local(expr.id, name);
|
|
Ok(())
|
|
}
|
|
Expr::VarDeclaration {
|
|
name, initializer, ..
|
|
} => {
|
|
// A function declaration is a var bound to a function literal.
|
|
// Define its name *before* resolving the body so it can recurse;
|
|
// a plain variable is defined *after* its initializer so that
|
|
// `var a = a;` is caught.
|
|
let is_function = matches!(
|
|
initializer.as_deref().map(|node| &node.node),
|
|
Some(Expr::Literal { value }) if matches!(**value, BaseValue::Function(_))
|
|
);
|
|
self.declare(name, &expr.source_slice);
|
|
if is_function {
|
|
self.define(name);
|
|
walk_expr(self, expr)?;
|
|
} else {
|
|
walk_expr(self, expr)?; // resolves the initializer, if any
|
|
self.define(name);
|
|
}
|
|
Ok(())
|
|
}
|
|
Expr::Block { .. } => {
|
|
self.scopes.begin_scope();
|
|
walk_expr(self, expr)?;
|
|
self.scopes.end_scope();
|
|
Ok(())
|
|
}
|
|
Expr::Return { .. } => {
|
|
if self.current_function == FunctionType::None {
|
|
self.errors.report(LoxError::ParseError {
|
|
source_slice: expr.source_slice.clone(),
|
|
message: "Can't return from top-level code.".to_string(),
|
|
});
|
|
}
|
|
walk_expr(self, expr) // resolve the returned expression
|
|
}
|
|
_ => walk_expr(self, expr),
|
|
}
|
|
}
|
|
|
|
fn visit_function(&mut self, function: &LoxFunction) -> LoxResult<()> {
|
|
let enclosing = std::mem::replace(&mut self.current_function, FunctionType::Function);
|
|
self.scopes.begin_scope();
|
|
// Parameters carry no source slice of their own; use the body's.
|
|
let param_slice = function.body.source_slice.clone();
|
|
for (param, _ty) in &function.parameters {
|
|
self.declare(param, ¶m_slice);
|
|
self.define(param);
|
|
}
|
|
walk_function(self, function)?; // resolves guard + body
|
|
self.scopes.end_scope();
|
|
self.current_function = enclosing;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::frontend::lexer::Lexer;
|
|
use crate::frontend::parser::Parser;
|
|
|
|
fn parse(src: &str) -> Vec<AstNode> {
|
|
let tokens = Lexer::new(src.to_string(), 0)
|
|
.scans_tokens()
|
|
.expect("source should lex");
|
|
Parser::new(tokens).parse().expect("source should parse")
|
|
}
|
|
|
|
fn resolve(src: &str) -> Result<HashMap<NodeId, usize>, Vec<LoxError>> {
|
|
Resolver::resolve_program(&parse(src))
|
|
}
|
|
|
|
#[test]
|
|
fn global_variables_are_not_resolved() {
|
|
// Top-level (global) scope is untracked, so nothing is recorded.
|
|
let locals = resolve("x := 1; print x;").expect("should resolve");
|
|
assert!(locals.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn local_read_resolves_to_distance_zero() {
|
|
let locals = resolve("do x := 1; print x; end").expect("should resolve");
|
|
assert_eq!(locals.len(), 1);
|
|
assert_eq!(*locals.values().next().unwrap(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn nested_scope_resolves_to_outer_distance() {
|
|
let src = "do x := 1; do print x; end end";
|
|
let locals = resolve(src).expect("should resolve");
|
|
assert_eq!(locals.len(), 1);
|
|
// `x` is read one scope above where it is read from.
|
|
assert_eq!(*locals.values().next().unwrap(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn reading_a_variable_in_its_own_initializer_is_an_error() {
|
|
let errors = resolve("do x := x; end").expect_err("should fail");
|
|
assert!(errors
|
|
.iter()
|
|
.any(|e| e.get_message().contains("its own initializer")));
|
|
}
|
|
|
|
#[test]
|
|
fn duplicate_declaration_in_same_scope_is_an_error() {
|
|
let errors = resolve("do x := 1; x := 2; end").expect_err("should fail");
|
|
assert!(errors
|
|
.iter()
|
|
.any(|e| e.get_message().contains("Already a variable")));
|
|
}
|
|
|
|
#[test]
|
|
fn return_at_top_level_is_an_error() {
|
|
let errors = resolve("return 1;").expect_err("should fail");
|
|
assert!(errors.iter().any(|e| e.get_message().contains("top-level")));
|
|
}
|
|
|
|
#[test]
|
|
fn return_inside_a_function_is_allowed() {
|
|
let locals = resolve("f :: fn (n): Number do return n; end;").expect("should resolve");
|
|
// `n` resolves from the body block up to the parameter scope.
|
|
assert_eq!(locals.len(), 1);
|
|
assert_eq!(*locals.values().next().unwrap(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn assignment_target_is_resolved() {
|
|
let locals = resolve("do x := 1; x = 2; end").expect("should resolve");
|
|
// Both the assignment target and... only the target is a reference here.
|
|
assert!(locals.values().all(|&d| d == 0));
|
|
assert!(!locals.is_empty());
|
|
}
|
|
}
|