From f35606616b1ab2814416c1b19bdcf000cc102c70 Mon Sep 17 00:00:00 2001 From: Michael Schimmel Date: Sun, 22 Feb 2026 08:42:22 +0100 Subject: [PATCH] Introduce Purity enum for optimizer Refactor the optimizer to use a Purity enum instead of a boolean for tracking function purity. This allows for a more granular representation of purity: - `Impure`: Functions with side effects. - `SideEffectFree`: Functions without side effects but may not be deterministic (e.g., `now()`, `random()`). - `Pure`: Functions without side effects and are deterministic. This change enhances the optimizer's ability to perform more aggressive optimizations by accurately determining function purity. --- src/ast/compiler/optimizer.rs | 96 ++++++++++++++++++++++------------- src/ast/environment.rs | 10 ++-- src/ast/rtl/core.rs | 37 +++++++------- src/ast/rtl/datetime.rs | 3 +- 4 files changed, 86 insertions(+), 60 deletions(-) diff --git a/src/ast/compiler/optimizer.rs b/src/ast/compiler/optimizer.rs index 4527bab..9fab411 100644 --- a/src/ast/compiler/optimizer.rs +++ b/src/ast/compiler/optimizer.rs @@ -6,13 +6,20 @@ use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::rc::Rc; +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum Purity { + Impure, // Has side effects (e.g. Set, Print) + SideEffectFree, // No side effects, but not deterministic (e.g. now(), random()) + Pure, // No side effects and deterministic (e.g. sin(x), 1 + 2) +} + /// The Optimizer performs Phase 2 (Cracking), Phase 2.5/2.6 (Aggressive Collapsing & DCE) /// and Phase 4 (Global Inlining). pub struct Optimizer { pub enabled: bool, max_passes: usize, pub globals: Option>>>, - pub global_purity: Option>>>, + pub global_purity: Option>>>, pub lambda_registry: Option>>>, } @@ -32,7 +39,7 @@ impl Optimizer { self } - pub fn with_purity(mut self, purity: Rc>>) -> Self { + pub fn with_purity(mut self, purity: Rc>>) -> Self { self.global_purity = Some(purity); self } @@ -239,7 +246,8 @@ impl Optimizer { if let BoundKind::Constant(Value::Object(ref obj)) = callee.kind && sub.inlining_depth < 5 && let Some(closure) = obj.as_any().downcast_ref::() - && (closure.upvalues.is_empty() || self.is_pure(&closure.function_node)) + && (closure.upvalues.is_empty() + || self.purity_of(&closure.function_node) >= Purity::SideEffectFree) { let mut closure_sub = SubstitutionMap::new(); closure_sub.inlining_depth = sub.inlining_depth + 1; @@ -268,7 +276,8 @@ impl Optimizer { && let BoundKind::Constant(Value::Object(ref obj)) = callee.kind && sub.inlining_depth < 5 && let Some(closure) = obj.as_any().downcast_ref::() - && (closure.upvalues.is_empty() || self.is_pure(&closure.function_node)) + && (closure.upvalues.is_empty() + || self.purity_of(&closure.function_node) >= Purity::SideEffectFree) { let mut closure_sub = SubstitutionMap::new(); closure_sub.inlining_depth = sub.inlining_depth + 1; @@ -396,7 +405,7 @@ impl Optimizer { BoundKind::DefLocal { slot, value, .. } => { !used_in_block.contains(slot) && !sub.captured_slots.contains(slot) - && self.is_pure(value) + && self.purity_of(value) >= Purity::SideEffectFree } BoundKind::DefGlobal { global_index, @@ -404,7 +413,7 @@ impl Optimizer { .. } => { !used_globals_in_block.contains(global_index) - && (self.is_pure(value) + && (self.purity_of(value) >= Purity::SideEffectFree || matches!(value.kind, BoundKind::Lambda { .. })) } BoundKind::Set { @@ -414,9 +423,9 @@ impl Optimizer { } => { !used_in_block.contains(slot) && !sub.captured_slots.contains(slot) - && self.is_pure(value) + && self.purity_of(value) >= Purity::SideEffectFree } - _ => self.is_pure(&e), + _ => self.purity_of(&e) >= Purity::SideEffectFree, }; if removable { @@ -562,7 +571,7 @@ impl Optimizer { sub.locals.remove(&slot); } - if self.is_pure(&value) { + if self.purity_of(&value) == Purity::Pure { sub.pure_slots.insert(slot); } else { sub.pure_slots.remove(&slot); @@ -589,10 +598,11 @@ impl Optimizer { if let BoundKind::Constant(val) = &value.kind { sub.add_global(global_index, val.clone()); } - if self.is_pure(&value) + let p = self.purity_of(&value); + if p > Purity::Impure && let Some(purity_rc) = &self.global_purity { - purity_rc.borrow_mut().insert(global_index, true); + purity_rc.borrow_mut().insert(global_index, p); } ( BoundKind::DefGlobal { @@ -678,62 +688,75 @@ impl Optimizer { } } - fn is_pure(&self, node: &TypedNode) -> bool { + fn purity_of(&self, node: &TypedNode) -> Purity { match &node.kind { - BoundKind::Constant(_) | BoundKind::Parameter { .. } | BoundKind::Nop => true, - BoundKind::Lambda { body, .. } => self.is_pure(body), + BoundKind::Constant(_) | BoundKind::Parameter { .. } | BoundKind::Nop => Purity::Pure, + BoundKind::Lambda { body, .. } => self.purity_of(body), BoundKind::Get { addr, .. } => match addr { Address::Global(idx) => { if let Some(purity_rc) = &self.global_purity { - *purity_rc.borrow().get(idx).unwrap_or(&false) + *purity_rc.borrow().get(idx).unwrap_or(&Purity::Impure) } else { - false + Purity::Impure } } - _ => true, + _ => Purity::Pure, }, - BoundKind::Tuple { elements } => elements.iter().all(|e| self.is_pure(e)), + BoundKind::Tuple { elements } => elements + .iter() + .map(|e| self.purity_of(e)) + .min() + .unwrap_or(Purity::Pure), BoundKind::Record { fields } => fields .iter() - .all(|(k, v)| self.is_pure(k) && self.is_pure(v)), + .map(|(k, v)| self.purity_of(k).min(self.purity_of(v))) + .min() + .unwrap_or(Purity::Pure), BoundKind::If { cond, then_br, else_br, } => { - self.is_pure(cond) - && self.is_pure(then_br) - && else_br.as_ref().is_none_or(|e| self.is_pure(e)) + let p = self.purity_of(cond).min(self.purity_of(then_br)); + if let Some(e) = else_br { + p.min(self.purity_of(e)) + } else { + p + } } - BoundKind::Block { exprs } => exprs.iter().all(|e| self.is_pure(e)), - BoundKind::Expansion { bound_expanded, .. } => self.is_pure(bound_expanded), - BoundKind::DefLocal { value, .. } => self.is_pure(value), - BoundKind::Set { .. } => false, + BoundKind::Block { exprs } => exprs + .iter() + .map(|e| self.purity_of(e)) + .min() + .unwrap_or(Purity::Pure), + BoundKind::Expansion { bound_expanded, .. } => self.purity_of(bound_expanded), + BoundKind::DefLocal { value, .. } => self.purity_of(value), + BoundKind::Set { .. } => Purity::Impure, BoundKind::Call { callee, args } => { - let callee_pure = match &callee.kind { + let callee_purity = match &callee.kind { BoundKind::Get { addr: Address::Global(idx), .. } => { if let Some(purity_rc) = &self.global_purity { - *purity_rc.borrow().get(idx).unwrap_or(&false) + *purity_rc.borrow().get(idx).unwrap_or(&Purity::Impure) } else { - false + Purity::Impure } } - BoundKind::Constant(Value::Function(_)) => true, + BoundKind::Constant(Value::Function(_)) => Purity::Pure, BoundKind::Constant(Value::Object(obj)) => { if let Some(closure) = obj.as_any().downcast_ref::() { - self.is_pure(&closure.function_node) + self.purity_of(&closure.function_node) } else { - false + Purity::Impure } } - _ => false, + _ => Purity::Impure, }; - callee_pure && self.is_pure(args) + callee_purity.min(self.purity_of(args)) } - _ => false, + _ => Purity::Impure, } } @@ -746,7 +769,8 @@ impl Optimizer { args: Box::new(args.clone()), }, }; - if !self.is_pure(&temp_call) { + // Folding requires the function AND arguments to be truly Pure (deterministic). + if self.purity_of(&temp_call) < Purity::Pure { return None; } let mut arg_nodes = Vec::new(); diff --git a/src/ast/environment.rs b/src/ast/environment.rs index da62e1a..626aaf6 100644 --- a/src/ast/environment.rs +++ b/src/ast/environment.rs @@ -12,7 +12,7 @@ use crate::ast::compiler::bound_nodes::{Address, BoundNode}; use crate::ast::compiler::dumper::Dumper; use crate::ast::compiler::lambda_collector::LambdaCollector; use crate::ast::compiler::macros::{MacroEvaluator, MacroExpander, MacroRegistry}; -use crate::ast::compiler::optimizer::Optimizer; +use crate::ast::compiler::optimizer::{Optimizer, Purity}; use crate::ast::compiler::specializer::{FunctionRegistry, MonoCache, Specializer}; use crate::ast::compiler::tco::{ExecNode, TCO}; use crate::ast::rtl; @@ -21,7 +21,7 @@ use crate::ast::rtl::intrinsics; pub struct Environment { pub global_names: Rc>>, pub global_types: Rc>>, - pub global_purity: Rc>>, + pub global_purity: Rc>>, pub global_values: Rc>>, pub function_registry: Rc>>, pub typed_function_registry: Rc>>, @@ -116,7 +116,7 @@ impl Environment { &self, name: &str, ty: StaticType, - is_pure: bool, + purity_level: Purity, func: impl Fn(Vec) -> Value + 'static, ) { let mut names = self.global_names.borrow_mut(); @@ -127,7 +127,7 @@ impl Environment { let idx = values.len() as u32; names.insert(Symbol::from(name), idx); types.insert(idx, ty); - purity.insert(idx, is_pure); + purity.insert(idx, purity_level); values.push(Value::Function(Rc::new(func))); } @@ -140,7 +140,7 @@ impl Environment { let idx = values.len() as u32; names.insert(Symbol::from(name), idx); types.insert(idx, ty); - purity.insert(idx, true); // Constants are always pure + purity.insert(idx, Purity::Pure); // Constants are always pure values.push(val); } diff --git a/src/ast/rtl/core.rs b/src/ast/rtl/core.rs index 59b7232..bedae24 100644 --- a/src/ast/rtl/core.rs +++ b/src/ast/rtl/core.rs @@ -1,3 +1,4 @@ +use crate::ast::compiler::optimizer::Purity; use crate::ast::environment::Environment; use crate::ast::types::{Signature, StaticType, Value}; use std::rc::Rc; @@ -39,7 +40,7 @@ fn register_arithmetic(env: &Environment) { ret: StaticType::DateTime, }, ]); - env.register_native("+", add_ty, true, |args| { + env.register_native("+", add_ty, Purity::Pure, |args| { if args.len() == 2 { match (&args[0], &args[1]) { (Value::Int(a), Value::Int(b)) => Value::Int(a + b), @@ -99,7 +100,7 @@ fn register_arithmetic(env: &Environment) { ret: StaticType::DateTime, }, ]); - env.register_native("-", sub_ty, true, |args| { + env.register_native("-", sub_ty, Purity::Pure, |args| { if args.is_empty() { return Value::Void; } @@ -152,7 +153,7 @@ fn register_arithmetic(env: &Environment) { ret: StaticType::Float, }, ]); - env.register_native("*", mul_ty, true, |args| { + env.register_native("*", mul_ty, Purity::Pure, |args| { if args.len() == 2 { match (&args[0], &args[1]) { (Value::Int(a), Value::Int(b)) => Value::Int(a * b), @@ -189,7 +190,7 @@ fn register_arithmetic(env: &Environment) { ret: StaticType::Float, }, ]); - env.register_native("/", div_ty, true, |args| { + env.register_native("/", div_ty, Purity::Pure, |args| { if args.len() != 2 { return Value::Void; } @@ -215,7 +216,7 @@ fn register_arithmetic(env: &Environment) { params: StaticType::Tuple(vec![StaticType::Int, StaticType::Int]), ret: StaticType::Int, })); - env.register_native("//", int_div_ty, true, |args| { + env.register_native("//", int_div_ty, Purity::Pure, |args| { if args.len() != 2 { return Value::Void; } @@ -238,7 +239,7 @@ fn register_arithmetic(env: &Environment) { params: StaticType::Tuple(vec![StaticType::Int, StaticType::Int]), ret: StaticType::Int, })); - env.register_native("%", mod_ty, true, |args| { + env.register_native("%", mod_ty, Purity::Pure, |args| { if args.len() != 2 { return Value::Void; } @@ -272,7 +273,7 @@ fn register_comparison(env: &Environment) { ]); // --- Greater Than (>) --- - env.register_native(">", cmp_ty.clone(), true, |args| { + env.register_native(">", cmp_ty.clone(), Purity::Pure, |args| { if args.len() != 2 { return Value::Void; } @@ -287,7 +288,7 @@ fn register_comparison(env: &Environment) { }); // --- Less Than (<) --- - env.register_native("<", cmp_ty.clone(), true, |args| { + env.register_native("<", cmp_ty.clone(), Purity::Pure, |args| { if args.len() != 2 { return Value::Void; } @@ -302,7 +303,7 @@ fn register_comparison(env: &Environment) { }); // --- Greater Or Equal (>=) --- - env.register_native(">=", cmp_ty.clone(), true, |args| { + env.register_native(">=", cmp_ty.clone(), Purity::Pure, |args| { if args.len() != 2 { return Value::Void; } @@ -317,7 +318,7 @@ fn register_comparison(env: &Environment) { }); // --- Less Or Equal (<=) --- - env.register_native("<=", cmp_ty.clone(), true, |args| { + env.register_native("<=", cmp_ty.clone(), Purity::Pure, |args| { if args.len() != 2 { return Value::Void; } @@ -336,7 +337,7 @@ fn register_comparison(env: &Environment) { params: StaticType::Tuple(vec![StaticType::Any, StaticType::Any]), ret: StaticType::Bool, })); - env.register_native("=", eq_ty.clone(), true, |args| { + env.register_native("=", eq_ty.clone(), Purity::Pure, |args| { if args.len() != 2 { return Value::Void; } @@ -357,7 +358,7 @@ fn register_comparison(env: &Environment) { }); // --- Not Equal (<>) --- - env.register_native("<>", eq_ty, true, |args| { + env.register_native("<>", eq_ty, Purity::Pure, |args| { if args.len() != 2 { return Value::Void; } @@ -388,7 +389,7 @@ fn register_logic(env: &Environment) { ret: StaticType::Int, }, ]); - env.register_native("not", not_ty, true, |args| { + env.register_native("not", not_ty, Purity::Pure, |args| { if args.len() != 1 { return Value::Void; } @@ -410,7 +411,7 @@ fn register_logic(env: &Environment) { ret: StaticType::Int, }, ]); - env.register_native("and", logic_op_ty.clone(), true, |args| { + env.register_native("and", logic_op_ty.clone(), Purity::Pure, |args| { if args.len() != 2 { return Value::Void; } @@ -422,7 +423,7 @@ fn register_logic(env: &Environment) { }); // --- Or (or) --- - env.register_native("or", logic_op_ty.clone(), true, |args| { + env.register_native("or", logic_op_ty.clone(), Purity::Pure, |args| { if args.len() != 2 { return Value::Void; } @@ -434,7 +435,7 @@ fn register_logic(env: &Environment) { }); // --- Xor (xor) --- - env.register_native("xor", logic_op_ty.clone(), true, |args| { + env.register_native("xor", logic_op_ty.clone(), Purity::Pure, |args| { if args.len() != 2 { return Value::Void; } @@ -450,7 +451,7 @@ fn register_logic(env: &Environment) { params: StaticType::Tuple(vec![StaticType::Int, StaticType::Int]), ret: StaticType::Int, })); - env.register_native("<<", shift_ty.clone(), true, |args| { + env.register_native("<<", shift_ty.clone(), Purity::Pure, |args| { if args.len() != 2 { return Value::Void; } @@ -461,7 +462,7 @@ fn register_logic(env: &Environment) { }); // --- Shift Right (>>) --- - env.register_native(">>", shift_ty, true, |args| { + env.register_native(">>", shift_ty, Purity::Pure, |args| { if args.len() != 2 { return Value::Void; } diff --git a/src/ast/rtl/datetime.rs b/src/ast/rtl/datetime.rs index 44e5b8e..94eb6b7 100644 --- a/src/ast/rtl/datetime.rs +++ b/src/ast/rtl/datetime.rs @@ -1,3 +1,4 @@ +use crate::ast::compiler::optimizer::Purity; use crate::ast::environment::Environment; use crate::ast::types::{Signature, StaticType, Value}; use chrono::{NaiveDate, NaiveDateTime}; @@ -8,7 +9,7 @@ pub fn register(env: &Environment) { ret: StaticType::DateTime, })); - env.register_native("date", date_ty, true, |args| { + env.register_native("date", date_ty, Purity::Pure, |args| { if let Value::Text(s) = &args[0] { // Try parse YYYY-MM-DD if let Ok(dt) = NaiveDate::parse_from_str(s, "%Y-%m-%d") {