Skip to main content

leekscript_analysis/
scope.rs

1//! Scope model for `LeekScript`: block chain with variables, globals, functions, classes.
2
3use sipha::types::Span;
4use std::collections::HashMap;
5
6use leekscript_core::doc_comment::DocComment;
7use leekscript_core::Type;
8
9/// Identifies a scope in the scope store (used for parent chain).
10#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
11pub struct ScopeId(pub usize);
12
13/// Kind of a scope block (determines what can be declared and how lookup works).
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum ScopeKind {
16    /// Root/main block: holds globals, user functions, classes.
17    Main,
18    /// Function or method body.
19    Function,
20    /// Class body (for method/constructor scope we use Function with class context if needed).
21    Class,
22    /// Loop body (for break/continue validation).
23    Loop,
24    /// Plain block `{ ... }`.
25    Block,
26}
27
28/// Variable binding: local, global, or parameter.
29#[derive(Clone, Debug)]
30pub struct VariableInfo {
31    pub name: String,
32    pub kind: VariableKind,
33    pub span: Span,
34    /// Declared type from annotation (e.g. `integer x = 0`), if any.
35    pub declared_type: Option<Type>,
36}
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub enum VariableKind {
40    Local,
41    Global,
42    Parameter,
43}
44
45/// One overload or default-param variant of a function.
46#[derive(Clone, Debug)]
47pub struct FunctionOverload {
48    pub min_arity: usize,
49    pub max_arity: usize,
50    pub span: Span,
51    /// When from .sig or annotated decl: param types and return type.
52    pub param_types: Option<Vec<Type>>,
53    pub return_type: Option<Type>,
54}
55
56/// Documentation and complexity metadata for a root function or global (from .sig).
57#[derive(Clone, Debug, Default)]
58pub struct SigMeta {
59    /// Doxygen-style doc (from `///` lines or `/**` … `*/` in .sig). May contain @param, @return, @complexity, etc.
60    pub doc: Option<DocComment>,
61    /// Complexity code 1–13 when set via @complexity in the doc (convenience copy; also in doc.complexity).
62    pub complexity: Option<u8>,
63}
64
65/// Display string for complexity code 1–13 from .sig `@complexity N`.
66#[must_use]
67pub fn complexity_display_string(code: u8) -> &'static str {
68    match code {
69        1 => "O(1)",
70        2 => "O(log(n))",
71        3 => "O(√n)",
72        4 => "O(n)",
73        5 => "O(nlog*(n))",
74        6 => "O(nlog(n))",
75        7 => "O(n²)",
76        8 => "O(n³)",
77        9 => "2^poly(log(n))",
78        10 => "2^poly(n)",
79        11 => "O(n!)",
80        12 => "2^2^poly(n)",
81        13 => "∞",
82        _ => "?",
83    }
84}
85
86/// Visibility of a class member. Properties and methods are **public by default** when no modifier is given.
87#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
88pub enum MemberVisibility {
89    #[default]
90    Public,
91    Protected,
92    Private,
93}
94
95/// Fields and methods of a class (for member access type inference: this.x, `this.method()`, Class.staticMember).
96#[derive(Clone, Debug, Default)]
97pub struct ClassMembers {
98    /// Instance field name -> (declared type, visibility).
99    pub fields: HashMap<String, (Type, MemberVisibility)>,
100    /// Instance method name -> (param types, return type, visibility).
101    pub methods: HashMap<String, (Vec<Type>, Type, MemberVisibility)>,
102    /// Static field name -> (declared type, visibility).
103    pub static_fields: HashMap<String, (Type, MemberVisibility)>,
104    /// Static method name -> (param types, return type, visibility).
105    pub static_methods: HashMap<String, (Vec<Type>, Type, MemberVisibility)>,
106}
107
108/// Single scope: variables and optional main-only data.
109#[derive(Debug)]
110pub struct Scope {
111    pub kind: ScopeKind,
112    pub parent: Option<ScopeId>,
113    variables: HashMap<String, VariableInfo>,
114    /// Main scope only: names declared as global.
115    globals: Option<std::collections::HashSet<String>>,
116    /// Main scope only: global name -> type (when from .sig).
117    global_types: Option<HashMap<String, Type>>,
118    /// Main scope only: user function name -> overloads (arity + optional types).
119    functions: Option<HashMap<String, Vec<FunctionOverload>>>,
120    /// Main scope only: class name -> first declaration span.
121    classes: Option<HashMap<String, Span>>,
122}
123
124impl Scope {
125    #[must_use]
126    pub fn new_main() -> Self {
127        Self {
128            kind: ScopeKind::Main,
129            parent: None,
130            variables: HashMap::new(),
131            globals: Some(std::collections::HashSet::new()),
132            global_types: Some(HashMap::new()),
133            functions: Some(HashMap::new()),
134            classes: Some(HashMap::new()),
135        }
136    }
137
138    #[must_use]
139    pub fn new_child(kind: ScopeKind, parent: ScopeId) -> Self {
140        Self {
141            kind,
142            parent: Some(parent),
143            variables: HashMap::new(),
144            globals: None,
145            global_types: None,
146            functions: None,
147            classes: None,
148        }
149    }
150
151    pub fn add_variable(&mut self, info: VariableInfo) {
152        self.variables.insert(info.name.clone(), info);
153    }
154
155    #[must_use]
156    pub fn has_variable(&self, name: &str) -> bool {
157        self.variables.contains_key(name)
158    }
159
160    /// Variable names in this scope (for LSP completion).
161    #[must_use]
162    pub fn variable_names(&self) -> Vec<String> {
163        self.variables.keys().cloned().collect()
164    }
165
166    /// Global names in this scope (main scope only; for LSP completion).
167    #[must_use]
168    pub fn global_names(&self) -> Option<Vec<String>> {
169        self.globals.as_ref().map(|g| g.iter().cloned().collect())
170    }
171
172    /// Function names in this scope (main scope only; for LSP completion).
173    #[must_use]
174    pub fn function_names(&self) -> Option<Vec<String>> {
175        self.functions.as_ref().map(|f| f.keys().cloned().collect())
176    }
177
178    /// Class names in this scope (main scope only; for LSP completion).
179    #[must_use]
180    pub fn class_names(&self) -> Option<Vec<String>> {
181        self.classes.as_ref().map(|c| c.keys().cloned().collect())
182    }
183
184    /// Add a global name (main scope only).
185    pub fn add_global(&mut self, name: String) {
186        if let Some(g) = &mut self.globals {
187            g.insert(name);
188        }
189    }
190
191    /// Add a user function (main scope only). Supports overloads and default params via (`min_arity`, `max_arity`).
192    pub fn add_function(&mut self, name: String, min_arity: usize, max_arity: usize, span: Span) {
193        if let Some(f) = &mut self.functions {
194            f.entry(name).or_default().push(FunctionOverload {
195                min_arity,
196                max_arity,
197                span,
198                param_types: None,
199                return_type: None,
200            });
201        }
202    }
203
204    /// Add a user function with optional param/return types (main scope only).
205    pub fn add_function_with_types(
206        &mut self,
207        name: String,
208        min_arity: usize,
209        max_arity: usize,
210        span: Span,
211        param_types: Option<Vec<Type>>,
212        return_type: Option<Type>,
213    ) {
214        if let Some(f) = &mut self.functions {
215            f.entry(name).or_default().push(FunctionOverload {
216                min_arity,
217                max_arity,
218                span,
219                param_types,
220                return_type,
221            });
222        }
223    }
224
225    /// Add a class name (main scope only). Keeps first declaration; span is for duplicate reporting.
226    /// A non-empty span (e.g. from the document) overwrites an empty one (e.g. from builtins/signatures)
227    /// so that LSP rename/definition use the real location.
228    pub fn add_class(&mut self, name: String, span: Span) {
229        if let Some(c) = &mut self.classes {
230            let is_empty = span.start >= span.end;
231            match c.entry(name) {
232                std::collections::hash_map::Entry::Vacant(e) => {
233                    e.insert(span);
234                }
235                std::collections::hash_map::Entry::Occupied(mut e) => {
236                    let existing = e.get();
237                    if existing.start >= existing.end && !is_empty {
238                        e.insert(span);
239                    }
240                }
241            }
242        }
243    }
244
245    #[must_use]
246    pub fn has_global(&self, name: &str) -> bool {
247        self.globals.as_ref().is_some_and(|g| g.contains(name))
248    }
249
250    /// True if this scope has a function with the given name that accepts the given arity.
251    #[must_use]
252    pub fn function_accepts_arity(&self, name: &str, arity: usize) -> bool {
253        self.functions.as_ref().is_some_and(|f| {
254            f.get(name).is_some_and(|ranges| {
255                ranges
256                    .iter()
257                    .any(|o| o.min_arity <= arity && arity <= o.max_arity)
258            })
259        })
260    }
261
262    /// Span of the first overload for a function name (for go-to-def).
263    #[must_use]
264    pub fn get_function_span(&self, name: &str) -> Option<Span> {
265        self.functions
266            .as_ref()
267            .and_then(|f| f.get(name).and_then(|v| v.first()).map(|o| o.span))
268    }
269
270    /// Span of an existing overload with the same (`min_arity`, `max_arity`), if any (for duplicate same-signature).
271    #[must_use]
272    pub fn get_function_span_for_arity_range(
273        &self,
274        name: &str,
275        min_arity: usize,
276        max_arity: usize,
277    ) -> Option<Span> {
278        self.functions.as_ref().and_then(|f| {
279            f.get(name).and_then(|ranges| {
280                ranges
281                    .iter()
282                    .find(|o| o.min_arity == min_arity && o.max_arity == max_arity)
283                    .map(|o| o.span)
284            })
285        })
286    }
287
288    /// Legacy: single arity (for resolve symbol). Returns first range's max if any.
289    #[must_use]
290    pub fn get_function_arity(&self, name: &str) -> Option<usize> {
291        self.functions
292            .as_ref()
293            .and_then(|f| f.get(name).and_then(|v| v.first()).map(|o| o.max_arity))
294    }
295
296    /// Get param types and return type for a function call that passes `arity` arguments, if known.
297    #[must_use]
298    pub fn get_function_type(&self, name: &str, arity: usize) -> Option<(Vec<Type>, Type)> {
299        let overloads = self.functions.as_ref()?.get(name)?;
300        let o = overloads
301            .iter()
302            .find(|o| o.min_arity <= arity && arity <= o.max_arity)?;
303        let params = o.param_types.clone()?;
304        let ret = o.return_type.clone().unwrap_or(Type::any());
305        Some((params, ret))
306    }
307
308    /// Get the type of a function when used as a value (e.g. `foo` without calling).
309    /// For a single overload with optional params (`min_arity` < `max_arity`), returns a union of
310    /// function types per arity, e.g. getMP(entity?) -> integer gives
311    /// `Function< => integer> | Function<integer => integer>`.
312    #[must_use]
313    pub fn get_function_type_as_value(&self, name: &str) -> Option<Type> {
314        let overloads = self.functions.as_ref()?.get(name)?;
315        let o = overloads.first()?;
316        let ret = o.return_type.clone().unwrap_or(Type::any());
317        let param_types = o.param_types.clone().unwrap_or_default();
318        if o.min_arity < o.max_arity && o.min_arity <= param_types.len() {
319            // Optional params: union of function types for each arity.
320            let mut variants = Vec::with_capacity(o.max_arity - o.min_arity + 1);
321            for arity in o.min_arity..=o.max_arity {
322                let args: Vec<Type> = param_types.iter().take(arity).cloned().collect();
323                variants.push(Type::function(args, ret.clone()));
324            }
325            Some(if variants.len() == 1 {
326                variants.into_iter().next().unwrap()
327            } else {
328                Type::compound(variants)
329            })
330        } else {
331            Some(Type::function(param_types, ret))
332        }
333    }
334
335    /// Add a global name with type (main scope only). Used when seeding from .sig.
336    pub fn add_global_with_type(&mut self, name: String, ty: Type) {
337        if let Some(g) = &mut self.globals {
338            g.insert(name.clone());
339        }
340        if let Some(gt) = &mut self.global_types {
341            gt.insert(name, ty);
342        }
343    }
344
345    /// Get the type of a global, if known (from .sig).
346    #[must_use]
347    pub fn get_global_type(&self, name: &str) -> Option<Type> {
348        self.global_types.as_ref()?.get(name).cloned()
349    }
350
351    #[must_use]
352    pub fn has_class(&self, name: &str) -> bool {
353        self.classes.as_ref().is_some_and(|c| c.contains_key(name))
354    }
355
356    /// First declaration span for a class in this scope (for duplicate diagnostic).
357    #[must_use]
358    pub fn get_class_first_span(&self, name: &str) -> Option<Span> {
359        self.classes.as_ref().and_then(|c| c.get(name).copied())
360    }
361
362    #[must_use]
363    pub fn get_variable(&self, name: &str) -> Option<&VariableInfo> {
364        self.variables.get(name)
365    }
366}
367
368/// Store for all scopes; root is at index 0.
369#[derive(Debug)]
370pub struct ScopeStore {
371    scopes: Vec<Scope>,
372    /// Class name -> its fields and methods (for this.x / `this.method()` type inference).
373    class_members: HashMap<String, ClassMembers>,
374    /// Root function name -> doc + complexity (from .sig).
375    root_function_meta: HashMap<String, SigMeta>,
376    /// Root global name -> doc + complexity (from .sig).
377    root_global_meta: HashMap<String, SigMeta>,
378}
379
380impl Default for ScopeStore {
381    fn default() -> Self {
382        Self::new()
383    }
384}
385
386impl ScopeStore {
387    #[must_use]
388    pub fn new() -> Self {
389        let mut store = Self {
390            scopes: Vec::new(),
391            class_members: HashMap::new(),
392            root_function_meta: HashMap::new(),
393            root_global_meta: HashMap::new(),
394        };
395        store.scopes.push(Scope::new_main());
396        store
397    }
398
399    #[must_use]
400    pub fn root_id(&self) -> ScopeId {
401        ScopeId(0)
402    }
403
404    #[must_use]
405    pub fn get(&self, id: ScopeId) -> Option<&Scope> {
406        self.scopes.get(id.0)
407    }
408
409    pub fn get_mut(&mut self, id: ScopeId) -> Option<&mut Scope> {
410        self.scopes.get_mut(id.0)
411    }
412
413    pub fn push(&mut self, kind: ScopeKind, parent: ScopeId) -> ScopeId {
414        let id = ScopeId(self.scopes.len());
415        self.scopes.push(Scope::new_child(kind, parent));
416        id
417    }
418
419    /// Add a global name to the root (main) scope. Used when seeding from signature files.
420    pub fn add_root_global(&mut self, name: String) {
421        if let Some(root) = self.scopes.get_mut(0) {
422            root.add_global(name);
423        }
424    }
425
426    /// Add a global name with type to the root scope. Used when seeding from .sig.
427    pub fn add_root_global_with_type(&mut self, name: String, ty: Type) {
428        if let Some(root) = self.scopes.get_mut(0) {
429            root.add_global_with_type(name, ty);
430        }
431    }
432
433    /// Add a function to the root (main) scope. Used when seeding from signature files.
434    /// Pass `Span::new(0, 0)` when no source span is available. Optional params (type?) give `min_arity` < `max_arity`.
435    pub fn add_root_function(
436        &mut self,
437        name: String,
438        min_arity: usize,
439        max_arity: usize,
440        span: Span,
441    ) {
442        if let Some(root) = self.scopes.get_mut(0) {
443            root.add_function(name, min_arity, max_arity, span);
444        }
445    }
446
447    /// Add a function with param/return types to the root scope. Used when seeding from .sig.
448    pub fn add_root_function_with_types(
449        &mut self,
450        name: String,
451        min_arity: usize,
452        max_arity: usize,
453        span: Span,
454        param_types: Option<Vec<Type>>,
455        return_type: Option<Type>,
456    ) {
457        if let Some(root) = self.scopes.get_mut(0) {
458            root.add_function_with_types(
459                name,
460                min_arity,
461                max_arity,
462                span,
463                param_types,
464                return_type,
465            );
466        }
467    }
468
469    /// Set doc and complexity for a root function (from .sig `///` and `@complexity`).
470    pub fn set_root_function_meta(&mut self, name: String, meta: SigMeta) {
471        self.root_function_meta.insert(name, meta);
472    }
473
474    /// Set doc and complexity for a root global (from .sig).
475    pub fn set_root_global_meta(&mut self, name: String, meta: SigMeta) {
476        self.root_global_meta.insert(name, meta);
477    }
478
479    /// Get doc and complexity for a root function, if any.
480    #[must_use]
481    pub fn get_root_function_meta(&self, name: &str) -> Option<&SigMeta> {
482        self.root_function_meta.get(name)
483    }
484
485    /// Get doc and complexity for a root global, if any.
486    #[must_use]
487    pub fn get_root_global_meta(&self, name: &str) -> Option<&SigMeta> {
488        self.root_global_meta.get(name)
489    }
490
491    /// Add a class name to the root (main) scope. Used for built-ins (e.g. Class).
492    /// Pass `Span::new(0, 0)` when no source span is available.
493    pub fn add_root_class(&mut self, name: String, span: Span) {
494        if let Some(root) = self.scopes.get_mut(0) {
495            root.add_class(name, span);
496        }
497    }
498
499    /// Register a class field for member type lookup (`this.field_name`). Visibility defaults to Public.
500    pub fn add_class_field(
501        &mut self,
502        class_name: &str,
503        field_name: String,
504        ty: Type,
505        visibility: MemberVisibility,
506    ) {
507        self.class_members
508            .entry(class_name.to_string())
509            .or_default()
510            .fields
511            .insert(field_name, (ty, visibility));
512    }
513
514    /// Register a class method for member type lookup (`this.method_name` returns function type). Visibility defaults to Public.
515    pub fn add_class_method(
516        &mut self,
517        class_name: &str,
518        method_name: String,
519        param_types: Vec<Type>,
520        return_type: Type,
521        visibility: MemberVisibility,
522    ) {
523        self.class_members
524            .entry(class_name.to_string())
525            .or_default()
526            .methods
527            .insert(method_name, (param_types, return_type, visibility));
528    }
529
530    /// Register a static field (ClassName.staticField).
531    pub fn add_class_static_field(
532        &mut self,
533        class_name: &str,
534        field_name: String,
535        ty: Type,
536        visibility: MemberVisibility,
537    ) {
538        self.class_members
539            .entry(class_name.to_string())
540            .or_default()
541            .static_fields
542            .insert(field_name, (ty, visibility));
543    }
544
545    /// Register a static method (ClassName.staticMethod).
546    pub fn add_class_static_method(
547        &mut self,
548        class_name: &str,
549        method_name: String,
550        param_types: Vec<Type>,
551        return_type: Type,
552        visibility: MemberVisibility,
553    ) {
554        self.class_members
555            .entry(class_name.to_string())
556            .or_default()
557            .static_methods
558            .insert(method_name, (param_types, return_type, visibility));
559    }
560
561    /// Type of a member (field or method) on a class instance. Returns None if unknown.
562    #[must_use]
563    pub fn get_class_member_type(&self, class_name: &str, member_name: &str) -> Option<Type> {
564        let members = self.class_members.get(class_name)?;
565        if let Some((ty, _)) = members.fields.get(member_name) {
566            return Some(ty.clone());
567        }
568        if let Some((params, ret, _)) = members.methods.get(member_name) {
569            return Some(Type::function(params.clone(), ret.clone()));
570        }
571        None
572    }
573
574    /// Type of a static member (ClassName.staticField or ClassName.staticMethod). Returns None if unknown.
575    #[must_use]
576    pub fn get_class_static_member_type(
577        &self,
578        class_name: &str,
579        member_name: &str,
580    ) -> Option<Type> {
581        let members = self.class_members.get(class_name)?;
582        if let Some((ty, _)) = members.static_fields.get(member_name) {
583            return Some(ty.clone());
584        }
585        if let Some((params, ret, _)) = members.static_methods.get(member_name) {
586            return Some(Type::function(params.clone(), ret.clone()));
587        }
588        None
589    }
590
591    /// Class members (instance and static fields/methods) for completion and member access.
592    #[must_use]
593    pub fn get_class_members(&self, class_name: &str) -> Option<&ClassMembers> {
594        self.class_members.get(class_name)
595    }
596
597    /// Get the type of a function when used as a value, searching from current scope up to root.
598    #[must_use]
599    pub fn get_function_type_as_value(&self, current: ScopeId, name: &str) -> Option<Type> {
600        let mut id = Some(current);
601        while let Some(scope_id) = id {
602            if let Some(scope) = self.get(scope_id) {
603                if let Some(ty) = scope.get_function_type_as_value(name) {
604                    return Some(ty);
605                }
606                id = scope.parent;
607            } else {
608                break;
609            }
610        }
611        None
612    }
613
614    /// True if the root (main) scope has a class with this name (for fallback type inference).
615    #[must_use]
616    pub fn root_has_class(&self, name: &str) -> bool {
617        self.get(ScopeId(0))
618            .is_some_and(|scope| scope.has_class(name))
619    }
620
621    /// Resolve a name: look in current scope and parents; also check main's functions and classes.
622    #[must_use]
623    pub fn resolve(&self, current: ScopeId, name: &str) -> Option<ResolvedSymbol> {
624        let mut id = Some(current);
625        while let Some(scope_id) = id {
626            let scope = self.get(scope_id)?;
627            if let Some(v) = scope.get_variable(name) {
628                return Some(ResolvedSymbol::Variable(v.clone()));
629            }
630            // Prefer class over global so that using a class name (e.g. PathManager.getCachedReachableCells) infers Class<T>.
631            if scope.has_class(name) {
632                return Some(ResolvedSymbol::Class(name.to_string()));
633            }
634            if scope.has_global(name) {
635                return Some(ResolvedSymbol::Global(name.to_string()));
636            }
637            if let Some(arity) = scope.get_function_arity(name) {
638                return Some(ResolvedSymbol::Function(name.to_string(), arity));
639            }
640            id = scope.parent;
641        }
642        None
643    }
644}
645
646#[derive(Clone, Debug)]
647pub enum ResolvedSymbol {
648    Variable(VariableInfo),
649    Global(String),
650    Function(String, usize),
651    Class(String),
652}