Skip to main content

leekscript_analysis/
lib.rs

1//! Semantic analysis: scope building and validation.
2//!
3//! **Signature files (.sig):** When `.sig` files are provided (e.g. stdlib), they are parsed
4//! with the signature grammar, then type expressions in each `function`/`global` declaration
5//! are turned into `Type` via `parse_type_expr` and seeded into the root scope. The type checker
6//! uses these for inference (e.g. call return types, global variable types). Use
7//! `analyze_with_signatures(program_root, signature_roots)` so that built-in names resolve and
8//! types are inferred from the .sig definitions.
9
10mod builtins;
11mod deprecation;
12mod error;
13mod node_helpers;
14mod scope;
15mod scope_builder;
16mod scope_extents;
17mod signature_loader;
18mod type_checker;
19mod type_expr;
20mod validator;
21
22pub use error::AnalysisError;
23pub use node_helpers::{
24    binary_expr_rhs, call_argument_count, call_argument_node, class_decl_info, class_field_info,
25    class_member_visibility, function_decl_info, member_expr_member_name,
26    member_expr_receiver_name, param_name, primary_expr_new_constructor,
27    primary_expr_resolvable_name, var_decl_info, ClassDeclInfo, FunctionDeclInfo, VarDeclInfo,
28    VarDeclKind,
29};
30pub use scope::{
31    complexity_display_string, MemberVisibility, ResolvedSymbol, Scope, ScopeId, ScopeKind,
32    ScopeStore, SigMeta, VariableInfo, VariableKind,
33};
34pub use scope_builder::{seed_scope_from_program, ScopeBuilder};
35pub use scope_extents::{build_scope_extents, scope_at_offset};
36pub use type_checker::{TypeChecker, TypeMapKey};
37pub use type_expr::{find_type_expr_child, parse_type_expr, TypeExprResult};
38pub use validator::Validator;
39
40use sipha::error::SemanticDiagnostic;
41use sipha::red::SyntaxNode;
42use sipha::types::Span;
43use sipha::walk::WalkOptions;
44use std::collections::HashMap;
45
46use leekscript_core::Type;
47
48/// Options for running analysis (single entry point for program or document).
49#[derive(Default)]
50pub struct AnalyzeOptions<'a> {
51    /// When set, scope is seeded from included files and (if present) `signature_roots`; then the main program is analyzed.
52    pub include_tree: Option<&'a leekscript_core::IncludeTree>,
53    /// When set, root scope is seeded with these signature roots (e.g. from `parse_signatures()`).
54    pub signature_roots: Option<&'a [SyntaxNode]>,
55}
56
57/// Run analysis with the given options. Dispatches to `analyze`, `analyze_with_signatures`, or `analyze_with_include_tree` as appropriate.
58#[must_use]
59pub fn analyze_with_options(
60    program_root: &SyntaxNode,
61    options: &AnalyzeOptions<'_>,
62) -> AnalysisResult {
63    if let Some(tree) = options.include_tree {
64        let sigs = options.signature_roots.unwrap_or(&[]);
65        return analyze_with_include_tree(tree, sigs);
66    }
67    if let Some(sigs) = options.signature_roots {
68        return analyze_with_signatures(program_root, sigs);
69    }
70    analyze(program_root)
71}
72
73/// Result of running scope building and validation.
74#[derive(Debug)]
75pub struct AnalysisResult {
76    pub diagnostics: Vec<SemanticDiagnostic>,
77    pub scope_store: ScopeStore,
78    /// Map from expression span (start, end) to inferred type (for formatter type annotations).
79    pub type_map: std::collections::HashMap<TypeMapKey, Type>,
80    /// Scope IDs in walk order (for LSP: compute scope at offset from scope-extent list).
81    pub scope_id_sequence: Vec<ScopeId>,
82}
83
84impl AnalysisResult {
85    #[must_use]
86    pub fn has_errors(&self) -> bool {
87        self.diagnostics
88            .iter()
89            .any(|d| d.severity == sipha::error::Severity::Error)
90    }
91
92    #[must_use]
93    pub fn is_valid(&self) -> bool {
94        !self.has_errors()
95    }
96}
97
98/// Run validation, type checking, and deprecation passes after scope building. Returns merged diagnostics and type map.
99fn run_pipeline(
100    program_root: &SyntaxNode,
101    store: &ScopeStore,
102    scope_id_sequence: &[ScopeId],
103    options: &WalkOptions,
104) -> (Vec<SemanticDiagnostic>, HashMap<TypeMapKey, Type>) {
105    let mut validator = Validator::new(store, scope_id_sequence);
106    let _ = program_root.walk(&mut validator, options);
107
108    let mut type_checker = TypeChecker::new(store, program_root);
109    let _ = program_root.walk(&mut type_checker, options);
110
111    let mut deprecation_checker = deprecation::DeprecationChecker::new();
112    let _ = program_root.walk(&mut deprecation_checker, options);
113
114    let mut diagnostics = validator.diagnostics;
115    diagnostics.extend(type_checker.diagnostics);
116    diagnostics.extend(deprecation_checker.diagnostics);
117
118    (diagnostics, type_checker.type_map)
119}
120
121/// Build scope from the tree and run validation. Returns diagnostics and the scope store.
122///
123/// Pass order: (1) `ScopeBuilder` runs first and builds the scope store and scope ID sequence.
124/// (2) Validator and (3) `TypeChecker` then use that store so resolution and type checking see the same scopes.
125#[must_use]
126pub fn analyze(root: &SyntaxNode) -> AnalysisResult {
127    let options = WalkOptions::nodes_only();
128    let mut builder = ScopeBuilder::new();
129    let _ = root.walk(&mut builder, &options);
130
131    for name in builtins::BUILTIN_CLASS_NAMES {
132        builder
133            .store
134            .add_root_class((*name).to_string(), Span::new(0, 0));
135    }
136
137    let (diagnostics, type_map) =
138        run_pipeline(root, &builder.store, &builder.scope_id_sequence, &options);
139
140    AnalysisResult {
141        diagnostics,
142        scope_store: builder.store,
143        type_map,
144        scope_id_sequence: builder.scope_id_sequence,
145    }
146}
147
148/// Seed the root scope from parsed signature file(s). Each element of `signature_roots`
149/// should be the root node returned by `parse_signatures()` (may be a wrapper or `NodeSigFile`).
150pub fn seed_scope_from_signatures(store: &mut ScopeStore, signature_roots: &[SyntaxNode]) {
151    signature_loader::seed_scope_from_signatures(store, signature_roots);
152}
153
154/// Analyze the main file of an include tree: seed scope from included files and signatures, then analyze the main AST.
155#[must_use]
156pub fn analyze_with_include_tree(
157    tree: &leekscript_core::IncludeTree,
158    signature_roots: &[SyntaxNode],
159) -> AnalysisResult {
160    let program_root = match &tree.root {
161        Some(r) => r.clone(),
162        None => {
163            return AnalysisResult {
164                diagnostics: Vec::new(),
165                scope_store: ScopeStore::new(),
166                type_map: std::collections::HashMap::new(),
167                scope_id_sequence: Vec::new(),
168            };
169        }
170    };
171    let options = WalkOptions::nodes_only();
172    let mut store = ScopeStore::new();
173    seed_scope_from_signatures(&mut store, signature_roots);
174    for (_, child) in &tree.includes {
175        if let Some(ref root) = child.root {
176            seed_scope_from_program(&mut store, root);
177        }
178    }
179    for name in builtins::BUILTIN_CLASS_NAMES {
180        store.add_root_class((*name).to_string(), Span::new(0, 0));
181    }
182    let mut builder = ScopeBuilder::with_store(store);
183    let _ = program_root.walk(&mut builder, &options);
184
185    let mut validator = Validator::new(&builder.store, &builder.scope_id_sequence);
186    let _ = program_root.walk(&mut validator, &options);
187
188    let mut type_checker = TypeChecker::new(&builder.store, &program_root);
189    let _ = program_root.walk(&mut type_checker, &options);
190
191    let mut deprecation_checker = deprecation::DeprecationChecker::new();
192    let _ = program_root.walk(&mut deprecation_checker, &options);
193
194    let mut diagnostics = validator.diagnostics;
195    diagnostics.extend(type_checker.diagnostics);
196    diagnostics.extend(deprecation_checker.diagnostics);
197
198    let type_map = type_checker.type_map.clone();
199    AnalysisResult {
200        diagnostics,
201        scope_store: builder.store,
202        type_map,
203        scope_id_sequence: builder.scope_id_sequence,
204    }
205}
206
207/// Analyze the program with the root scope pre-seeded from signature files (e.g. stdlib constants and functions).
208/// This allows references to global constants and built-in functions to resolve without errors.
209/// Also seeds built-in type/class names (e.g. `Class`, `bool`) so that common `LeekScript` code validates.
210#[must_use]
211pub fn analyze_with_signatures(
212    program_root: &SyntaxNode,
213    signature_roots: &[SyntaxNode],
214) -> AnalysisResult {
215    let options = WalkOptions::nodes_only();
216    let mut store = ScopeStore::new();
217    seed_scope_from_signatures(&mut store, signature_roots);
218    for name in builtins::BUILTIN_CLASS_NAMES {
219        store.add_root_class((*name).to_string(), Span::new(0, 0));
220    }
221    let mut builder = ScopeBuilder::with_store(store);
222    let _ = program_root.walk(&mut builder, &options);
223
224    let (diagnostics, type_map) = run_pipeline(
225        program_root,
226        &builder.store,
227        &builder.scope_id_sequence,
228        &options,
229    );
230
231    AnalysisResult {
232        diagnostics,
233        scope_store: builder.store,
234        type_map,
235        scope_id_sequence: builder.scope_id_sequence,
236    }
237}
238
239#[cfg(test)]
240mod tests;