feat: Add AST analysis pass for purity and recursion

This commit introduces a new AST analysis pass that identifies function
purity and recursion. This information is then used by the optimizer and
specializer to make more informed decisions, particularly regarding
inlining.

The `Analyzer` struct and its associated `Analysis` struct are
responsible for traversing the AST and collecting this data.

Key changes include:
- A new `analyzer` module is added to `ast::compiler`.
- `Analyzer::analyze` performs a two-pass traversal to collect
  global-to-lambda mappings and then analyze purity and recursion.
- The `Optimizer` and `Specializer` are updated to accept and utilize
  the `Analysis` data.
- Recursion checks in `Optimizer` and `Specializer` are replaced with
  checks against the pre-computed `Analysis.is_recursive` set.
- The `Environment` now stores and passes the `Analysis` results to the
  compiler stages.
This commit is contained in:
Michael Schimmel
2026-02-22 15:00:20 +01:00
parent 5a017fb932
commit 8f7947bde1
6 changed files with 267 additions and 53 deletions
+15 -2
View File
@@ -1,3 +1,4 @@
use crate::ast::compiler::analyzer::{Analysis, Analyzer};
use crate::ast::compiler::binder::Binder;
use crate::ast::compiler::{TypeChecker, TypedNode};
use crate::ast::nodes::{Node, Symbol, UntypedKind};
@@ -29,6 +30,7 @@ pub struct Environment {
pub monomorph_cache: Rc<RefCell<MonoCache>>,
pub debug_mode: bool,
pub optimization: bool,
pub last_analysis: RefCell<Analysis>,
}
struct EnvFunctionRegistry {
@@ -96,6 +98,7 @@ impl Environment {
monomorph_cache: Rc::new(RefCell::new(HashMap::new())),
debug_mode: false,
optimization: true,
last_analysis: RefCell::new(Analysis::default()),
};
env.register_stdlib();
env
@@ -204,6 +207,10 @@ impl Environment {
// 7. Collect Typed Lambdas
LambdaCollector::collect(&typed_ast, &mut self.typed_function_registry.borrow_mut());
// 8. Analyze (Purity, Recursion)
let analysis = Analyzer::analyze(&typed_ast, &self.global_purity.borrow());
*self.last_analysis.borrow_mut() = analysis;
Ok(typed_ast)
}
@@ -216,7 +223,8 @@ impl Environment {
let optimizer = Optimizer::new(self.optimization)
.with_globals(self.global_values.clone())
.with_purity(self.global_purity.clone())
.with_registry(self.typed_function_registry.clone());
.with_registry(self.typed_function_registry.clone())
.with_analysis(self.last_analysis.borrow().clone());
let optimized = optimizer.optimize(specialized);
// 3. TCO (Always performed, converts to ExecNode)
@@ -302,7 +310,9 @@ impl Environment {
let global_types = self.global_types.clone();
let global_purity = self.global_purity.clone();
let optimization = self.optimization;
let analysis = self.last_analysis.borrow().clone();
let compiler_analysis = analysis.clone();
let compiler = Rc::new(
move |func_template: BoundNode,
arg_types: &[StaticType]|
@@ -323,6 +333,7 @@ impl Environment {
None,
Some(sub_rtl_lookup),
Some(mono_cache.clone()),
compiler_analysis.clone(),
);
let specialized_ast = sub_specializer.specialize(retyped_ast);
@@ -330,7 +341,8 @@ impl Environment {
// 3. Optimize (Phase 2: Cracking & Folding)
let optimizer = Optimizer::new(optimization)
.with_globals(global_values.clone())
.with_purity(global_purity.clone());
.with_purity(global_purity.clone())
.with_analysis(compiler_analysis.clone());
let optimized_ast = optimizer.optimize(specialized_ast);
// 4. TCO (converts to ExecNode)
@@ -359,6 +371,7 @@ impl Environment {
Some(compiler),
Some(rtl_lookup),
Some(self.monomorph_cache.clone()),
analysis,
);
specializer.specialize(node)