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