pine_sema/scope.rs
1//! The scope/symbol table — Tier 0.
2//!
3//! A stack of lexical scopes. The global program is the bottom scope; every
4//! function body and every `if`/`for`/`while` block pushes a new scope (Pine
5//! locals are visible only within their block). Name resolution walks the stack
6//! from innermost to outermost.
7
8use std::collections::HashMap;
9
10/// What a declared name refers to. This drives rules like "you can't reassign a
11/// function" — only [`SymbolKind::Var`] is a reassignable value.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum SymbolKind {
14 /// A variable (`x = …`, loop variable, tuple binding, parameter).
15 Var,
16 Function,
17 Type,
18 Enum,
19 /// An import alias (`import foo/bar/1 as alias`).
20 Import,
21}
22
23impl SymbolKind {
24 /// A human-readable noun for diagnostics.
25 pub fn noun(self) -> &'static str {
26 match self {
27 SymbolKind::Var => "variable",
28 SymbolKind::Function => "function",
29 SymbolKind::Type => "type",
30 SymbolKind::Enum => "enum",
31 SymbolKind::Import => "import",
32 }
33 }
34}
35
36/// A stack of scopes; the last element is the innermost (current) scope.
37pub struct ScopeStack {
38 scopes: Vec<HashMap<String, SymbolKind>>,
39}
40
41impl ScopeStack {
42 /// Create a stack with a single (global) scope already open.
43 pub fn new() -> Self {
44 Self {
45 scopes: vec![HashMap::new()],
46 }
47 }
48
49 pub fn push(&mut self) {
50 self.scopes.push(HashMap::new());
51 }
52
53 pub fn pop(&mut self) {
54 // The global scope is never popped.
55 debug_assert!(self.scopes.len() > 1, "attempted to pop the global scope");
56 self.scopes.pop();
57 }
58
59 /// True when the current scope is the global one.
60 pub fn at_global(&self) -> bool {
61 self.scopes.len() == 1
62 }
63
64 /// Declare `name` in the current scope. Returns the previously declared
65 /// kind if `name` already exists *in this same scope* (a redeclaration),
66 /// otherwise `None`.
67 pub fn declare(&mut self, name: &str, kind: SymbolKind) -> Option<SymbolKind> {
68 let scope = self
69 .scopes
70 .last_mut()
71 .expect("scope stack always has the global scope");
72 scope.insert(name.to_string(), kind)
73 }
74
75 /// Resolve `name` against all enclosing scopes, innermost first.
76 pub fn resolve(&self, name: &str) -> Option<SymbolKind> {
77 self.scopes
78 .iter()
79 .rev()
80 .find_map(|scope| scope.get(name).copied())
81 }
82}
83
84impl Default for ScopeStack {
85 fn default() -> Self {
86 Self::new()
87 }
88}
89
90/// Functions Pine only permits at **global** scope (never inside `if`, loops, or
91/// function bodies).
92const GLOBAL_ONLY_FUNCTIONS: &[&str] = &[
93 "plot",
94 "plotshape",
95 "plotchar",
96 "plotcandle",
97 "plotbar",
98 "plotarrow",
99 "fill",
100];
101
102/// May `name` only be called at global scope?
103pub fn is_global_only(name: &str) -> bool {
104 GLOBAL_ONLY_FUNCTIONS.contains(&name)
105}