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:
		
							parent
							
								
									0cee44838c
								
							
						
					
					
						commit
						cece9eae4a
					
				
					 5 changed files with 133 additions and 13 deletions
				
			
		|  | @ -4,7 +4,7 @@ | ||||||
| //! In the case of recursive scopes these cases share almost all of their
 | //! In the case of recursive scopes these cases share almost all of their
 | ||||||
| //! (fairly complex) logic.
 | //! (fairly complex) logic.
 | ||||||
| 
 | 
 | ||||||
| use rnix::ast::AttrpathValue; | use rnix::ast::{AttrpathValue, HasEntry}; | ||||||
| 
 | 
 | ||||||
| use super::*; | 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
 | // Data structures to track the bindings observed in the second pass, and
 | ||||||
| // forward the information needed to compile their value.
 | // forward the information needed to compile their value.
 | ||||||
| enum Binding { | enum Binding { | ||||||
|  | @ -39,19 +62,60 @@ enum Binding { | ||||||
|     Plain { |     Plain { | ||||||
|         expr: ast::Expr, |         expr: ast::Expr, | ||||||
|     }, |     }, | ||||||
|  | 
 | ||||||
|  |     Set(AttributeSet), | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl Binding { | impl Binding { | ||||||
|     fn merge(&mut self, _expr: ast::Expr) -> Option<ErrorKind> { |     fn merge(&mut self, c: &mut Compiler, expr: ast::Expr) { | ||||||
|         match self { |         match self { | ||||||
|             Binding::InheritFrom { name, .. } => { |             Binding::InheritFrom { name, ref span, .. } => { | ||||||
|                 Some(ErrorKind::UnmergeableInherit { name: name.clone() }) |                 c.emit_error(span, ErrorKind::UnmergeableInherit { name: name.clone() }) | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             Binding::Plain { expr } => match expr { |             Binding::Plain { expr: existing } => match existing { | ||||||
|                 ast::Expr::AttrSet(_) => todo!(), |                 ast::Expr::AttrSet(existing) => { | ||||||
|                 _ => Some(ErrorKind::UnmergeableValue), |                     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!
 |         // No more excuses ... the binding can be merged!
 | ||||||
|         if let Some(err) = binding.binding.merge(value) { |         binding.binding.merge(c, value); | ||||||
|             c.emit_error(entry, err); |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         true |         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.
 | /// AST-traversing functions related to bindings.
 | ||||||
| impl Compiler<'_> { | impl Compiler<'_> { | ||||||
|     /// Compile all inherits of a node with entries that do *not* have a
 |     /// Compile all inherits of a node with entries that do *not* have a
 | ||||||
|  | @ -162,7 +251,7 @@ impl Compiler<'_> { | ||||||
|         node: &N, |         node: &N, | ||||||
|     ) -> Vec<(ast::Expr, SmolStr, Span)> |     ) -> Vec<(ast::Expr, SmolStr, Span)> | ||||||
|     where |     where | ||||||
|         N: ToSpan + ast::HasEntry, |         N: ToSpan + HasEntryProxy, | ||||||
|     { |     { | ||||||
|         // Pass over all inherits, resolving only those without namespaces.
 |         // Pass over all inherits, resolving only those without namespaces.
 | ||||||
|         // Since they always resolve in a higher scope, we can just compile and
 |         // Since they always resolve in a higher scope, we can just compile and
 | ||||||
|  | @ -309,7 +398,7 @@ impl Compiler<'_> { | ||||||
|         bindings: &mut TrackedBindings, |         bindings: &mut TrackedBindings, | ||||||
|         node: &N, |         node: &N, | ||||||
|     ) where |     ) where | ||||||
|         N: ToSpan + ast::HasEntry, |         N: ToSpan + HasEntryProxy, | ||||||
|     { |     { | ||||||
|         for entry in node.attrpath_values() { |         for entry in node.attrpath_values() { | ||||||
|             if bindings.try_merge(self, &entry) { |             if bindings.try_merge(self, &entry) { | ||||||
|  | @ -450,6 +539,14 @@ impl Compiler<'_> { | ||||||
|                 // Binding is "just" a plain expression that needs to be
 |                 // Binding is "just" a plain expression that needs to be
 | ||||||
|                 // compiled.
 |                 // compiled.
 | ||||||
|                 Binding::Plain { expr } => self.compile(binding.value_slot, expr), |                 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
 |             // 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) |     fn compile_bindings<N>(&mut self, slot: LocalIdx, kind: BindingsKind, node: &N) | ||||||
|     where |     where | ||||||
|         N: ToSpan + ast::HasEntry, |         N: ToSpan + HasEntryProxy, | ||||||
|     { |     { | ||||||
|         let mut count = 0; |         let mut count = 0; | ||||||
|         self.scope_mut().begin_scope(); |         self.scope_mut().begin_scope(); | ||||||
|  |  | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | { set = { a = 1; b = 2; }; } | ||||||
|  | @ -0,0 +1,9 @@ | ||||||
|  | { | ||||||
|  |   set = { | ||||||
|  |     a = 1; | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   set = { | ||||||
|  |     b = 2; | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | { set = { a = 21; b = 42; }; } | ||||||
|  | @ -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; | ||||||
|  |   }; | ||||||
|  | } | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue