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