about summary refs log tree commit diff
path: root/tvix
diff options
context:
space:
mode:
authorVincent Ambo <mail@tazj.in>2022-09-05T02·12+0300
committertazjin <tazjin@tvl.su>2022-09-11T10·32+0000
commitf86327beac05de96e01ede78a3cfd4d57e8319a5 (patch)
tree912631720e44fdef955eaed1bec38c21f8656b86 /tvix
parent41b6586ee155a7712e8856b8a03aac41d602d8ba (diff)
feat(tvix/eval): implement "formals" function parameters r/4790
The comment explains how this works fairly well.

Note that this does not yet have the ability to check "closed
formals", i.e. without an ellipsis Tvix will *NOT* fail if unexpected
attribute set keys are provided.

Change-Id: I0d2b77e893243093d2789baa57f876d35d0a32ff
Reviewed-on: https://cl.tvl.fyi/c/depot/+/6463
Reviewed-by: sterni <sternenseemann@systemli.org>
Tested-by: BuildkiteCI
Diffstat (limited to 'tvix')
-rw-r--r--tvix/eval/src/compiler/mod.rs98
-rw-r--r--tvix/eval/src/warnings.rs4
2 files changed, 101 insertions, 1 deletions
diff --git a/tvix/eval/src/compiler/mod.rs b/tvix/eval/src/compiler/mod.rs
index 314ae86d43..3fd43db53c 100644
--- a/tvix/eval/src/compiler/mod.rs
+++ b/tvix/eval/src/compiler/mod.rs
@@ -873,6 +873,101 @@ impl Compiler<'_, '_> {
         self.end_scope(&node);
     }
 
+    /// Compiles pattern function arguments, such as `{ a, b }: ...`.
+    ///
+    /// These patterns are treated as a special case of locals binding
+    /// where the attribute set itself is placed on the first stack
+    /// slot of the call frame (either as a phantom, or named in case
+    /// of an `@` binding), and the function call sets up the rest of
+    /// the stack as if the parameters were rewritten into a `let`
+    /// binding.
+    ///
+    /// For example:
+    ///
+    /// ```nix
+    /// ({ a, b ? 2, c ? a * b, ... }@args: <body>)  { a = 10; }
+    /// ```
+    ///
+    /// would be compiled similarly to a binding such as
+    ///
+    /// ```nix
+    /// let args = { a = 10; };
+    /// in let a = args.a;
+    ///        b = args.a or 2;
+    ///        c = args.c or a * b;
+    ///    in <body>
+    /// ```
+    ///
+    /// The only tricky bit being that bindings have to fail if too
+    /// many arguments are provided. This is done by emitting a
+    /// special instruction that checks the set of keys from a
+    /// constant containing the expected keys.
+    fn compile_param_pattern(&mut self, pattern: ast::Pattern) {
+        let span = self.span_for(&pattern);
+        let set_idx = match pattern.pat_bind() {
+            Some(name) => self.declare_local(&name, name.ident().unwrap().to_string()),
+            None => self.scope_mut().declare_phantom(span),
+        };
+
+        // At call time, the attribute set is already at the top of
+        // the stack.
+        self.scope_mut().mark_initialised(set_idx);
+        self.emit_force(&pattern);
+
+        // Similar to `let ... in ...`, we now do multiple passes over
+        // the bindings to first declare them, then populate them, and
+        // then finalise any necessary recursion into the scope.
+        let mut entries: Vec<(LocalIdx, ast::PatEntry)> = vec![];
+        let mut indices: Vec<LocalIdx> = vec![];
+
+        for entry in pattern.pat_entries() {
+            let ident = entry.ident().unwrap();
+            let idx = self.declare_local(&ident, ident.to_string());
+            entries.push((idx, entry));
+            indices.push(idx);
+        }
+
+        // For each of the bindings, push the set on the stack and
+        // attempt to select from it.
+        let stack_idx = self.scope().stack_index(set_idx);
+        for (idx, entry) in entries.into_iter() {
+            self.push_op(OpCode::OpGetLocal(stack_idx), &pattern);
+            self.emit_literal_ident(&entry.ident().unwrap());
+
+            // Use the same mechanism as `compile_select_or` if a
+            // default value was provided, or simply select otherwise.
+            if let Some(default_expr) = entry.default() {
+                self.push_op(OpCode::OpAttrsTrySelect, &entry.ident().unwrap());
+
+                let jump_to_default =
+                    self.push_op(OpCode::OpJumpIfNotFound(JumpOffset(0)), &default_expr);
+
+                let jump_over_default = self.push_op(OpCode::OpJump(JumpOffset(0)), &default_expr);
+
+                self.patch_jump(jump_to_default);
+                self.compile(idx, default_expr);
+                self.patch_jump(jump_over_default);
+            } else {
+                self.push_op(OpCode::OpAttrsSelect, &entry.ident().unwrap());
+            }
+
+            self.scope_mut().mark_initialised(idx);
+        }
+
+        for idx in indices {
+            if self.scope()[idx].needs_finaliser {
+                let stack_idx = self.scope().stack_index(idx);
+                self.push_op(OpCode::OpFinalise(stack_idx), &pattern);
+            }
+        }
+
+        // TODO: strictly check if all keys have been consumed if
+        // there is no ellipsis.
+        if pattern.ellipsis_token().is_none() {
+            self.emit_warning(span, WarningKind::NotImplemented("closed formals"));
+        }
+    }
+
     fn compile_lambda(&mut self, outer_slot: LocalIdx, node: ast::Lambda) {
         self.new_context();
         let span = self.span_for(&node);
@@ -881,7 +976,8 @@ impl Compiler<'_, '_> {
 
         // Compile the function itself
         match node.param().unwrap() {
-            ast::Param::Pattern(_) => todo!("formals function definitions"),
+            ast::Param::Pattern(pat) => self.compile_param_pattern(pat),
+
             ast::Param::IdentParam(param) => {
                 let name = param
                     .ident()
diff --git a/tvix/eval/src/warnings.rs b/tvix/eval/src/warnings.rs
index faf786d848..3bae2554cf 100644
--- a/tvix/eval/src/warnings.rs
+++ b/tvix/eval/src/warnings.rs
@@ -7,6 +7,10 @@ pub enum WarningKind {
     UselessInherit,
     UnusedBinding,
     ShadowedGlobal(&'static str),
+
+    /// Tvix internal warning for features triggered by users that are
+    /// not actually implemented yet.
+    NotImplemented(&'static str),
 }
 
 #[derive(Debug)]