feat(tvix/eval): merge attribute sets in bindings

This is a significant step towards correctly implemented nested
bindings. All attribute sets defined within the same binding scope
will now be merged as in Nix, if they use the same key.

Change-Id: I13e056693d5e73192280043c6dd93b47d1306ed6
Reviewed-on: https://cl.tvl.fyi/c/depot/+/6800
Tested-by: BuildkiteCI
Reviewed-by: sterni <sternenseemann@systemli.org>
This commit is contained in:
Vincent Ambo 2022-09-28 16:26:57 +03:00 committed by tazjin
parent 0cee44838c
commit cece9eae4a
5 changed files with 133 additions and 13 deletions

View file

@ -4,7 +4,7 @@
//! In the case of recursive scopes these cases share almost all of their
//! (fairly complex) logic.
use rnix::ast::AttrpathValue;
use rnix::ast::{AttrpathValue, HasEntry};
use super::*;
@ -27,6 +27,29 @@ impl BindingsKind {
}
}
// Internal representation of an attribute set used for merging sets, or
// inserting nested keys.
#[derive(Clone)]
struct AttributeSet {
/// Original span at which this set was first encountered.
span: Span,
/// Tracks the kind of set (rec or not).
kind: BindingsKind,
/// All inherited entries
inherits: Vec<ast::Inherit>,
/// All internal entries
entries: Vec<ast::AttrpathValue>,
}
impl ToSpan for AttributeSet {
fn span_for(&self, _: &codemap::File) -> Span {
self.span
}
}
// Data structures to track the bindings observed in the second pass, and
// forward the information needed to compile their value.
enum Binding {
@ -39,19 +62,60 @@ enum Binding {
Plain {
expr: ast::Expr,
},
Set(AttributeSet),
}
impl Binding {
fn merge(&mut self, _expr: ast::Expr) -> Option<ErrorKind> {
fn merge(&mut self, c: &mut Compiler, expr: ast::Expr) {
match self {
Binding::InheritFrom { name, .. } => {
Some(ErrorKind::UnmergeableInherit { name: name.clone() })
Binding::InheritFrom { name, ref span, .. } => {
c.emit_error(span, ErrorKind::UnmergeableInherit { name: name.clone() })
}
Binding::Plain { expr } => match expr {
ast::Expr::AttrSet(_) => todo!(),
_ => Some(ErrorKind::UnmergeableValue),
Binding::Plain { expr: existing } => match existing {
ast::Expr::AttrSet(existing) => {
if let ast::Expr::AttrSet(new) = expr {
let merged = AttributeSet {
span: c.span_for(existing),
// Kind of the attrs depends on the first time it is
// encountered. We actually believe this to be a Nix
// bug: https://github.com/NixOS/nix/issues/7111
kind: if existing.rec_token().is_some() {
BindingsKind::RecAttrs
} else {
BindingsKind::Attrs
},
inherits: ast::HasEntry::inherits(existing)
.chain(ast::HasEntry::inherits(&new))
.collect(),
entries: ast::HasEntry::attrpath_values(existing)
.chain(ast::HasEntry::attrpath_values(&new))
.collect(),
};
*self = Binding::Set(merged);
} else {
todo!()
}
}
_ => c.emit_error(&expr, ErrorKind::UnmergeableValue),
},
Binding::Set(existing) => {
if let ast::Expr::AttrSet(new) = expr {
existing.inherits.extend(ast::HasEntry::inherits(&new));
existing
.entries
.extend(ast::HasEntry::attrpath_values(&new));
} else {
todo!()
}
}
}
}
}
@ -133,9 +197,7 @@ impl TrackedBindings {
};
// No more excuses ... the binding can be merged!
if let Some(err) = binding.binding.merge(value) {
c.emit_error(entry, err);
}
binding.binding.merge(c, value);
true
}
@ -150,6 +212,33 @@ impl TrackedBindings {
}
}
/// Wrapper around the `ast::HasEntry` trait as that trait can not be
/// implemented for custom types.
trait HasEntryProxy {
fn inherits(&self) -> Box<dyn Iterator<Item = ast::Inherit>>;
fn attrpath_values(&self) -> Box<dyn Iterator<Item = ast::AttrpathValue>>;
}
impl<N: HasEntry> HasEntryProxy for N {
fn inherits(&self) -> Box<dyn Iterator<Item = ast::Inherit>> {
Box::new(ast::HasEntry::inherits(self))
}
fn attrpath_values(&self) -> Box<dyn Iterator<Item = ast::AttrpathValue>> {
Box::new(ast::HasEntry::attrpath_values(self))
}
}
impl HasEntryProxy for AttributeSet {
fn inherits(&self) -> Box<dyn Iterator<Item = ast::Inherit>> {
Box::new(self.inherits.clone().into_iter())
}
fn attrpath_values(&self) -> Box<dyn Iterator<Item = ast::AttrpathValue>> {
Box::new(self.entries.clone().into_iter())
}
}
/// AST-traversing functions related to bindings.
impl Compiler<'_> {
/// Compile all inherits of a node with entries that do *not* have a
@ -162,7 +251,7 @@ impl Compiler<'_> {
node: &N,
) -> Vec<(ast::Expr, SmolStr, Span)>
where
N: ToSpan + ast::HasEntry,
N: ToSpan + HasEntryProxy,
{
// Pass over all inherits, resolving only those without namespaces.
// Since they always resolve in a higher scope, we can just compile and
@ -309,7 +398,7 @@ impl Compiler<'_> {
bindings: &mut TrackedBindings,
node: &N,
) where
N: ToSpan + ast::HasEntry,
N: ToSpan + HasEntryProxy,
{
for entry in node.attrpath_values() {
if bindings.try_merge(self, &entry) {
@ -450,6 +539,14 @@ impl Compiler<'_> {
// Binding is "just" a plain expression that needs to be
// compiled.
Binding::Plain { expr } => self.compile(binding.value_slot, expr),
// Binding is a merged or nested attribute set, and needs to be
// recursively compiled as another binding.
Binding::Set(set) => self.thunk(binding.value_slot, &set, |c, n, s| {
c.scope_mut().begin_scope();
c.compile_bindings(binding.value_slot, set.kind, &set);
c.scope_mut().end_scope();
}),
}
// Any code after this point will observe the value in the right
@ -469,7 +566,7 @@ impl Compiler<'_> {
fn compile_bindings<N>(&mut self, slot: LocalIdx, kind: BindingsKind, node: &N)
where
N: ToSpan + ast::HasEntry,
N: ToSpan + HasEntryProxy,
{
let mut count = 0;
self.scope_mut().begin_scope();

View file

@ -0,0 +1 @@
{ set = { a = 1; b = 2; }; }

View file

@ -0,0 +1,9 @@
{
set = {
a = 1;
};
set = {
b = 2;
};
}

View file

@ -0,0 +1 @@
{ set = { a = 21; b = 42; }; }

View file

@ -0,0 +1,12 @@
{
set = rec {
a = 21;
};
set = {
# Fun fact: This might be the only case in Nix where a lexical
# resolution of an identifier can only be resolved by looking at
# *siblings* in the AST.
b = 2 * a;
};
}