Files
RustAst/src/ast/environment.rs
T
Michael Schimmel 0107264026 feat: Implement purity inference for optimization
This commit introduces purity inference to the compiler's optimizer.
This allows for more aggressive constant folding and dead code
elimination by tracking which functions and global variables
are free of side effects.

Key changes include:

- Added `global_purity` field to `Optimizer` and `Environment`.
- Modified `Optimizer::is_pure` to recursively determine if an
  AST node represents a pure computation.
- Introduced `Optimizer::try_fold_pure` to replace the old
  `try_fold_intrinsic`, enabling folding of pure function calls
  with constant arguments.
- Updated `Environment::register_native` and
  `Environment::register_constant`
  to optionally record purity.
- Added purity flags to several built-in functions in `core.rs` and
  `datetime.rs`.
- A new example `optimizer_purity.myc` demonstrates the new feature.
2026-02-22 00:16:04 +01:00

350 lines
13 KiB
Rust

use crate::ast::compiler::binder::Binder;
use crate::ast::compiler::{TypeChecker, TypedNode};
use crate::ast::nodes::{Node, Symbol, UntypedKind};
use crate::ast::parser::Parser;
use crate::ast::types::{Object, StaticType, Value};
use crate::ast::vm::{TracingObserver, VM};
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
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::specializer::{FunctionRegistry, MonoCache, Specializer};
use crate::ast::compiler::tco::{TCO, ExecNode};
use crate::ast::rtl;
use crate::ast::rtl::intrinsics;
pub struct Environment {
pub global_names: Rc<RefCell<HashMap<Symbol, u32>>>,
pub global_types: Rc<RefCell<HashMap<u32, StaticType>>>,
pub global_purity: Rc<RefCell<HashMap<u32, bool>>>,
pub global_values: Rc<RefCell<Vec<Value>>>,
pub function_registry: Rc<RefCell<HashMap<u32, BoundNode>>>,
pub monomorph_cache: Rc<RefCell<MonoCache>>,
pub debug_mode: bool,
pub optimization_level: u32,
}
struct EnvFunctionRegistry {
registry: Rc<RefCell<HashMap<u32, BoundNode>>>,
}
impl FunctionRegistry for EnvFunctionRegistry {
fn resolve(&self, addr: Address) -> Option<BoundNode> {
if let Address::Global(idx) = addr {
self.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>>>,
global_values: Rc<RefCell<Vec<Value>>>,
}
impl MacroEvaluator for RuntimeMacroEvaluator {
fn evaluate(
&self,
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 mut vm = VM::new(self.global_values.clone());
vm.run(&exec_ast)
}
}
impl Default for Environment {
fn default() -> Self {
Self::new()
}
}
impl Environment {
pub fn new() -> Self {
let env = Self {
global_names: Rc::new(RefCell::new(HashMap::new())),
global_types: Rc::new(RefCell::new(HashMap::new())),
global_purity: Rc::new(RefCell::new(HashMap::new())),
global_values: Rc::new(RefCell::new(Vec::new())),
function_registry: Rc::new(RefCell::new(HashMap::new())),
monomorph_cache: Rc::new(RefCell::new(HashMap::new())),
debug_mode: false,
optimization_level: 0,
};
env.register_stdlib();
env
}
pub fn set_debug_mode(&mut self, enabled: bool) {
self.debug_mode = enabled;
}
fn get_expander(&self) -> MacroExpander<RuntimeMacroEvaluator> {
let evaluator = RuntimeMacroEvaluator {
global_names: self.global_names.clone(),
global_types: self.global_types.clone(),
global_values: self.global_values.clone(),
};
MacroExpander::new(MacroRegistry::new(), evaluator)
}
pub fn register_native(
&self,
name: &str,
ty: StaticType,
is_pure: bool,
func: impl Fn(Vec<Value>) -> Value + 'static,
) {
let mut names = self.global_names.borrow_mut();
let mut types = self.global_types.borrow_mut();
let mut values = self.global_values.borrow_mut();
let mut purity = self.global_purity.borrow_mut();
let idx = values.len() as u32;
names.insert(Symbol::from(name), idx);
types.insert(idx, ty);
purity.insert(idx, is_pure);
values.push(Value::Function(Rc::new(func)));
}
pub fn register_constant(&self, name: &str, ty: StaticType, val: Value) {
let mut names = self.global_names.borrow_mut();
let mut types = self.global_types.borrow_mut();
let mut values = self.global_values.borrow_mut();
let mut purity = self.global_purity.borrow_mut();
let idx = values.len() as u32;
names.insert(Symbol::from(name), idx);
types.insert(idx, ty);
purity.insert(idx, true); // Constants are always pure
values.push(val);
}
fn register_stdlib(&self) {
// Register all standard library functions via RTL module
rtl::register(self);
}
pub fn dump_ast(&self, source: &str) -> Result<String, String> {
let compiled = self.compile(source)?;
let linked = self.link(compiled);
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(),
);
}
// 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, &[])?;
Ok(typed_ast)
}
/// Backend: Optimization (TCO, etc.)
pub fn link(&self, node: TypedNode) -> ExecNode {
// 1. Specialize (Always performed for correctness)
let specialized = self.specialize_node(node);
// 2. Optimize (Level 1: Cracking, Level 2: Collapsing)
let optimizer = Optimizer::new(self.optimization_level)
.with_globals(self.global_values.clone())
.with_purity(self.global_purity.clone());
let optimized = optimizer.optimize(specialized);
// 3. TCO (Always performed, converts to ExecNode)
TCO::optimize(optimized)
}
fn specialize_node(&self, node: TypedNode) -> TypedNode {
let registry = Rc::new(EnvFunctionRegistry {
registry: self.function_registry.clone(),
});
let rtl_lookup = Rc::new(|name: &str, args: &[StaticType]| intrinsics::lookup(name, args));
let func_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 opt_level = self.optimization_level;
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 sub_registry = Rc::new(EnvFunctionRegistry {
registry: func_reg.clone(),
});
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()),
);
let specialized_ast = sub_specializer.specialize(retyped_ast);
// 3. Optimize (Phase 2: Cracking & Folding)
let optimizer = Optimizer::new(opt_level)
.with_globals(global_values.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
};
Ok((compiled_val, ret_type))
},
);
let specializer = Specializer::new(
Some(registry),
Some(compiler),
Some(rtl_lookup),
Some(self.monomorph_cache.clone()),
);
specializer.specialize(node)
}
/// Runtime: Execute the linked AST in the VM
pub fn run(&self, node: &ExecNode) -> Result<Value, String> {
let mut vm = VM::new(self.global_values.clone());
let mut result = vm.run(node)?;
// Handle potential script body closure
if let Value::Object(obj) = &result
&& let Some(closure) = obj.as_any().downcast_ref::<crate::ast::vm::Closure>()
{
result = vm.run(&closure.exec_node)?;
}
// IMPORTANT: Resolve any pending tail call requests from the top-level execution
while let 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(closure, next_args)?;
} else {
return Err(format!(
"Tail call target is not a closure: {}",
next_obj.type_name()
));
}
}
Ok(result)
}
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);
}
res
} else {
let compiled = self.compile(source)?;
let linked = self.link(compiled);
self.run(&linked)
}
}
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()
));
break;
}
}
Ok((result, observer.logs))
}
}