Feat: Add global inlining and DCE
Introduce global inlining by allowing the Optimizer to access the environment's global values. This enables replacing global variable accesses with their constant values when appropriate. Additionally, implement Dead Code Elimination (DCE) for global definitions. A global definition can be removed if it was only used for inlining within the current script and has no side effects. Update the Optimizer struct to hold an optional reference to the global values and modify the `Optimizer::new` constructor to accept this. The `Environment::specialize` and `Environment::compile_script` methods are updated to pass the global values to the optimizer. The `SubstitutionMap` is also updated to track script-local global substitutions and used global indices, supporting both global inlining and global DCE.
This commit is contained in:
@@ -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<Rc<RefCell<Vec<Value>>>>,
|
||||
}
|
||||
|
||||
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<RefCell<Vec<Value>>>) -> 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>() {
|
||||
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<u32, Value>,
|
||||
/// Mapping Address::Upvalue(idx) -> Value
|
||||
upvalues: HashMap<u32, Value>,
|
||||
/// Mapping Address::Global(idx) -> Value (Script-local substitution)
|
||||
globals: HashMap<u32, Value>,
|
||||
/// Tracking which local slots are actually used (not inlined)
|
||||
used_slots: HashSet<u32>,
|
||||
/// Tracking which global indices are used
|
||||
used_globals: HashSet<u32>,
|
||||
/// Tracking which local slots are captured by lambdas
|
||||
captured_slots: HashSet<u32>,
|
||||
}
|
||||
@@ -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<u32>]) -> 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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user