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:
Michael Schimmel
2026-02-21 23:29:24 +01:00
parent 1edc2e56e4
commit ac73aaf59e
2 changed files with 91 additions and 16 deletions
+89 -14
View File
@@ -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);
+2 -2
View File
@@ -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)