diff --git a/examples/destructuring.myc b/examples/destructuring.myc new file mode 100644 index 0000000..b4e9043 --- /dev/null +++ b/examples/destructuring.myc @@ -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" diff --git a/src/ast/compiler/binder.rs b/src/ast/compiler/binder.rs index 3337e04..42d79e5 100644 --- a/src/ast/compiler/binder.rs +++ b/src/ast/compiler/binder.rs @@ -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, })) }, diff --git a/src/ast/compiler/bound_nodes.rs b/src/ast/compiler/bound_nodes.rs index 7bb999f..7da9ba8 100644 --- a/src/ast/compiler/bound_nodes.rs +++ b/src/ast/compiler/bound_nodes.rs @@ -72,10 +72,12 @@ pub enum BoundKind { }, Lambda { - params: Box>, + params: Rc>, // The list of variables captured from enclosing scopes upvalues: Vec
, body: Rc>, + /// Static optimization: number of positional parameters if the pattern is flat. + positional_count: Option, }, Call { diff --git a/src/ast/compiler/dumper.rs b/src/ast/compiler/dumper.rs index 7bcab7b..65165eb 100644 --- a/src/ast/compiler/dumper.rs +++ b/src/ast/compiler/dumper.rs @@ -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; diff --git a/src/ast/compiler/specializer.rs b/src/ast/compiler/specializer.rs index d57b408..3da884d 100644 --- a/src/ast/compiler/specializer.rs +++ b/src/ast/compiler/specializer.rs @@ -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 { + 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: () }; diff --git a/src/ast/compiler/tco.rs b/src/ast/compiler/tco.rs index 3407a2d..066ef4f 100644 --- a/src/ast/compiler/tco.rs +++ b/src/ast/compiler/tco.rs @@ -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 } diff --git a/src/ast/compiler/type_checker.rs b/src/ast/compiler/type_checker.rs index 7514142..2001367 100644 --- a/src/ast/compiler/type_checker.rs +++ b/src/ast/compiler/type_checker.rs @@ -48,7 +48,7 @@ impl TypeChecker { pub fn check(&self, node: BoundNode, arg_types: &[StaticType]) -> Result { 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) }, diff --git a/src/ast/environment.rs b/src/ast/environment.rs index 6a97929..355af27 100644 --- a/src/ast/environment.rs +++ b/src/ast/environment.rs @@ -231,15 +231,24 @@ impl Environment { /// Runtime: Execute the linked AST in the VM pub fn run(&self, node: &TypedNode) -> Result { 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::() { - // 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::() { + 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::() { + 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)) } } diff --git a/src/ast/parser.rs b/src/ast/parser.rs index 79903c6..a0d7402 100644 --- a/src/ast/parser.rs +++ b/src/ast/parser.rs @@ -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)?; diff --git a/src/ast/vm.rs b/src/ast/vm.rs index 9be5b8b..482c114 100644 --- a/src/ast/vm.rs +++ b/src/ast/vm.rs @@ -7,8 +7,12 @@ use crate::ast::types::{Value, Object}; #[derive(Debug, Clone)] pub struct Closure { + pub parameter_node: Rc, pub function_node: Rc, pub upvalues: Vec>>, + /// 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, } 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>) 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) -> Result { + 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(&mut self, observer: &mut O, closure: &Closure, args: Vec) -> Result { + 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(&mut self, observer: &mut O, root: &TypedNode) -> Result { self.stack.clear(); self.frames.clear(); @@ -514,6 +598,104 @@ impl VM { }, } } + + fn flatten_value(val: Value, into: &mut Vec) { + 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, 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(&mut self, observer: &mut O, args: &TypedNode) -> Result, 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) -> 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(&mut self, observer: &mut O, elements: &[TypedNode], into: &mut Vec) -> 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), }, }), }, diff --git a/src/integration_test.rs b/src/integration_test.rs index cc514f3..0807131 100644 --- a/src/integration_test.rs +++ b/src/integration_test.rs @@ -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"); + } }