diff --git a/Cargo.toml b/Cargo.toml index 35c1bd9..fd18b87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,4 +2,14 @@ members = [ "autonix", + "eval", ] + + +[workspace.dependencies] + +# Nix parser +rnix = "0.11.0" + +# Immutable data structures; allow efficient manipulation of attribute sets +im = "15.1.0" diff --git a/autonix/Cargo.toml b/autonix/Cargo.toml index b3fe132..81415a3 100644 --- a/autonix/Cargo.toml +++ b/autonix/Cargo.toml @@ -4,6 +4,3 @@ version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -rnix = "0.11.0" diff --git a/eval/Cargo.toml b/eval/Cargo.toml new file mode 100644 index 0000000..857c6b6 --- /dev/null +++ b/eval/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "autonix_eval" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rnix.workspace = true +im.workspace = true diff --git a/eval/src/attr_set.rs b/eval/src/attr_set.rs new file mode 100644 index 0000000..f50fcee --- /dev/null +++ b/eval/src/attr_set.rs @@ -0,0 +1,49 @@ +/* +* Copyright (C) 2023 Valentin Lorentz + +* This program is free software: you can redistribute it and/or modify it under the +* terms of the GNU Affero General Public License version 3, as published by the Free +* Software Foundation. +* +* This program is distributed in the hope that it will be useful, but WITHOUT ANY +* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +* PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License along with +* this program. If not, see . +*/ + +/// Type and functions to manipulate attribute sets +use std::ops::Deref; + +use im::HashMap; + +use super::NixValue; + +/// Representation of a Nix attribute set +#[derive(Clone, Debug, Default, PartialEq)] +pub struct AttrSet(HashMap); + +impl AttrSet { + pub fn new(entries: HashMap) -> AttrSet { + AttrSet(entries) + } + + /// Returns the old value, if any. See [`HashMap::insert`] + pub fn insert(&mut self, key: String, value: NixValue) -> Option { + self.0.insert(key, value) + } + + #[inline(always)] + pub fn get_or_insert(&mut self, key: String, value: NixValue) -> &mut NixValue { + self.0.entry(key).or_insert(value) + } +} + +impl Deref for AttrSet { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/eval/src/lib.rs b/eval/src/lib.rs new file mode 100644 index 0000000..d026436 --- /dev/null +++ b/eval/src/lib.rs @@ -0,0 +1,139 @@ +/* +* Copyright (C) 2023 Valentin Lorentz + +* This program is free software: you can redistribute it and/or modify it under the +* terms of the GNU Affero General Public License version 3, as published by the Free +* Software Foundation. +* +* This program is distributed in the hope that it will be useful, but WITHOUT ANY +* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +* PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License along with +* this program. If not, see . +*/ + +use im::HashMap; +use rnix::ast::{Expr, HasEntry}; + +mod attr_set; + +pub use attr_set::AttrSet; + +#[derive(Clone, Debug, PartialEq)] +pub enum NixValue { + String(String), + Integer(u64), + Float(f64), + Path(NixPath), + Boolean(bool), + Null, + List(Vec), + AttrSet(AttrSet), +} + +impl From for NixValue { + fn from(set: AttrSet) -> NixValue { + NixValue::AttrSet(set) + } +} + +#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct NixPath(Vec); + +impl, I: IntoIterator> From for AttrSet { + fn from(items: I) -> AttrSet { + AttrSet::new( + items + .into_iter() + .map(|(k, v)| (k.to_string(), v.into())) + .collect(), + ) + } +} + +/// An [`Evaluator`] that does not allow any side-effect +pub struct PureEvaluator; + +impl Evaluator for PureEvaluator {} + +pub trait Evaluator { + fn eval_expr(&mut self, expr: &Expr) -> NixValue { + match expr { + Expr::AttrSet(attr_set) => self.eval_attrset(attr_set), + Expr::Str(s) => NixValue::String(self.eval_str(s)), + Expr::Select(select) => { + let mut value = self.eval_expr(&select.expr().unwrap()); + assert_eq!(select.default_expr(), None); + for attr in select.attrpath().unwrap().attrs() { + value = match value { + NixValue::AttrSet(attr_set) => attr_set + .get(&self.eval_attr(&attr)) + .expect("No such attribute") + .clone(), + value => panic!("Cannot get attribute from{:?}", value), + }; + } + value + } + + _ => unimplemented!("{:?}", expr), + } + } + + fn eval_attrset(&mut self, attr_set: &rnix::ast::AttrSet) -> NixValue { + let mut attributes = AttrSet::new(HashMap::new()); + + for entry in attr_set.entries() { + match entry { + rnix::ast::Entry::AttrpathValue(attrpathvalue) => { + let attrpath = match attrpathvalue.attrpath() { + Some(attrpath) => attrpath, + None => unimplemented!("attrpathvalue.attrpath() == None"), + }; + let inner_value = match attrpathvalue.value() { + Some(value) => value, + None => unimplemented!("attrpathvalue.value() == None"), + }; + let mut value = &mut attributes; + let mut attrpath: Vec<_> = attrpath.attrs().collect(); + let last_attr = attrpath.pop().expect("Empty path"); + for attr in attrpath { + let attr = self.eval_attr(&attr); + value = match value + .get_or_insert(attr, NixValue::AttrSet(AttrSet::default())) + { + NixValue::AttrSet(attr_set) => attr_set, + value => panic!("Cannot set attribute on {:?}", value), + }; + } + let last_attr = self.eval_attr(&last_attr); + value.insert(last_attr, self.eval_expr(&inner_value)); + } + + _ => unimplemented!("{:?}", entry), + } + } + + NixValue::AttrSet(attributes) + } + + fn eval_attr(&mut self, attr: &rnix::ast::Attr) -> String { + match attr { + rnix::ast::Attr::Str(s) => self.eval_str(&s), + rnix::ast::Attr::Ident(ident) => ident.to_string(), + _ => unimplemented!("{:?}", attr), + } + } + + fn eval_str(&mut self, s: &rnix::ast::Str) -> String { + s.normalized_parts() + .into_iter() + .map(|part| match part { + rnix::ast::InterpolPart::Literal(part) => part, + _ => unimplemented!("{:?}", part), + }) + .collect::>() + .join("") + } +} diff --git a/eval/tests/test_eval.rs b/eval/tests/test_eval.rs new file mode 100644 index 0000000..15459c7 --- /dev/null +++ b/eval/tests/test_eval.rs @@ -0,0 +1,45 @@ +/* +* Copyright (C) 2023 Valentin Lorentz + +* This program is free software: you can redistribute it and/or modify it under the +* terms of the GNU Affero General Public License version 3, as published by the Free +* Software Foundation. +* +* This program is distributed in the hope that it will be useful, but WITHOUT ANY +* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +* PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License along with +* this program. If not, see . +*/ + +use autonix_eval::{AttrSet, Evaluator, NixValue, PureEvaluator}; +use rnix::ast::Expr; + +fn parse(s: &str) -> Expr { + rnix::Root::parse(s).ok().unwrap().expr().unwrap() +} + +#[test] +fn test_attrset() { + assert_eq!( + PureEvaluator.eval_expr(&parse(r#"{ a = "Foo"; b = "Bar"; }"#)), + AttrSet::from(vec![ + ("a", NixValue::String("Foo".to_owned())), + ("b", NixValue::String("Bar".to_owned())) + ]) + .into() + ); +} + +#[test] +fn test_attrset_get() { + assert_eq!( + PureEvaluator.eval_expr(&parse(r#"{ a = "Foo"; b = "Bar"; }.a"#)), + NixValue::String("Foo".to_owned()) + ); + assert_eq!( + PureEvaluator.eval_expr(&parse(r#"{ a = "Foo"; b = "Bar"; }.b"#)), + NixValue::String("Bar".to_owned()) + ); +}