From f86327beac05de96e01ede78a3cfd4d57e8319a5 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Mon, 5 Sep 2022 05:12:50 +0300 Subject: [PATCH] feat(tvix/eval): implement "formals" function parameters 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 Tested-by: BuildkiteCI --- tvix/eval/src/compiler/mod.rs | 98 ++++++++++++++++++++++++++++++++++- tvix/eval/src/warnings.rs | 4 ++ 2 files changed, 101 insertions(+), 1 deletion(-) diff --git a/tvix/eval/src/compiler/mod.rs b/tvix/eval/src/compiler/mod.rs index 314ae86d4..3fd43db53 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: ) { 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 + /// ``` + /// + /// 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 = 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 faf786d84..3bae2554c 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)]