about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--tvix/eval/src/compiler/mod.rs24
-rw-r--r--tvix/eval/src/opcode.rs12
-rw-r--r--tvix/eval/src/value/function.rs12
-rw-r--r--tvix/eval/src/vm.rs9
4 files changed, 51 insertions, 6 deletions
diff --git a/tvix/eval/src/compiler/mod.rs b/tvix/eval/src/compiler/mod.rs
index 3ade6831c286..0c41f06cbde1 100644
--- a/tvix/eval/src/compiler/mod.rs
+++ b/tvix/eval/src/compiler/mod.rs
@@ -856,9 +856,27 @@ impl Compiler {
             crate::disassembler::disassemble_chunk(&compiled.lambda.chunk);
         }
 
-        self.emit_constant(Value::Closure(Closure {
-            lambda: compiled.lambda,
-        }));
+        // If the function is not a closure, just emit it directly and
+        // move on.
+        if compiled.lambda.upvalue_count == 0 {
+            self.emit_constant(Value::Closure(Closure::new(compiled.lambda)));
+            return;
+        }
+
+        // If the function is a closure, we need to emit the variable
+        // number of operands that allow the runtime to close over the
+        // upvalues.
+        let closure_idx = self
+            .chunk()
+            .push_constant(Value::Closure(Closure::new(compiled.lambda)));
+
+        self.chunk().push_op(OpCode::OpClosure(closure_idx));
+        for upvalue in compiled.scope.upvalues {
+            match upvalue {
+                Upvalue::Stack(idx) => self.chunk().push_op(OpCode::DataLocalIdx(idx)),
+                Upvalue::Upvalue(idx) => self.chunk().push_op(OpCode::DataUpvalueIdx(idx)),
+            };
+        }
     }
 
     fn compile_apply(&mut self, node: ast::Apply) {
diff --git a/tvix/eval/src/opcode.rs b/tvix/eval/src/opcode.rs
index 6cce7ea9b176..ca99602862d5 100644
--- a/tvix/eval/src/opcode.rs
+++ b/tvix/eval/src/opcode.rs
@@ -102,7 +102,17 @@ pub enum OpCode {
     // Asserts stack top is a boolean, and true.
     OpAssert,
 
-    // Lambdas
+    // Lambdas & closures
     OpCall,
     OpGetUpvalue(UpvalueIdx),
+    OpClosure(ConstantIdx),
+
+    // The closure and thunk creation instructions have a variable
+    // number of arguments to the instruction, which is represented
+    // here by making their data part of the opcodes.
+    //
+    // The VM skips over these by advancing the instruction pointer
+    // according to the count.
+    DataLocalIdx(StackIdx),
+    DataUpvalueIdx(UpvalueIdx),
 }
diff --git a/tvix/eval/src/value/function.rs b/tvix/eval/src/value/function.rs
index 5d7247416ee0..2b5fcf6c9819 100644
--- a/tvix/eval/src/value/function.rs
+++ b/tvix/eval/src/value/function.rs
@@ -1,7 +1,7 @@
 //! This module implements the runtime representation of functions.
 use std::rc::Rc;
 
-use crate::chunk::Chunk;
+use crate::{chunk::Chunk, Value};
 
 #[derive(Clone, Debug)]
 pub struct Lambda {
@@ -27,4 +27,14 @@ impl Lambda {
 #[derive(Clone, Debug)]
 pub struct Closure {
     pub lambda: Lambda,
+    pub upvalues: Vec<Value>,
+}
+
+impl Closure {
+    pub fn new(lambda: Lambda) -> Self {
+        Closure {
+            upvalues: Vec::with_capacity(lambda.upvalue_count),
+            lambda,
+        }
+    }
 }
diff --git a/tvix/eval/src/vm.rs b/tvix/eval/src/vm.rs
index 1c7772f2a7e0..5ebc95e7f15e 100644
--- a/tvix/eval/src/vm.rs
+++ b/tvix/eval/src/vm.rs
@@ -357,7 +357,7 @@ impl VM {
                 OpCode::OpCall => {
                     let callable = self.pop();
                     match callable {
-                        Value::Closure(Closure { lambda }) => self.call(lambda, 1),
+                        Value::Closure(Closure { lambda, .. }) => self.call(lambda, 1),
                         Value::Builtin(builtin) => {
                             let arg = self.pop();
                             let result = builtin.apply(arg)?;
@@ -368,6 +368,13 @@ impl VM {
                 }
 
                 OpCode::OpGetUpvalue(_) => todo!("getting upvalues"),
+                OpCode::OpClosure(_) => todo!("creating closure objects"),
+
+                // Data-carrying operands should never be executed,
+                // that is a critical error in the VM.
+                OpCode::DataLocalIdx(_) | OpCode::DataUpvalueIdx(_) => {
+                    panic!("VM bug: attempted to execute data-carrying operand")
+                }
             }
 
             #[cfg(feature = "disassembler")]