feat(tvix/vm): implement first nested attribute set construction
This can construct non-overlapping nested attribute sets (i.e. `{ a.b = 1; b.c = 2; }`, but not `{ a.b = 1; a.c = 2; }`). In order to do the latter, it's necessary to gain the ability to manipulate the in-progress attribute set construction. There's multiple different options for this ... Change-Id: If1a762a720b175e8eb4216cbf96a7434d22640fb Reviewed-on: https://cl.tvl.fyi/c/depot/+/6106 Tested-by: BuildkiteCI Reviewed-by: grfn <grfn@gws.fyi>
This commit is contained in:
parent
8c2bc683cd
commit
295d6e1d59
2 changed files with 134 additions and 39 deletions
|
@ -6,6 +6,10 @@ pub enum Error {
|
|||
key: String,
|
||||
},
|
||||
|
||||
InvalidKeyType {
|
||||
given: &'static str,
|
||||
},
|
||||
|
||||
TypeError {
|
||||
expected: &'static str,
|
||||
actual: &'static str,
|
||||
|
|
|
@ -17,18 +17,20 @@ pub struct VM {
|
|||
}
|
||||
|
||||
impl VM {
|
||||
fn push(&mut self, value: Value) {
|
||||
self.stack.push(value)
|
||||
}
|
||||
|
||||
fn pop(&mut self) -> Value {
|
||||
self.stack.pop().expect("TODO")
|
||||
fn inc_ip(&mut self) -> OpCode {
|
||||
let op = self.chunk.code[self.ip];
|
||||
self.ip += 1;
|
||||
op
|
||||
}
|
||||
|
||||
fn peek(&self, at: usize) -> &Value {
|
||||
&self.stack[self.stack.len() - 1 - at]
|
||||
}
|
||||
|
||||
fn pop(&mut self) -> Value {
|
||||
self.stack.pop().expect("TODO")
|
||||
}
|
||||
|
||||
fn pop_number_pair(&mut self) -> EvalResult<NumberPair> {
|
||||
let v2 = self.pop();
|
||||
let v1 = self.pop();
|
||||
|
@ -53,10 +55,8 @@ impl VM {
|
|||
}
|
||||
}
|
||||
|
||||
fn inc_ip(&mut self) -> OpCode {
|
||||
let op = self.chunk.code[self.ip];
|
||||
self.ip += 1;
|
||||
op
|
||||
fn push(&mut self, value: Value) {
|
||||
self.stack.push(value)
|
||||
}
|
||||
|
||||
fn run(&mut self) -> EvalResult<Value> {
|
||||
|
@ -132,6 +132,24 @@ impl VM {
|
|||
}
|
||||
}
|
||||
|
||||
// Construct runtime representation of an attr path (essentially
|
||||
// just a list of strings).
|
||||
//
|
||||
// The difference to the list construction operation is that this
|
||||
// forces all elements into strings, as attribute set keys are
|
||||
// required to be strict in Nix.
|
||||
fn run_attr_path(&mut self, count: usize) -> EvalResult<()> {
|
||||
debug_assert!(count > 1, "AttrPath needs at least two fragments");
|
||||
let mut path = Vec::with_capacity(count);
|
||||
|
||||
for _ in 0..count {
|
||||
path.push(self.pop().as_string()?);
|
||||
}
|
||||
|
||||
self.push(Value::AttrPath(path));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_attrset(&mut self, count: usize) -> EvalResult<()> {
|
||||
// If the attribute count happens to be 2, we might be able to
|
||||
// create the optimised name/value struct instead.
|
||||
|
@ -210,33 +228,52 @@ impl VM {
|
|||
|
||||
for _ in 0..count {
|
||||
let value = self.pop();
|
||||
let key = self.pop().as_string()?; // TODO(tazjin): attrpath
|
||||
|
||||
if attrs.insert(key.clone(), value).is_some() {
|
||||
return Err(Error::DuplicateAttrsKey { key: key.0 });
|
||||
// It is at this point that nested attribute sets need to
|
||||
// be constructed (if they exist).
|
||||
//
|
||||
let key = self.pop();
|
||||
match key {
|
||||
Value::String(ks) => {
|
||||
// TODO(tazjin): try_insert (rust#82766) or entry API
|
||||
if attrs.insert(ks.clone(), value).is_some() {
|
||||
return Err(Error::DuplicateAttrsKey { key: ks.0 });
|
||||
}
|
||||
}
|
||||
|
||||
Value::AttrPath(mut path) => {
|
||||
set_nested_attr(
|
||||
&mut attrs,
|
||||
path.pop().expect("AttrPath is never empty"),
|
||||
path,
|
||||
value,
|
||||
)?;
|
||||
}
|
||||
|
||||
other => {
|
||||
return Err(Error::InvalidKeyType {
|
||||
given: other.type_of(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(tazjin): extend_reserve(count) (rust#72631)
|
||||
|
||||
self.push(Value::Attrs(Rc::new(NixAttrs::Map(attrs))));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Construct runtime representation of an attr path (essentially
|
||||
// just a list of strings).
|
||||
//
|
||||
// The difference to the list construction operation is that this
|
||||
// forces all elements into strings, as attribute set keys are
|
||||
// required to be strict in Nix.
|
||||
fn run_attr_path(&mut self, count: usize) -> EvalResult<()> {
|
||||
let mut path = vec![NixString(String::new()); count];
|
||||
// Interpolate string fragments by popping the specified number of
|
||||
// fragments of the stack, evaluating them to strings, and pushing
|
||||
// the concatenated result string back on the stack.
|
||||
fn run_interpolate(&mut self, count: usize) -> EvalResult<()> {
|
||||
let mut out = String::new();
|
||||
|
||||
for idx in 0..count {
|
||||
path[count - idx - 1] = self.pop().as_string()?
|
||||
for _ in 0..count {
|
||||
out.push_str(&self.pop().as_string()?.0);
|
||||
}
|
||||
|
||||
self.push(Value::AttrPath(path));
|
||||
self.push(Value::String(NixString(out)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -254,20 +291,6 @@ impl VM {
|
|||
self.push(Value::List(NixList(list)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Interpolate string fragments by popping the specified number of
|
||||
// fragments of the stack, evaluating them to strings, and pushing
|
||||
// the concatenated result string back on the stack.
|
||||
fn run_interpolate(&mut self, count: usize) -> EvalResult<()> {
|
||||
let mut out = String::new();
|
||||
|
||||
for _ in 0..count {
|
||||
out.push_str(&self.pop().as_string()?.0);
|
||||
}
|
||||
|
||||
self.push(Value::String(NixString(out)));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
|
@ -276,6 +299,74 @@ pub enum NumberPair {
|
|||
Integer(i64, i64),
|
||||
}
|
||||
|
||||
// Set a nested attribute inside of an attribute set, throwing a
|
||||
// duplicate key error if a non-hashmap entry already exists on the
|
||||
// path.
|
||||
//
|
||||
// There is some optimisation potential for this simple implementation
|
||||
// if it becomes a problem.
|
||||
fn set_nested_attr(
|
||||
attrs: &mut BTreeMap<NixString, Value>,
|
||||
key: NixString,
|
||||
mut path: Vec<NixString>,
|
||||
value: Value,
|
||||
) -> EvalResult<()> {
|
||||
let entry = attrs.entry(key);
|
||||
|
||||
// If there is no next key we are at the point where we
|
||||
// should insert the value itself.
|
||||
if path.is_empty() {
|
||||
match entry {
|
||||
std::collections::btree_map::Entry::Occupied(entry) => {
|
||||
return Err(Error::DuplicateAttrsKey {
|
||||
key: entry.key().0.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
std::collections::btree_map::Entry::Vacant(entry) => {
|
||||
entry.insert(value);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// If there is not we go one step further down, in which case we
|
||||
// need to ensure that there either is no entry, or the existing
|
||||
// entry is a hashmap into which to insert the next value.
|
||||
//
|
||||
// If a value of a different type exists, the user specified a
|
||||
// duplicate key.
|
||||
match entry {
|
||||
// Vacant entry -> new attribute set is needed.
|
||||
std::collections::btree_map::Entry::Vacant(entry) => {
|
||||
let mut map = BTreeMap::new();
|
||||
|
||||
// TODO(tazjin): technically recursing further is not
|
||||
// required, we can create the whole hierarchy here, but
|
||||
// it's noisy.
|
||||
set_nested_attr(&mut map, path.pop().expect("next key exists"), path, value)?;
|
||||
|
||||
entry.insert(Value::Attrs(Rc::new(NixAttrs::Map(map))));
|
||||
}
|
||||
|
||||
// Occupied entry: Either error out if there is something
|
||||
// other than attrs, or insert the next value.
|
||||
std::collections::btree_map::Entry::Occupied(mut entry) => match entry.get_mut() {
|
||||
Value::Attrs(_attrs) => {
|
||||
todo!("implement mutable attrsets")
|
||||
}
|
||||
|
||||
_ => {
|
||||
return Err(Error::DuplicateAttrsKey {
|
||||
key: entry.key().0.clone(),
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run_chunk(chunk: Chunk) -> EvalResult<Value> {
|
||||
let mut vm = VM {
|
||||
chunk,
|
||||
|
|
Loading…
Reference in a new issue