Add positional_count field to Lambda

This field is used for static optimization, determining if parameters
are purely positional.
This commit is contained in:
Michael Schimmel
2026-02-20 14:40:56 +01:00
parent 56d6c3bbde
commit 4e812c1afb
11 changed files with 361 additions and 62 deletions
+25
View File
@@ -0,0 +1,25 @@
;; Comprehensive Destructuring Test
;; Covers: Nested tuples, mixed params, dynamic passing
(do
;; 1. Deeply nested
(def deep (fn [[a [[b c] d]]] (+ a (+ b (+ c d)))))
;; 2. Mixed: Tuple and Single
(def mixed (fn [[x y] z] (+ (+ x y) z)))
;; 3. Dynamic: Passing a list variable
(def call-dynamic (fn [f data] (f data)))
(def data [10 [20 30]])
;; Validation
(if (= (deep [1 [[2 3] 4]]) 10)
(if (= (mixed [1 2] 3) 6)
(if (= (call-dynamic (fn [[a [b c]]] (+ a (+ b c))) data) 60)
"PASS"
"FAIL-DYNAMIC")
"FAIL-MIXED")
"FAIL-DEEP"))
;; Output: "PASS"
+21 -1
View File
@@ -200,10 +200,30 @@ impl Binder {
let compiled_fn = self.functions.pop().unwrap();
// 3. Static optimization: check if parameters are purely positional
let positional_count = match &params_bound.kind {
BoundKind::Tuple { elements } => {
let mut count = 0;
let mut all_params = true;
for e in elements {
if matches!(e.kind, BoundKind::Parameter { .. }) {
count += 1;
} else {
all_params = false;
break;
}
}
if all_params { Some(count) } else { None }
}
BoundKind::Parameter { .. } => Some(1),
_ => None,
};
Ok(self.make_node(identity, BoundKind::Lambda {
params: Box::new(params_bound),
params: Rc::new(params_bound),
upvalues: compiled_fn.upvalues,
body: Rc::new(body_bound),
positional_count,
}))
},
+3 -1
View File
@@ -72,10 +72,12 @@ pub enum BoundKind<T = ()> {
},
Lambda {
params: Box<BoundNode<T>>,
params: Rc<BoundNode<T>>,
// The list of variables captured from enclosing scopes
upvalues: Vec<Address>,
body: Rc<BoundNode<T>>,
/// Static optimization: number of positional parameters if the pattern is flat.
positional_count: Option<u32>,
},
Call {
+1 -1
View File
@@ -117,7 +117,7 @@ impl Dumper {
self.indent -= 1;
}
BoundKind::Lambda { params, upvalues, body } => {
BoundKind::Lambda { params, upvalues, body, .. } => {
self.log(&format!("Lambda (Upvalues: {})", upvalues.len()), node);
self.indent += 1;
+31 -7
View File
@@ -69,10 +69,10 @@ impl Specializer {
let exprs = exprs.into_iter().map(|e| self.visit_node(e)).collect();
(BoundKind::Block { exprs }, node.ty)
},
BoundKind::Lambda { params, upvalues, body } => {
let params = Box::new(self.visit_node(*params));
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).clone()));
(BoundKind::Lambda { params, upvalues, body }, node.ty)
(BoundKind::Lambda { params, upvalues, body, positional_count }, node.ty)
},
BoundKind::DefLocal { name, slot, value, captured_by } => {
let value = Box::new(self.visit_node(*value));
@@ -194,15 +194,25 @@ impl Specializer {
// Store in cache
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 {
identity: new_args.identity.clone(),
kind: BoundKind::Tuple { elements: flat_elements },
ty: StaticType::Tuple(flat_types),
};
let specialized_callee = Node {
identity: new_callee.identity.clone(),
kind: BoundKind::Constant(res_val),
ty: StaticType::Function(Box::new(Signature {
params: StaticType::Tuple(arg_types),
params: flattened_args.ty.clone(),
ret: res_ty.clone(),
})),
};
return (specialized_callee, new_args, res_ty);
return (specialized_callee, flattened_args, res_ty);
},
Err(_) => {
// Fallback on error
@@ -214,6 +224,19 @@ impl Specializer {
// Fallback: Dynamic Call
(new_callee, new_args, original_ty)
}
fn flatten_tuple(&self, node: TypedNode) -> Vec<TypedNode> {
match node.kind {
BoundKind::Tuple { elements } => {
let mut flat = Vec::new();
for el in elements {
flat.extend(self.flatten_tuple(el));
}
flat
}
_ => vec![node],
}
}
}
#[cfg(test)]
@@ -268,7 +291,7 @@ mod tests {
let func_node = BoundNode {
identity: make_identity(),
kind: BoundKind::Lambda {
params: Box::new(BoundNode {
params: Rc::new(BoundNode {
identity: make_identity(),
kind: BoundKind::Tuple {
elements: vec![
@@ -282,7 +305,8 @@ mod tests {
ty: ()
}),
upvalues: vec![],
body: Rc::new(BoundNode { identity: make_identity(), kind: BoundKind::Nop, ty: () })
body: Rc::new(BoundNode { identity: make_identity(), kind: BoundKind::Nop, ty: () }),
positional_count: Some(1),
},
ty: ()
};
+3 -2
View File
@@ -75,15 +75,16 @@ impl TCO {
}
},
BoundKind::Lambda { params, upvalues, body } => {
BoundKind::Lambda { params, upvalues, body, positional_count } => {
// The body of a lambda is implicitly in tail position when the lambda is called.
let new_body = Rc::new(Self::transform((*body).clone(), true));
Node {
kind: BoundKind::Lambda {
params,
params: params.clone(),
upvalues,
body: new_body,
positional_count,
},
..node
}
+13 -10
View File
@@ -48,7 +48,7 @@ impl TypeChecker {
pub fn check(&self, node: BoundNode, arg_types: &[StaticType]) -> Result<TypedNode, String> {
match node.kind {
BoundKind::Lambda { params, upvalues, body } => {
BoundKind::Lambda { params, upvalues, body, positional_count } => {
// 1. Determine types of captured variables (Root lambdas have none)
let mut upvalue_types = Vec::with_capacity(upvalues.len());
for _ in &upvalues {
@@ -66,7 +66,7 @@ impl TypeChecker {
StaticType::Tuple(arg_types.to_vec())
};
let params_typed = self.check_params(*params, &arg_tuple_ty, &mut lambda_ctx)?;
let params_typed = self.check_params(params.as_ref().clone(), &arg_tuple_ty, &mut lambda_ctx)?;
// 4. Check body with the new types
let body_typed = self.check_node((*body).clone(), &mut lambda_ctx)?;
@@ -83,9 +83,10 @@ impl TypeChecker {
Ok(Node {
identity: node.identity,
kind: BoundKind::Lambda {
params: Box::new(params_typed),
params: Rc::new(params_typed),
upvalues,
body: Rc::new(body_typed)
body: Rc::new(body_typed),
positional_count,
},
ty: fn_ty,
})
@@ -95,13 +96,14 @@ impl TypeChecker {
let virtual_lambda = BoundNode {
identity: node.identity.clone(),
kind: BoundKind::Lambda {
params: Box::new(Node {
params: Rc::new(Node {
identity: node.identity.clone(),
kind: BoundKind::Tuple { elements: vec![] },
ty: (),
}),
upvalues: vec![],
body: Rc::new(node)
body: Rc::new(node),
positional_count: Some(0),
},
ty: (),
};
@@ -232,7 +234,7 @@ impl TypeChecker {
(BoundKind::Block { exprs: typed_exprs }, last_ty)
},
BoundKind::Lambda { params, upvalues, body } => {
BoundKind::Lambda { params, upvalues, body, positional_count } => {
// 1. Determine types of captured variables
let mut upvalue_types = Vec::with_capacity(upvalues.len());
for &addr in &upvalues {
@@ -243,7 +245,7 @@ impl TypeChecker {
let mut lambda_ctx = TypeContext::new(64, upvalue_types, Some(ctx));
// 3. Check parameters and body
let params_typed = self.check_params(*params, &StaticType::Any, &mut lambda_ctx)?;
let params_typed = self.check_params(params.as_ref().clone(), &StaticType::Any, &mut lambda_ctx)?;
let body_typed = self.check_node((*body).clone(), &mut lambda_ctx)?;
let ret_ty = body_typed.ty.clone();
@@ -254,9 +256,10 @@ impl TypeChecker {
}));
(BoundKind::Lambda {
params: Box::new(params_typed),
params: Rc::new(params_typed),
upvalues,
body: Rc::new(body_typed)
body: Rc::new(body_typed),
positional_count,
}, fn_ty)
},
+24 -4
View File
@@ -231,15 +231,24 @@ impl Environment {
/// Runtime: Execute the linked AST in the VM
pub fn run(&self, node: &TypedNode) -> Result<Value, String> {
let mut vm = VM::new(self.global_values.clone());
let result = vm.run(node)?;
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>()
{
// Execute the script body
return vm.run(&closure.function_node);
result = vm.run(&closure.function_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)
}
@@ -273,6 +282,17 @@ impl Environment {
result = vm.run_with_observer(&mut observer, &closure.function_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))
}
}
+9 -4
View File
@@ -227,17 +227,22 @@ impl<'a> Parser<'a> {
let mut elements = Vec::new();
while *self.peek() != TokenKind::RightBracket {
let next_token = self.advance()?;
let p_identity = Rc::new(NodeIdentity { location: next_token.location });
match next_token.kind {
let next_peek = self.peek();
match next_peek {
TokenKind::Identifier(name) => {
let name = name.clone();
let token = self.advance()?;
let p_identity = Rc::new(NodeIdentity { location: token.location });
elements.push(Node {
identity: p_identity,
kind: UntypedKind::Parameter(name.into()),
ty: (),
});
},
_ => return Err(format!("Expected identifier in param vector, found {:?}", next_token.kind)),
TokenKind::LeftBracket => {
elements.push(self.parse_param_vector()?);
},
_ => return Err(format!("Expected identifier or nested parameter vector, found {:?}", next_peek)),
}
}
self.expect(TokenKind::RightBracket)?;
+215 -32
View File
@@ -7,8 +7,12 @@ use crate::ast::types::{Value, Object};
#[derive(Debug, Clone)]
pub struct Closure {
pub parameter_node: Rc<TypedNode>,
pub function_node: Rc<TypedNode>,
pub upvalues: Vec<Rc<RefCell<Value>>>,
/// Optimization: If the parameter pattern is a simple flat tuple,
/// store the count to skip recursive unpacking in the hot path.
pub positional_count: Option<u32>,
}
impl Object for Closure {
@@ -163,7 +167,10 @@ macro_rules! dispatch_eval {
Ok(last)
},
BoundKind::Lambda { params: _, upvalues, body } => {
BoundKind::Lambda { params, upvalues, body, positional_count } => {
// PERFORMANCE: Pre-calculated in Binder. Just copy.
let positional_count = *positional_count;
// PERFORMANCE: Creating a closure captures upvalues.
// The actual execution of the lambda (in Call branch) now skips
// the Lambda node itself and jumps directly to the body.
@@ -173,8 +180,10 @@ macro_rules! dispatch_eval {
}
let closure = Closure {
parameter_node: params.clone(),
function_node: body.clone(),
upvalues: captured,
positional_count,
};
Ok(Value::Object(Rc::new(closure)))
@@ -183,28 +192,36 @@ macro_rules! dispatch_eval {
BoundKind::TailCall { callee, args } => {
let func_val = $self.$eval_method($($observer,)? callee)?;
// PERFORMANCE OPTIMIZATION: "Everything is a Tuple" Unification
// To avoid heap-allocating a Value::List (Rc<Vec<Value>>) for every function call,
// we check if the arguments are a literal tuple. If so, we evaluate them
// directly into our stack-ready vector.
let mut arg_vals = Vec::new();
match &args.kind {
let arg_vals = match &args.kind {
BoundKind::Tuple { elements } => {
arg_vals.reserve(elements.len());
// FAST-PATH: If it's a flat tuple, evaluate directly into Vec
let mut vals = Vec::with_capacity(elements.len());
let mut is_complex = false;
for e in elements {
arg_vals.push($self.$eval_method($($observer,)? e)?);
if matches!(e.kind, BoundKind::Tuple { .. }) {
is_complex = true;
break;
}
vals.push($self.$eval_method($($observer,)? e)?);
}
if is_complex {
macro_rules! get_args {
($s:ident, $a:ident) => { $s.prepare_args($a)? };
($s:ident, $o:ident, $a:ident) => { $s.prepare_args_observed($o, $a)? };
}
get_args!($self, $($observer,)? args)
} else {
vals
}
}
_ => {
// Fallback for dynamic tuples (e.g. arguments passed as a variable)
let v = $self.$eval_method($($observer,)? args)?;
if let Value::List(l) = v {
arg_vals = (*l).clone();
} else {
arg_vals.push(v);
macro_rules! get_args {
($s:ident, $a:ident) => { $s.prepare_args($a)? };
($s:ident, $o:ident, $a:ident) => { $s.prepare_args_observed($o, $a)? };
}
get_args!($self, $($observer,)? args)
}
}
};
match func_val {
Value::Object(obj) => Ok(Value::TailCallRequest(Box::new((obj, arg_vals)))),
@@ -216,25 +233,35 @@ macro_rules! dispatch_eval {
BoundKind::Call { callee, args } => {
let mut func_val = $self.$eval_method($($observer,)? callee)?;
// PERFORMANCE OPTIMIZATION: Same as in TailCall above.
// Short-circuiting the Tuple -> Value::List -> Vec conversion to save heap cycles.
let mut arg_vals = Vec::new();
match &args.kind {
let mut arg_vals = match &args.kind {
BoundKind::Tuple { elements } => {
arg_vals.reserve(elements.len());
let mut vals = Vec::with_capacity(elements.len());
let mut is_complex = false;
for e in elements {
arg_vals.push($self.$eval_method($($observer,)? e)?);
if matches!(e.kind, BoundKind::Tuple { .. }) {
is_complex = true;
break;
}
vals.push($self.$eval_method($($observer,)? e)?);
}
if is_complex {
macro_rules! get_args {
($s:ident, $a:ident) => { $s.prepare_args($a)? };
($s:ident, $o:ident, $a:ident) => { $s.prepare_args_observed($o, $a)? };
}
get_args!($self, $($observer,)? args)
} else {
vals
}
}
_ => {
let v = $self.$eval_method($($observer,)? args)?;
if let Value::List(l) = v {
arg_vals = (*l).clone();
} else {
arg_vals.push(v);
macro_rules! get_args {
($s:ident, $a:ident) => { $s.prepare_args($a)? };
($s:ident, $o:ident, $a:ident) => { $s.prepare_args_observed($o, $a)? };
}
get_args!($self, $($observer,)? args)
}
}
};
loop {
match func_val {
@@ -244,13 +271,21 @@ macro_rules! dispatch_eval {
let old_stack_top = $self.stack.len();
let closure_rc = Rc::new(closure.clone());
$self.stack.extend(arg_vals);
$self.frames.push(CallFrame {
stack_base: old_stack_top,
closure: Some(closure_rc.clone()),
});
// PERFORMANCE FAST-PATH: If the function is purely positional and arguments match, just extend.
if let Some(count) = closure.positional_count
&& arg_vals.len() == count as usize
{
$self.stack.extend(arg_vals);
} else {
// Unpack arguments into slots based on the closure's parameter pattern
$self.unpack(&closure.parameter_node, &arg_vals, &mut 0)?;
}
let result = $self.$eval_method($($observer,)? &closure.function_node);
$self.frames.pop();
@@ -340,13 +375,20 @@ impl VM {
// Reset stack for the next call (TCO)
self.stack.clear();
self.stack.extend(next_args);
self.frames.push(CallFrame {
stack_base: old_stack_top,
closure: Some(closure_rc),
});
if let Some(count) = closure.positional_count
&& next_args.len() == count as usize
{
self.stack.extend(next_args);
} else {
self.unpack(&closure.parameter_node, &next_args, &mut 0)?;
}
result = self.eval(&closure.function_node);
self.frames.pop();
@@ -359,6 +401,48 @@ impl VM {
}
}
pub fn run_with_args(&mut self, closure: &Closure, args: Vec<Value>) -> Result<Value, String> {
self.stack.clear();
self.frames.clear();
let closure_rc = Rc::new(closure.clone());
self.frames.push(CallFrame {
stack_base: 0,
closure: Some(closure_rc),
});
if let Some(count) = closure.positional_count
&& args.len() == count as usize
{
self.stack.extend(args);
} else {
self.unpack(&closure.parameter_node, &args, &mut 0)?;
}
self.eval(&closure.function_node)
}
pub fn run_with_args_observed<O: VMObserver>(&mut self, observer: &mut O, closure: &Closure, args: Vec<Value>) -> Result<Value, String> {
self.stack.clear();
self.frames.clear();
let closure_rc = Rc::new(closure.clone());
self.frames.push(CallFrame {
stack_base: 0,
closure: Some(closure_rc),
});
if let Some(count) = closure.positional_count
&& args.len() == count as usize
{
self.stack.extend(args);
} else {
self.unpack(&closure.parameter_node, &args, &mut 0)?;
}
self.eval_observed(observer, &closure.function_node)
}
pub fn run_with_observer<O: VMObserver>(&mut self, observer: &mut O, root: &TypedNode) -> Result<Value, String> {
self.stack.clear();
self.frames.clear();
@@ -514,6 +598,104 @@ impl VM {
},
}
}
fn flatten_value(val: Value, into: &mut Vec<Value>) {
if let Value::List(l) = val {
for item in l.iter() {
Self::flatten_value(item.clone(), into);
}
} else {
into.push(val);
}
}
fn prepare_args(&mut self, args: &TypedNode) -> Result<Vec<Value>, String> {
let mut arg_vals = Vec::new();
match &args.kind {
BoundKind::Tuple { elements } => {
self.eval_and_flatten(elements, &mut arg_vals)?;
}
_ => {
let v = self.eval(args)?;
VM::flatten_value(v, &mut arg_vals);
}
}
Ok(arg_vals)
}
fn prepare_args_observed<O: VMObserver>(&mut self, observer: &mut O, args: &TypedNode) -> Result<Vec<Value>, String> {
let mut arg_vals = Vec::new();
match &args.kind {
BoundKind::Tuple { elements } => {
self.eval_observed_and_flatten(observer, elements, &mut arg_vals)?;
}
_ => {
let v = self.eval_observed(observer, args)?;
VM::flatten_value(v, &mut arg_vals);
}
}
Ok(arg_vals)
}
fn eval_and_flatten(&mut self, elements: &[TypedNode], into: &mut Vec<Value>) -> Result<(), String> {
for e in elements {
match &e.kind {
BoundKind::Tuple { elements: sub } => self.eval_and_flatten(sub, into)?,
_ => into.push(self.eval(e)?),
}
}
Ok(())
}
fn eval_observed_and_flatten<O: VMObserver>(&mut self, observer: &mut O, elements: &[TypedNode], into: &mut Vec<Value>) -> Result<(), String> {
for e in elements {
match &e.kind {
BoundKind::Tuple { elements: sub } => self.eval_observed_and_flatten(observer, sub, into)?,
_ => into.push(self.eval_observed(observer, e)?),
}
}
Ok(())
}
/// Maps values into stack slots based on the parameter pattern.
/// Returns the number of slots filled.
fn unpack(&mut self, pattern: &TypedNode, values: &[Value], offset: &mut usize) -> Result<(), String> {
match &pattern.kind {
BoundKind::Parameter { slot, .. } => {
let val = values.get(*offset).cloned().unwrap_or(Value::Void);
*offset += 1;
let frame = self.frames.last().ok_or("No call frame")?;
let abs_index = frame.stack_base + (*slot as usize);
if abs_index == self.stack.len() {
self.stack.push(val);
} else if abs_index < self.stack.len() {
self.stack[abs_index] = val;
} else {
return Err(format!("Stack gap during unpack at slot {}", slot));
}
Ok(())
}
BoundKind::Tuple { elements } => {
// If the current value at offset is a List, we dive into it.
// Otherwise, we assume the list was already flattened (e.g. by Specializer).
if let Some(Value::List(l)) = values.get(*offset) {
*offset += 1;
let mut sub_offset = 0;
for el in elements {
self.unpack(el, l, &mut sub_offset)?;
}
} else {
for el in elements {
self.unpack(el, values, offset)?;
}
}
Ok(())
}
_ => Err("Invalid node in parameter pattern".to_string()),
}
}
}
#[cfg(test)]
@@ -581,13 +763,14 @@ mod tests {
identity: id.clone(),
ty: StaticType::Any,
kind: BoundKind::Lambda {
params: Box::new(Node {
params: Rc::new(Node {
identity: id.clone(),
ty: StaticType::Tuple(vec![]),
kind: BoundKind::Tuple { elements: vec![] },
}),
upvalues: vec![Address::Local(0)], // Capture x
body: Rc::new(lambda_body),
positional_count: Some(0),
},
}),
},
+16
View File
@@ -183,4 +183,20 @@ mod tests {
panic!("Expected DateTime, got {:?}", res);
}
}
#[test]
fn test_dynamic_call_destructuring_underflow() {
let env = Environment::new();
let source = "(do
(def call-dynamic (fn [f data] (f data)))
(def data [10 [20 30]])
(def x (fn [[a [b c]]] (+ a (+ b c))))
(call-dynamic x data))";
let result = env.run_script(source);
if let Err(e) = &result {
panic!("Failed: {}", e);
}
assert_eq!(format!("{}", result.unwrap()), "60");
}
}