Refactor: Analyze node purity and recursion

The Analyzer has been refactored to decorate `TypedNode`s with their
purity and recursion status. This involves creating a new `AnalyzedNode`
type and a `NodeMetrics` struct to hold this information. The `Analyzer`
now returns an `AnalyzedNode` instead of a separate `Analysis` struct.
This change lays the groundwork for future optimizations and analysis
passes.
This commit is contained in:
Michael Schimmel
2026-02-22 16:11:46 +01:00
parent 8f7947bde1
commit 2fdeff1db4
7 changed files with 503 additions and 1130 deletions
+103 -62
View File
@@ -1,37 +1,32 @@
use crate::ast::compiler::bound_nodes::{Address, BoundKind, TypedNode};
use crate::ast::types::{Identity, Purity, StaticType};
use crate::ast::compiler::bound_nodes::{Address, AnalyzedNode, BoundKind, NodeMetrics, TypedNode};
use crate::ast::types::Purity;
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, Default)]
pub struct Analysis {
pub purity: HashMap<Identity, Purity>,
pub is_recursive: HashSet<Identity>,
}
use std::rc::Rc;
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>,
lambda_stack: Vec<crate::ast::types::Identity>,
/// Map of global index to its Lambda identity if known.
globals_to_lambdas: HashMap<u32, Identity>,
globals_to_lambdas: HashMap<u32, crate::ast::types::Identity>,
/// Set of identities that were found to be recursive.
recursive_identities: HashSet<crate::ast::types::Identity>,
}
impl<'a> Analyzer<'a> {
pub fn analyze(node: &TypedNode, global_purity: &'a HashMap<u32, Purity>) -> Analysis {
pub fn analyze(node: &TypedNode, global_purity: &'a HashMap<u32, Purity>) -> AnalyzedNode {
let mut analyzer = Self {
global_purity,
results: Analysis::default(),
lambda_stack: Vec::new(),
globals_to_lambdas: HashMap::new(),
recursive_identities: HashSet::new(),
};
// First pass: map globals to their lambda identities
analyzer.collect_globals(node);
// Second pass: full analysis
analyzer.visit(node);
analyzer.results
// Second pass: full analysis (decorating TypedNode into AnalyzedNode)
analyzer.visit(Rc::new(node.clone()))
}
fn collect_globals(&mut self, node: &TypedNode) {
@@ -46,104 +41,147 @@ impl<'a> Analyzer<'a> {
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
},
fn visit(&mut self, node_rc: Rc<TypedNode>) -> AnalyzedNode {
let node = &*node_rc;
let mut is_recursive = false;
BoundKind::Set { .. } => Purity::Impure,
let (new_kind, purity) = match &node.kind {
BoundKind::Constant(v) => (BoundKind::Constant(v.clone()), Purity::Pure),
BoundKind::Nop => (BoundKind::Nop, Purity::Pure),
BoundKind::Parameter { name, slot } => {
(BoundKind::Parameter { name: name.clone(), slot: *slot }, Purity::Pure)
}
BoundKind::DefLocal { value, .. } | BoundKind::DefGlobal { value, .. } => {
self.visit(value)
BoundKind::Get { addr, name } => {
let p = match addr {
Address::Global(idx) => self.global_purity.get(idx).cloned().unwrap_or(Purity::Pure),
_ => Purity::Pure,
};
(BoundKind::Get { addr: *addr, name: name.clone() }, p)
}
BoundKind::Set { addr, value } => {
let val_m = self.visit(Rc::new((**value).clone()));
(BoundKind::Set { addr: *addr, value: Box::new(val_m) }, Purity::Impure)
}
BoundKind::DefLocal { name, slot, value, captured_by } => {
let val_m = self.visit(Rc::new((**value).clone()));
let p = val_m.ty.purity;
(BoundKind::DefLocal { name: name.clone(), slot: *slot, value: Box::new(val_m), captured_by: captured_by.clone() }, p)
}
BoundKind::DefGlobal { name, global_index, value } => {
let val_m = self.visit(Rc::new((**value).clone()));
let p = val_m.ty.purity;
(BoundKind::DefGlobal { name: name.clone(), global_index: *global_index, value: Box::new(val_m) }, p)
}
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)
let cond_m = self.visit(Rc::new((**cond).clone()));
let then_m = self.visit(Rc::new((**then_br).clone()));
let else_m = else_br.as_ref().map(|e| self.visit(Rc::new((**e).clone())));
let mut p = cond_m.ty.purity.min(then_m.ty.purity);
if let Some(ref em) = else_m { p = p.min(em.ty.purity); }
(BoundKind::If { cond: Box::new(cond_m), then_br: Box::new(then_m), else_br: else_m.map(Box::new) }, p)
}
BoundKind::Lambda { body, .. } => {
BoundKind::Lambda { params, upvalues, body, positional_count } => {
self.lambda_stack.push(node.identity.clone());
self.visit(body);
let params_m = self.visit(params.clone());
let body_m = self.visit(body.clone());
self.lambda_stack.pop();
Purity::Pure // Creating a lambda is pure
is_recursive = self.recursive_identities.contains(&node.identity);
(BoundKind::Lambda { params: Rc::new(params_m), upvalues: upvalues.clone(), body: Rc::new(body_m), positional_count: *positional_count }, Purity::Pure)
}
BoundKind::Call { callee, args } => {
let p_callee = self.visit(callee);
let p_args = self.visit(args);
// Detect recursion
let callee_m = self.visit(Rc::new((**callee).clone()));
let args_m = self.visit(Rc::new((**args).clone()));
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());
self.recursive_identities.insert(lambda_id.clone());
is_recursive = true;
}
// 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)
let p = callee_m.ty.purity.min(args_m.ty.purity).min(p_func);
(BoundKind::Call { callee: Box::new(callee_m), args: Box::new(args_m) }, p)
}
BoundKind::Block { exprs } => {
let mut new_exprs = Vec::with_capacity(exprs.len());
let mut p = Purity::Pure;
for e in exprs {
p = p.min(self.visit(e));
let em = self.visit(Rc::new(e.clone()));
p = p.min(em.ty.purity);
new_exprs.push(em);
}
p
(BoundKind::Block { exprs: new_exprs }, p)
}
BoundKind::Tuple { elements } => {
let mut new_elements = Vec::with_capacity(elements.len());
let mut p = Purity::Pure;
for e in elements {
p = p.min(self.visit(e));
let em = self.visit(Rc::new(e.clone()));
p = p.min(em.ty.purity);
new_elements.push(em);
}
p
(BoundKind::Tuple { elements: new_elements }, p)
}
BoundKind::Record { fields } => {
let mut new_fields = Vec::with_capacity(fields.len());
let mut p = Purity::Pure;
for (k, v) in fields {
p = p.min(self.visit(k)).min(self.visit(v));
let km = self.visit(Rc::new(k.clone()));
let vm = self.visit(Rc::new(v.clone()));
p = p.min(km.ty.purity).min(vm.ty.purity);
new_fields.push((km, vm));
}
p
(BoundKind::Record { fields: new_fields }, p)
}
_ => Purity::Impure,
BoundKind::Expansion { original_call, bound_expanded } => {
let expanded_m = self.visit(Rc::new((**bound_expanded).clone()));
(BoundKind::Expansion { original_call: original_call.clone(), bound_expanded: Box::new(expanded_m.clone()) }, expanded_m.ty.purity)
}
BoundKind::Extension(_) => (BoundKind::Nop, Purity::Impure),
};
self.results.purity.insert(node.identity.clone(), purity);
purity
crate::ast::nodes::Node {
identity: node.identity.clone(),
kind: new_kind,
ty: NodeMetrics {
original: node_rc,
purity,
is_recursive,
},
}
}
}
/// Extension trait to make traversal easier
trait NodeExt {
fn for_each_child<F: FnMut(&TypedNode)>(&self, f: F);
}
impl NodeExt for BoundKind<StaticType> {
impl NodeExt for BoundKind<crate::ast::types::StaticType> {
fn for_each_child<F: FnMut(&TypedNode)>(&self, mut f: F) {
match self {
BoundKind::If { cond, then_br, else_br } => {
@@ -159,14 +197,17 @@ impl NodeExt for BoundKind<StaticType> {
f(callee); f(args);
}
BoundKind::Block { exprs } => {
for e in exprs { f(e); } // Block
for e in exprs { f(e); }
}
BoundKind::Tuple { elements } => {
for e in elements { f(e); } // Tuple
for e in elements { f(e); }
}
BoundKind::Record { fields } => {
for (k, v) in fields { f(k); f(v); }
}
BoundKind::Expansion { bound_expanded, .. } => {
f(bound_expanded);
}
_ => {}
}
}
+11
View File
@@ -27,6 +27,17 @@ pub type BoundNode<T = ()> = Node<BoundKind<T>, T>;
/// Type alias for a node that has been fully type-checked.
pub type TypedNode = BoundNode<StaticType>;
/// Metrics collected during the analysis phase.
#[derive(Debug, Clone, PartialEq)]
pub struct NodeMetrics {
pub original: Rc<TypedNode>,
pub purity: crate::ast::types::Purity,
pub is_recursive: bool,
}
/// Type alias for a node that has been analyzed.
pub type AnalyzedNode = BoundNode<NodeMetrics>;
#[derive(Debug, Clone)]
pub enum BoundKind<T = ()> {
Nop,
File diff suppressed because it is too large Load Diff
+92 -179
View File
@@ -1,7 +1,6 @@
use crate::ast::compiler::analyzer::Analysis;
use crate::ast::compiler::bound_nodes::{Address, BoundKind, BoundNode, TypedNode};
use crate::ast::compiler::bound_nodes::{Address, AnalyzedNode, BoundKind, BoundNode, NodeMetrics};
use crate::ast::nodes::Node;
use crate::ast::types::{Signature, StaticType, Value};
use crate::ast::types::{Purity, Signature, StaticType, Value};
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
@@ -17,13 +16,13 @@ pub type RtlLookupFunc = Rc<dyn Fn(&str, &[StaticType]) -> Option<(Value, Static
pub trait FunctionRegistry {
fn resolve(&self, addr: Address) -> Option<BoundNode>;
fn resolve_analyzed(&self, _addr: Address) -> Option<AnalyzedNode> { None }
}
pub type MonoCache = HashMap<MonoCacheKey, (Value, StaticType)>;
pub struct Specializer {
pub cache: Rc<RefCell<MonoCache>>,
pub analysis: Analysis,
registry: Option<Rc<dyn FunctionRegistry>>,
compiler: Option<CompileFunc>,
rtl_lookup: Option<RtlLookupFunc>,
@@ -35,279 +34,193 @@ impl Specializer {
compiler: Option<CompileFunc>,
rtl_lookup: Option<RtlLookupFunc>,
cache: Option<Rc<RefCell<MonoCache>>>,
analysis: Analysis,
) -> Self {
Self {
cache: cache.unwrap_or_else(|| Rc::new(RefCell::new(HashMap::new()))),
analysis,
registry,
compiler,
rtl_lookup,
}
}
pub fn specialize(&self, node: TypedNode) -> TypedNode {
pub fn specialize(&self, node: AnalyzedNode) -> AnalyzedNode {
self.visit_node(node)
}
fn visit_node(&self, node: TypedNode) -> TypedNode {
let (new_kind, new_ty) = match node.kind {
fn visit_node(&self, node: AnalyzedNode) -> AnalyzedNode {
let (new_kind, metrics) = match node.kind {
BoundKind::Call { callee, args } => {
let (new_callee, new_args, ret_ty) =
self.specialize_call_logic(*callee, *args, node.ty.clone());
let (new_callee, new_args, _ret_ty) =
self.specialize_call_logic(*callee, *args, node.ty.original.ty.clone());
let new_metrics = node.ty.clone();
(
BoundKind::Call {
callee: Box::new(new_callee),
args: Box::new(new_args),
},
ret_ty,
new_metrics,
)
}
// Recursive traversal for other nodes
BoundKind::If {
cond,
then_br,
else_br,
} => {
BoundKind::If { cond, then_br, else_br } => {
let cond = Box::new(self.visit_node(*cond));
let then_br = Box::new(self.visit_node(*then_br));
let else_br = else_br.map(|e| Box::new(self.visit_node(*e)));
(
BoundKind::If {
cond,
then_br,
else_br,
},
node.ty,
)
(BoundKind::If { cond, then_br, else_br }, node.ty.clone())
}
BoundKind::Block { exprs } => {
let exprs = exprs.into_iter().map(|e| self.visit_node(e)).collect();
(BoundKind::Block { exprs }, node.ty)
(BoundKind::Block { exprs }, node.ty.clone())
}
BoundKind::Lambda {
params,
upvalues,
body,
positional_count,
} => {
BoundKind::Lambda { params, upvalues, body, positional_count } => {
let params = Rc::new(self.visit_node(params.as_ref().clone()));
let body = Rc::new(self.visit_node((*body).clone()));
(
BoundKind::Lambda {
params,
upvalues,
body,
positional_count,
},
node.ty,
)
(BoundKind::Lambda { params, upvalues, body, positional_count }, node.ty.clone())
}
BoundKind::DefLocal {
name,
slot,
value,
captured_by,
} => {
BoundKind::DefLocal { name, slot, value, captured_by } => {
let value = Box::new(self.visit_node(*value));
(
BoundKind::DefLocal {
name,
slot,
value,
captured_by,
},
node.ty,
)
(BoundKind::DefLocal { name, slot, value, captured_by }, node.ty.clone())
}
BoundKind::DefGlobal {
name,
global_index,
value,
} => {
BoundKind::DefGlobal { name, global_index, value } => {
let value = Box::new(self.visit_node(*value));
(
BoundKind::DefGlobal {
name,
global_index,
value,
},
node.ty,
)
(BoundKind::DefGlobal { name, global_index, value }, node.ty.clone())
}
BoundKind::Set { addr, value } => {
let value = Box::new(self.visit_node(*value));
(BoundKind::Set { addr, value }, node.ty)
(BoundKind::Set { addr, value }, node.ty.clone())
}
BoundKind::Tuple { elements } => {
let elements = elements.into_iter().map(|e| self.visit_node(e)).collect();
(BoundKind::Tuple { elements }, node.ty)
(BoundKind::Tuple { elements }, node.ty.clone())
}
BoundKind::Record { fields } => {
let fields = fields
.into_iter()
.map(|(k, v)| (self.visit_node(k), self.visit_node(v)))
.collect();
(BoundKind::Record { fields }, node.ty)
(BoundKind::Record { fields }, node.ty.clone())
}
BoundKind::Expansion {
original_call,
bound_expanded,
} => {
BoundKind::Expansion { original_call, bound_expanded } => {
let bound_expanded = Box::new(self.visit_node(*bound_expanded));
(
BoundKind::Expansion {
original_call,
bound_expanded,
},
node.ty,
)
(BoundKind::Expansion { original_call, bound_expanded }, node.ty.clone())
}
// Leaf nodes or uninteresting nodes
k => (k, node.ty),
k => (k, node.ty.clone()),
};
Node {
identity: node.identity,
kind: new_kind,
ty: new_ty,
ty: metrics,
}
}
fn specialize_call_logic(
&self,
callee: TypedNode,
args: TypedNode,
callee: AnalyzedNode,
args: AnalyzedNode,
original_ty: StaticType,
) -> (TypedNode, TypedNode, StaticType) {
// 1. Specialize children first
) -> (AnalyzedNode, AnalyzedNode, StaticType) {
let new_callee = self.visit_node(callee);
let new_args = self.visit_node(args);
// 2. Check if this call is a candidate (Callee is Get(Address))
let address = if let BoundKind::Get { addr, .. } = &new_callee.kind {
*addr
} else {
// Not a direct call to a named function/variable
return (new_callee, new_args, original_ty);
};
// 3. Check if all argument types are statically known
let arg_types: Vec<StaticType> = if let StaticType::Tuple(elements) = &new_args.ty {
let arg_types: Vec<StaticType> = if let StaticType::Tuple(elements) = &new_args.ty.original.ty {
elements.clone()
} else {
vec![new_args.ty.clone()]
vec![new_args.ty.original.ty.clone()]
};
if arg_types.iter().any(|t| matches!(t, StaticType::Any)) {
// Cannot specialize with unknown types
return (new_callee, new_args, original_ty);
}
// --- Optimization Candidate ---
let key = MonoCacheKey {
address,
arg_types: arg_types.clone(),
};
// 4. Check Cache
if let Some((val, ret_ty)) = self.cache.borrow().get(&key) {
// Cache Hit! Replace Callee with Constant(Function)
let specialized_callee = Node {
identity: new_callee.identity.clone(),
kind: BoundKind::Constant(val.clone()),
ty: StaticType::Function(Box::new(Signature {
params: StaticType::Tuple(arg_types),
ret: ret_ty.clone(),
})),
};
let specialized_callee = self.make_constant_node(val.clone(), StaticType::Function(Box::new(Signature {
params: StaticType::Tuple(arg_types),
ret: ret_ty.clone(),
})), &new_callee);
return (specialized_callee, new_args, ret_ty.clone());
}
// 5. Check RTL (Host Functions)
if let Some(rtl_lookup) = &self.rtl_lookup
&& let BoundKind::Get { name, .. } = &new_callee.kind
&& let Some((val, ret_ty)) = rtl_lookup(&name.name, &arg_types)
{
// Cache Hit (RTL)
self.cache
.borrow_mut()
.insert(key.clone(), (val.clone(), ret_ty.clone()));
let specialized_callee = Node {
identity: new_callee.identity.clone(),
kind: BoundKind::Constant(val.clone()),
ty: StaticType::Function(Box::new(Signature {
params: StaticType::Tuple(arg_types),
ret: ret_ty.clone(),
})),
};
self.cache.borrow_mut().insert(key.clone(), (val.clone(), ret_ty.clone()));
let specialized_callee = self.make_constant_node(val.clone(), StaticType::Function(Box::new(Signature {
params: StaticType::Tuple(arg_types),
ret: ret_ty.clone(),
})), &new_callee);
return (specialized_callee, new_args, ret_ty);
}
// 6. Resolve Function Definition
if let Some(func_node) = self.registry.as_ref().and_then(|r| r.resolve(address)) {
// Check for recursion from pre-pass.
if self.analysis.is_recursive.contains(&func_node.identity) {
return (new_callee, new_args, original_ty);
}
// Check constraints (no closures with state)
if let BoundKind::Lambda { upvalues, .. } = &func_node.kind {
if !upvalues.is_empty() {
return (new_callee, new_args, original_ty);
}
} else {
return (new_callee, new_args, original_ty);
}
// 7. Compile Specialization (User Code)
if let Some(compiler) = &self.compiler {
match compiler(func_node, &arg_types) {
Ok((compiled_val, ret_ty)) => {
let res_val: Value = compiled_val;
let res_ty: StaticType = ret_ty;
// Store in cache
self.cache
.borrow_mut()
.insert(key, (res_val.clone(), res_ty.clone()));
// PERFORMANCE: Flatten the argument tuple to match the specialized signature.
let flat_elements = self.flatten_tuple(new_args.clone());
let flat_types = flat_elements.iter().map(|e| e.ty.clone()).collect();
let flattened_args = Node {
identity: new_args.identity.clone(),
kind: BoundKind::Tuple {
elements: flat_elements,
},
ty: StaticType::Tuple(flat_types),
};
let specialized_callee = Node {
identity: new_callee.identity.clone(),
kind: BoundKind::Constant(res_val),
ty: StaticType::Function(Box::new(Signature {
params: flattened_args.ty.clone(),
ret: res_ty.clone(),
})),
};
return (specialized_callee, flattened_args, res_ty);
}
Err(_) => {
// Fallback on error
}
}
}
if let Some(registry) = &self.registry
&& let Some(func_node) = registry.resolve_analyzed(address)
&& func_node.ty.is_recursive
{
return (new_callee, new_args, original_ty);
}
if let Some(compiler) = &self.compiler
&& let Some(func_node) = self.registry.as_ref().and_then(|r| r.resolve(address))
&& let Ok((compiled_val, ret_ty)) = compiler(func_node, &arg_types)
{
self.cache.borrow_mut().insert(key, (compiled_val.clone(), ret_ty.clone()));
let flat_elements = self.flatten_tuple(new_args.clone());
let flat_types = flat_elements.iter().map(|e| e.ty.original.ty.clone()).collect();
let flattened_args = Node {
identity: new_args.identity.clone(),
kind: BoundKind::Tuple { elements: flat_elements },
ty: NodeMetrics {
original: Rc::new(Node {
identity: new_args.identity.clone(),
kind: BoundKind::Tuple { elements: vec![] },
ty: StaticType::Tuple(flat_types),
}),
purity: new_args.ty.purity,
is_recursive: new_args.ty.is_recursive,
},
};
let specialized_callee = self.make_constant_node(compiled_val, StaticType::Function(Box::new(Signature {
params: flattened_args.ty.original.ty.clone(),
ret: ret_ty.clone(),
})), &new_callee);
return (specialized_callee, flattened_args, ret_ty);
}
// Fallback: Dynamic Call
(new_callee, new_args, original_ty)
}
fn flatten_tuple(&self, node: TypedNode) -> Vec<TypedNode> {
fn make_constant_node(&self, val: Value, ty: StaticType, template: &AnalyzedNode) -> AnalyzedNode {
let typed_original = Rc::new(Node {
identity: template.identity.clone(),
kind: BoundKind::Constant(val.clone()),
ty: ty.clone(),
});
Node {
identity: template.identity.clone(),
kind: BoundKind::Constant(val),
ty: NodeMetrics {
original: typed_original,
purity: Purity::Pure,
is_recursive: false,
},
}
}
fn flatten_tuple(&self, node: AnalyzedNode) -> Vec<AnalyzedNode> {
match node.kind {
BoundKind::Tuple { elements } => {
let mut flat = Vec::new();
+8 -9
View File
@@ -1,16 +1,15 @@
use crate::ast::compiler::bound_nodes::{BoundKind, TypedNode};
use crate::ast::compiler::bound_nodes::{AnalyzedNode, BoundKind};
use crate::ast::nodes::Node;
use crate::ast::types::StaticType;
use std::rc::Rc;
use std::fmt::Debug;
#[derive(Clone)]
pub struct RuntimeMetadata {
pub ty: StaticType,
pub is_tail: bool,
/// The original, high-level typed node. Perfect for Debuggers and Optimizers.
pub original: Rc<TypedNode>,
/// The analyzed node, containing metrics and a link to the original TypedNode.
pub original: Rc<AnalyzedNode>,
}
impl Debug for RuntimeMetadata {
@@ -22,18 +21,18 @@ impl Debug for RuntimeMetadata {
}
}
/// The ExecNode is the AST used by the VM. It carries TCO flags and links to source.
/// The ExecNode is the AST used by the VM. It carries TCO flags and links to metrics.
pub type ExecNode = Node<BoundKind<RuntimeMetadata>, RuntimeMetadata>;
pub struct TCO;
impl TCO {
/// Lowers a TypedNode (Compiler-AST) to an ExecNode (VM-AST) and marks tail positions.
pub fn optimize(node: TypedNode) -> ExecNode {
/// Lowers an AnalyzedNode to an ExecNode and marks tail positions.
pub fn optimize(node: AnalyzedNode) -> ExecNode {
Self::transform(Rc::new(node), true)
}
fn transform(node_rc: Rc<TypedNode>, is_tail_position: bool) -> ExecNode {
fn transform(node_rc: Rc<AnalyzedNode>, is_tail_position: bool) -> ExecNode {
let node = &*node_rc;
let new_kind = match &node.kind {
BoundKind::Call { callee, args } => BoundKind::Call {
@@ -158,7 +157,7 @@ impl TCO {
identity: node.identity.clone(),
kind: new_kind,
ty: RuntimeMetadata {
ty: node.ty.clone(),
ty: node.ty.original.ty.clone(),
is_tail: is_tail_position,
original: node_rc,
},
+43 -98
View File
@@ -1,4 +1,4 @@
use crate::ast::compiler::analyzer::{Analysis, Analyzer};
use crate::ast::compiler::analyzer::Analyzer;
use crate::ast::compiler::binder::Binder;
use crate::ast::compiler::{TypeChecker, TypedNode};
use crate::ast::nodes::{Node, Symbol, UntypedKind};
@@ -8,7 +8,7 @@ use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use crate::ast::compiler::bound_nodes::{Address, BoundKind, BoundNode};
use crate::ast::compiler::bound_nodes::{Address, AnalyzedNode, BoundKind, BoundNode};
use crate::ast::compiler::dumper::Dumper;
use crate::ast::compiler::lambda_collector::LambdaCollector;
use crate::ast::compiler::macros::{MacroEvaluator, MacroExpander, MacroRegistry};
@@ -26,15 +26,15 @@ pub struct Environment {
pub global_values: Rc<RefCell<Vec<Value>>>,
pub prng: Rc<RefCell<fastrand::Rng>>,
pub function_registry: Rc<RefCell<HashMap<u32, BoundNode>>>,
pub typed_function_registry: Rc<RefCell<HashMap<u32, TypedNode>>>,
pub typed_function_registry: Rc<RefCell<HashMap<u32, AnalyzedNode>>>,
pub monomorph_cache: Rc<RefCell<MonoCache>>,
pub debug_mode: bool,
pub optimization: bool,
pub last_analysis: RefCell<Analysis>,
}
struct EnvFunctionRegistry {
registry: Rc<RefCell<HashMap<u32, BoundNode>>>,
analyzed_registry: Rc<RefCell<HashMap<u32, AnalyzedNode>>>,
}
impl FunctionRegistry for EnvFunctionRegistry {
@@ -45,9 +45,15 @@ impl FunctionRegistry for EnvFunctionRegistry {
None
}
}
fn resolve_analyzed(&self, addr: Address) -> Option<AnalyzedNode> {
if let Address::Global(idx) = addr {
self.analyzed_registry.borrow().get(&idx).cloned()
} else {
None
}
}
}
/// Evaluator used during macro expansion to allow compile-time logic.
struct RuntimeMacroEvaluator {
global_names: Rc<RefCell<HashMap<Symbol, u32>>>,
global_types: Rc<RefCell<HashMap<u32, StaticType>>>,
@@ -60,19 +66,16 @@ impl MacroEvaluator for RuntimeMacroEvaluator {
node: &Node<UntypedKind>,
bindings: &HashMap<Rc<str>, Node<UntypedKind>>,
) -> Result<Value, String> {
// 1. Check if it's a simple parameter substitution
if let UntypedKind::Identifier(sym) = &node.kind
&& let Some(arg_node) = bindings.get(&sym.name)
{
return Ok(Value::Object(Rc::new(arg_node.clone()) as Rc<dyn Object>));
}
// 2. Full evaluation for complex compile-time expressions
let bound_ast = Binder::bind_root(self.global_names.clone(), node)?;
let checker = TypeChecker::new(self.global_types.clone());
let typed_ast = checker.check(bound_ast, &[])?;
let exec_ast = TCO::optimize(typed_ast);
let exec_ast = TCO::optimize(Analyzer::analyze(&typed_ast, &HashMap::new())); // Minimal analysis for macro eval
let mut vm = VM::new(self.global_values.clone());
vm.run(&exec_ast)
@@ -98,7 +101,6 @@ 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
@@ -135,7 +137,6 @@ impl Environment {
values.push(Value::Function(func));
}
/// Utility to register a native function from a closure.
pub fn register_native_fn(
&self,
name: &str,
@@ -162,12 +163,11 @@ impl Environment {
let idx = values.len() as u32;
names.insert(Symbol::from(name), idx);
types.insert(idx, ty);
purity.insert(idx, Purity::Pure); // Constants are always pure
purity.insert(idx, Purity::Pure);
values.push(val);
}
fn register_stdlib(&self) {
// Register all standard library functions via RTL module
rtl::register(self);
}
@@ -177,79 +177,54 @@ impl Environment {
Ok(Dumper::dump(&linked))
}
/// Frontend: Parse -> Expand Macros -> Bind -> Type Check
pub fn compile(&self, source: &str) -> Result<TypedNode, String> {
// 1. Parse
let mut parser = Parser::new(source)?;
let untyped_ast = parser.parse_expression()?;
// 2. Check for trailing tokens
if !parser.at_eof() {
return Err(
"Unexpected trailing expressions in script. Use (do ...) for sequences."
.to_string(),
);
return Err("Unexpected trailing expressions in script.".to_string());
}
// 3. Expand Macros
let expanded_ast = self.get_expander().expand(untyped_ast)?;
// 4. Bind
let bound_ast = Binder::bind_root(self.global_names.clone(), &expanded_ast)?;
// 5. Collect Lambdas (Populate the registry with untyped templates)
LambdaCollector::collect(&bound_ast, &mut self.function_registry.borrow_mut());
// 6. Type Check
let checker = TypeChecker::new(self.global_types.clone());
let typed_ast = checker.check(bound_ast, &[])?;
// 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)
}
/// Backend Phase 1: Optimization (TCO, etc.) and Lowering
pub fn link(&self, node: TypedNode) -> ExecNode {
// 1. Specialize (Always performed for correctness)
let specialized = self.specialize_node(node);
// 1. Analyze
let analyzed = Analyzer::analyze(&node, &self.global_purity.borrow());
// 2. Collect Analyzed Lambdas
LambdaCollector::collect(&analyzed, &mut self.typed_function_registry.borrow_mut());
// 2. Optimize (Level 1: Cracking, Level 2: Collapsing)
// 3. Specialize
let specialized = self.specialize_node(analyzed);
// 4. Optimize
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_analysis(self.last_analysis.borrow().clone());
.with_registry(self.typed_function_registry.clone());
let optimized = optimizer.optimize(specialized);
// 3. TCO (Always performed, converts to ExecNode)
// 5. TCO
TCO::optimize(optimized)
}
/// Backend Phase 2: Packaging into an invokable NativeFunction
pub fn instantiate(&self, node: ExecNode) -> Rc<crate::ast::types::NativeFunction> {
let global_values = self.global_values.clone();
// OPTIMIZATION: Fast path for top-level Lambdas.
// If the script root is a Lambda with no captures (root functions usually have none),
// we can pre-create the closure and call it directly.
if let BoundKind::Lambda {
params,
upvalues,
body,
positional_count,
} = &node.kind
if let BoundKind::Lambda { params, upvalues, body, positional_count } = &node.kind
&& upvalues.is_empty()
{
let closure = Rc::new(crate::ast::vm::Closure::new(
params.ty.original.clone(),
body.ty.original.clone(),
body.clone(),
params.ty.original.clone(),
body.ty.original.clone(),
body.clone(),
vec![],
*positional_count,
));
@@ -267,20 +242,16 @@ impl Environment {
});
}
// FALLBACK: Generic script body (Block, If, etc.)
let exec_node = Rc::new(node);
Rc::new(crate::ast::types::NativeFunction {
purity: Purity::Impure,
func: Rc::new(move |args| {
let mut vm = VM::new(global_values.clone());
// 1. Execute the main body (Block, etc.)
let res = match vm.run(&exec_node) {
Ok(v) => v,
Err(e) => panic!("Myc Runtime Error: {}", e),
};
// 2. Auto-apply: If the script returned a closure, we apply arguments to it.
let mut final_res = res;
if let Value::Object(obj) = &final_res
&& let Some(closure) = obj.as_any().downcast_ref::<crate::ast::vm::Closure>()
@@ -290,78 +261,63 @@ impl Environment {
Err(e) => panic!("Myc Runtime Error (Closure): {}", e),
};
}
// 3. Resolve Tail Calls
vm.resolve_tail_calls(final_res)
}),
})
}
fn specialize_node(&self, node: TypedNode) -> TypedNode {
fn specialize_node(&self, node: AnalyzedNode) -> AnalyzedNode {
let registry = Rc::new(EnvFunctionRegistry {
registry: self.function_registry.clone(),
analyzed_registry: self.typed_function_registry.clone(),
});
let rtl_lookup = Rc::new(|name: &str, args: &[StaticType]| intrinsics::lookup(name, args));
let func_reg = self.function_registry.clone();
let typed_reg = self.typed_function_registry.clone();
let untyped_reg = self.function_registry.clone();
let mono_cache = self.monomorph_cache.clone();
let global_values = self.global_values.clone();
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]|
-> Result<(Value, StaticType), String> {
// 1. Re-TypeCheck the template with concrete argument types
let checker = TypeChecker::new(global_types.clone());
let retyped_ast = checker.check(func_template, arg_types)?;
// 2. Specialize (Recursive)
let analyzed = Analyzer::analyze(&retyped_ast, &global_purity.borrow());
let sub_registry = Rc::new(EnvFunctionRegistry {
registry: func_reg.clone(),
registry: untyped_reg.clone(),
analyzed_registry: typed_reg.clone(),
});
let sub_rtl_lookup =
Rc::new(|name: &str, args: &[StaticType]| intrinsics::lookup(name, args));
let sub_rtl_lookup = Rc::new(|name: &str, args: &[StaticType]| intrinsics::lookup(name, args));
let sub_specializer = Specializer::new(
Some(sub_registry),
None,
Some(sub_rtl_lookup),
Some(mono_cache.clone()),
compiler_analysis.clone(),
);
let specialized_ast = sub_specializer.specialize(retyped_ast);
let specialized_ast = sub_specializer.specialize(analyzed);
// 3. Optimize (Phase 2: Cracking & Folding)
let optimizer = Optimizer::new(optimization)
.with_globals(global_values.clone())
.with_purity(global_purity.clone())
.with_analysis(compiler_analysis.clone());
.with_purity(global_purity.clone());
let optimized_ast = optimizer.optimize(specialized_ast);
// 4. TCO (converts to ExecNode)
let tco_ast = TCO::optimize(optimized_ast);
// 5. Compile to Value (VM)
let mut vm = VM::new(global_values.clone());
let compiled_val = match vm.run(&tco_ast) {
Ok(v) => v,
Err(e) => return Err(format!("VM Error during specialization: {}", e)),
};
// 6. Determine correct return type from the newly inferred function signature
let ret_type = if let StaticType::Function(sig) = &tco_ast.ty.ty {
sig.ret.clone()
} else {
StaticType::Any
};
let ret_type = tco_ast.ty.ty.clone();
Ok((compiled_val, ret_type))
},
);
@@ -371,7 +327,6 @@ impl Environment {
Some(compiler),
Some(rtl_lookup),
Some(self.monomorph_cache.clone()),
analysis,
);
specializer.specialize(node)
@@ -380,9 +335,7 @@ impl Environment {
pub fn run_script(&self, source: &str) -> Result<Value, String> {
if self.debug_mode {
let (res, logs) = self.run_debug(source)?;
for line in logs {
println!("{}", line);
}
for line in logs { println!("{}", line); }
res
} else {
let compiled = self.compile(source)?;
@@ -395,33 +348,25 @@ impl Environment {
pub fn run_debug(&self, source: &str) -> Result<(Result<Value, String>, Vec<String>), String> {
let compiled = self.compile(source)?;
let linked = self.link(compiled);
// Execute with TracingObserver
let mut vm = VM::new(self.global_values.clone());
let mut observer = TracingObserver::new();
let mut result = vm.run_with_observer(&mut observer, &linked);
// If result is a closure (script entry), execute the body too
if let Ok(Value::Object(obj)) = &result
&& let Some(closure) = obj.as_any().downcast_ref::<crate::ast::vm::Closure>()
{
result = vm.run_with_observer(&mut observer, &closure.exec_node);
}
// Resolve top-level tail calls
while let Ok(Value::TailCallRequest(payload)) = result {
let (next_obj, next_args) = *payload;
if let Some(closure) = next_obj.as_any().downcast_ref::<crate::ast::vm::Closure>() {
result = vm.run_with_args_observed(&mut observer, closure, next_args);
} else {
result = Err(format!(
"Tail call target is not a closure: {}",
next_obj.type_name()
));
result = Err(format!("Tail call target is not a closure: {}", next_obj.type_name()));
break;
}
}
Ok((result, observer.logs))
}
}
+20 -117
View File
@@ -1,4 +1,4 @@
use crate::ast::compiler::bound_nodes::{Address, BoundKind, TypedNode};
use crate::ast::compiler::bound_nodes::{Address, AnalyzedNode, BoundKind};
use crate::ast::compiler::tco::ExecNode;
use crate::ast::nodes::Node;
use crate::ast::types::{Object, Value};
@@ -8,8 +8,11 @@ use std::rc::Rc;
#[derive(Debug, Clone)]
pub struct Closure {
pub parameter_node: Rc<TypedNode>,
pub function_node: Rc<TypedNode>,
/// The analyzed parameter pattern.
pub parameter_node: Rc<AnalyzedNode>,
/// The analyzed body (before TCO).
pub function_node: Rc<AnalyzedNode>,
/// The executable node (after TCO).
pub exec_node: Rc<ExecNode>,
pub upvalues: Vec<Rc<RefCell<Value>>>,
pub positional_count: Option<u32>,
@@ -18,8 +21,8 @@ pub struct Closure {
impl Closure {
#[inline]
pub fn new(
params: Rc<TypedNode>,
body: Rc<TypedNode>,
params: Rc<AnalyzedNode>,
body: Rc<AnalyzedNode>,
exec: Rc<ExecNode>,
upvalues: Vec<Rc<RefCell<Value>>>,
positional_count: Option<u32>,
@@ -85,11 +88,14 @@ impl VMObserver for TracingObserver {
const ACTIVE: bool = true;
fn before_eval(&mut self, _vm: &VM, node: &ExecNode) {
let pad = self.pad();
let metrics = &node.ty.original.ty;
self.logs.push(format!(
"{}{} [{}]: {{",
"{}{} [{} | P:{:?}{}]: {{",
pad,
node.kind.display_name(),
node.ty.ty
node.ty.ty,
metrics.purity,
if metrics.is_recursive { " | REC" } else { "" }
));
self.indent += 1;
}
@@ -166,8 +172,13 @@ macro_rules! dispatch_eval {
BoundKind::Lambda { params, upvalues, body, positional_count } => {
let mut captured = Vec::with_capacity(upvalues.len());
for addr in upvalues { captured.push($self.capture_upvalue(*addr)?); }
// CRITICAL FIX: body.clone() is O(1), body.as_ref().clone() was O(N)!
let closure = Closure::new(params.ty.original.clone(), body.ty.original.clone(), body.clone(), captured, *positional_count);
let closure = Closure::new(
params.ty.original.clone(),
body.ty.original.clone(),
body.clone(),
captured,
*positional_count
);
Ok(Value::Object(Rc::new(closure)))
},
BoundKind::Call { callee, args } => {
@@ -361,7 +372,6 @@ impl VM {
dispatch_eval!(self, node, eval)
}
/// Resolves potential tail call requests iteratively until a final value is reached.
pub fn resolve_tail_calls(&mut self, mut result: Value) -> Value {
while let Value::TailCallRequest(payload) = result {
let (next_obj, next_args) = *payload;
@@ -622,110 +632,3 @@ impl VM {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::compiler::tco::TCO;
use crate::ast::nodes::{Node, Symbol};
use crate::ast::types::{NodeIdentity, SourceLocation, StaticType};
fn make_dummy_identity() -> Rc<NodeIdentity> {
Rc::new(NodeIdentity {
location: SourceLocation { line: 0, col: 0 },
})
}
#[test]
fn test_capture_boxing_modification() {
let id = make_dummy_identity();
let lambda_body = Node {
identity: id.clone(),
ty: StaticType::Void,
kind: BoundKind::Set {
addr: Address::Upvalue(0),
value: Box::new(Node {
identity: id.clone(),
ty: StaticType::Int,
kind: BoundKind::Constant(Value::Int(20)),
}),
},
};
let root = Node {
identity: id.clone(),
ty: StaticType::Int,
kind: BoundKind::Block {
exprs: vec![
Node {
identity: id.clone(),
ty: StaticType::Int,
kind: BoundKind::Set {
addr: Address::Local(0),
value: Box::new(Node {
identity: id.clone(),
ty: StaticType::Int,
kind: BoundKind::Constant(Value::Int(10)),
}),
},
},
Node {
identity: id.clone(),
ty: StaticType::Any,
kind: BoundKind::Set {
addr: Address::Local(1),
value: Box::new(Node {
identity: id.clone(),
ty: StaticType::Any,
kind: BoundKind::Lambda {
params: Rc::new(Node {
identity: id.clone(),
ty: StaticType::Tuple(vec![]),
kind: BoundKind::Tuple { elements: vec![] },
}),
upvalues: vec![Address::Local(0)],
body: Rc::new(lambda_body),
positional_count: Some(0),
},
}),
},
},
Node {
identity: id.clone(),
ty: StaticType::Void,
kind: BoundKind::Call {
callee: Box::new(Node {
identity: id.clone(),
ty: StaticType::Any,
kind: BoundKind::Get {
addr: Address::Local(1),
name: Symbol::from("f"),
},
}),
args: Box::new(Node {
identity: id.clone(),
ty: StaticType::Tuple(vec![]),
kind: BoundKind::Tuple { elements: vec![] },
}),
},
},
Node {
identity: id.clone(),
ty: StaticType::Int,
kind: BoundKind::Get {
addr: Address::Local(0),
name: Symbol::from("x"),
},
},
],
},
};
let globals = Rc::new(RefCell::new(Vec::new()));
let mut vm = VM::new(globals);
let exec_root = TCO::optimize(root);
let result = vm.run(&exec_root);
match result {
Ok(Value::Int(val)) => assert_eq!(val, 20),
_ => panic!("Expected Int(20)"),
}
}
}