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
+173
View File
@@ -0,0 +1,173 @@
use crate::ast::compiler::bound_nodes::{Address, BoundKind, TypedNode};
use crate::ast::types::{Identity, Purity, StaticType};
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, Default)]
pub struct Analysis {
pub purity: HashMap<Identity, Purity>,
pub is_recursive: HashSet<Identity>,
}
pub struct Analyzer<'a> {
global_purity: &'a HashMap<u32, Purity>,
results: Analysis,
/// Stack of currently visiting lambdas to detect direct recursion.
lambda_stack: Vec<Identity>,
/// Map of global index to its Lambda identity if known.
globals_to_lambdas: HashMap<u32, Identity>,
}
impl<'a> Analyzer<'a> {
pub fn analyze(node: &TypedNode, global_purity: &'a HashMap<u32, Purity>) -> Analysis {
let mut analyzer = Self {
global_purity,
results: Analysis::default(),
lambda_stack: Vec::new(),
globals_to_lambdas: HashMap::new(),
};
// First pass: map globals to their lambda identities
analyzer.collect_globals(node);
// Second pass: full analysis
analyzer.visit(node);
analyzer.results
}
fn collect_globals(&mut self, node: &TypedNode) {
match &node.kind {
BoundKind::DefGlobal { global_index, value, .. } => {
if let BoundKind::Lambda { .. } = &value.kind {
self.globals_to_lambdas.insert(*global_index, value.identity.clone());
}
self.collect_globals(value);
}
BoundKind::Block { exprs } => {
for e in exprs { self.collect_globals(e); }
}
_ => {
// Simplified traversal for global collection
node.kind.for_each_child(|child| self.collect_globals(child));
}
}
}
fn visit(&mut self, node: &TypedNode) -> Purity {
let purity = match &node.kind {
BoundKind::Constant(_) | BoundKind::Nop | BoundKind::Parameter { .. } => Purity::Pure,
BoundKind::Get { addr, .. } => match addr {
Address::Global(idx) => self.global_purity.get(idx).cloned().unwrap_or(Purity::Pure),
_ => Purity::Pure, // Locals are considered pure access in this model
},
BoundKind::Set { .. } => Purity::Impure,
BoundKind::DefLocal { value, .. } | BoundKind::DefGlobal { value, .. } => {
self.visit(value)
}
BoundKind::If { cond, then_br, else_br } => {
let p_cond = self.visit(cond);
let p_then = self.visit(then_br);
let p_else = else_br.as_ref().map(|e| self.visit(e)).unwrap_or(Purity::Pure);
p_cond.min(p_then).min(p_else)
}
BoundKind::Lambda { body, .. } => {
self.lambda_stack.push(node.identity.clone());
self.visit(body);
self.lambda_stack.pop();
Purity::Pure // Creating a lambda is pure
}
BoundKind::Call { callee, args } => {
let p_callee = self.visit(callee);
let p_args = self.visit(args);
// Detect recursion
if let BoundKind::Get { addr: Address::Global(idx), .. } = &callee.kind
&& let Some(lambda_id) = self.globals_to_lambdas.get(idx)
&& self.lambda_stack.contains(lambda_id)
{
self.results.is_recursive.insert(lambda_id.clone());
// Also mark the call itself if needed
self.results.is_recursive.insert(node.identity.clone());
}
// For purity, we'd need to know the function's purity.
// For now, if it's a call, we conservatively check if it's a known pure global.
let p_func = if let BoundKind::Get { addr: Address::Global(idx), .. } = &callee.kind {
self.global_purity.get(idx).cloned().unwrap_or(Purity::Impure)
} else {
Purity::Impure
};
p_callee.min(p_args).min(p_func)
}
BoundKind::Block { exprs } => {
let mut p = Purity::Pure;
for e in exprs {
p = p.min(self.visit(e));
}
p
}
BoundKind::Tuple { elements } => {
let mut p = Purity::Pure;
for e in elements {
p = p.min(self.visit(e));
}
p
}
BoundKind::Record { fields } => {
let mut p = Purity::Pure;
for (k, v) in fields {
p = p.min(self.visit(k)).min(self.visit(v));
}
p
}
_ => Purity::Impure,
};
self.results.purity.insert(node.identity.clone(), purity);
purity
}
}
/// Extension trait to make traversal easier
trait NodeExt {
fn for_each_child<F: FnMut(&TypedNode)>(&self, f: F);
}
impl NodeExt for BoundKind<StaticType> {
fn for_each_child<F: FnMut(&TypedNode)>(&self, mut f: F) {
match self {
BoundKind::If { cond, then_br, else_br } => {
f(cond); f(then_br); if let Some(e) = else_br { f(e); }
}
BoundKind::DefLocal { value, .. } | BoundKind::DefGlobal { value, .. } | BoundKind::Set { value, .. } => {
f(value);
}
BoundKind::Lambda { params, body, .. } => {
f(params); f(body);
}
BoundKind::Call { callee, args } => {
f(callee); f(args);
}
BoundKind::Block { exprs } => {
for e in exprs { f(e); } // Block
}
BoundKind::Tuple { elements } => {
for e in elements { f(e); } // Tuple
}
BoundKind::Record { fields } => {
for (k, v) in fields { f(k); f(v); }
}
_ => {}
}
}
}