Add positional_count field to Lambda
This field is used for static optimization, determining if parameters are purely positional.
This commit is contained in:
+215
-32
@@ -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),
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user