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