about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--tvix/eval/src/builtins/mod.rs30
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-thunked-function-calls.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-thunked-function-calls.nix31
-rw-r--r--tvix/eval/src/value/thunk.rs31
4 files changed, 88 insertions, 5 deletions
diff --git a/tvix/eval/src/builtins/mod.rs b/tvix/eval/src/builtins/mod.rs
index d774e88138..53ad6f3f8e 100644
--- a/tvix/eval/src/builtins/mod.rs
+++ b/tvix/eval/src/builtins/mod.rs
@@ -17,7 +17,7 @@ use crate::vm::generators::{self, GenCo};
 use crate::warnings::WarningKind;
 use crate::{
     errors::ErrorKind,
-    value::{CoercionKind, NixAttrs, NixList, NixString, SharedThunkSet, Value},
+    value::{CoercionKind, NixAttrs, NixList, NixString, SharedThunkSet, Thunk, Value},
 };
 
 use self::versions::{VersionPart, VersionPartsIter};
@@ -307,6 +307,9 @@ mod pure_builtins {
         let mut nul = nul;
         let list = list.to_list()?;
         for val in list {
+            // Every call of `op` is forced immediately, but `nul` is not, see
+            // https://github.com/NixOS/nix/blob/940e9eb8/src/libexpr/primops.cc#L3069-L3070C36
+            // and our tests for foldl'.
             nul = generators::request_call_with(&co, op.clone(), [nul, val]).await;
             nul = generators::request_force(&co, nul).await;
         }
@@ -397,9 +400,15 @@ mod pure_builtins {
     ) -> Result<Value, ErrorKind> {
         let mut out = imbl::Vector::<Value>::new();
         let len = length.as_int()?;
+        // the best span we can get…
+        let span = generators::request_span(&co).await;
 
         for i in 0..len {
-            let val = generators::request_call_with(&co, generator.clone(), [i.into()]).await;
+            let val = Value::Thunk(Thunk::new_suspended_call(
+                generator.clone(),
+                i.into(),
+                span.clone(),
+            ));
             out.push_back(val);
         }
 
@@ -551,8 +560,11 @@ mod pure_builtins {
     async fn builtin_map(co: GenCo, f: Value, list: Value) -> Result<Value, ErrorKind> {
         let mut out = imbl::Vector::<Value>::new();
 
+        // the best span we can get…
+        let span = generators::request_span(&co).await;
+
         for val in list.to_list()? {
-            let result = generators::request_call_with(&co, f.clone(), [val]).await;
+            let result = Value::Thunk(Thunk::new_suspended_call(f.clone(), val, span.clone()));
             out.push_back(result)
         }
 
@@ -564,9 +576,17 @@ mod pure_builtins {
         let attrs = attrs.to_attrs()?;
         let mut out = imbl::OrdMap::new();
 
+        // the best span we can get…
+        let span = generators::request_span(&co).await;
+
         for (key, value) in attrs.into_iter() {
-            let result =
-                generators::request_call_with(&co, f.clone(), [key.clone().into(), value]).await;
+            let result = Value::Thunk(Thunk::new_suspended_call(
+                f.clone(),
+                key.clone().into(),
+                span.clone(),
+            ));
+            let result = Value::Thunk(Thunk::new_suspended_call(result, value, span.clone()));
+
             out.insert(key, result);
         }
 
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-thunked-function-calls.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-thunked-function-calls.exp
new file mode 100644
index 0000000000..3d4204d5a8
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-thunked-function-calls.exp
@@ -0,0 +1 @@
+[ 2 [ "Hans" "James" "Joachim" ] 2 [ "Clawdia" "Mynheer" ] 981 3 2 2 ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-thunked-function-calls.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-thunked-function-calls.nix
new file mode 100644
index 0000000000..d96ddb3bd1
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-thunked-function-calls.nix
@@ -0,0 +1,31 @@
+[
+  # This is independent of builtins
+  (builtins.length [ (builtins.throw "Ferge") (builtins.throw "Wehsal") ])
+  (builtins.attrNames {
+    Hans = throw "Castorp";
+    Joachim = throw "Ziemßen";
+    James = "Tienappel";
+  })
+
+  (builtins.length (builtins.map builtins.throw [ "Settembrini" "Naphta" ]))
+
+  (builtins.attrNames (builtins.mapAttrs builtins.throw {
+    Clawdia = "Chauchat";
+    Mynheer = "Peeperkorn";
+  }))
+
+  (builtins.length (builtins.genList (builtins.add "Marusja") 981))
+  (builtins.length (builtins.genList builtins.throw 3))
+
+  # These are hard to get wrong since the outer layer needs to be forced anyways
+  (builtins.length (builtins.genericClosure {
+    startSet = [
+      { key = 1; initial = true; }
+    ];
+    operator = { key, initial, ... }:
+      if initial
+      then [ { key = key - 1; initial = false;  value = throw "lol"; } ]
+      else [ ];
+  }))
+  (builtins.length (builtins.concatMap (m: [ m (builtins.throw m) ]) [ "Marusja" ]))
+]
diff --git a/tvix/eval/src/value/thunk.rs b/tvix/eval/src/value/thunk.rs
index 1acb50a94b..9c048d40b4 100644
--- a/tvix/eval/src/value/thunk.rs
+++ b/tvix/eval/src/value/thunk.rs
@@ -27,6 +27,7 @@ use std::{
 
 use crate::{
     errors::ErrorKind,
+    opcode::OpCode,
     spans::LightSpan,
     upvalues::Upvalues,
     value::Closure,
@@ -124,6 +125,36 @@ impl Thunk {
         )))))
     }
 
+    /// Helper function to create a [`Thunk`] that calls a function given as the
+    /// [`Value`] `callee` with the argument `arg` when it is forced. This is
+    /// particularly useful in builtin implementations if the result of calling
+    /// a function does not need to be forced immediately, because e.g. it is
+    /// stored in an attribute set.
+    pub fn new_suspended_call(callee: Value, arg: Value, light_span: LightSpan) -> Self {
+        let mut lambda = Lambda::default();
+        let span = light_span.span();
+
+        let arg_idx = lambda.chunk().push_constant(arg);
+        let f_idx = lambda.chunk().push_constant(callee);
+
+        // This is basically a recreation of compile_apply():
+        // We need to push the argument onto the stack and then the function.
+        // The function (not the argument) needs to be forced before calling.
+        lambda.chunk.push_op(OpCode::OpConstant(arg_idx), span);
+        lambda.chunk().push_op(OpCode::OpConstant(f_idx), span);
+        lambda.chunk.push_op(OpCode::OpForce, span);
+        lambda.chunk.push_op(OpCode::OpCall, span);
+
+        // Inform the VM that the chunk has ended
+        lambda.chunk.push_op(OpCode::OpReturn, span);
+
+        Thunk(Rc::new(RefCell::new(ThunkRepr::Suspended {
+            upvalues: Rc::new(Upvalues::with_capacity(0)),
+            lambda: Rc::new(lambda),
+            light_span,
+        })))
+    }
+
     fn prepare_blackhole(&self, forced_at: LightSpan) -> ThunkRepr {
         match &*self.0.borrow() {
             ThunkRepr::Suspended {