about summary refs log tree commit diff
diff options
context:
space:
mode:
authorVincent Ambo <mail@tazj.in>2022-08-09T13·53+0300
committertazjin <tazjin@tvl.su>2022-08-13T15·31+0000
commit57a723aaa991b91f58a9a2b64c641d60bdbee2bb (patch)
tree2ea87ca1595a0d5bc318c0802151fa5af1a181ad
parent20fc7bc0b264f4904e0855b6b29513092118fdae (diff)
feat(tvix/eval): implement trivial attribute set literals r/4427
Implements attribute set literals without nesting. Technically this
already supports dynamic key fragments (evaluating to strings), though
the only way to create these (interpolation) is not yet implemented.

However, creating simple attribute sets like `{ }`, or `{ a = 15; }`
or `{ a = 10 * 2; }` works.

Recursive attribute sets are not yet implemented as we do not have any
kind of scope access yet anyways.

This is implemented using a new instruction that creates an attribute
set with a given number of elements by popping key/value pairs off the
stack.

Change-Id: I0f9aac7a131a112d3f66b131297686b38aaeddf2
Reviewed-on: https://cl.tvl.fyi/c/depot/+/6091
Tested-by: BuildkiteCI
Reviewed-by: grfn <grfn@gws.fyi>
-rw-r--r--tvix/eval/src/compiler.rs69
-rw-r--r--tvix/eval/src/opcode.rs3
-rw-r--r--tvix/eval/src/vm.rs19
3 files changed, 88 insertions, 3 deletions
diff --git a/tvix/eval/src/compiler.rs b/tvix/eval/src/compiler.rs
index 4526bf16d6..d7d6bb38a7 100644
--- a/tvix/eval/src/compiler.rs
+++ b/tvix/eval/src/compiler.rs
@@ -4,9 +4,10 @@
 use crate::chunk::Chunk;
 use crate::errors::EvalResult;
 use crate::opcode::OpCode;
-use crate::value::Value;
+use crate::value::{NixString, Value};
+
 use rnix;
-use rnix::types::{TokenWrapper, TypedNode, Wrapper};
+use rnix::types::{EntryHolder, TokenWrapper, TypedNode, Wrapper};
 
 struct Compiler {
     chunk: Chunk,
@@ -46,6 +47,11 @@ impl Compiler {
                 self.compile_ident(node)
             }
 
+            rnix::SyntaxKind::NODE_ATTR_SET => {
+                let node = rnix::types::AttrSet::cast(node).unwrap();
+                self.compile_attr_set(node)
+            }
+
             kind => {
                 println!("visiting unsupported node: {:?}", kind);
                 Ok(())
@@ -125,6 +131,65 @@ impl Compiler {
 
         Ok(())
     }
+
+    // Compile attribute set literals into equivalent bytecode.
+    //
+    // This is complicated by a number of features specific to Nix
+    // attribute sets, most importantly:
+    //
+    // 1. Keys can be dynamically constructed through interpolation.
+    // 2. Keys can refer to nested attribute sets.
+    // 3. Attribute sets can (optionally) be recursive.
+    fn compile_attr_set(&mut self, node: rnix::types::AttrSet) -> EvalResult<()> {
+        let mut count = 0;
+
+        for kv in node.entries() {
+            count += 1;
+
+            // Because attribute set literals can contain nested keys,
+            // there is potentially more than one key fragment. If
+            // this is the case, a special operation to construct a
+            // runtime value representing the attribute path is
+            // emitted.
+            let mut key_count = 0;
+            for fragment in kv.key().unwrap().path() {
+                key_count += 1;
+
+                match fragment.kind() {
+                    rnix::SyntaxKind::NODE_IDENT => {
+                        let ident = rnix::types::Ident::cast(fragment).unwrap();
+
+                        // TODO(tazjin): intern!
+                        let idx = self
+                            .chunk
+                            .add_constant(Value::String(NixString(ident.as_str().to_string())));
+                        self.chunk.add_op(OpCode::OpConstant(idx));
+                    }
+
+                    // For all other expression types, we simply
+                    // compile them as normal. The operation should
+                    // result in a string value, which is checked at
+                    // runtime on construction.
+                    _ => self.compile(fragment)?,
+                }
+            }
+
+            // We're done with the key if there was only one fragment,
+            // otherwise we need to emit an instruction to construct
+            // the attribute path.
+            if key_count > 1 {
+                todo!("emit OpAttrPath(n) instruction")
+            }
+
+            // The value is just compiled as normal so that its
+            // resulting value is on the stack when the attribute set
+            // is constructed at runtime.
+            self.compile(kv.value().unwrap())?;
+        }
+
+        self.chunk.add_op(OpCode::OpAttrs(count));
+        Ok(())
+    }
 }
 
 pub fn compile(ast: rnix::AST) -> EvalResult<Chunk> {
diff --git a/tvix/eval/src/opcode.rs b/tvix/eval/src/opcode.rs
index a9f7f89040..58372f61cb 100644
--- a/tvix/eval/src/opcode.rs
+++ b/tvix/eval/src/opcode.rs
@@ -29,4 +29,7 @@ pub enum OpCode {
 
     // Logical binary operators
     OpEqual,
+
+    // Attribute sets
+    OpAttrs(usize),
 }
diff --git a/tvix/eval/src/vm.rs b/tvix/eval/src/vm.rs
index 9a718500b5..e8c0551caf 100644
--- a/tvix/eval/src/vm.rs
+++ b/tvix/eval/src/vm.rs
@@ -1,11 +1,13 @@
 //! This module implements the virtual (or abstract) machine that runs
 //! Tvix bytecode.
 
+use std::{collections::BTreeMap, rc::Rc};
+
 use crate::{
     chunk::Chunk,
     errors::{Error, EvalResult},
     opcode::OpCode,
-    value::Value,
+    value::{NixAttrs, NixString, Value},
 };
 
 pub struct VM {
@@ -114,6 +116,7 @@ impl VM {
                 OpCode::OpNull => self.push(Value::Null),
                 OpCode::OpTrue => self.push(Value::Bool(true)),
                 OpCode::OpFalse => self.push(Value::Bool(false)),
+                OpCode::OpAttrs(count) => self.run_attrset(count)?,
             }
 
             if self.ip == self.chunk.code.len() {
@@ -121,6 +124,20 @@ impl VM {
             }
         }
     }
+
+    fn run_attrset(&mut self, count: usize) -> EvalResult<()> {
+        let mut attrs: BTreeMap<NixString, Value> = BTreeMap::new();
+
+        for _ in 0..count {
+            let value = self.pop();
+            let key = self.pop().as_string()?; // TODO(tazjin): attrpath
+            attrs.insert(key, value);
+        }
+        // TODO(tazjin): extend_reserve(count) (rust#72631)
+
+        self.push(Value::Attrs(Rc::new(NixAttrs::Map(attrs))));
+        Ok(())
+    }
 }
 
 #[derive(Clone, Copy, Debug, PartialEq)]