feat: Implement closure cracking and inlining

Introduces a new optimizer pass that can "crack" closures, allowing for
more aggressive specialization. It also enables inlining of upvalues
that point to immutable global variables. This removes overhead for
higher-order functions and currying when arguments are statically
resolvable.
This commit is contained in:
Michael Schimmel
2026-02-21 18:49:55 +01:00
parent 74ea38248e
commit 0bbe35eeec
11 changed files with 386 additions and 286 deletions
+61
View File
@@ -0,0 +1,61 @@
# Optimierung: Closure-Cracking und Upvalue-Inlining
* **Datum:** 21.02.2026
* **Status:** Entwurf / Motivation
## 1. Motivation
Die aktuelle Monomorphisierung (`Specializer`) ist auf zustandslose Funktionen (Funktionen ohne Upvalues) beschränkt. Sobald eine Funktion Variablen aus ihrem äußeren Scope einfängt (Closures), wird sie für den Specializer zur "Blackbox". Dies verhindert die Optimierung von Higher-Order Functions (HOFs) und Currying, was in einer funktional orientierten DSL essenziell für die Performance ist.
## 2. Die Garantie
Zwei Faktoren in MyC erlauben eine aggressive Optimierung von Closures, die in anderen Sprachen (wie JavaScript oder Python) aufgrund von Mutabilität schwierig ist:
1. **Immutability von Globals:** Nach der Bootstrap-Phase sind `Address::Global`-Werte unveränderlich.
2. **Partial Evaluation:** Der Specializer nutzt die VM, um konstante Teilausdrücke bereits zur Compile-Zeit zu evaluieren. Das Ergebnis sind oft `Constant(Value::Object(Closure))`-Knoten.
## 3. Das Konzept
### 3.1 Closure-Cracking
Wenn der `Specializer` einen Funktionsaufruf sieht, dessen Callee eine **`Constant(Closure)`** ist (z. B. das Ergebnis einer Partial Application), kann er die Closure "knacken":
1. **Extraktion:** Der AST-Body der Closure und das dazugehörige `upvalues`-Array (die konkreten Daten) werden extrahiert.
2. **Substitution:** Jeder Zugriff auf einen Upvalue (`Get(UpValue(i))`) im AST-Body wird durch einen `Constant(Value)`-Knoten mit dem echten Wert aus dem `upvalues`-Array ersetzt.
3. **Rekursive Spezialisierung:** Der nun "flache" (upvalue-freie) AST wird erneut durch den Specializer geschickt.
### 3.2 Upvalue-Inlining (Global-Propagation)
Wenn eine Closure Upvalues besitzt, die auf **`Address::Global`** verweisen, können diese ebenfalls inlined werden:
* Da Globals unveränderlich sind, kann der `Specializer` den Wert des Globals zum Zeitpunkt der Spezialisierung aus dem `Environment` lesen und direkt als `Constant` in den AST einbauen.
* Dies eliminiert den Indirektions-Overhead über das Upvalue-Array und den Global-Lookup zur Laufzeit.
## 4. Beispiel-Analyse
Betrachte den Ausdruck: `(((fn [x] (fn [] x)) 5))`
1. **Innerer Call:** `((fn [x] (fn [] x)) 5)`
* Der Specializer erkennt das Lambda und das Argument `5`.
* Die VM evaluiert dies zu einer `Closure`-Instanz mit `upvalues: [Value::Int(5)]`.
* Der Knoten wird zu `Constant(Value::Object(Closure))`.
2. **Äußerer Call:** `( <Constant(Closure)> )`
* **Ohne Optimierung:** Dynamischer Dispatch der Closure zur Laufzeit.
* **Mit Closure-Cracking:**
* Der Specializer sieht die `Constant(Closure)`.
* Er extrahiert den Body `(Get x (UpValue 0))`.
* Er ersetzt `Get x` durch `Constant(5)`.
* Der gesamte Call wird zu `Constant(5)` reduziert.
## 5. Vorteile
* **Zero-Cost Abstractions:** HOFs und Currying haben keinen Laufzeit-Overhead mehr, wenn die Argumente statisch auflösbar sind.
* **Flattening:** Komplexe, verschachtelte Funktionshierarchien kollabieren zu flachen, effizienten Operationen.
* **JIT-Vorbereitung:** Ein upvalue-freier, spezialisierter AST ist die ideale Eingabe für einen späteren LLVM- oder Cranelift-Backend.
## 6. Implementation-Plan
1. **`specializer.rs`:** Erweiterung der `specialize_call_logic`, um neben `BoundKind::Get` auch `BoundKind::Constant` (mit Typ-Check auf Closure) als Callee zu akzeptieren.
2. **`environment.rs`:** Der `compiler`-Closure innerhalb von `specialize_node` muss Zugriff auf das `upvalues`-Array der aktuellen Closure-Instanz erhalten, um die Substitution durchzuführen.
3. **Substitution-Visitor:** Implementierung eines einfachen AST-Transformers, der `Get(UpValue(i))` gegen `Constant(values[i])` austauscht.
+2
View File
@@ -0,0 +1,2 @@
;; Output: 5
(((fn [x] (fn [] x)) 5))
+5
View File
@@ -0,0 +1,5 @@
;; Output: 5
(do
(def f (fn [x] (fn [] x)))
((f 5))
)
+5
View File
@@ -0,0 +1,5 @@
;; Output: 42
(do
(def f (fn [a] (fn [b] (fn [c] (+ a (+ b c))))))
(((f 10) 12) 20)
)
+2
View File
@@ -0,0 +1,2 @@
;; Output: 30
(+ 10 20)
+5 -6
View File
@@ -23,10 +23,9 @@ impl<'a> LambdaCollector<'a> {
}
BoundKind::DefGlobal { global_index, value, .. } => {
// If we define a global that is a lambda, register its BODY as the template.
// This allows the VM to skip the Lambda/Parameter nodes during execution.
if let BoundKind::Lambda { body, .. } = &value.kind {
self.registry.insert(*global_index, (**body).clone());
// Register the full Lambda node as the template.
if let BoundKind::Lambda { .. } = &value.kind {
self.registry.insert(*global_index, (**value).clone());
}
self.visit(value);
}
@@ -34,9 +33,9 @@ impl<'a> LambdaCollector<'a> {
BoundKind::Set { addr, value } => {
// Also track assignments to globals if they hold lambdas.
if let Address::Global(global_index) = addr
&& let BoundKind::Lambda { body, .. } = &value.kind
&& let BoundKind::Lambda { .. } = &value.kind
{
self.registry.insert(*global_index, (**body).clone());
self.registry.insert(*global_index, (**value).clone());
}
self.visit(value);
}
+2
View File
@@ -6,6 +6,7 @@ pub mod dumper;
pub mod macros;
pub mod type_checker;
pub mod specializer;
pub mod optimizer;
pub mod lambda_collector;
pub use binder::*;
@@ -16,3 +17,4 @@ pub use dumper::*;
pub use macros::*;
pub use type_checker::*;
pub use specializer::*;
pub use optimizer::*;
+274
View File
@@ -0,0 +1,274 @@
use std::rc::Rc;
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;
/// The Optimizer performs Phase 2: Partial Evaluation & Closure Cracking.
pub struct Optimizer {
/// 0: None, 1: Cracking (Stateless transformation), 2: Aggressive (Collapsing)
pub level: u32,
max_passes: usize,
}
impl Optimizer {
pub fn new(level: u32) -> Self {
Self { level, max_passes: 3 }
}
pub fn optimize(&self, node: TypedNode) -> TypedNode {
if self.level == 0 {
return node;
}
let mut current = node;
for _ in 0..self.max_passes {
let next = self.visit_node(current.clone());
current = next;
}
current
}
fn visit_node(&self, node: TypedNode) -> TypedNode {
let (new_kind, new_ty) = match node.kind {
BoundKind::Call { callee, args } => {
let callee = self.visit_node(*callee);
let args = self.visit_node(*args);
// --- Cracking & Collapsing ---
if let BoundKind::Constant(Value::Object(ref obj)) = callee.kind {
if let Some(closure) = obj.as_any().downcast_ref::<Closure>() {
// Level 1+: Inlining Upvalues (Stateless transformation)
// Structure is PRESERVED (Call still exists), but code is specialized.
let body = (*closure.function_node).clone();
let inlined_body = self.inline_upvalues(body, &closure.upvalues, 0);
// Level 2: Aggressive Collapsing
if self.level >= 2 {
// 1. Collapse 0-parameter calls
if let Some(0) = closure.positional_count {
let is_empty_args = match &args.kind {
BoundKind::Nop => true,
BoundKind::Tuple { elements } => elements.is_empty(),
_ => false,
};
if is_empty_args {
return self.visit_node(inlined_body);
}
}
// 2. Beta-Reduction (TODO: Inlining parameters)
}
// Stateless transformation (Cracking)
let cracked_lambda = Node {
identity: callee.identity.clone(),
ty: callee.ty.clone(),
kind: BoundKind::Lambda {
params: closure.parameter_node.clone(),
upvalues: vec![],
body: Rc::new(inlined_body),
positional_count: closure.positional_count,
},
};
return Node {
identity: node.identity,
kind: BoundKind::Call {
callee: Box::new(cracked_lambda),
args: Box::new(args)
},
ty: node.ty,
};
}
}
// Level 2: Intrinsic Folding (Arithmetic on constants)
if self.level >= 2 {
if let Some(folded) = self.try_fold_intrinsic(&callee, &args) {
return folded;
}
}
(BoundKind::Call { callee: Box::new(callee), args: Box::new(args) }, node.ty)
},
BoundKind::If { cond, then_br, else_br } => {
let cond = self.visit_node(*cond);
if self.level >= 2 {
if let BoundKind::Constant(ref val) = cond.kind {
if val.is_truthy() {
return self.visit_node(*then_br);
} else if let Some(else_node) = else_br {
return self.visit_node(*else_node);
} else {
return Node {
identity: node.identity,
kind: BoundKind::Nop,
ty: StaticType::Void,
};
}
}
}
let then_br = Box::new(self.visit_node(*then_br));
let else_br = else_br.map(|e| Box::new(self.visit_node(*e)));
(BoundKind::If { cond: Box::new(cond), then_br, else_br }, node.ty)
},
BoundKind::Block { exprs } => {
let mut new_exprs = Vec::with_capacity(exprs.len());
for e in exprs {
let opt = self.visit_node(e);
if self.level >= 2 && matches!(opt.kind, BoundKind::Nop) {
continue;
}
new_exprs.push(opt);
}
if self.level >= 2 {
if new_exprs.is_empty() {
return Node { identity: node.identity, kind: BoundKind::Nop, ty: StaticType::Void };
} else if new_exprs.len() == 1 {
return new_exprs.pop().unwrap();
}
}
let ty = if new_exprs.is_empty() { StaticType::Void } else { new_exprs.last().unwrap().ty.clone() };
(BoundKind::Block { exprs: new_exprs }, ty)
},
// Recursive Traversal
BoundKind::Lambda { params, upvalues, body, positional_count } => {
let params = Rc::new(self.visit_node(params.as_ref().clone()));
let body = Rc::new(self.visit_node(body.as_ref().clone()));
(BoundKind::Lambda { params, upvalues, body, positional_count }, node.ty)
},
BoundKind::DefLocal { name, slot, value, captured_by } => {
let value = Box::new(self.visit_node(*value));
(BoundKind::DefLocal { name, slot, value, captured_by }, node.ty)
},
BoundKind::DefGlobal { name, global_index, value } => {
let value = Box::new(self.visit_node(*value));
(BoundKind::DefGlobal { name, global_index, value }, node.ty)
},
BoundKind::Set { addr, value } => {
let value = Box::new(self.visit_node(*value));
(BoundKind::Set { addr, value }, node.ty)
},
BoundKind::Tuple { elements } => {
let elements = elements.into_iter().map(|e| self.visit_node(e)).collect();
(BoundKind::Tuple { elements }, node.ty)
},
BoundKind::Record { fields } => {
let fields = fields.into_iter().map(|(k, v)| (self.visit_node(k), self.visit_node(v))).collect();
(BoundKind::Record { fields }, node.ty)
},
BoundKind::Expansion { original_call, bound_expanded } => {
let bound_expanded = Box::new(self.visit_node(*bound_expanded));
(BoundKind::Expansion { original_call, bound_expanded }, node.ty)
},
k => (k, node.ty),
};
Node {
identity: node.identity,
kind: new_kind,
ty: new_ty,
}
}
fn try_fold_intrinsic(&self, callee: &TypedNode, args: &TypedNode) -> Option<TypedNode> {
// Simple constant folding for + and - on Ints
if let BoundKind::Get { name, .. } = &callee.kind {
if let BoundKind::Tuple { elements } = &args.kind {
if elements.len() == 2 {
if let (BoundKind::Constant(Value::Int(a)), BoundKind::Constant(Value::Int(b))) = (&elements[0].kind, &elements[1].kind) {
let res = match &*name.name {
"+" => Some(Value::Int(a + b)),
"-" => Some(Value::Int(a - b)),
"*" => Some(Value::Int(a * b)),
"/" if *b != 0 => Some(Value::Int(a / b)),
_ => None
};
if let Some(val) = res {
return Some(Node {
identity: callee.identity.clone(),
ty: StaticType::Int,
kind: BoundKind::Constant(val),
});
}
}
}
}
}
None
}
fn inline_upvalues(&self, node: TypedNode, upvalues: &[Rc<std::cell::RefCell<Value>>], depth: usize) -> TypedNode {
let (new_kind, new_ty) = match node.kind {
BoundKind::Get { addr: Address::Upvalue(idx), name } => {
if depth == 0 {
if let Some(cell) = upvalues.get(idx as usize) {
let val = cell.borrow().clone();
let ty = val.static_type();
(BoundKind::Constant(val), ty)
} else {
(BoundKind::Get { addr: Address::Upvalue(idx), name }, node.ty)
}
} else {
(BoundKind::Get { addr: Address::Upvalue(idx), name }, node.ty)
}
},
BoundKind::If { cond, then_br, else_br } => {
let cond = Box::new(self.inline_upvalues(*cond, upvalues, depth));
let then_br = Box::new(self.inline_upvalues(*then_br, upvalues, depth));
let else_br = else_br.map(|e| Box::new(self.inline_upvalues(*e, upvalues, depth)));
(BoundKind::If { cond, then_br, else_br }, node.ty)
},
BoundKind::Block { exprs } => {
let exprs = exprs.into_iter().map(|e| self.inline_upvalues(e, upvalues, depth)).collect();
(BoundKind::Block { exprs }, node.ty)
},
BoundKind::Call { callee, args } => {
let callee = Box::new(self.inline_upvalues(*callee, upvalues, depth));
let args = Box::new(self.inline_upvalues(*args, upvalues, depth));
(BoundKind::Call { callee, args }, node.ty)
},
BoundKind::TailCall { callee, args } => {
let callee = Box::new(self.inline_upvalues(*callee, upvalues, depth));
let args = Box::new(self.inline_upvalues(*args, upvalues, depth));
(BoundKind::TailCall { callee, args }, node.ty)
},
BoundKind::DefLocal { name, slot, value, captured_by } => {
let value = Box::new(self.inline_upvalues(*value, upvalues, depth));
(BoundKind::DefLocal { name, slot, value, captured_by }, node.ty)
},
BoundKind::Set { addr, value } => {
let value = Box::new(self.inline_upvalues(*value, upvalues, depth));
(BoundKind::Set { addr, value }, node.ty)
},
BoundKind::Tuple { elements } => {
let elements = elements.into_iter().map(|e| self.inline_upvalues(e, upvalues, depth)).collect();
(BoundKind::Tuple { elements }, node.ty)
},
BoundKind::Record { fields } => {
let fields = fields.into_iter().map(|(k, v)| (self.inline_upvalues(k, upvalues, depth), self.inline_upvalues(v, upvalues, depth))).collect();
(BoundKind::Record { fields }, node.ty)
},
BoundKind::Lambda { params, upvalues: lambda_upvalues, body, positional_count } => {
let body = Rc::new(self.inline_upvalues(body.as_ref().clone(), upvalues, depth + 1));
(BoundKind::Lambda { params, upvalues: lambda_upvalues, body, positional_count }, node.ty)
},
k => (k, node.ty),
};
Node {
identity: node.identity,
kind: new_kind,
ty: new_ty,
}
}
}
+1 -268
View File
@@ -93,8 +93,7 @@ impl Specializer {
BoundKind::Record { fields } => {
let fields = fields.into_iter().map(|(k, v)| (self.visit_node(k), self.visit_node(v))).collect();
(BoundKind::Record { fields }, node.ty)
}
,
},
BoundKind::Expansion { original_call, bound_expanded } => {
let bound_expanded = Box::new(self.visit_node(*bound_expanded));
(BoundKind::Expansion { original_call, bound_expanded }, node.ty)
@@ -177,11 +176,9 @@ impl Specializer {
// Check constraints (no closures with state)
if let BoundKind::Lambda { upvalues, .. } = &func_node.kind {
if !upvalues.is_empty() {
// Cannot specialize stateful closures trivially
return (new_callee, new_args, original_ty);
}
} else {
// Not a lambda?
return (new_callee, new_args, original_ty);
}
@@ -196,7 +193,6 @@ impl Specializer {
self.cache.borrow_mut().insert(key, (res_val.clone(), res_ty.clone()));
// PERFORMANCE: Flatten the argument tuple to match the specialized signature.
// Since we are specializing, we can convert [[1 2] 3] into a flat [1 2 3] Tuple node.
let flat_elements = self.flatten_tuple(new_args.clone());
let flat_types = flat_elements.iter().map(|e| e.ty.clone()).collect();
let flattened_args = Node {
@@ -239,266 +235,3 @@ impl Specializer {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::types::{Identity, NodeIdentity, SourceLocation, StaticType, Value, Signature};
use crate::ast::compiler::bound_nodes::{BoundKind, Address, TypedNode};
use crate::ast::nodes::Symbol;
use std::rc::Rc;
fn make_identity() -> Identity {
Rc::new(NodeIdentity { location: SourceLocation { line: 0, col: 0 } })
}
fn make_typed_node(kind: BoundKind<StaticType>, ty: StaticType) -> TypedNode {
crate::ast::nodes::Node {
identity: make_identity(),
kind,
ty,
}
}
// Mock Registry
struct MockRegistry {
functions: HashMap<Address, BoundNode>,
}
impl MockRegistry {
fn new() -> Self {
Self { functions: HashMap::new() }
}
fn register(&mut self, addr: Address, node: BoundNode) {
self.functions.insert(addr, node);
}
}
impl FunctionRegistry for MockRegistry {
fn resolve(&self, addr: Address) -> Option<BoundNode> {
self.functions.get(&addr).cloned()
}
}
#[test]
fn test_specialize_compiles_user_function() {
// Setup Registry with a function definition
let mut registry = MockRegistry::new();
let addr = Address::Local(0);
let name = Symbol::from("test_func");
// Def: (fn [x] x) -- generic identity
let func_node = BoundNode {
identity: make_identity(),
kind: BoundKind::Lambda {
params: Rc::new(BoundNode {
identity: make_identity(),
kind: BoundKind::Tuple {
elements: vec![
BoundNode {
identity: make_identity(),
kind: BoundKind::Parameter { name: name.clone(), slot: 0 },
ty: ()
}
]
},
ty: ()
}),
upvalues: vec![],
body: Rc::new(BoundNode { identity: make_identity(), kind: BoundKind::Nop, ty: () }),
positional_count: Some(1),
},
ty: ()
};
registry.register(addr, func_node);
// Setup Compiler Mock
let compiler: CompileFunc = Rc::new(|_node: BoundNode, _args: &[StaticType]| -> Result<(Value, StaticType), String> {
// Return a specialized "compiled" value
Ok((Value::Int(12345), StaticType::Int))
});
let spec = Specializer::new(Some(Rc::new(registry)), Some(compiler), None, None);
// Call(Get(Local(0)), Tuple([Arg(Int)]))
let callee = make_typed_node(BoundKind::Get { addr, name: name.clone() }, StaticType::Any);
let arg = make_typed_node(BoundKind::Constant(Value::Int(1)), StaticType::Int);
let args_tuple = make_typed_node(BoundKind::Tuple { elements: vec![arg] }, StaticType::Tuple(vec![StaticType::Int]));
let call_node = make_typed_node(
BoundKind::Call { callee: Box::new(callee), args: Box::new(args_tuple) },
StaticType::Any
);
let result = spec.specialize(call_node);
// Should be Call(Constant(12345), ...)
if let BoundKind::Call { callee, .. } = result.kind {
if let BoundKind::Constant(val) = callee.kind {
match val {
Value::Int(12345) => (),
_ => panic!("Expected compiled value 12345"),
}
} else {
panic!("Expected Constant callee");
}
} else {
panic!("Expected Call node");
}
}
#[test]
fn test_specialize_skips_unknown_types() {
let spec = Specializer::new(None, None, None, None);
let name0 = Symbol::from("f");
let name1 = Symbol::from("x");
// Call(Get(Local(0)), Tuple([Get(Local(1))])) where arg is Any
let callee = make_typed_node(BoundKind::Get { addr: Address::Local(0), name: name0 }, StaticType::Function(Box::new(Signature { params: StaticType::Tuple(vec![StaticType::Any]), ret: StaticType::Void })));
let arg = make_typed_node(BoundKind::Get { addr: Address::Local(1), name: name1 }, StaticType::Any);
let args_tuple = make_typed_node(BoundKind::Tuple { elements: vec![arg] }, StaticType::Tuple(vec![StaticType::Any]));
let call_node = make_typed_node(
BoundKind::Call { callee: Box::new(callee), args: Box::new(args_tuple) },
StaticType::Void
);
let result = spec.specialize(call_node);
// Should remain a generic Call because arg type is Any
if let BoundKind::Call { callee, .. } = result.kind {
if let BoundKind::Get { .. } = callee.kind {
// Correct: Still a Get, not a Constant(Function)
} else {
panic!("Expected generic Call to Get, got {:?}", callee.kind);
}
} else {
panic!("Expected Call node");
}
}
#[test]
fn test_specialize_uses_cache() {
// Setup cache with a pre-specialized function for (Int) -> Int
let spec = Specializer::new(None, None, None, None);
let addr = Address::Local(0);
let name = Symbol::from("cached_func");
let arg_types = vec![StaticType::Int];
let key = MonoCacheKey { address: addr, arg_types: arg_types.clone() };
// Mock a specialized function pointer
let specialized_val = Value::Int(999); // Dummy value representing function
let ret_ty = StaticType::Int;
spec.cache.borrow_mut().insert(key, (specialized_val.clone(), ret_ty.clone()));
// Create the call node: Call(Get(0), Tuple([Arg(Int)]))
let callee = make_typed_node(BoundKind::Get { addr, name }, StaticType::Any);
let arg = make_typed_node(BoundKind::Constant(Value::Int(1)), StaticType::Int);
let args_tuple = make_typed_node(BoundKind::Tuple { elements: vec![arg] }, StaticType::Tuple(vec![StaticType::Int]));
let call_node = make_typed_node(
BoundKind::Call { callee: Box::new(callee), args: Box::new(args_tuple) },
StaticType::Any
);
let result = spec.specialize(call_node);
// Should now be Call(Constant(999), ...)
if let BoundKind::Call { callee, .. } = result.kind {
if let BoundKind::Constant(val) = callee.kind {
match val {
Value::Int(999) => (), // Success
_ => panic!("Expected specialized value 999"),
}
} else {
panic!("Expected Constant callee, got {:?}", callee.kind);
}
} else {
panic!("Expected Call node");
}
}
#[test]
fn test_specialize_uses_rtl_lookup() {
// Setup RTL Lookup Mock
let rtl_lookup: RtlLookupFunc = Rc::new(|name, _args| {
if name == "rtl_func" {
Some((Value::Int(888), StaticType::Int))
} else {
None
}
});
let spec = Specializer::new(None, None, Some(rtl_lookup), None);
let addr = Address::Global(10);
let name = Symbol::from("rtl_func");
let callee = make_typed_node(BoundKind::Get { addr, name }, StaticType::Any);
let arg = make_typed_node(BoundKind::Constant(Value::Int(1)), StaticType::Int);
let args_tuple = make_typed_node(BoundKind::Tuple { elements: vec![arg] }, StaticType::Tuple(vec![StaticType::Int]));
let call_node = make_typed_node(
BoundKind::Call { callee: Box::new(callee), args: Box::new(args_tuple) },
StaticType::Any
);
let result = spec.specialize(call_node);
if let BoundKind::Call { callee, .. } = result.kind {
if let BoundKind::Constant(val) = callee.kind {
match val {
Value::Int(888) => (),
_ => panic!("Expected RTL value 888"),
}
} else {
panic!("Expected Constant callee from RTL");
}
} else {
panic!("Expected Call node");
}
}
#[test]
fn test_specialize_preserves_tail_call() {
let spec = Specializer::new(None, None, None, None);
let addr = Address::Local(0);
let name = Symbol::from("tail_func");
let arg_types = vec![StaticType::Int];
let key = MonoCacheKey { address: addr, arg_types: arg_types.clone() };
let specialized_val = Value::Int(777);
let ret_ty = StaticType::Int;
spec.cache.borrow_mut().insert(key, (specialized_val.clone(), ret_ty.clone()));
let callee = make_typed_node(BoundKind::Get { addr, name }, StaticType::Any);
let arg = make_typed_node(BoundKind::Constant(Value::Int(1)), StaticType::Int);
let args_tuple = make_typed_node(BoundKind::Tuple { elements: vec![arg] }, StaticType::Tuple(vec![StaticType::Int]));
// Use TailCall here
let call_node = make_typed_node(
BoundKind::TailCall { callee: Box::new(callee), args: Box::new(args_tuple) },
StaticType::Any
);
let result = spec.specialize(call_node);
if let BoundKind::TailCall { callee, .. } = result.kind {
if let BoundKind::Constant(val) = callee.kind {
match val {
Value::Int(777) => (),
_ => panic!("Expected specialized value 777"),
}
} else {
panic!("Expected Constant callee");
}
} else {
panic!("Expected TailCall node, got {:?}", result.kind);
}
}
}
+21 -9
View File
@@ -13,6 +13,7 @@ use crate::ast::compiler::lambda_collector::LambdaCollector;
use crate::ast::compiler::dumper::Dumper;
use crate::ast::compiler::macros::{MacroExpander, MacroRegistry, MacroEvaluator};
use crate::ast::compiler::specializer::{Specializer, MonoCache, FunctionRegistry};
use crate::ast::compiler::optimizer::Optimizer;
use crate::ast::rtl;
use crate::ast::rtl::intrinsics;
use crate::ast::compiler::bound_nodes::{Address, BoundNode};
@@ -24,6 +25,7 @@ pub struct Environment {
pub function_registry: Rc<RefCell<HashMap<u32, BoundNode>>>,
pub monomorph_cache: Rc<RefCell<MonoCache>>,
pub debug_mode: bool,
pub optimization_level: u32,
}
struct EnvFunctionRegistry {
@@ -81,6 +83,7 @@ impl Environment {
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
@@ -161,11 +164,15 @@ impl Environment {
/// Backend: Optimization (TCO, etc.)
pub fn link(&self, node: TypedNode) -> TypedNode {
// 1. Specialize
// 1. Specialize (Always performed for correctness)
let specialized = self.specialize_node(node);
// 2. Optimize
TCO::optimize(specialized)
// 2. Optimize (Level 1: Cracking, Level 2: Collapsing)
let optimizer = Optimizer::new(self.optimization_level);
let optimized = optimizer.optimize(specialized);
// 3. TCO (Always performed)
TCO::optimize(optimized)
}
fn specialize_node(&self, node: TypedNode) -> TypedNode {
@@ -179,6 +186,7 @@ impl Environment {
let mono_cache = self.monomorph_cache.clone();
let global_values = self.global_values.clone();
let global_types = self.global_types.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
@@ -198,18 +206,22 @@ impl Environment {
let specialized_ast = sub_specializer.specialize(retyped_ast);
// 3. Optimize (TCO)
let optimized_ast = TCO::optimize(specialized_ast);
// 3. Optimize (Phase 2: Cracking & Folding)
let optimizer = Optimizer::new(opt_level);
let optimized_ast = optimizer.optimize(specialized_ast);
// 4. Compile to Value (VM)
// 4. TCO
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(&optimized_ast) {
let compiled_val = match vm.run(&tco_ast) {
Ok(v) => v,
Err(e) => return Err(format!("VM Error during specialization: {}", e)),
};
// 5. Determine correct return type from the newly inferred function signature
let ret_type = if let StaticType::Function(sig) = &optimized_ast.ty {
// 6. Determine correct return type from the newly inferred function signature
let ret_type = if let StaticType::Function(sig) = &tco_ast.ty {
sig.ret.clone()
} else {
StaticType::Any
+5
View File
@@ -30,11 +30,16 @@ struct Cli {
/// Run with TracingObserver enabled
#[arg(short, long)]
trace: bool,
/// Optimization level (0: None, 1: Cracking, 2: Aggressive)
#[arg(short, long, default_value_t = 0)]
opt: u32,
}
fn main() {
let cli = Cli::parse();
let mut env = Environment::new();
env.optimization_level = cli.opt;
if cli.bench || cli.update_bench {
if cfg!(debug_assertions) {