plotnik_compiler/analyze/
symbol_table.rs

1//! Symbol table: name resolution and reference checking.
2//!
3//! Two-pass approach:
4//! 1. Collect all `Name = expr` definitions from all sources
5//! 2. Check that all `(UpperIdent)` references are defined
6
7use indexmap::IndexMap;
8
9use crate::Diagnostics;
10use crate::diagnostics::DiagnosticKind;
11use crate::parser::{Root, ast, token_src};
12
13use super::visitor::Visitor;
14use crate::query::{SourceId, SourceMap};
15
16/// Sentinel name for unnamed definitions (bare expressions at root level).
17/// Code generators can emit whatever name they want for this.
18pub const UNNAMED_DEF: &str = "_";
19
20/// Registry of named definitions in a query.
21///
22/// Stores the mapping from definition names to their AST expressions,
23/// along with source file information for diagnostics.
24#[derive(Clone, Debug, Default)]
25pub struct SymbolTable {
26    /// Maps symbol name to its AST expression.
27    table: IndexMap<String, ast::Expr>,
28    /// Maps symbol name to the source file where it's defined.
29    files: IndexMap<String, SourceId>,
30}
31
32impl SymbolTable {
33    pub fn new() -> Self {
34        Self::default()
35    }
36
37    /// Insert a symbol definition.
38    ///
39    /// Returns `true` if the symbol was newly inserted, `false` if it already existed
40    /// (in which case the old value is replaced).
41    pub fn insert(&mut self, name: &str, source_id: SourceId, expr: ast::Expr) -> bool {
42        let is_new = !self.table.contains_key(name);
43        self.table.insert(name.to_owned(), expr);
44        self.files.insert(name.to_owned(), source_id);
45        is_new
46    }
47
48    /// Remove a symbol definition.
49    pub fn remove(&mut self, name: &str) -> Option<(SourceId, ast::Expr)> {
50        let expr = self.table.shift_remove(name)?;
51        let source_id = self.files.shift_remove(name)?;
52        Some((source_id, expr))
53    }
54
55    /// Check if a symbol is defined.
56    pub fn contains(&self, name: &str) -> bool {
57        self.table.contains_key(name)
58    }
59
60    /// Get the expression for a symbol.
61    pub fn get(&self, name: &str) -> Option<&ast::Expr> {
62        self.table.get(name)
63    }
64
65    /// Get the source file where a symbol is defined.
66    pub fn source_id(&self, name: &str) -> Option<SourceId> {
67        self.files.get(name).copied()
68    }
69
70    /// Get both the source ID and expression for a symbol.
71    pub fn get_full(&self, name: &str) -> Option<(SourceId, &ast::Expr)> {
72        let expr = self.table.get(name)?;
73        let source_id = self.files.get(name).copied()?;
74        Some((source_id, expr))
75    }
76
77    /// Number of symbols in the symbol table.
78    pub fn len(&self) -> usize {
79        self.table.len()
80    }
81
82    /// Check if the symbol table is empty.
83    pub fn is_empty(&self) -> bool {
84        self.table.is_empty()
85    }
86
87    /// Iterate over symbol names in insertion order.
88    pub fn keys(&self) -> impl Iterator<Item = &str> {
89        self.table.keys().map(String::as_str)
90    }
91
92    /// Iterate over (name, expr) pairs in insertion order.
93    pub fn iter(&self) -> impl Iterator<Item = (&str, &ast::Expr)> {
94        self.table.iter().map(|(k, v)| (k.as_str(), v))
95    }
96
97    /// Iterate over (name, source_id, expr) tuples in insertion order.
98    pub fn iter_full(&self) -> impl Iterator<Item = (&str, SourceId, &ast::Expr)> {
99        self.table.iter().map(|(k, v)| {
100            let source_id = self.files[k];
101            (k.as_str(), source_id, v)
102        })
103    }
104}
105
106pub fn resolve_names(
107    source_map: &SourceMap,
108    ast_map: &IndexMap<SourceId, Root>,
109    diag: &mut Diagnostics,
110) -> SymbolTable {
111    let mut symbol_table = SymbolTable::new();
112
113    // Pass 1: collect definitions from all sources
114    for (&source_id, ast) in ast_map {
115        let src = source_map.content(source_id);
116        let mut resolver = ReferenceResolver {
117            src,
118            source_id,
119            diag,
120            symbol_table: &mut symbol_table,
121        };
122        resolver.visit(ast);
123    }
124
125    // Pass 2: validate references from all sources
126    for (&source_id, ast) in ast_map {
127        let mut validator = ReferenceValidator {
128            source_id,
129            diag,
130            symbol_table: &symbol_table,
131        };
132        validator.visit(ast);
133    }
134
135    symbol_table
136}
137
138struct ReferenceResolver<'q, 'd, 'a> {
139    src: &'q str,
140    source_id: SourceId,
141    diag: &'d mut Diagnostics,
142    symbol_table: &'a mut SymbolTable,
143}
144
145impl Visitor for ReferenceResolver<'_, '_, '_> {
146    fn visit_def(&mut self, def: &ast::Def) {
147        let Some(body) = def.body() else { return };
148
149        if let Some(token) = def.name() {
150            // Named definition: `Name = ...`
151            let name = token_src(&token, self.src);
152            if self.symbol_table.contains(name) {
153                self.diag
154                    .report(
155                        self.source_id,
156                        DiagnosticKind::DuplicateDefinition,
157                        token.text_range(),
158                    )
159                    .message(name)
160                    .emit();
161            } else {
162                self.symbol_table.insert(name, self.source_id, body);
163            }
164        } else {
165            // Unnamed definition: `...` (root expression)
166            // Parser already validates multiple unnamed defs; we keep the last one.
167            if self.symbol_table.contains(UNNAMED_DEF) {
168                self.symbol_table.remove(UNNAMED_DEF);
169            }
170            self.symbol_table.insert(UNNAMED_DEF, self.source_id, body);
171        }
172    }
173}
174
175struct ReferenceValidator<'d, 'a> {
176    source_id: SourceId,
177    diag: &'d mut Diagnostics,
178    symbol_table: &'a SymbolTable,
179}
180
181impl Visitor for ReferenceValidator<'_, '_> {
182    fn visit_ref(&mut self, r: &ast::Ref) {
183        let Some(name_token) = r.name() else { return };
184        let name = name_token.text();
185
186        if self.symbol_table.contains(name) {
187            return;
188        }
189
190        self.diag
191            .report(
192                self.source_id,
193                DiagnosticKind::UndefinedReference,
194                name_token.text_range(),
195            )
196            .message(name)
197            .emit();
198    }
199}