Skip to main content

leekscript_analysis/
validator.rs

1//! Validation pass: resolve identifiers, check break/continue in loop, duplicate declarations.
2
3use sipha::error::SemanticDiagnostic;
4use sipha::red::SyntaxNode;
5use sipha::types::Span;
6use sipha::walk::{Visitor, WalkResult};
7use std::collections::HashMap;
8
9use leekscript_core::syntax::Kind;
10
11use super::error::AnalysisError;
12use super::node_helpers::{
13    class_decl_info, expr_identifier, for_in_loop_vars, function_decl_info, param_name,
14    var_decl_info,
15};
16use super::scope::{ScopeId, ScopeKind, ScopeStore};
17
18/// Collects diagnostics and maintains scope stack (replaying scope structure from first pass).
19/// Uses the same scope ID sequence as `ScopeBuilder` so `resolve()` looks up the correct scope.
20pub struct Validator<'a> {
21    pub store: &'a ScopeStore,
22    stack: Vec<ScopeId>,
23    /// Names declared in the current scope with span of first declaration (for duplicate detection and related info).
24    declared_in_scope: Vec<HashMap<String, Span>>,
25    /// Index into `scope_id_sequence` for the next push.
26    scope_id_index: usize,
27    /// Scope IDs in walk order (from `ScopeBuilder`) so we push the same IDs.
28    scope_id_sequence: &'a [ScopeId],
29    pub diagnostics: Vec<SemanticDiagnostic>,
30}
31
32impl<'a> Validator<'a> {
33    #[must_use]
34    pub fn new(store: &'a ScopeStore, scope_id_sequence: &'a [ScopeId]) -> Self {
35        Self {
36            store,
37            stack: vec![ScopeId(0)],
38            declared_in_scope: vec![HashMap::new()],
39            scope_id_index: 0,
40            scope_id_sequence,
41            diagnostics: Vec::new(),
42        }
43    }
44
45    fn current_scope(&self) -> ScopeId {
46        *self.stack.last().unwrap_or(&ScopeId(0))
47    }
48
49    fn push_scope(&mut self) {
50        let id = self
51            .scope_id_sequence
52            .get(self.scope_id_index)
53            .copied()
54            .unwrap_or_else(|| ScopeId(self.scope_id_index + 1));
55        self.scope_id_index += 1;
56        self.stack.push(id);
57        self.declared_in_scope.push(HashMap::new());
58    }
59
60    fn pop_scope(&mut self) {
61        if self.stack.len() > 1 {
62            self.stack.pop();
63            self.declared_in_scope.pop();
64        }
65    }
66
67    fn resolve(&self, name: &str) -> bool {
68        if self.store.resolve(self.current_scope(), name).is_some() {
69            return true;
70        }
71        // Fallback: variable may be declared in current or outer scope but not yet in store
72        // (e.g. same pass order); treat as resolved if we've seen it in declared_in_scope.
73        self.declared_in_scope
74            .iter()
75            .any(|map| map.contains_key(name))
76    }
77
78    fn in_loop(&self) -> bool {
79        self.stack.iter().rev().any(|&id| {
80            self.store
81                .get(id)
82                .is_some_and(|s| s.kind == ScopeKind::Loop)
83        })
84    }
85
86    /// True when we're in the main program block (not inside a function or class body).
87    fn in_main_block(&self) -> bool {
88        self.stack.iter().all(|&id| {
89            self.store
90                .get(id)
91                .is_none_or(|s| s.kind == ScopeKind::Main || s.kind == ScopeKind::Block)
92        })
93    }
94
95    /// True when we're inside a function scope (used to disallow nested function declarations).
96    fn in_function_scope(&self) -> bool {
97        self.stack.iter().any(|&id| {
98            self.store
99                .get(id)
100                .is_some_and(|s| s.kind == ScopeKind::Function)
101        })
102    }
103
104    /// True when we're inside a class body (method or constructor).
105    fn in_class_scope(&self) -> bool {
106        self.stack.iter().any(|&id| {
107            self.store
108                .get(id)
109                .is_some_and(|s| s.kind == ScopeKind::Class)
110        })
111    }
112
113    /// True when we're inside a method (function scope whose parent chain includes a class).
114    fn in_method_scope(&self) -> bool {
115        self.in_class_scope() && self.in_function_scope()
116    }
117}
118
119impl Visitor for Validator<'_> {
120    fn enter_node(&mut self, node: &SyntaxNode) -> WalkResult {
121        let kind = match node.kind_as::<Kind>() {
122            Some(k) => k,
123            None => return WalkResult::Continue(()),
124        };
125
126        match kind {
127            Kind::NodeBlock
128            | Kind::NodeWhileStmt
129            | Kind::NodeForStmt
130            | Kind::NodeForInStmt
131            | Kind::NodeDoWhileStmt => {
132                self.push_scope();
133                if matches!(kind, Kind::NodeForInStmt) {
134                    for (name, span) in for_in_loop_vars(node) {
135                        if let Some(declared) = self.declared_in_scope.last_mut() {
136                            declared.entry(name).or_insert(span);
137                        }
138                    }
139                }
140            }
141            Kind::NodeInclude => {
142                if !self.in_main_block() {
143                    if let Some(tok) = node.first_token() {
144                        self.diagnostics
145                            .push(AnalysisError::IncludeOnlyInMainBlock.at(tok.text_range()));
146                    }
147                }
148            }
149            Kind::NodeFunctionDecl => {
150                // Nested functions are not allowed (methods inside classes are; they don't have Function on stack yet).
151                if self.in_function_scope() {
152                    if let Some(info) = function_decl_info(node) {
153                        self.diagnostics
154                            .push(AnalysisError::FunctionOnlyInMainBlock.at(info.name_span));
155                    }
156                } else if self.in_main_block() {
157                    // User-defined top-level functions: no optional/default parameters allowed.
158                    if let Some(info) = function_decl_info(node) {
159                        if info.min_arity < info.max_arity {
160                            self.diagnostics.push(
161                                AnalysisError::OptionalParamsOnlyInStandardFunctionsOrMethods
162                                    .at(info.name_span),
163                            );
164                        }
165                        // Duplicate: same name and same (min_arity, max_arity) signature.
166                        if let Some(main) = self.store.get(self.store.root_id()) {
167                            if let Some(existing_span) = main.get_function_span_for_arity_range(
168                                &info.name,
169                                info.min_arity,
170                                info.max_arity,
171                            ) {
172                                if existing_span != info.name_span
173                                    && existing_span != Span::new(0, 0)
174                                {
175                                    self.diagnostics.push(
176                                        AnalysisError::DuplicateFunctionName.at_with_related(
177                                            info.name_span,
178                                            vec![(existing_span, "first declared here")],
179                                        ),
180                                    );
181                                }
182                            }
183                        }
184                    }
185                }
186                self.push_scope();
187            }
188            Kind::NodeClassDecl => {
189                if let Some(info) = class_decl_info(node) {
190                    if let Some(main) = self.store.get(self.store.root_id()) {
191                        if let Some(first_span) = main.get_class_first_span(&info.name) {
192                            if first_span != info.name_span && first_span != Span::new(0, 0) {
193                                self.diagnostics.push(
194                                    AnalysisError::DuplicateClassName.at_with_related(
195                                        info.name_span,
196                                        vec![(first_span, "first declared here")],
197                                    ),
198                                );
199                            }
200                        }
201                    }
202                }
203                self.push_scope();
204            }
205            Kind::NodeConstructorDecl => {
206                self.push_scope();
207            }
208            Kind::NodeParam => {
209                if let Some((name, span)) = param_name(node) {
210                    if let Some(declared) = self.declared_in_scope.last_mut() {
211                        declared.entry(name).or_insert(span);
212                    }
213                }
214            }
215            Kind::NodeVarDecl => {
216                if let Some(info) = var_decl_info(node) {
217                    if info.kind == super::node_helpers::VarDeclKind::Global
218                        && !self.in_main_block()
219                    {
220                        self.diagnostics
221                            .push(AnalysisError::GlobalOnlyInMainBlock.at(info.name_span));
222                    }
223                    if let Some(declared) = self.declared_in_scope.last_mut() {
224                        match declared.entry(info.name.clone()) {
225                            std::collections::hash_map::Entry::Occupied(entry) => {
226                                let first_span = *entry.get();
227                                self.diagnostics.push(
228                                    AnalysisError::VariableNameUnavailable.at_with_related(
229                                        info.name_span,
230                                        vec![(first_span, "first declared here")],
231                                    ),
232                                );
233                            }
234                            std::collections::hash_map::Entry::Vacant(entry) => {
235                                entry.insert(info.name_span);
236                            }
237                        }
238                    }
239                }
240            }
241            Kind::NodePrimaryExpr => {
242                if let Some((name, span)) = expr_identifier(node) {
243                    if name == "this" && self.in_method_scope() {
244                        // "this" is valid in method scope.
245                    } else if !self.resolve(&name) {
246                        self.diagnostics
247                            .push(AnalysisError::UnknownVariableOrFunction.at(span));
248                    }
249                }
250            }
251            Kind::NodeBreakStmt => {
252                if !self.in_loop() {
253                    if let Some(tok) = node.first_token() {
254                        self.diagnostics
255                            .push(AnalysisError::BreakOutOfLoop.at(tok.text_range()));
256                    }
257                }
258            }
259            Kind::NodeContinueStmt => {
260                if !self.in_loop() {
261                    if let Some(tok) = node.first_token() {
262                        self.diagnostics
263                            .push(AnalysisError::ContinueOutOfLoop.at(tok.text_range()));
264                    }
265                }
266            }
267            _ => {}
268        }
269
270        WalkResult::Continue(())
271    }
272
273    fn leave_node(&mut self, node: &SyntaxNode) -> WalkResult {
274        let kind = match node.kind_as::<Kind>() {
275            Some(k) => k,
276            None => return WalkResult::Continue(()),
277        };
278
279        match kind {
280            Kind::NodeBlock
281            | Kind::NodeFunctionDecl
282            | Kind::NodeClassDecl
283            | Kind::NodeConstructorDecl
284            | Kind::NodeWhileStmt
285            | Kind::NodeForStmt
286            | Kind::NodeForInStmt
287            | Kind::NodeDoWhileStmt => {
288                self.pop_scope();
289            }
290            _ => {}
291        }
292
293        WalkResult::Continue(())
294    }
295}