tvl-depot/tvix/eval/docs/opcodes-attrsets.md

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

123 lines
3.3 KiB
Markdown
Raw Normal View History

# attrset-opcodes
The problem with attrset literals is twofold:
1. The keys of attribute sets may be dynamically evaluated.
Access:
```nix
let
k = "foo";
attrs = { /* etc. */ };
in attrs."${k}"
```
Literal:
```nix
let
k = "foo";
in {
"${k}" = 42;
}
```
The problem with this is that the attribute set key is not known at
compile time, and needs to be dynamically evaluated by the VM as an
expression.
For the most part this should be pretty simple, assuming a
theoretical instruction set:
```
0000 OP_CONSTANT(0) # key "foo"
0001 OP_CONSTANT(1) # value 42
0002 OP_ATTR_SET(1) # construct attrset from 2 stack values
```
The operation pushing the key needs to be replaced with one that
leaves a single value (the key) on the stack, i.e. the code for the
expression, e.g.:
```
0000..000n <operations leaving a string value on the stack>
000n+1 OP_CONSTANT(1) # value 42
000n+2 OP_ATTR_SET(1) # construct attrset from 2 stack values
```
This is fairly easy to do by simply recursing in the compiler when
the key expression is encountered.
2. The keys of attribute sets may be nested.
This is the non-trivial part of dealing with attribute set
literals. Specifically, the nesting can be arbitrarily deep and the
AST does not guarantee that related set keys are located
adjacently.
Furthermore, this frequently occurs in practice in Nix. We need a
bytecode representation that makes it possible to construct nested
attribute sets at runtime.
Proposal: AttrPath values
If we can leave a value representing an attribute path on the
stack, we can offload the construction of nested attribute sets to
the `OpAttrSet` operation.
Under the hood, OpAttrSet in practice constructs a `Map<NixString,
Value>` attribute set in most cases. This means it expects to pop
the value of the key of the stack, but is otherwise free to do
whatever it wants with the underlying map.
In a simple example, we could have code like this:
```nix
{
a.b = 15;
}
```
This would be compiled to a new `OpAttrPath` instruction that
constructs and pushes an attribute path from a given number of
fragments (which are popped off the stack).
For example,
```
0000 OP_CONSTANT(0) # key "a"
0001 OP_CONSTANT(1) # key "b"
0002 OP_ATTR_PATH(2) # construct attrpath from 2 fragments
0003 OP_CONSTANT(2) # value 42
0004 OP_ATTRS(1) # construct attrset from one pair
```
Right before `0004` the stack would be left like this:
[ AttrPath[a,b], 42 ]
Inside of the `OP_ATTRS` instruction we could then begin
construction of the map and insert the nested attribute sets as
required, as well as validate that there are no duplicate keys.
3. Both of these cases can occur simultaneously, but this is not a
problem as the opcodes combine perfectly fine, e.g.:
```nix
let
k = "a";
in {
"${k}".b = 42;
}
```
results in
```
0000..000n <operations leaving a string value on the stack>
000n+1 OP_CONSTANT(1) # key "b"
000n+2 OP_ATTR_PATH(2) # construct attrpath from 2 fragments
000n+3 OP_CONSTANT(2) # value 42
000n+4 OP_ATTR_SET(1) # construct attrset from 2 stack values
```