diff --git a/src/ast/compiler/optimizer.rs b/src/ast/compiler/optimizer.rs index 68c8123..d3500ec 100644 --- a/src/ast/compiler/optimizer.rs +++ b/src/ast/compiler/optimizer.rs @@ -1,19 +1,27 @@ use std::rc::Rc; +use std::cell::RefCell; use crate::ast::compiler::bound_nodes::{BoundKind, TypedNode, Address}; use crate::ast::types::{Value, StaticType}; use crate::ast::vm::Closure; use crate::ast::nodes::Node; use std::collections::{HashMap, HashSet}; -/// The Optimizer performs Phase 2 (Cracking) and Phase 2.5/2.6 (Aggressive Collapsing & DCE). +/// The Optimizer performs Phase 2 (Cracking), Phase 2.5/2.6 (Aggressive Collapsing & DCE) +/// and Phase 4 (Global Inlining). pub struct Optimizer { pub level: u32, max_passes: usize, + pub globals: Option>>>, } impl Optimizer { pub fn new(level: u32) -> Self { - Self { level, max_passes: 5 } + Self { level, max_passes: 5, globals: None } + } + + pub fn with_globals(mut self, globals: Rc>>) -> Self { + self.globals = Some(globals); + self } pub fn optimize(&self, node: TypedNode) -> TypedNode { @@ -56,7 +64,32 @@ impl Optimizer { }; } } - _ => {} + Address::Global(idx) => { + // Phase 4: Global Inlining + // 1. Check script-local global substitution map + if let Some(val) = sub.globals.get(&idx) { + return Node { + identity: node.identity, + ty: val.static_type(), + kind: BoundKind::Constant(val.clone()), + }; + } + // 2. Check environment global values + if let Some(globals_rc) = &self.globals { + let globals = globals_rc.borrow(); + if let Some(val) = globals.get(idx as usize) { + // Only inline "stable" values: scalars and stateless functions + if self.is_inlinable_value(val) { + return Node { + identity: node.identity, + ty: val.static_type(), + kind: BoundKind::Constant(val.clone()), + }; + } + } + } + sub.used_globals.insert(idx); + } } (BoundKind::Get { addr, name }, node.ty) } @@ -149,8 +182,6 @@ impl Optimizer { } if self.level >= 2 { - // Phase 2.6: Dead Code Elimination (DCE) - // Remove side-effect-free expressions from the block that are not the result. if !new_exprs.is_empty() { let last_idx = new_exprs.len() - 1; let mut filtered = Vec::with_capacity(new_exprs.len()); @@ -160,12 +191,15 @@ impl Optimizer { continue; } - // A statement is removable if it's pure (no side effects) - // or if it's a local assignment to a variable that is never used or captured. let removable = match &e.kind { BoundKind::DefLocal { slot, .. } | BoundKind::Set { addr: Address::Local(slot), .. } => { !sub.used_slots.contains(slot) && !sub.captured_slots.contains(slot) && self.is_side_effect_free(&e) } + BoundKind::DefGlobal { global_index, .. } => { + // Phase 4: DCE for Globals + // A global can be removed if it was only used for inlining in this script. + !sub.used_globals.contains(global_index) && self.is_side_effect_free(&e) + } _ => self.is_side_effect_free(&e), }; @@ -197,7 +231,6 @@ impl Optimizer { match capture_addr { Address::Local(slot) => { if let Some(val) = sub.locals.get(slot) { inlined_val = Some(val.clone()); } - // Mark slot as captured in parent scope sub.captured_slots.insert(*slot); } Address::Upvalue(idx) => { @@ -246,16 +279,30 @@ impl Optimizer { }, BoundKind::DefGlobal { name, global_index, value } => { let value = Box::new(self.visit_node(*value, sub)); + // Phase 4: Track script-local global definitions for inlining + if let BoundKind::Constant(val) = &value.kind { + sub.add_global(global_index, val.clone()); + } (BoundKind::DefGlobal { name, global_index, value }, node.ty) }, BoundKind::Set { addr, value } => { let value = Box::new(self.visit_node(*value, sub)); - if let Address::Local(slot) = addr { - if let BoundKind::Constant(val) = &value.kind { - sub.add_local(slot, val.clone()); - } else { - sub.locals.remove(&slot); + match addr { + Address::Local(slot) => { + if let BoundKind::Constant(val) = &value.kind { + sub.add_local(slot, val.clone()); + } else { + sub.locals.remove(&slot); + } } + Address::Global(idx) => { + if let BoundKind::Constant(val) = &value.kind { + sub.add_global(idx, val.clone()); + } else { + sub.globals.remove(&idx); + } + } + _ => {} } (BoundKind::Set { addr, value }, node.ty) }, @@ -277,6 +324,21 @@ impl Optimizer { Node { identity: node.identity, kind: new_kind, ty: new_ty } } + fn is_inlinable_value(&self, val: &Value) -> bool { + match val { + Value::Int(_) | Value::Float(_) | Value::Bool(_) | Value::Text(_) | Value::Keyword(_) | Value::DateTime(_) => true, + Value::Object(obj) => { + // Inline Closures ONLY if they are stateless (cracked) + if let Some(closure) = obj.as_any().downcast_ref::() { + closure.upvalues.is_empty() + } else { + false + } + } + _ => false, + } + } + fn is_side_effect_free(&self, node: &TypedNode) -> bool { match &node.kind { BoundKind::Constant(_) | BoundKind::Get { .. } | BoundKind::Parameter { .. } | BoundKind::Nop | BoundKind::Lambda { .. } => true, @@ -402,8 +464,12 @@ struct SubstitutionMap { locals: HashMap, /// Mapping Address::Upvalue(idx) -> Value upvalues: HashMap, + /// Mapping Address::Global(idx) -> Value (Script-local substitution) + globals: HashMap, /// Tracking which local slots are actually used (not inlined) used_slots: HashSet, + /// Tracking which global indices are used + used_globals: HashSet, /// Tracking which local slots are captured by lambdas captured_slots: HashSet, } @@ -413,7 +479,9 @@ impl SubstitutionMap { Self { locals: HashMap::new(), upvalues: HashMap::new(), + globals: HashMap::new(), used_slots: HashSet::new(), + used_globals: HashSet::new(), captured_slots: HashSet::new(), } } @@ -426,6 +494,10 @@ impl SubstitutionMap { self.upvalues.insert(idx, val); } + fn add_global(&mut self, idx: u32, val: Value) { + self.globals.insert(idx, val); + } + /// Re-maps Get(Upvalue(old_idx)) to Get(Upvalue(new_idx)) based on a mapping table. fn reindex_upvalues(&self, node: TypedNode, mapping: &[Option]) -> TypedNode { let (new_kind, new_ty) = match node.kind { @@ -474,6 +546,10 @@ impl SubstitutionMap { let value = Box::new(self.reindex_upvalues(*value, mapping)); (BoundKind::DefLocal { name, slot, value, captured_by }, node.ty) }, + BoundKind::DefGlobal { name, global_index, value } => { + let value = Box::new(self.reindex_upvalues(*value, mapping)); + (BoundKind::DefGlobal { name, global_index, value }, node.ty) + }, BoundKind::Set { addr, value } => { let value = Box::new(self.reindex_upvalues(*value, mapping)); (BoundKind::Set { addr, value }, node.ty) @@ -512,7 +588,6 @@ mod tests { #[test] fn test_opt_local_inlining() { - // Wrap in a do-block within a lambda to force DefLocal let source = "(fn [] (do (def x 10) (+ x 5)))"; let dump = get_optimized_dump(source, 2); insta::assert_snapshot!(dump); diff --git a/src/ast/environment.rs b/src/ast/environment.rs index be6e532..e4558ce 100644 --- a/src/ast/environment.rs +++ b/src/ast/environment.rs @@ -182,7 +182,7 @@ impl Environment { let specialized = self.specialize_node(node); // 2. Optimize (Level 1: Cracking, Level 2: Collapsing) - let optimizer = Optimizer::new(self.optimization_level); + let optimizer = Optimizer::new(self.optimization_level).with_globals(self.global_values.clone()); let optimized = optimizer.optimize(specialized); // 3. TCO (Always performed, converts to ExecNode) @@ -227,7 +227,7 @@ impl Environment { let specialized_ast = sub_specializer.specialize(retyped_ast); // 3. Optimize (Phase 2: Cracking & Folding) - let optimizer = Optimizer::new(opt_level); + let optimizer = Optimizer::new(opt_level).with_globals(global_values.clone()); let optimized_ast = optimizer.optimize(specialized_ast); // 4. TCO (converts to ExecNode)