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;
11
12use pine_ast::{Argument, Expr, Literal, Program, Stmt};
13use pine_interpreter::{BuiltinSignature, PineOutput, Value};
14
15use crate::scope::{is_global_only, ScopeStack, SymbolKind};
16use pine_diagnostics::Diagnostic;
17
18pub struct Analyzer<'a, O: PineOutput> {
19    scopes: ScopeStack,
20    diagnostics: Vec<Diagnostic>,
21    /// Number of enclosing loops in the *current function*. Reset across
22    /// function boundaries — a loop never spans a function.
23    loop_depth: u32,
24    /// The runtime's registered built-ins — namespaces, global functions, and
25    /// per-bar variables that exist without a user declaration. Supplied by the
26    /// caller rather than hardcoded here. Kept as the full value map (not just
27    /// names) so later passes can inspect the objects' types.
28    builtins: &'a HashMap<String, Value<O>>,
29    /// How many script declarations (`indicator`/`strategy`/`library`) have been
30    /// seen. A script may have at most one.
31    declarations: u32,
32}
33
34/// The script-declaration functions — a script must have exactly one.
35const SCRIPT_DECLARATIONS: &[&str] = &["study", "indicator", "strategy", "library"];
36
37/// The called name as written, for diagnostics: `plot` or `ta.sma`.
38fn callee_name(callee: &Expr) -> String {
39    match callee {
40        Expr::Variable(name) => name.clone(),
41        Expr::MemberAccess { object, member } => match object.as_ref() {
42            Expr::Variable(namespace) => format!("{namespace}.{member}"),
43            _ => member.clone(),
44        },
45        _ => String::new(),
46    }
47}
48
49/// How to name a literal's type in a diagnostic.
50fn describe_literal(literal: &Literal) -> &'static str {
51    match literal {
52        Literal::Int(_) | Literal::Number(_) => "a number",
53        Literal::String(_) => "a string",
54        Literal::Bool(_) => "a bool",
55        Literal::HexColor(_) => "a color",
56        Literal::Na => "na",
57    }
58}
59
60impl<'a, O: PineOutput> Analyzer<'a, O> {
61    pub fn new(builtins: &'a HashMap<String, Value<O>>) -> Self {
62        Self {
63            scopes: ScopeStack::new(),
64            diagnostics: Vec::new(),
65            loop_depth: 0,
66            builtins,
67            declarations: 0,
68        }
69    }
70
71    fn is_builtin(&self, name: &str) -> bool {
72        self.builtins.contains_key(name)
73    }
74
75    /// The arguments the called builtin accepts, if the callee names one.
76    ///
77    /// Resolves both a bare name (`plot`) and a namespaced one (`ta.sma`, whose
78    /// namespace is an object of builtins). A name the script has declared
79    /// itself shadows the builtin, and a builtin written by hand carries no
80    /// parameters — both yield `None`, so nothing is checked.
81    fn builtin_signature(&self, callee: &Expr) -> Option<BuiltinSignature> {
82        let value = match callee {
83            Expr::Variable(name) => {
84                if self.scopes.resolve(name).is_some() {
85                    return None;
86                }
87                self.builtins.get(name)?.clone()
88            }
89            Expr::MemberAccess { object, member } => {
90                let Expr::Variable(namespace) = object.as_ref() else {
91                    return None;
92                };
93                if self.scopes.resolve(namespace).is_some() {
94                    return None;
95                }
96                match self.builtins.get(namespace)? {
97                    Value::Object { fields, .. } => fields.borrow().get(member)?.clone(),
98                    _ => return None,
99                }
100            }
101            _ => return None,
102        };
103
104        match value {
105            Value::BuiltinFunction(builtin) if !builtin.signature.params.is_empty() => {
106                Some(builtin.signature)
107            }
108            _ => None,
109        }
110    }
111
112    /// Check a call's arguments against the builtin's parameters: too many
113    /// arguments, an unknown named argument, and an argument whose literal type
114    /// the parameter cannot accept.
115    fn check_builtin_args(
116        &mut self,
117        name: &str,
118        signature: &BuiltinSignature,
119        args: &[Argument],
120        pos: Option<(u32, u32)>,
121    ) {
122        let positional = args
123            .iter()
124            .filter(|arg| matches!(arg, Argument::Positional(_)))
125            .count();
126
127        if let Some(max) = signature.max_positional() {
128            if positional > max {
129                self.emit(
130                    "too-many-arguments",
131                    pos,
132                    format!("`{name}` takes at most {max} arguments, found {positional}"),
133                );
134            }
135        }
136
137        let mut index = 0;
138        for arg in args {
139            let (param, value) = match arg {
140                Argument::Positional(value) => {
141                    let param = signature.positional(index);
142                    index += 1;
143                    (param, value)
144                }
145                Argument::Named { name: label, value } => match signature.named(label) {
146                    Some(param) => (Some(param), value),
147                    None => {
148                        self.emit(
149                            "unknown-argument",
150                            pos,
151                            format!("`{name}` has no argument named `{label}`"),
152                        );
153                        continue;
154                    }
155                },
156            };
157
158            // Only a literal's type is known without inference; anything else
159            // is left to the runtime.
160            let (Some(param), Expr::Literal(literal)) = (param, value) else {
161                continue;
162            };
163            if !param.ty.accepts(literal) {
164                let found = describe_literal(literal);
165                let expected = param.ty.describe();
166                let label = param.name.clone();
167                self.emit(
168                    "argument-type",
169                    pos,
170                    format!("`{name}` expects {expected} for `{label}`, found {found}"),
171                );
172            }
173        }
174    }
175
176    /// Analyze a whole program, returning the errors found.
177    pub fn analyze(mut self, program: &Program) -> Vec<Diagnostic> {
178        for stmt in &program.statements {
179            self.check_stmt(stmt);
180        }
181        self.diagnostics
182    }
183
184    fn emit(&mut self, rule: &'static str, pos: Option<(u32, u32)>, message: impl Into<String>) {
185        self.diagnostics.push(Diagnostic::error(rule, pos, message));
186    }
187
188    /// Declare `name` in the current scope, reporting a duplicate if it already
189    /// exists there. Pine has no hoisting — names become visible in source
190    /// order — so this is called at each declaration's position.
191    fn declare(&mut self, name: &str, kind: SymbolKind) {
192        if self.scopes.declare(name, kind).is_some() {
193            self.emit(
194                "duplicate-declaration",
195                None,
196                format!("`{name}` is already declared in this scope"),
197            );
198        }
199    }
200
201    /// Visit a non-loop nested block (an `if`/`else` branch) in its own scope.
202    fn block(&mut self, body: &[Stmt]) {
203        self.scopes.push();
204        for stmt in body {
205            self.check_stmt(stmt);
206        }
207        self.scopes.pop();
208    }
209
210    /// Visit a loop body: its own scope, with `loop_depth` raised so
211    /// `break`/`continue` are legal inside it.
212    fn loop_body(&mut self, body: &[Stmt]) {
213        self.loop_depth += 1;
214        for stmt in body {
215            self.check_stmt(stmt);
216        }
217        self.loop_depth -= 1;
218    }
219
220    /// Visit a function/method/lambda body in a fresh scope with `params`
221    /// bound. Loop context does not cross into a function.
222    fn function_body<'p>(
223        &mut self,
224        params: impl Iterator<Item = (&'p str, Option<&'p Expr>)>,
225        body: &[Stmt],
226    ) {
227        self.scopes.push();
228        let saved_loop_depth = self.loop_depth;
229        self.loop_depth = 0;
230        for (name, default) in params {
231            if let Some(default) = default {
232                self.check_expr(default);
233            }
234            self.scopes.declare(name, SymbolKind::Var);
235        }
236        for stmt in body {
237            self.check_stmt(stmt);
238        }
239        self.loop_depth = saved_loop_depth;
240        self.scopes.pop();
241    }
242
243    fn check_stmt(&mut self, stmt: &Stmt) {
244        match stmt {
245            Stmt::VarDecl {
246                name, initializer, ..
247            } => {
248                // Check the initializer *before* declaring the name, so a
249                // self-reference (`x = x`) resolves against the outer scope.
250                if let Some(init) = initializer {
251                    self.check_expr(init);
252                }
253                if self.scopes.declare(name, SymbolKind::Var).is_some() {
254                    self.emit(
255                        "duplicate-declaration",
256                        None,
257                        format!(
258                            "`{name}` is already declared in this scope (use `:=` to reassign)"
259                        ),
260                    );
261                }
262            }
263            Stmt::Assignment { target, value } => {
264                self.check_expr(value);
265                self.check_assign_target(target);
266            }
267            Stmt::TupleAssignment { names, value } => {
268                self.check_expr(value);
269                for name in names {
270                    if self.scopes.declare(name, SymbolKind::Var).is_some() {
271                        self.emit(
272                            "duplicate-declaration",
273                            None,
274                            format!("`{name}` is already declared in this scope"),
275                        );
276                    }
277                }
278            }
279            Stmt::Expression(expr) => self.check_expr(expr),
280            Stmt::If {
281                condition,
282                then_branch,
283                else_if_branches,
284                else_branch,
285            } => {
286                self.check_expr(condition);
287                self.block(then_branch);
288                for (cond, body) in else_if_branches {
289                    self.check_expr(cond);
290                    self.block(body);
291                }
292                if let Some(body) = else_branch {
293                    self.block(body);
294                }
295            }
296            Stmt::For {
297                var_name,
298                from,
299                to,
300                body,
301            } => {
302                self.check_expr(from);
303                self.check_expr(to);
304                self.scopes.push();
305                self.scopes.declare(var_name, SymbolKind::Var);
306                self.loop_body(body);
307                self.scopes.pop();
308            }
309            Stmt::ForIn {
310                index_var,
311                item_var,
312                collection,
313                body,
314            } => {
315                self.check_expr(collection);
316                self.scopes.push();
317                if let Some(idx) = index_var {
318                    self.scopes.declare(idx, SymbolKind::Var);
319                }
320                self.scopes.declare(item_var, SymbolKind::Var);
321                self.loop_body(body);
322                self.scopes.pop();
323            }
324            Stmt::While { condition, body } => {
325                self.check_expr(condition);
326                self.scopes.push();
327                self.loop_body(body);
328                self.scopes.pop();
329            }
330            Stmt::Break => self.check_loop_keyword("break"),
331            Stmt::Continue => self.check_loop_keyword("continue"),
332            Stmt::FunctionDecl {
333                name, params, body, ..
334            } => {
335                // Declare the name first so the body may reference it.
336                self.declare(name, SymbolKind::Function);
337                self.function_body(
338                    params
339                        .iter()
340                        .map(|p| (p.name.as_str(), p.default_value.as_ref())),
341                    body,
342                );
343            }
344            Stmt::MethodDecl { params, body, .. } => {
345                // Methods may share a name (overload by receiver type), so the
346                // name is not declared/duplicate-checked; just check the body.
347                self.function_body(
348                    params
349                        .iter()
350                        .map(|p| (p.name.as_str(), p.default_value.as_ref())),
351                    body,
352                );
353            }
354            Stmt::TypeDecl { name, .. } => self.declare(name, SymbolKind::Type),
355            Stmt::EnumDecl { name, .. } => self.declare(name, SymbolKind::Enum),
356            Stmt::Import { alias, .. } => self.declare(alias, SymbolKind::Import),
357            // `export` re-exports an already-declared item; nothing to resolve.
358            Stmt::Export { .. } => {}
359        }
360    }
361
362    fn check_loop_keyword(&mut self, keyword: &str) {
363        if self.loop_depth == 0 {
364            self.emit(
365                "break-outside-loop",
366                None,
367                format!("`{keyword}` is only valid inside a loop"),
368            );
369        }
370    }
371
372    /// Validate the left-hand side of a `:=` reassignment.
373    fn check_assign_target(&mut self, target: &Expr) {
374        match target {
375            Expr::Variable(name) => match self.scopes.resolve(name) {
376                Some(SymbolKind::Var) => {}
377                Some(other) => self.emit(
378                    "invalid-assignment",
379                    None,
380                    format!("cannot assign to `{name}`, it is a {}", other.noun()),
381                ),
382                None if self.is_builtin(name) => self.emit(
383                    "reassign-builtin",
384                    None,
385                    format!("cannot reassign built-in `{name}`"),
386                ),
387                None => self.emit(
388                    "invalid-assignment",
389                    None,
390                    format!(
391                        "cannot assign to undeclared variable `{name}` (declare it with `=` first)"
392                    ),
393                ),
394            },
395            // `obj.field := …` or `arr[i] := …`: validate the object/index.
396            other => self.check_expr(other),
397        }
398    }
399
400    fn check_expr(&mut self, expr: &Expr) {
401        match expr {
402            Expr::Variable(name) => {
403                if self.scopes.resolve(name).is_none() && !self.is_builtin(name) {
404                    self.emit(
405                        "undeclared-variable",
406                        None,
407                        format!("undeclared variable `{name}`"),
408                    );
409                }
410            }
411            Expr::Call {
412                callee, args, loc, ..
413            } => {
414                if let Expr::Variable(fname) = callee.as_ref() {
415                    let pos = loc.position();
416                    if is_global_only(fname) && !self.scopes.at_global() {
417                        self.emit(
418                            "global-scope-required",
419                            pos,
420                            format!("`{fname}` may only be called in the global scope"),
421                        );
422                    }
423                    if SCRIPT_DECLARATIONS.contains(&fname.as_str()) {
424                        self.declarations += 1;
425                        if self.declarations > 1 {
426                            self.emit(
427                                "duplicate-declaration",
428                                pos,
429                                "a script may only have one indicator/strategy/library declaration",
430                            );
431                        }
432                    }
433                    if self.scopes.resolve(fname).is_none() && !self.is_builtin(fname) {
434                        self.emit(
435                            "unknown-function",
436                            pos,
437                            format!("unknown function `{fname}`"),
438                        );
439                    }
440                } else {
441                    self.check_expr(callee);
442                }
443                if let Some(signature) = self.builtin_signature(callee) {
444                    let name = callee_name(callee);
445                    self.check_builtin_args(&name, &signature, args, loc.position());
446                }
447                for arg in args {
448                    match arg {
449                        Argument::Positional(e) => self.check_expr(e),
450                        Argument::Named { value, .. } => self.check_expr(value),
451                    }
452                }
453            }
454            Expr::Binary { left, right, .. } => {
455                self.check_expr(left);
456                self.check_expr(right);
457            }
458            Expr::Unary { expr, .. } => self.check_expr(expr),
459            Expr::Index { expr, index } => {
460                self.check_expr(expr);
461                self.check_expr(index);
462            }
463            // Members are not validated (that is Tier 3 signature checking);
464            // only the base object must resolve.
465            Expr::MemberAccess { object, .. } => self.check_expr(object),
466            Expr::Ternary {
467                condition,
468                then_expr,
469                else_expr,
470            } => {
471                self.check_expr(condition);
472                self.check_expr(then_expr);
473                self.check_expr(else_expr);
474            }
475            Expr::IfExpr {
476                condition,
477                then_expr,
478                else_if_branches,
479                else_expr,
480            } => {
481                self.check_expr(condition);
482                self.check_expr(then_expr);
483                for (cond, e) in else_if_branches {
484                    self.check_expr(cond);
485                    self.check_expr(e);
486                }
487                if let Some(e) = else_expr {
488                    self.check_expr(e);
489                }
490            }
491            Expr::Switch { value, cases } => {
492                self.check_expr(value);
493                for (pattern, result) in cases {
494                    self.check_expr(pattern);
495                    self.check_expr(result);
496                }
497            }
498            Expr::Array(elements) => {
499                for e in elements {
500                    self.check_expr(e);
501                }
502            }
503            // A lambda: its own scope with parameters bound.
504            Expr::Function { params, body } => {
505                self.function_body(
506                    params
507                        .iter()
508                        .map(|p| (p.name.as_str(), p.default_value.as_ref())),
509                    body,
510                );
511            }
512            Expr::Literal(_) => {}
513        }
514    }
515}