Skip to main content

pine_sema/
analyzer.rs

1//! The semantic analyzer: a scope-aware walk that emits Tier 1 (name
2//! resolution) and Tier 4 (structural) errors.
3//!
4//! This intentionally does **not** use the shared [`pine_ast::Visitor`]. That
5//! traversal is for observational passes; sema needs to push/pop a scope at
6//! every block boundary, hoist declarations, and track context (loop depth,
7//! global-vs-local), which the default recurse-everything walk doesn't express.
8//! So we hand-write the recursion and interleave the scope bookkeeping.
9
10use std::collections::{HashMap, HashSet};
11
12use pine_ast::{Argument, ExportItem, Expr, FunctionParam, Literal, Loc, Program, Stmt};
13use pine_core::{LibraryLoader, PineOutput};
14use pine_interpreter::{BuiltinSignature, Value};
15use pine_parser::Parser;
16
17use crate::scope::{is_global_only, Namespace, SymbolKind};
18use crate::symbols::{FileId, ScopeId, ScopeKind, Symbol, SymbolId, SymbolTable};
19use pine_diagnostics::Diagnostic;
20
21pub struct Analyzer<'a, O: PineOutput> {
22    diagnostics: Vec<Diagnostic>,
23    /// Enclosing loops in the current function (reset at function boundaries).
24    loop_depth: u32,
25    /// The runtime's registered built-ins (namespaces, globals, per-bar variables).
26    builtins: &'a HashMap<String, Value<O>>,
27    /// Script declarations seen (indicator/strategy/library); at most one allowed.
28    declarations: u32,
29    /// Free functions, `name -> (required, total)` param counts, for arity checks.
30    functions: HashMap<String, (usize, usize)>,
31    /// User type/enum names, collected up front for forward-referencing annotations.
32    user_types: HashSet<String>,
33    /// Functions enclosing the current point; the last is the caller of any call.
34    fn_stack: Vec<String>,
35    /// The call graph `(caller, callee, call-site)`, scanned afterwards for cycles.
36    call_edges: Vec<CallEdge>,
37    /// The durable symbol table; `scope_ids` tracks the current (innermost-last) scope.
38    symbols: SymbolTable,
39    scope_ids: Vec<ScopeId>,
40    /// Resolves `import` paths to source; absent means no cross-file resolution.
41    loader: Option<&'a dyn LibraryLoader>,
42}
43
44/// Per-file state saved and restored around analyzing a library.
45struct FileState {
46    scope_ids: Vec<ScopeId>,
47    loop_depth: u32,
48    functions: HashMap<String, (usize, usize)>,
49    user_types: HashSet<String>,
50    declarations: u32,
51    fn_stack: Vec<String>,
52    call_edges: Vec<CallEdge>,
53}
54
55/// One call-graph edge: `(caller, callee, call-site position)`.
56type CallEdge = (String, String, Option<(u32, u32)>);
57
58/// The script-declaration functions — a script must have exactly one.
59const SCRIPT_DECLARATIONS: &[&str] = &["study", "indicator", "strategy", "library"];
60
61/// Built-in type names an annotation may use without a user declaration.
62const BUILTIN_TYPES: &[&str] = &[
63    "int", "float", "bool", "string", "color", "line", "linefill", "label", "box", "table",
64    "polyline", "array", "matrix", "map",
65];
66
67/// The called name as written, for diagnostics: `plot` or `ta.sma`.
68fn callee_name(callee: &Expr) -> String {
69    match callee {
70        Expr::Variable { name, .. } => name.clone(),
71        Expr::MemberAccess { object, member, .. } => match object.as_ref() {
72            Expr::Variable {
73                name: namespace, ..
74            } => format!("{namespace}.{member}"),
75            _ => member.clone(),
76        },
77        _ => String::new(),
78    }
79}
80
81/// Whether `start` reaches `target` in the call graph (a self-call counts).
82fn reaches(start: &str, target: &str, adjacency: &HashMap<&str, Vec<&str>>) -> bool {
83    if start == target {
84        return true;
85    }
86    let mut stack = vec![start];
87    let mut seen = HashSet::new();
88    while let Some(node) = stack.pop() {
89        if !seen.insert(node) {
90            continue;
91        }
92        if let Some(callees) = adjacency.get(node) {
93            for &callee in callees {
94                if callee == target {
95                    return true;
96                }
97                stack.push(callee);
98            }
99        }
100    }
101    false
102}
103
104/// The type names within an annotation: the base and every generic argument,
105/// with `[]`/`<>`/`,` stripped (`map<string, Point>` -> `map`, `string`, `Point`).
106fn type_names(annotation: &str) -> impl Iterator<Item = &str> {
107    annotation
108        .split(['<', '>', ',', '[', ']', ' '])
109        .filter(|name| !name.is_empty())
110}
111
112/// How to name a literal's type in a diagnostic.
113fn describe_literal(literal: &Literal) -> &'static str {
114    match literal {
115        Literal::Int(_) | Literal::Number(_) => "a number",
116        Literal::String(_) => "a string",
117        Literal::Bool(_) => "a bool",
118        Literal::HexColor(_) => "a color",
119        Literal::Na => "na",
120    }
121}
122
123impl<'a, O: PineOutput> Analyzer<'a, O> {
124    pub fn new(
125        builtins: &'a HashMap<String, Value<O>>,
126        loader: Option<&'a dyn LibraryLoader>,
127    ) -> Self {
128        Self {
129            diagnostics: Vec::new(),
130            loop_depth: 0,
131            builtins,
132            declarations: 0,
133            functions: HashMap::new(),
134            user_types: HashSet::new(),
135            fn_stack: Vec::new(),
136            call_edges: Vec::new(),
137            symbols: SymbolTable::new(),
138            scope_ids: vec![SymbolTable::GLOBAL],
139            loader,
140        }
141    }
142
143    /// The innermost open scope — where names resolve from and declarations are
144    /// recorded into.
145    fn current_scope(&self) -> ScopeId {
146        *self.scope_ids.last().expect("scope stack is never empty")
147    }
148
149    fn current_file(&self) -> FileId {
150        self.symbols.scope_file(self.current_scope())
151    }
152
153    fn current_lib(&self) -> Option<String> {
154        let file = self.current_file();
155        (file != SymbolTable::MAIN).then(|| self.symbols.file_path(file).to_string())
156    }
157
158    /// Open a nested scope in the symbol tree and make it current.
159    fn enter_scope(&mut self, kind: ScopeKind) {
160        let child = self.symbols.open_scope(self.current_scope(), kind);
161        self.scope_ids.push(child);
162    }
163
164    /// Close the current scope; its symbols stay in the table as a child scope.
165    fn exit_scope(&mut self) {
166        self.scope_ids.pop();
167    }
168
169    /// Declare a symbol, stamped with the current file.
170    fn record(&mut self, mut symbol: Symbol) -> SymbolId {
171        symbol.file = self.current_file();
172        self.symbols.declare(symbol)
173    }
174
175    /// Resolve `name` from the current scope outward.
176    fn resolve(&self, name: &str) -> Option<SymbolKind> {
177        self.symbols
178            .resolve(self.current_scope(), name)
179            .map(|symbol| symbol.kind)
180    }
181
182    /// Record a use of `name`, if it resolves to a user symbol.
183    fn record_use(&mut self, name: &str, loc: Loc) {
184        let scope = self.current_scope();
185        if let Some(id) = self.symbols.resolve_id(scope, name) {
186            let file = self.current_file();
187            self.symbols.record_use(file, loc.position(), id);
188        }
189    }
190
191    /// A declaration's user type, from an annotation or a `Type.new()` initializer.
192    fn infer_var_type(
193        &self,
194        type_annotation: Option<&String>,
195        initializer: Option<&Expr>,
196    ) -> Option<SymbolId> {
197        if let Some(annotation) = type_annotation {
198            let base = annotation.trim_end_matches("[]");
199            if let Some(id) = self.symbols.resolve_id(self.current_scope(), base) {
200                if matches!(
201                    self.symbols.symbol(id).kind,
202                    SymbolKind::Type | SymbolKind::Enum
203                ) {
204                    return Some(id);
205                }
206            }
207        }
208        // A `Type.new(...)` (or `lib.Type.new(...)`) constructor initializer.
209        if let Some(Expr::Call { callee, .. }) = initializer {
210            if let Expr::MemberAccess { object, member, .. } = callee.as_ref() {
211                if member == "new" {
212                    if let Some(id) = self.expr_type(object) {
213                        if self.symbols.symbol(id).kind == SymbolKind::Type {
214                            return Some(id);
215                        }
216                    }
217                }
218            }
219        }
220        None
221    }
222
223    /// The type/enum a symbol denotes: itself, or a variable's `type_ref`.
224    fn owner_type(&self, id: SymbolId) -> Option<SymbolId> {
225        let symbol = self.symbols.symbol(id);
226        match symbol.kind {
227            SymbolKind::Type | SymbolKind::Enum => Some(id),
228            SymbolKind::Var => symbol.type_ref,
229            _ => None,
230        }
231    }
232
233    /// The type/enum an expression's member access reads from (`Enum.Case`,
234    /// `v.field`, `lib.Point`), or `None` when the type is unknown.
235    fn expr_type(&self, expr: &Expr) -> Option<SymbolId> {
236        let id = match expr {
237            Expr::Variable { name, .. } => self.symbols.resolve_id(self.current_scope(), name)?,
238            Expr::MemberAccess { object, member, .. } => self.resolve_member(object, member)?,
239            _ => return None,
240        };
241        self.owner_type(id)
242    }
243
244    /// The declaration `object.member` points at: a library export, or a member
245    /// of the object's user type.
246    fn resolve_member(&self, object: &Expr, member: &str) -> Option<SymbolId> {
247        if let Some(module) = self.alias_module(object) {
248            return self.symbols.exported_id(module, member);
249        }
250        let owner = self.expr_type(object)?;
251        self.symbols.member_id(owner, member)
252    }
253
254    /// The imported library's global scope, if `object` is an import alias.
255    fn alias_module(&self, object: &Expr) -> Option<ScopeId> {
256        let Expr::Variable { name, .. } = object else {
257            return None;
258        };
259        let id = self.symbols.resolve_id(self.current_scope(), name)?;
260        let symbol = self.symbols.symbol(id);
261        (symbol.kind == SymbolKind::Import)
262            .then_some(symbol.module)
263            .flatten()
264    }
265
266    fn is_builtin(&self, name: &str) -> bool {
267        self.builtins.contains_key(name)
268    }
269
270    /// The signature of the builtin the callee names (`plot` or `ta.sma`), or
271    /// `None` — a user-shadowed name or a builtin with no declared parameters.
272    fn builtin_signature(&self, callee: &Expr) -> Option<BuiltinSignature> {
273        let value = match callee {
274            Expr::Variable { name, .. } => {
275                if self.resolve(name).is_some() {
276                    return None;
277                }
278                self.builtins.get(name)?.clone()
279            }
280            Expr::MemberAccess { object, member, .. } => {
281                let Expr::Variable {
282                    name: namespace, ..
283                } = object.as_ref()
284                else {
285                    return None;
286                };
287                if self.resolve(namespace).is_some() {
288                    return None;
289                }
290                match self.builtins.get(namespace)? {
291                    Value::Object { fields, .. } => fields.borrow().get(member)?.clone(),
292                    _ => return None,
293                }
294            }
295            _ => return None,
296        };
297
298        match value {
299            Value::BuiltinFunction(builtin) if !builtin.signature.params.is_empty() => {
300                Some(builtin.signature)
301            }
302            _ => None,
303        }
304    }
305
306    /// Check a call against the builtin's parameters: too many/few arguments, an
307    /// unknown named argument, and a literal of the wrong type.
308    fn check_builtin_args(
309        &mut self,
310        name: &str,
311        signature: &BuiltinSignature,
312        args: &[Argument],
313        pos: Option<(u32, u32)>,
314    ) {
315        let positional = args
316            .iter()
317            .filter(|arg| matches!(arg, Argument::Positional(_)))
318            .count();
319
320        if let Some(max) = signature.max_positional() {
321            if positional > max {
322                self.emit(
323                    "too-many-arguments",
324                    pos,
325                    format!("`{name}` takes at most {max} arguments, found {positional}"),
326                );
327            }
328        }
329
330        let mut index = 0;
331        for arg in args {
332            let (param, value) = match arg {
333                Argument::Positional(value) => {
334                    let param = signature.positional(index);
335                    index += 1;
336                    (param, value)
337                }
338                Argument::Named { name: label, value } => match signature.named(label) {
339                    Some(param) => (Some(param), value),
340                    None => {
341                        self.emit(
342                            "unknown-argument",
343                            pos,
344                            format!("`{name}` has no argument named `{label}`"),
345                        );
346                        continue;
347                    }
348                },
349            };
350
351            // Only a literal's type is known without inference; anything else
352            // is left to the runtime.
353            let (Some(param), Expr::Literal(literal)) = (param, value) else {
354                continue;
355            };
356            if !param.ty.accepts(literal) {
357                let found = describe_literal(literal);
358                let expected = param.ty.describe();
359                let label = param.name.clone();
360                self.emit(
361                    "argument-type",
362                    pos,
363                    format!("`{name}` expects {expected} for `{label}`, found {found}"),
364                );
365            }
366        }
367
368        // Counting (not position-matching) required params stays sound for
369        // leading-optional overloads like `ta.highest(length)`.
370        let required = signature
371            .params
372            .iter()
373            .filter(|param| param.required)
374            .count();
375        if args.len() < required {
376            self.emit(
377                "too-few-arguments",
378                pos,
379                format!(
380                    "`{name}` requires at least {required} arguments, found {}",
381                    args.len()
382                ),
383            );
384        }
385    }
386
387    /// Analyze one file in its own scope, type set, and call graph.
388    fn run_file(&mut self, program: &Program) {
389        // Types may be referenced before their declaration, so collect them first.
390        for stmt in &program.statements {
391            match stmt {
392                Stmt::TypeDecl { name, .. } | Stmt::EnumDecl { name, .. } => {
393                    self.user_types.insert(name.clone());
394                }
395                _ => {}
396            }
397        }
398        for stmt in &program.statements {
399            self.check_stmt(stmt);
400        }
401        self.detect_recursion();
402    }
403
404    /// Swap in fresh state for a library, returning the caller's to restore.
405    fn enter_file(&mut self, root: ScopeId) -> FileState {
406        FileState {
407            scope_ids: std::mem::replace(&mut self.scope_ids, vec![root]),
408            loop_depth: std::mem::take(&mut self.loop_depth),
409            functions: std::mem::take(&mut self.functions),
410            user_types: std::mem::take(&mut self.user_types),
411            declarations: std::mem::take(&mut self.declarations),
412            fn_stack: std::mem::take(&mut self.fn_stack),
413            call_edges: std::mem::take(&mut self.call_edges),
414        }
415    }
416
417    fn exit_file(&mut self, saved: FileState) {
418        self.scope_ids = saved.scope_ids;
419        self.loop_depth = saved.loop_depth;
420        self.functions = saved.functions;
421        self.user_types = saved.user_types;
422        self.declarations = saved.declarations;
423        self.fn_stack = saved.fn_stack;
424        self.call_edges = saved.call_edges;
425    }
426
427    /// Analyze the library at `path` once. Registering it before the walk lets a
428    /// re-entrant import find it, which breaks cycles.
429    fn resolve_import(&mut self, path: &str) -> Option<(FileId, ScopeId)> {
430        if let Some(file) = self.symbols.file_by_path(path) {
431            return Some((file, self.symbols.file_root(file)));
432        }
433        let loader = self.loader?;
434        let source = match loader.load_library(path) {
435            Ok(source) => source,
436            Err(err) => {
437                self.emit(
438                    "import-error",
439                    None,
440                    format!("cannot load library `{path}`: {err}"),
441                );
442                return None;
443            }
444        };
445        let program = match Parser::parse_source(&source) {
446            Ok(program) => program,
447            Err(err) => {
448                self.emit(
449                    "import-parse-error",
450                    None,
451                    format!("cannot parse library `{path}`: {err}"),
452                );
453                return None;
454            }
455        };
456        let (file, root) = self.symbols.add_file(path);
457        let saved = self.enter_file(root);
458        self.run_file(&program);
459        self.exit_file(saved);
460        Some((file, root))
461    }
462
463    /// Analyze a whole program, returning the errors found.
464    pub fn analyze(mut self, program: &Program) -> Vec<Diagnostic> {
465        self.run_file(program);
466        self.diagnostics
467    }
468
469    /// Analyze a whole program, returning both the errors and the symbol table
470    /// reconstructed from the same walk.
471    pub fn into_analysis(mut self, program: &Program) -> (Vec<Diagnostic>, SymbolTable) {
472        self.run_file(program);
473        (self.diagnostics, self.symbols)
474    }
475
476    /// Report call-graph cycles as recursion (Pine forbids it), at the call site.
477    fn detect_recursion(&mut self) {
478        let cycles: Vec<CallEdge> = {
479            let mut adjacency: HashMap<&str, Vec<&str>> = HashMap::new();
480            for (caller, callee, _) in &self.call_edges {
481                adjacency.entry(caller).or_default().push(callee);
482            }
483            self.call_edges
484                .iter()
485                .filter(|(caller, callee, _)| reaches(callee, caller, &adjacency))
486                .cloned()
487                .collect()
488        };
489        for (caller, callee, pos) in cycles {
490            let message = if caller == callee {
491                format!("`{caller}` calls itself; Pine does not allow recursion")
492            } else {
493                format!("`{caller}` and `{callee}` call each other; Pine does not allow recursion")
494            };
495            self.emit("recursion", pos, message);
496        }
497    }
498
499    fn emit(&mut self, rule: &'static str, pos: Option<(u32, u32)>, message: impl Into<String>) {
500        self.diagnostics
501            .push(Diagnostic::error(rule, pos, message).in_file(self.current_lib()));
502    }
503
504    fn warn(&mut self, rule: &'static str, pos: Option<(u32, u32)>, message: impl Into<String>) {
505        self.diagnostics
506            .push(Diagnostic::warning(rule, pos, message).in_file(self.current_lib()));
507    }
508
509    /// Warn that a declaration shadows a built-in (Pine allows it, but warns).
510    fn check_shadow(&mut self, name: &str) {
511        if self.is_builtin(name) {
512            self.warn(
513                "shadows-builtin",
514                None,
515                format!("declaration of `{name}` shadows a built-in"),
516            );
517        }
518    }
519
520    /// Reject a type annotation naming a type that is neither a built-in nor a
521    /// declared type — including every name inside a generic like `array<Foo>`.
522    fn check_type_annotation(&mut self, annotation: Option<&String>) {
523        let Some(annotation) = annotation else {
524            return;
525        };
526        for name in type_names(annotation) {
527            if !BUILTIN_TYPES.contains(&name) && !self.user_types.contains(name) {
528                self.emit("unknown-type", None, format!("unknown type `{name}`"));
529                return;
530            }
531        }
532    }
533
534    /// Check a user-function call's argument count against its parameters.
535    fn check_call_arity(
536        &mut self,
537        name: &str,
538        supplied: usize,
539        required: usize,
540        total: usize,
541        pos: Option<(u32, u32)>,
542    ) {
543        if supplied < required {
544            self.emit(
545                "too-few-arguments",
546                pos,
547                format!("`{name}` requires at least {required} arguments, found {supplied}"),
548            );
549        } else if supplied > total {
550            self.emit(
551                "too-many-arguments",
552                pos,
553                format!("`{name}` takes at most {total} arguments, found {supplied}"),
554            );
555        }
556    }
557
558    /// Record a user function and walk its body. Declared before the body so a
559    /// self-call inside reads as recursion.
560    fn analyze_function(
561        &mut self,
562        name: &str,
563        loc: Loc,
564        params: &[FunctionParam],
565        body: &[Stmt],
566    ) -> SymbolId {
567        let scope = self.current_scope();
568        if self
569            .symbols
570            .declared_locally_in(scope, name, Namespace::Value)
571        {
572            self.emit(
573                "duplicate-declaration",
574                None,
575                format!("`{name}` is already declared in this scope"),
576            );
577        }
578        let id = self.record(
579            Symbol::new(name, SymbolKind::Function, loc.position(), scope)
580                .with_params(params.iter().map(|p| p.name.clone()).collect()),
581        );
582        // A parameter with a default may be omitted, so it is not required.
583        let required = params.iter().filter(|p| p.default_value.is_none()).count();
584        self.functions
585            .insert(name.to_string(), (required, params.len()));
586        for param in params {
587            self.check_type_annotation(param.type_annotation.as_ref());
588        }
589        self.fn_stack.push(name.to_string());
590        self.function_body(
591            params.iter().map(|p| {
592                (
593                    p.name.as_str(),
594                    p.default_value.as_ref(),
595                    p.loc,
596                    p.type_annotation.as_ref(),
597                )
598            }),
599            body,
600        );
601        self.fn_stack.pop();
602        id
603    }
604
605    /// Declare `name` in the current scope, reporting a same-scope duplicate.
606    fn declare(&mut self, name: &str, kind: SymbolKind, loc: Loc) -> SymbolId {
607        let scope = self.current_scope();
608        if self
609            .symbols
610            .declared_locally_in(scope, name, kind.namespace())
611        {
612            self.emit(
613                "duplicate-declaration",
614                None,
615                format!("`{name}` is already declared in this scope"),
616            );
617        }
618        self.record(Symbol::new(name, kind, loc.position(), scope))
619    }
620
621    /// Visit a non-loop nested block (an `if`/`else` branch) in its own scope.
622    fn block(&mut self, body: &[Stmt]) {
623        self.enter_scope(ScopeKind::Block);
624        for stmt in body {
625            self.check_stmt(stmt);
626        }
627        self.exit_scope();
628    }
629
630    /// Visit a loop body with `loop_depth` raised so `break`/`continue` are legal.
631    fn loop_body(&mut self, body: &[Stmt]) {
632        self.loop_depth += 1;
633        for stmt in body {
634            self.check_stmt(stmt);
635        }
636        self.loop_depth -= 1;
637    }
638
639    /// Visit a function body in a fresh scope with `params` bound.
640    fn function_body<'p>(
641        &mut self,
642        params: impl Iterator<Item = (&'p str, Option<&'p Expr>, Loc, Option<&'p String>)>,
643        body: &[Stmt],
644    ) {
645        self.enter_scope(ScopeKind::Function);
646        let saved_loop_depth = self.loop_depth;
647        self.loop_depth = 0;
648        let scope = self.current_scope();
649        for (name, default, loc, type_annotation) in params {
650            if let Some(default) = default {
651                self.check_expr(default);
652            }
653            self.check_shadow(name);
654            if self.symbols.declared_locally(scope, name) {
655                self.emit(
656                    "duplicate-parameter",
657                    None,
658                    format!("parameter `{name}` is declared more than once"),
659                );
660            }
661            self.record(
662                Symbol::new(name, SymbolKind::Var, loc.position(), scope)
663                    .with_type(type_annotation.cloned()),
664            );
665        }
666        for stmt in body {
667            self.check_stmt(stmt);
668        }
669        self.loop_depth = saved_loop_depth;
670        self.exit_scope();
671    }
672
673    fn check_stmt(&mut self, stmt: &Stmt) {
674        match stmt {
675            Stmt::VarDecl {
676                name,
677                initializer,
678                type_annotation,
679                loc,
680                ..
681            } => {
682                self.check_type_annotation(type_annotation.as_ref());
683                self.check_shadow(name);
684                if let Some(Expr::Function { params, body }) = initializer {
685                    // A named function `f(x) => …`, lowered to a lambda-valued var.
686                    self.analyze_function(name, *loc, params, body);
687                } else {
688                    // Check the initializer *before* declaring the name, so a
689                    // self-reference (`x = x`) resolves against the outer scope.
690                    if let Some(init) = initializer {
691                        self.check_expr(init);
692                    }
693                    let scope = self.current_scope();
694                    if self
695                        .symbols
696                        .declared_locally_in(scope, name, Namespace::Value)
697                    {
698                        self.emit(
699                            "duplicate-declaration",
700                            None,
701                            format!(
702                                "`{name}` is already declared in this scope (use `:=` to reassign)"
703                            ),
704                        );
705                    }
706                    let type_ref =
707                        self.infer_var_type(type_annotation.as_ref(), initializer.as_ref());
708                    self.record(
709                        Symbol::new(name, SymbolKind::Var, loc.position(), scope)
710                            .with_type(type_annotation.clone())
711                            .with_type_ref(type_ref),
712                    );
713                }
714            }
715            Stmt::Assignment { target, value } => {
716                self.check_expr(value);
717                self.check_assign_target(target);
718            }
719            Stmt::TupleAssignment {
720                names, value, loc, ..
721            } => {
722                self.check_expr(value);
723                let scope = self.current_scope();
724                for name in names {
725                    // `_` is a discard, not a binding: it never collides and is
726                    // not recorded.
727                    if name == "_" {
728                        continue;
729                    }
730                    self.check_shadow(name);
731                    if self
732                        .symbols
733                        .declared_locally_in(scope, name, Namespace::Value)
734                    {
735                        self.emit(
736                            "duplicate-declaration",
737                            None,
738                            format!("`{name}` is already declared in this scope"),
739                        );
740                    }
741                    self.record(Symbol::new(name, SymbolKind::Var, loc.position(), scope));
742                }
743            }
744            Stmt::Expression(expr) => self.check_expr(expr),
745            Stmt::If {
746                condition,
747                then_branch,
748                else_if_branches,
749                else_branch,
750            } => {
751                self.check_expr(condition);
752                self.block(then_branch);
753                for (cond, body) in else_if_branches {
754                    self.check_expr(cond);
755                    self.block(body);
756                }
757                if let Some(body) = else_branch {
758                    self.block(body);
759                }
760            }
761            Stmt::For {
762                var_name,
763                from,
764                to,
765                body,
766                loc,
767            } => {
768                self.check_expr(from);
769                self.check_expr(to);
770                self.enter_scope(ScopeKind::Block);
771                self.check_shadow(var_name);
772                let scope = self.current_scope();
773                self.record(Symbol::new(
774                    var_name,
775                    SymbolKind::Var,
776                    loc.position(),
777                    scope,
778                ));
779                self.loop_body(body);
780                self.exit_scope();
781            }
782            Stmt::ForIn {
783                index_var,
784                item_var,
785                collection,
786                body,
787                loc,
788            } => {
789                self.check_expr(collection);
790                self.enter_scope(ScopeKind::Block);
791                let scope = self.current_scope();
792                if let Some(idx) = index_var {
793                    self.check_shadow(idx);
794                    self.record(Symbol::new(idx, SymbolKind::Var, loc.position(), scope));
795                }
796                self.check_shadow(item_var);
797                self.record(Symbol::new(
798                    item_var,
799                    SymbolKind::Var,
800                    loc.position(),
801                    scope,
802                ));
803                self.loop_body(body);
804                self.exit_scope();
805            }
806            Stmt::While { condition, body } => {
807                self.check_expr(condition);
808                self.enter_scope(ScopeKind::Block);
809                self.loop_body(body);
810                self.exit_scope();
811            }
812            Stmt::Break => self.check_loop_keyword("break"),
813            Stmt::Continue => self.check_loop_keyword("continue"),
814            Stmt::FunctionDecl {
815                name,
816                params,
817                body,
818                export,
819                loc,
820            } => {
821                self.check_shadow(name);
822                let id = self.analyze_function(name, *loc, params, body);
823                if *export {
824                    self.symbols.mark_exported(id);
825                }
826            }
827            Stmt::MethodDecl {
828                name,
829                params,
830                body,
831                export,
832                loc,
833            } => {
834                // Methods overload by receiver type, so the name is not duplicate-checked.
835                let scope = self.current_scope();
836                let id = self.record(
837                    Symbol::new(name, SymbolKind::Function, loc.position(), scope)
838                        .with_params(params.iter().map(|p| p.name.clone()).collect()),
839                );
840                if *export {
841                    self.symbols.mark_exported(id);
842                }
843                for param in params {
844                    self.check_type_annotation(param.type_annotation.as_ref());
845                }
846                self.function_body(
847                    params.iter().map(|p| {
848                        (
849                            p.name.as_str(),
850                            p.default_value.as_ref(),
851                            p.loc,
852                            p.type_annotation.as_ref(),
853                        )
854                    }),
855                    body,
856                );
857            }
858            Stmt::TypeDecl {
859                name,
860                fields,
861                export,
862                loc,
863            } => {
864                let owner = self.declare(name, SymbolKind::Type, *loc);
865                if *export {
866                    self.symbols.mark_exported(owner);
867                }
868                for field in fields {
869                    self.check_type_annotation(Some(&field.type_annotation));
870                    self.symbols.declare_member(
871                        owner,
872                        &field.name,
873                        field.loc.position(),
874                        Some(field.type_annotation.clone()),
875                    );
876                }
877            }
878            Stmt::EnumDecl {
879                name,
880                fields,
881                export,
882                loc,
883            } => {
884                let owner = self.declare(name, SymbolKind::Enum, *loc);
885                if *export {
886                    self.symbols.mark_exported(owner);
887                }
888                for field in fields {
889                    self.symbols
890                        .declare_member(owner, &field.name, field.loc.position(), None);
891                }
892            }
893            Stmt::Import { path, alias, loc } => {
894                let id = self.declare(alias, SymbolKind::Import, *loc);
895                if let Some((_, root)) = self.resolve_import(path) {
896                    self.symbols.set_module(id, root);
897                }
898            }
899            // `export name` re-exports an already-declared item: mark it exported.
900            Stmt::Export { item } => {
901                let name = match item {
902                    ExportItem::Function(name) | ExportItem::Type(name) => name,
903                };
904                if let Some(id) = self.symbols.resolve_id(self.current_scope(), name) {
905                    self.symbols.mark_exported(id);
906                }
907            }
908        }
909    }
910
911    fn check_loop_keyword(&mut self, keyword: &str) {
912        if self.loop_depth == 0 {
913            self.emit(
914                "break-outside-loop",
915                None,
916                format!("`{keyword}` is only valid inside a loop"),
917            );
918        }
919    }
920
921    /// Validate the left-hand side of a `:=` reassignment.
922    fn check_assign_target(&mut self, target: &Expr) {
923        match target {
924            Expr::Variable { name, loc } => match self.resolve(name) {
925                Some(SymbolKind::Var) => self.record_use(name, *loc),
926                Some(other) => self.emit(
927                    "invalid-assignment",
928                    None,
929                    format!("cannot assign to `{name}`, it is a {}", other.noun()),
930                ),
931                None if self.is_builtin(name) => self.emit(
932                    "reassign-builtin",
933                    None,
934                    format!("cannot reassign built-in `{name}`"),
935                ),
936                None => self.emit(
937                    "invalid-assignment",
938                    None,
939                    format!(
940                        "cannot assign to undeclared variable `{name}` (declare it with `=` first)"
941                    ),
942                ),
943            },
944            // `obj.field := …` or `arr[i] := …`: validate the object/index.
945            other => self.check_expr(other),
946        }
947    }
948
949    fn check_expr(&mut self, expr: &Expr) {
950        match expr {
951            Expr::Variable { name, loc } => {
952                if self.resolve(name).is_none() && !self.is_builtin(name) {
953                    self.emit(
954                        "undeclared-variable",
955                        None,
956                        format!("undeclared variable `{name}`"),
957                    );
958                } else {
959                    self.record_use(name, *loc);
960                }
961            }
962            Expr::Call {
963                callee, args, loc, ..
964            } => {
965                if let Expr::Variable {
966                    name: fname,
967                    loc: fname_loc,
968                } = callee.as_ref()
969                {
970                    let pos = loc.position();
971                    self.record_use(fname, *fname_loc);
972                    if is_global_only(fname) && self.current_scope() != SymbolTable::GLOBAL {
973                        self.emit(
974                            "global-scope-required",
975                            pos,
976                            format!("`{fname}` may only be called in the global scope"),
977                        );
978                    }
979                    if SCRIPT_DECLARATIONS.contains(&fname.as_str()) {
980                        self.declarations += 1;
981                        if self.declarations > 1 {
982                            self.emit(
983                                "duplicate-declaration",
984                                pos,
985                                "a script may only have one indicator/strategy/library declaration",
986                            );
987                        }
988                    }
989                    match self.resolve(fname) {
990                        Some(SymbolKind::Function) => {
991                            // Record the call as an edge out of the enclosing
992                            // function; cycles are found once the walk finishes.
993                            if let Some(caller) = self.fn_stack.last() {
994                                self.call_edges.push((caller.clone(), fname.clone(), pos));
995                            }
996                            if let Some(&(required, total)) = self.functions.get(fname) {
997                                self.check_call_arity(fname, args.len(), required, total, pos);
998                            }
999                        }
1000                        // A value, type or enum is not callable.
1001                        Some(kind @ (SymbolKind::Var | SymbolKind::Type | SymbolKind::Enum)) => {
1002                            self.emit(
1003                                "not-callable",
1004                                pos,
1005                                format!("`{fname}` is a {}, not a function", kind.noun()),
1006                            );
1007                        }
1008                        // An import alias is called through its members, not directly.
1009                        Some(SymbolKind::Import) => {}
1010                        None => {
1011                            if !self.is_builtin(fname) {
1012                                self.emit(
1013                                    "unknown-function",
1014                                    pos,
1015                                    format!("unknown function `{fname}`"),
1016                                );
1017                            }
1018                        }
1019                    }
1020                } else {
1021                    self.check_expr(callee);
1022                }
1023                if let Some(signature) = self.builtin_signature(callee) {
1024                    let name = callee_name(callee);
1025                    self.check_builtin_args(&name, &signature, args, loc.position());
1026                }
1027                for arg in args {
1028                    match arg {
1029                        Argument::Positional(e) => self.check_expr(e),
1030                        Argument::Named { value, .. } => self.check_expr(value),
1031                    }
1032                }
1033            }
1034            Expr::Binary { left, right, .. } => {
1035                self.check_expr(left);
1036                self.check_expr(right);
1037            }
1038            Expr::Unary { expr, .. } => self.check_expr(expr),
1039            Expr::Index { expr, index } => {
1040                self.check_expr(expr);
1041                self.check_expr(index);
1042            }
1043            // When the object's type is known, record the member's occurrence.
1044            Expr::MemberAccess {
1045                object,
1046                member,
1047                member_loc,
1048            } => {
1049                self.check_expr(object);
1050                if let Some(id) = self.resolve_member(object, member) {
1051                    let file = self.current_file();
1052                    self.symbols.record_use(file, member_loc.position(), id);
1053                }
1054            }
1055            Expr::Ternary {
1056                condition,
1057                then_expr,
1058                else_expr,
1059            } => {
1060                self.check_expr(condition);
1061                self.check_expr(then_expr);
1062                self.check_expr(else_expr);
1063            }
1064            Expr::IfExpr {
1065                condition,
1066                then_expr,
1067                else_if_branches,
1068                else_expr,
1069            } => {
1070                self.check_expr(condition);
1071                self.check_expr(then_expr);
1072                for (cond, e) in else_if_branches {
1073                    self.check_expr(cond);
1074                    self.check_expr(e);
1075                }
1076                if let Some(e) = else_expr {
1077                    self.check_expr(e);
1078                }
1079            }
1080            Expr::Switch { value, cases } => {
1081                self.check_expr(value);
1082                for (pattern, result) in cases {
1083                    self.check_expr(pattern);
1084                    self.check_expr(result);
1085                }
1086            }
1087            Expr::Array(elements) => {
1088                for e in elements {
1089                    self.check_expr(e);
1090                }
1091            }
1092            // A lambda: its own scope with parameters bound.
1093            Expr::Function { params, body } => {
1094                self.function_body(
1095                    params.iter().map(|p| {
1096                        (
1097                            p.name.as_str(),
1098                            p.default_value.as_ref(),
1099                            p.loc,
1100                            p.type_annotation.as_ref(),
1101                        )
1102                    }),
1103                    body,
1104                );
1105            }
1106            Expr::Literal(_) => {}
1107        }
1108    }
1109}