From 872d6a2f493235cd71c89fd6438ed304332b8187 Mon Sep 17 00:00:00 2001 From: Tim Fennis Date: Sun, 24 May 2026 15:58:10 +0200 Subject: [PATCH] =?UTF-8?q?perf(vm):=20lazily=20clone=20parent=20upvalues?= =?UTF-8?q?=20on=20Closure=20=F0=9F=AA=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Walk the closure's CaptureSource list directly instead of pre-cloning the entire parent upvalues Vec. Closures only pay for the upvalues they actually reference; ones that only capture locals pay nothing extra. Co-Authored-By: Claude Opus 4.7 (1M context) --- ndc_vm/src/vm.rs | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/ndc_vm/src/vm.rs b/ndc_vm/src/vm.rs index dba538f..b02c5ff 100644 --- a/ndc_vm/src/vm.rs +++ b/ndc_vm/src/vm.rs @@ -498,28 +498,30 @@ impl Vm { // and before `capture_upvalue` needs `&mut self`. let idx = *idx; let values = Rc::clone(values); - let frame = self.frames.last().expect("no frame"); - let Value::Object(obj) = frame.closure.prototype.body.constant(idx) else { - panic!("Closure opcode: constant at index {idx} is not an Object"); - }; - let Object::Function(Function::Compiled(compiled)) = &**obj else { - panic!( - "Closure opcode: constant at index {idx} is not a Compiled function" - ); + let (compiled, frame_pointer) = { + let frame = self.frames.last().expect("no frame"); + let Value::Object(obj) = frame.closure.prototype.body.constant(idx) else { + panic!("Closure opcode: constant at index {idx} is not an Object"); + }; + let Object::Function(Function::Compiled(compiled)) = &**obj else { + panic!( + "Closure opcode: constant at index {idx} is not a Compiled function" + ); + }; + (Rc::clone(compiled), frame.frame_pointer) }; - let compiled = Rc::clone(compiled); - let frame_pointer = frame.frame_pointer; - // Pre-clone parent upvalue Rcs so we can drop the frame borrow - // before calling capture_upvalue (which needs &mut self). - let parent_upvalues: Vec<_> = - frame.closure.upvalues.iter().map(Rc::clone).collect(); + // Resolve captures lazily: only clone the specific parent upvalue + // Rcs that this closure references, and capture locals on demand. let upvalues = values .iter() .map(|c| match c { CaptureSource::Local(slot) => { self.capture_upvalue(frame_pointer + slot) } - CaptureSource::Upvalue(slot) => Rc::clone(&parent_upvalues[*slot]), + CaptureSource::Upvalue(slot) => { + let frame = self.frames.last().expect("no frame"); + Rc::clone(&frame.closure.upvalues[*slot]) + } }) .collect(); let closure = Value::function(Function::Closure(ClosureFunction {