plotnik_lib/query/
symbol_table.rs

1//! Symbol table: name resolution and reference checking.
2//!
3//! Two-pass approach:
4//! 1. Collect all `Name = expr` definitions
5//! 2. Check that all `(UpperIdent)` references are defined
6
7use indexmap::IndexMap;
8
9/// Sentinel name for unnamed definitions (bare expressions at root level).
10/// Code generators can emit whatever name they want for this.
11pub const UNNAMED_DEF: &str = "_";
12
13use crate::diagnostics::DiagnosticKind;
14use crate::parser::{Expr, Ref, ast, token_src};
15
16use super::Query;
17
18pub type SymbolTable<'src> = IndexMap<&'src str, ast::Expr>;
19
20impl<'a> Query<'a> {
21    pub(super) fn resolve_names(&mut self) {
22        // Pass 1: collect definitions
23        for def in self.ast.defs() {
24            let (name, is_named) = match def.name() {
25                Some(token) => (token_src(&token, self.source), true),
26                None => (UNNAMED_DEF, false),
27            };
28
29            // Skip duplicate check for unnamed definitions (already diagnosed by parser)
30            if is_named && self.symbol_table.contains_key(name) {
31                let name_token = def.name().unwrap();
32                self.resolve_diagnostics
33                    .report(DiagnosticKind::DuplicateDefinition, name_token.text_range())
34                    .message(name)
35                    .emit();
36                continue;
37            }
38
39            // For unnamed defs, only keep the last one (parser already warned about others)
40            if !is_named && self.symbol_table.contains_key(name) {
41                self.symbol_table.shift_remove(name);
42            }
43
44            let Some(body) = def.body() else {
45                continue;
46            };
47            self.symbol_table.insert(name, body);
48        }
49
50        // Pass 2: check references
51        let defs: Vec<_> = self.ast.defs().collect();
52        for def in defs {
53            let Some(body) = def.body() else { continue };
54            self.collect_reference_diagnostics(&body);
55        }
56
57        // Parser wraps all top-level exprs in Def nodes, so this should be empty
58        assert!(
59            self.ast.exprs().next().is_none(),
60            "symbol_table: unexpected bare Expr in Root (parser should wrap in Def)"
61        );
62    }
63
64    fn collect_reference_diagnostics(&mut self, expr: &Expr) {
65        if let Expr::Ref(r) = expr {
66            self.check_ref_diagnostic(r);
67        }
68
69        for child in expr.children() {
70            self.collect_reference_diagnostics(&child);
71        }
72    }
73
74    fn check_ref_diagnostic(&mut self, r: &Ref) {
75        let Some(name_token) = r.name() else { return };
76        let name = name_token.text();
77
78        if self.symbol_table.contains_key(name) {
79            return;
80        }
81
82        self.resolve_diagnostics
83            .report(DiagnosticKind::UndefinedReference, name_token.text_range())
84            .message(name)
85            .emit();
86    }
87}