diff --git a/Cargo.toml b/Cargo.toml index 39d5233..42e2b13 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,10 +24,10 @@ rnix = "0.10.2" serde = "1.0.138" serde_json = "1.0.82" nixpkgs-fmt-rnix = "1.2.0" +maplit = "1" [dev-dependencies] stoppable_thread = "0.2.1" -maplit = "1" [features] diff --git a/src/error.rs b/src/error.rs index a03e80a..27b10dc 100644 --- a/src/error.rs +++ b/src/error.rs @@ -79,7 +79,7 @@ pub struct Located { #[unsafe_ignore_trace] pub range: TextRange, /// The nature of the issue - pub kind: T + pub kind: T, } impl std::error::Error for Located {} @@ -117,7 +117,9 @@ impl std::fmt::Display for ValueError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { ValueError::DivisionByZero => write!(f, "division by zero"), - ValueError::AttrAlreadyDefined(name) => write!(f, "attribute `{}` defined more than once", name), + ValueError::AttrAlreadyDefined(name) => { + write!(f, "attribute `{}` defined more than once", name) + } ValueError::TypeError(msg) => write!(f, "{}", msg), ValueError::UnboundIdentifier(name) => write!(f, "identifier {} is unbound", name), } diff --git a/src/eval.rs b/src/eval.rs index 0871c22..0eb0dca 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -8,6 +8,17 @@ use rnix::TextRange; use std::borrow::Borrow; use std::collections::HashMap; +/// A string part +/// +/// in `"foo${bar}baz"`, parts are `Literal("foo")`, `Expression(bar)` and `Literal("baz")` +#[derive(Debug, Trace, Finalize)] +pub enum StringPartSource { + /// Literal string + Literal(String), + /// Interpolated expression + Expression(ExprResultBox), +} + // Expressions like BinOp have the only copy of their Expr children, // so they use ExprResultBox. Expressions like Map, which may have // contents copied in multiple places, need ExprResultGc. @@ -36,6 +47,18 @@ pub enum ExprSource { /// ``` definitions: Vec, }, + LetIn { + /// We use a list because the user might define the same top-level + /// attribute in multiple places via path syntax. For example: + /// ```nix + /// { + /// xyz.foo = true; + /// xyz.bar = false; + /// } + /// ``` + definitions: Vec, + body: ExprResultBox, + }, /// See the AttrSet handling in Expr::parse for more details. /// Note that this syntax is the exact opposite of Expr::Select. KeyValuePair { @@ -50,19 +73,76 @@ pub enum ExprSource { from: ExprResultGc, index: ExprResultBox, }, + /// `assert condition; body` + Assert { + /// the asserted condition + condition: ExprResultBox, + /// the body which is only evaluated if the assertion is true + body: ExprResultBox, + }, /// Dynamic attribute, such as the curly braces in `foo.${toString (1+1)}` Dynamic { inner: ExprResultBox, }, + /// `with inner; body` + With { + /// the expression used to provide bindings + inner: ExprResultGc, + /// the body evaluated with the new bindings + body: ExprResultBox, + }, Ident { name: String, }, Literal { value: NixValue, }, + /// `if condition then body else else_body` + IfElse { + /// the condition evaluated + condition: ExprResultBox, + /// the body evaluated when the condition is true + body: ExprResultBox, + /// the body evaluated when the condition is false + else_body: ExprResultBox, + }, + /// A string, possibly interpolated + String { + /// interpolated and literal parts of this string + parts: Vec, + }, + /// `a.b or c` + OrDefault { + /// `a.b`, of type `Select` + index: ExprResultBox, + /// `c` + default: ExprResultBox, + }, Paren { inner: ExprResultBox, }, + /// `{ arg1, ... } @ args` in a lambda definition `{ arg1, ... } @ args: body` + Pattern { + /// for `{ arg1, arg2 ? default }: body`, a map `"arg1" => None, "arg2" => default` + entries: HashMap>, + /// whether the patter is incomplete (contains `...`) + ellipsis: bool, + /// the identifier bound by `@` + at: Option, + }, + Lambda { + /// A `Pattern` or an `Identifier` + arg: ExprResultBox, + /// the body of the function + body: ExprResultBox, + }, + /// Function application: `f x` + Apply { + /// the function `f` applied + function: ExprResultBox, + /// the argument `x` + arg: ExprResultBox, + }, BinOp { op: BinOpKind, left: ExprResultBox, @@ -86,6 +166,9 @@ pub enum ExprSource { UnaryNegate { value: ExprResultBox, }, + List { + elements: Vec, + }, } /// Syntax node that has context and can be lazily evaluated. @@ -119,7 +202,7 @@ impl Expr { // from an .eval(), which is probably infinite recursion. return Err(EvalError::Internal(InternalError::Unimplemented( "infinite recursion".to_string(), - ))) + ))); } }; if let Some(ref value) = *value_borrow { @@ -135,6 +218,7 @@ impl Expr { fn eval_uncached(&self) -> Result, EvalError> { match &self.source { ExprSource::Paren { inner } => inner.as_ref()?.eval(), + ExprSource::LetIn { body, .. } => body.as_ref()?.eval(), ExprSource::Literal { value } => Ok(Gc::new(value.clone())), ExprSource::BoolAnd { left, right } => { if left.as_ref()?.eval()?.as_bool()? { @@ -236,6 +320,33 @@ impl Expr { } })) } + ExprSource::IfElse { .. } => Err(EvalError::Internal(InternalError::Unimplemented( + "evaluating `if` is not implemented".to_string(), + ))), + ExprSource::Assert { .. } => Err(EvalError::Internal(InternalError::Unimplemented( + "evaluating `assert` operator is not implemented".to_string(), + ))), + ExprSource::OrDefault { .. } => Err(EvalError::Internal(InternalError::Unimplemented( + "evaluating `or` default operator is not implemented".to_string(), + ))), + ExprSource::With { .. } => Err(EvalError::Internal(InternalError::Unimplemented( + "evaluating with expressions is not implemented".to_string(), + ))), + ExprSource::String { .. } => Err(EvalError::Internal(InternalError::Unimplemented( + "evaluating strings is not implemented".to_string(), + ))), + ExprSource::List { .. } => Err(EvalError::Internal(InternalError::Unimplemented( + "evaluating lists is not implemented".to_string(), + ))), + ExprSource::Apply { .. } => Err(EvalError::Internal(InternalError::Unimplemented( + "evaluating function application is not implemented".to_string(), + ))), + ExprSource::Lambda { .. } => Err(EvalError::Internal(InternalError::Unimplemented( + "evaluating function is not implemented".to_string(), + ))), + ExprSource::Pattern { .. } => Err(EvalError::Internal(InternalError::Unimplemented( + "evaluating function argument pattern is not implemented".to_string(), + ))), ExprSource::AttrSet { .. } => Err(EvalError::Internal(InternalError::Unexpected( "eval_uncached ExprSource::Map should be unreachable, ".to_string() + "since the Expr::value should be initialized at creation", @@ -282,9 +393,51 @@ impl Expr { ExprSource::BinOp { op: _, left, right } => vec![left, right], ExprSource::BoolAnd { left, right } => vec![left, right], ExprSource::BoolOr { left, right } => vec![left, right], + ExprSource::IfElse { condition, body, else_body } => vec![condition, body, else_body], + ExprSource::Assert { condition, body } => vec![condition, body], ExprSource::Implication { left, right } => vec![left, right], + ExprSource::OrDefault { index, default } => vec![index, default], ExprSource::UnaryInvert { value } => vec![value], ExprSource::UnaryNegate { value } => vec![value], + ExprSource::Apply { function, arg } => vec![function, arg], + ExprSource::With { inner, body } => { + let mut res = vec![]; + if let Ok(inner) = inner { + res.push(inner.as_ref()) + } + if let Ok(body) = body { + res.push(body.as_ref()) + } + return res + }, + ExprSource::Pattern { entries, .. } => { + let mut res = vec![]; + for entry in entries.values() { + if let Some(Ok(default)) = entry { + res.push(default.as_ref()); + } + } + return res + }, + ExprSource::Lambda { arg, body } => vec![arg, body], + ExprSource::List { elements } => { + let mut res = vec![]; + for entry in elements.iter() { + if let Ok(default) = entry { + res.push(default.as_ref()); + } + } + return res + } + ExprSource::String { parts } => { + let mut res = vec![]; + for entry in parts.iter() { + if let StringPartSource::Expression(Ok(inner)) = entry { + res.push(inner.as_ref()); + } + } + return res + } ExprSource::AttrSet { definitions, } => { @@ -300,6 +453,21 @@ impl Expr { .map(|x| x.as_ref()) .collect(); } + ExprSource::LetIn { + definitions, + body + } => { + let mut out = vec![]; + for def in definitions { + if let Ok(x) = def.as_ref() { + out.push(x.as_ref()) + } + } + if let Ok(b) = body { + out.push(b.as_ref()) + } + return out; + } ExprSource::KeyValuePair { key, value } => { let mut out = vec![]; if let Ok(x) = value { diff --git a/src/parse.rs b/src/parse.rs index e037037..bda242c 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -4,15 +4,16 @@ use std::convert::TryFrom; use std::iter::FromIterator; use crate::error::{EvalError, InternalError, ValueError, ERR_PARSING}; -use crate::eval::merge_set_literal; +use crate::eval::{merge_set_literal, StringPartSource}; use crate::value::*; use crate::{ eval::{Expr, ExprSource}, scope::Scope, }; use gc::{Finalize, Gc, GcCell, Trace}; +use maplit::hashset; use rnix::types::{EntryHolder, TokenWrapper, TypedNode}; -use rnix::TextRange; +use rnix::{TextRange, StrPart, SyntaxKind}; use rnix::{ types::{ParsedType, Wrapper}, SyntaxNode, @@ -36,216 +37,263 @@ pub enum BinOpKind { NotEqual, } -impl Expr { - /// Convert a rnix-parser tree into a syntax tree that can be lazily evaluated. - /// - /// Note that the lsp searches inward from the root of the file, so if a - /// rnix::SyntaxNode isn't recognized, we don't get tooling for its children. - pub fn parse(node: SyntaxNode, scope: Gc) -> Result { - let range = Some(node.text_range()); - let recurse_box = |node| Expr::parse(node, scope.clone()).map(|x| Box::new(x)); - let recurse_gc = |node| Expr::parse(node, scope.clone()).map(|x| Gc::new(x)); - let source = match ParsedType::try_from(node.clone()).map_err(|_| ERR_PARSING)? { - ParsedType::Select(select) => ExprSource::Select { - from: recurse_gc(select.set().ok_or(ERR_PARSING)?), - index: recurse_box(select.index().ok_or(ERR_PARSING)?), - }, - ParsedType::AttrSet(set) => { - let is_recursive = set.recursive(); - - // Create a new scope if we're a recursive attr set. We'll later - // populate this scope with the non-dynamic keys of the set. - let new_scope = if is_recursive { - let new = Scope::Let { - parent: scope.clone(), - contents: GcCell::new(HashMap::new()), - }; - Gc::new(new) - } else { - scope.clone() - }; +/// Parse a node with `key = value;` pairs like an attrset or let in +/// +/// Returns the scope unchanged if is_recursive is false or a new one with the `key = value;` +/// bindings added. +fn parse_entry_holder( + node: &T, + scope: Gc, + is_recursive: bool, +) -> Result< + ( + HashMap>, + Vec, EvalError>>, + Gc, + ), + EvalError, +> { + // Create a new scope if we're a recursive attr set. We'll later + // populate this scope with the non-dynamic keys of the set. + let new_scope = if is_recursive { + let new = Scope::Let { + parent: scope.clone(), + contents: GcCell::new(HashMap::new()), + }; + Gc::new(new) + } else { + scope.clone() + }; - // Used for the NixValue of this attribute set. - let mut value_map = HashMap::new(); - // Used for the ExprSource. See ExprSource::AttrSet for - // details on why we create both a hashmap and a vector. - let mut definitions = vec![]; + // Used for the NixValue of this attribute set. + let mut value_map = HashMap::new(); + // Used for the ExprSource. See ExprSource::AttrSet for + // details on why we create both a hashmap and a vector. + let mut definitions = vec![]; - for entry in set.entries() { - // Where x, y, z are KeyValuePairs: - // - // services.bluetooth.enable = true; - // +------------+ x - // +----------------------+ y - // +-------------------------------+ z - // - // Hovering over `x` should show `true`. - // Hovering over `y` should show `{ enable }`. - // Hovering over `z` should show `{ bluetooth }`. - // - // This matches what we would see for verbose syntax: - // - // services = { bluetooth = { enable = true; }; }; - // - // So, we rewrite paths into the verbose syntax. + for entry in node.entries() { + // Where x, y, z are KeyValuePairs: + // + // services.bluetooth.enable = true; + // +------------+ x + // +----------------------+ y + // +-------------------------------+ z + // + // Hovering over `x` should show `true`. + // Hovering over `y` should show `{ enable }`. + // Hovering over `z` should show `{ bluetooth }`. + // + // This matches what we would see for verbose syntax: + // + // services = { bluetooth = { enable = true; }; }; + // + // So, we rewrite paths into the verbose syntax. - let mut path = entry - .key() - .ok_or(ERR_PARSING)? - .path() - .map(|node| Self::parse(node, scope.clone()).map(Gc::new)) - .collect::>(); + let mut path = entry + .key() + .ok_or(ERR_PARSING)? + .path() + .map(|node| Expr::parse(node, scope.clone()).map(Gc::new)) + .collect::>(); - // NOTE: This pops from the end, so we want to remove - // the inmost element before reversing - let inmost_key = path.pop().unwrap()?; + // NOTE: This pops from the end, so we want to remove + // the inmost element before reversing + let inmost_key = path.pop().unwrap()?; - // After this, our path lists path elements from right to left - path.reverse(); + // After this, our path lists path elements from right to left + path.reverse(); - let inmost_value_syntax = entry.value().ok_or(ERR_PARSING)?; - let entry_end = inmost_value_syntax.text_range().end(); - let inmost_value = Self::parse(inmost_value_syntax, new_scope.clone())?; + let inmost_value_syntax = entry.value().ok_or(ERR_PARSING)?; + let entry_end = inmost_value_syntax.text_range().end(); + let inmost_value = Expr::parse(inmost_value_syntax, new_scope.clone())?; - let here_start = inmost_key.range.ok_or(ERR_PARSING)?.start(); + let here_start = inmost_key.range.ok_or(ERR_PARSING)?.start(); - let mut cursor_range = TextRange::new(here_start, entry_end); - let mut cursor_key_name = inmost_key.as_ident()?; - let mut cursor_value = Gc::new(Expr { - value: GcCell::new(None), - source: ExprSource::KeyValuePair { - key: Ok(inmost_key), - value: Ok(Gc::new(inmost_value)), - }, - range: Some(cursor_range), - scope: new_scope.clone(), - }); + let mut cursor_range = TextRange::new(here_start, entry_end); + let mut cursor_key_name = inmost_key.as_ident()?; + let mut cursor_value = Gc::new(Expr { + value: GcCell::new(None), + source: ExprSource::KeyValuePair { + key: Ok(inmost_key), + value: Ok(Gc::new(inmost_value)), + }, + range: Some(cursor_range), + scope: new_scope.clone(), + }); - for element in path { - let here_start = element.as_ref()?.range.ok_or(ERR_PARSING)?.start(); + for element in path { + let here_start = element.as_ref()?.range.ok_or(ERR_PARSING)?.start(); - // Create an invisible attr set - let tmp_map = NixValue::Map(HashMap::from_iter(vec![( - cursor_key_name, - cursor_value.clone(), - )])); - let tmp_attr_set = Gc::new(Expr { - value: GcCell::new(Some(Gc::new(tmp_map))), - source: ExprSource::AttrSet { - definitions: vec![Ok(cursor_value)], - }, - range: Some(cursor_range), - scope: new_scope.clone(), - }); + // Create an invisible attr set + let tmp_map = NixValue::Map(HashMap::from_iter(vec![( + cursor_key_name, + cursor_value.clone(), + )])); + let tmp_attr_set = Gc::new(Expr { + value: GcCell::new(Some(Gc::new(tmp_map))), + source: ExprSource::AttrSet { + definitions: vec![Ok(cursor_value)], + }, + range: Some(cursor_range), + scope: new_scope.clone(), + }); - cursor_range = TextRange::new(here_start, entry_end); - cursor_key_name = element.as_ref()?.as_ident()?; - cursor_value = Gc::new(Expr { - value: GcCell::new(None), - source: ExprSource::KeyValuePair { - key: element, - value: Ok(tmp_attr_set), - }, - range: Some(cursor_range.clone()), - scope: new_scope.clone(), - }); - } + cursor_range = TextRange::new(here_start, entry_end); + cursor_key_name = element.as_ref()?.as_ident()?; + cursor_value = Gc::new(Expr { + value: GcCell::new(None), + source: ExprSource::KeyValuePair { + key: element, + value: Ok(tmp_attr_set), + }, + range: Some(cursor_range.clone()), + scope: new_scope.clone(), + }); + } - definitions.push(Ok(cursor_value.clone())); + definitions.push(Ok(cursor_value.clone())); - // Merge values if needed. For example: - // { a.b = 1; a.c = 2; } => { a = { b = 1; c = 2; }; } - let merged_value = match value_map.get(&cursor_key_name) as Option<&Gc> { - Some(existing) => merge_set_literal( - cursor_key_name.clone(), - existing.clone(), - cursor_value.clone(), - )?, - None => cursor_value, - }; - value_map.insert(cursor_key_name, merged_value); - } + // Merge values if needed. For example: + // { a.b = 1; a.c = 2; } => { a = { b = 1; c = 2; }; } + let merged_value = match value_map.get(&cursor_key_name) as Option<&Gc> { + Some(existing) => merge_set_literal( + cursor_key_name.clone(), + existing.clone(), + cursor_value.clone(), + )?, + None => cursor_value, + }; + value_map.insert(cursor_key_name, merged_value); + } - use std::collections::hash_map::Entry; + use std::collections::hash_map::Entry; - // Note that we don't query the scope yet, since that would - // cause expressions like `with pkgs; { inherit htop; }` to - // evaluate the `with` statement earlier than needed. Instead - // we create ExprSource::Ident and ExprSource::Select expressions - // then put those in the attribute set. - for inherit in set.inherits() { - // Handle syntax like `inherit (some_expression) foo` by - // rewriting it to `foo = some_expression.foo`, allowing - // `some_expression` to be lazily evaluated. - if let Some(from) = inherit.from() { - let from = Gc::new(Self::parse( - from.inner().ok_or(ERR_PARSING)?, - new_scope.clone(), - )?); + // Note that we don't query the scope yet, since that would + // cause expressions like `with pkgs; { inherit htop; }` to + // evaluate the `with` statement earlier than needed. Instead + // we create ExprSource::Ident and ExprSource::Select expressions + // then put those in the attribute set. + for inherit in node.inherits() { + // Handle syntax like `inherit (some_expression) foo` by + // rewriting it to `foo = some_expression.foo`, allowing + // `some_expression` to be lazily evaluated. + if let Some(from) = inherit.from() { + let from = Gc::new(Expr::parse( + from.inner().ok_or(ERR_PARSING)?, + new_scope.clone(), + )?); - // For our example described above, add `some_expression`, - // `foo`, and `bar` to the ExprSource so they're all visible - // to interactive tooling. - definitions.push(Ok(from.clone())); + // For our example described above, add `some_expression`, + // `foo`, and `bar` to the ExprSource so they're all visible + // to interactive tooling. + definitions.push(Ok(from.clone())); - for ident in inherit.idents() { - let name = ident.as_str(); - let index = Box::new(Expr { - value: GcCell::new(None), - source: ExprSource::Ident { - name: name.to_string(), - }, - range: None, - scope: scope.clone(), - }); - let attr = Gc::new(Expr { - value: GcCell::new(None), - source: ExprSource::Select { - from: Ok(from.clone()), - index: Ok(index), - }, - range: Some(ident.node().text_range()), - scope: scope.clone(), - }); - definitions.push(Ok(attr.clone())); - let name = name.to_string(); - match value_map.entry(name.clone()) { - Entry::Occupied(_) => { - return Err(EvalError::Value(ValueError::AttrAlreadyDefined(name))) - } - Entry::Vacant(entry) => entry.insert(attr), - }; - } - } else { - // Handle `inherit` from scope - for ident in inherit.idents() { - let name = ident.as_str(); - let attr = Gc::new(Expr { - value: GcCell::new(None), - source: ExprSource::Ident { - name: name.to_string(), - }, - range: Some(ident.node().text_range()), - scope: scope.clone(), - }); - definitions.push(Ok(attr.clone())); - let name = name.to_string(); - match value_map.entry(name.clone()) { - Entry::Occupied(_) => { - return Err(EvalError::Value(ValueError::AttrAlreadyDefined(name))) - } - Entry::Vacant(entry) => entry.insert(attr), - }; - } + for ident in inherit.idents() { + let name = ident.as_str(); + let index = Box::new(Expr { + value: GcCell::new(None), + source: ExprSource::Ident { + name: name.to_string(), + }, + range: None, + scope: scope.clone(), + }); + let attr = Gc::new(Expr { + value: GcCell::new(None), + source: ExprSource::Select { + from: Ok(from.clone()), + index: Ok(index), + }, + range: Some(ident.node().text_range()), + scope: scope.clone(), + }); + definitions.push(Ok(attr.clone())); + let name = name.to_string(); + match value_map.entry(name.clone()) { + Entry::Occupied(_) => { + return Err(EvalError::Value(ValueError::AttrAlreadyDefined(name))) } - } - - if is_recursive { - // update the scope to include our hashmap - if let Scope::Let { contents, .. } = new_scope.borrow() { - *contents.borrow_mut() = value_map.clone(); + Entry::Vacant(entry) => entry.insert(attr), + }; + } + } else { + // Handle `inherit` from scope + for ident in inherit.idents() { + let name = ident.as_str(); + let attr = Gc::new(Expr { + value: GcCell::new(None), + source: ExprSource::Ident { + name: name.to_string(), + }, + range: Some(ident.node().text_range()), + scope: scope.clone(), + }); + definitions.push(Ok(attr.clone())); + let name = name.to_string(); + match value_map.entry(name.clone()) { + Entry::Occupied(_) => { + return Err(EvalError::Value(ValueError::AttrAlreadyDefined(name))) } - } + Entry::Vacant(entry) => entry.insert(attr), + }; + } + } + } + + if is_recursive { + // update the scope to include our hashmap + if let Scope::Let { contents, .. } = new_scope.borrow() { + *contents.borrow_mut() = value_map.clone(); + } + } + Ok((value_map, definitions, new_scope)) +} + +impl Expr { + /// Convert a rnix-parser tree into a syntax tree that can be lazily evaluated. + /// + /// Note that the lsp searches inward from the root of the file, so if a + /// rnix::SyntaxNode isn't recognized, we don't get tooling for its children. + pub fn parse(node: SyntaxNode, scope: Gc) -> Result { + let range = Some(node.text_range()); + let recurse_option_box = |node| match node { + None => Err(ERR_PARSING), + Some(node) => Expr::parse(node, scope.clone()).map(|x| Box::new(x)), + }; + let recurse_option_gc = |node| match node { + None => Err(ERR_PARSING), + Some(node) => Expr::parse(node, scope.clone()).map(|x| Gc::new(x)), + }; + let recurse_box = |node| Expr::parse(node, scope.clone()).map(|x| Box::new(x)); + let recurse_gc = |node| Expr::parse(node, scope.clone()).map(|x| Gc::new(x)); + let process_select = |select: &rnix::types::Select, scope| { + let source = ExprSource::Select { + from: recurse_option_gc(select.set()), + index: recurse_option_box(select.index()), + }; + Ok(Self { + value: GcCell::new(None), + source, + range, + scope, + }) + }; + let source = match ParsedType::try_from(node.clone()).map_err(|_| ERR_PARSING)? { + ParsedType::Select(select) => { + return process_select(&select, scope.clone()); + } + ParsedType::OrDefault(or) => ExprSource::OrDefault { + default: recurse_option_box(or.default()), + index: match or.index() { + None => Err(ERR_PARSING), + Some(s) => process_select(&s, scope.clone()).map(Box::new), + }, + }, + ParsedType::AttrSet(set) => { + let is_recursive = set.recursive(); + + let (value_map, definitions, new_scope) = + parse_entry_holder(&set, scope, is_recursive)?; return Ok(Expr { value: GcCell::new(Some(Gc::new(NixValue::Map(value_map)))), @@ -254,16 +302,13 @@ impl Expr { scope: new_scope, }); } - ParsedType::Paren(paren) => { - let inner = paren.inner().ok_or(ERR_PARSING)?; - ExprSource::Paren { - inner: recurse_box(inner), - } - } + ParsedType::Paren(paren) => ExprSource::Paren { + inner: recurse_option_box(paren.inner()), + }, ParsedType::BinOp(binop) => { use rnix::types::BinOpKind::*; - let left = recurse_box(binop.lhs().ok_or(ERR_PARSING)?); - let right = recurse_box(binop.rhs().ok_or(ERR_PARSING)?); + let left = recurse_option_box(binop.lhs()); + let right = recurse_option_box(binop.rhs()); macro_rules! binop_source { ( $op:expr ) => { ExprSource::BinOp { @@ -300,10 +345,10 @@ impl Expr { use rnix::types::UnaryOpKind; match unary.operator() { UnaryOpKind::Invert => ExprSource::UnaryInvert { - value: recurse_box(unary.value().ok_or(ERR_PARSING)?), + value: recurse_option_box(unary.value()), }, UnaryOpKind::Negate => ExprSource::UnaryNegate { - value: recurse_box(unary.value().ok_or(ERR_PARSING)?), + value: recurse_option_box(unary.value()), }, } } @@ -313,7 +358,7 @@ impl Expr { } } ParsedType::Dynamic(dynamic) => ExprSource::Dynamic { - inner: recurse_box(dynamic.inner().ok_or(ERR_PARSING)?), + inner: recurse_option_box(dynamic.inner()), }, ParsedType::Value(literal) => { use rnix::value::Value::*; @@ -335,7 +380,148 @@ impl Expr { }, } } - node => { + ParsedType::LetIn(letin) => { + let (_value_map, definitions, new_scope) = + parse_entry_holder(&letin, scope.clone(), true)?; + let body = letin.body().ok_or(ERR_PARSING)?; + let body_source = Expr::parse(body, new_scope.clone()).map(Box::new); + return Ok(Expr { + value: GcCell::new(None), + source: ExprSource::LetIn { + definitions, + body: body_source, + }, + range, + scope: new_scope, + }); + } + ParsedType::Apply(apply) => ExprSource::Apply { + function: recurse_option_box(apply.lambda()), + arg: recurse_option_box(apply.value()), + }, + ParsedType::Pattern(pattern) => { + let mut names = std::collections::HashSet::new(); + let at = match pattern.at() { + None => None, + Some(at) => { + let string = at.as_str().to_string(); + names.insert(string.clone()); + Some(recurse_box(at.node().clone())) + } + }; + for entry in pattern.entries() { + match entry.name() { + None => { + return Err(EvalError::Internal(InternalError::Unimplemented("none name for pattern entry".to_string()))); + } + Some(name) => { + names.insert(name.as_str().to_string()); + }, + } + } + let new_scope = Gc::new(Scope::FunctionArguments { + parent: scope.clone(), + names: GcCell::new(names), + }); + let mut entries = HashMap::new(); + for entry in pattern.entries() { + if let Some(name) = entry.name() { + let default = entry.default().map(|default| { + Expr::parse(default, new_scope.clone()).map(|x| Gc::new(x)) + }); + if entries.insert(name.as_str().to_string(), default).is_some() { + return Err(EvalError::Value(ValueError::AttrAlreadyDefined(format!( + "function has duplicate formal argument {}", + name.as_str() + )))); + } + } + } + return Ok(Expr { + value: GcCell::new(None), + source: ExprSource::Pattern { + entries, + ellipsis: pattern.ellipsis(), + at, + }, + range, + scope: new_scope, + }); + } + ParsedType::Lambda(fun) => { + let arg = recurse_box(fun.arg().ok_or(ERR_PARSING)?)?; + let new_scope = match &arg.source { + ExprSource::Ident { name } => { + Gc::new(Scope::FunctionArguments { + parent: scope.clone(), + names: GcCell::new(hashset!{ name.as_str().to_string() }), + }) + }, + ExprSource::Pattern { .. } => arg.scope.clone(), + _ => return Err(ERR_PARSING), + }; + let body = + Expr::parse(fun.body().ok_or(ERR_PARSING)?, new_scope.clone()).map(Box::new); + ExprSource::Lambda { arg: Ok(arg), body } + } + ParsedType::List(list) => ExprSource::List { + elements: list.items().map(recurse_gc).collect(), + }, + ParsedType::With(with) => { + let inner = recurse_gc(with.namespace().ok_or(ERR_PARSING)?)?; + let new_scope = Gc::new(Scope::With { + parent: scope.clone(), + contents: inner.clone(), + }); + let body = + Expr::parse(with.body().ok_or(ERR_PARSING)?, new_scope.clone()).map(Box::new); + ExprSource::With { + inner: Ok(inner), + body, + } + } + ParsedType::Str(string) => { + let mut parts = Vec::new(); + for part in string.parts() { + match part { + StrPart::Literal(l) => { + parts.push(StringPartSource::Literal(l)); + }, + StrPart::Ast(node) => { + debug_assert_eq!(node.kind(), SyntaxKind::NODE_STRING_INTERPOL); + // I only expect one child, but just in case... + for child in node.children() { + parts.push(StringPartSource::Expression(recurse_box(child))); + } + } + }; + } + ExprSource::String { parts } + } + ParsedType::Assert(assert) => { + ExprSource::Assert { + body: recurse_option_box(assert.body()), + condition: recurse_option_box(assert.condition()), + } + }, + ParsedType::IfElse(ifelse) => { + ExprSource::IfElse { + body: recurse_option_box(ifelse.body()), + else_body: recurse_option_box(ifelse.else_body()), + condition: recurse_option_box(ifelse.condition()), + } + }, + ParsedType::Key(_) | ParsedType::KeyValue(_) + | ParsedType::Inherit(_) | ParsedType::InheritFrom(_) + | ParsedType::PatBind(_) | ParsedType::PatEntry(_) => { + // keys are handled in KeyValuePair + // inherit in let in/attrset + // patterns in lambda + return Err(EvalError::Internal(InternalError::Unexpected(format!("this kind of node {:?} should have been handled as part of another node type", node)))) + } + ParsedType::Error(_) => return Err(ERR_PARSING), + ParsedType::PathWithInterpol(_) | + ParsedType::LegacyLet(_) | ParsedType::Root(_) => { return Err(EvalError::Internal(InternalError::Unimplemented(format!( "rnix-parser node {:?}", node diff --git a/src/scope.rs b/src/scope.rs index 32c56f1..37e1b6c 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -1,6 +1,7 @@ use crate::eval::{Expr, ExprSource}; use crate::value::NixValue; use gc::{Finalize, Gc, GcCell, Trace}; +use std::collections::HashSet; use std::{collections::HashMap, path::PathBuf}; /// A parent Expr's scope is used to provide tooling for its child Exprs. @@ -25,9 +26,40 @@ pub enum Scope { parent: Gc, contents: GcCell>>, }, + /// Binding introduced as a function argument or function argument pattern + FunctionArguments { + parent: Gc, + /// names bound + names: GcCell>, + }, + /// Binding introduced by `with` + With { + /// parent scope + /// + /// This is a synctatical relation: bindings introduced by with are always resolved after + /// other bindings, so this scope might be shadowed by its parent. + parent: Gc, + /// the expression `e` in `with e;` + contents: Gc, + }, None, } +/// How a binding might be defined +#[derive(Debug, Clone)] +pub enum Definition { + /// Might be either unbound or bound dynamically by `with` + PossiblyDynamic, + /// Here is the expression that defines this variable + Expression(Gc), + /// this variable is a formal argument of a function + Argument, + /// this variable is definitely not defined + Unbound, + /// builtins + Ambient, +} + impl Scope { /// Finds the Expr of an identifier in the scope. /// @@ -52,26 +84,160 @@ impl Scope { } pub fn get_let(&self, name: &str) -> Option> { - match self { - Scope::None | Scope::Root(_) => Some(Gc::new(Expr { - range: None, - value: GcCell::new(None), - source: ExprSource::Literal { - // TODO: add more keys here, such as `builtins` - value: match name { - "true" => NixValue::Bool(true), - "false" => NixValue::Bool(false), - "null" => NixValue::Null, - _ => return None, + match self.get_definition(name) { + Definition::Expression(x) => Some(x), + _ => None, + } + } + + /// how/if this name is defined + pub fn get_definition(&self, name: &str) -> Definition { + fn get_definition_rec(scope: &Scope, name: &str, possibly_dynamic: bool) -> Definition { + match scope { + Scope::None | Scope::Root(_) => Definition::Expression(Gc::new(Expr { + range: None, + value: GcCell::new(None), + source: ExprSource::Literal { + // TODO: add more keys here, such as `builtins` + value: match name { + "true" => NixValue::Bool(true), + "false" => NixValue::Bool(false), + "null" => NixValue::Null, + // list obtained with `nix repl` tab completion + "abort" + | "__add" + | "__addErrorContext" + | "__all" + | "__any" + | "__appendContext" + | "__attrNames" + | "__attrValues" + | "baseNameOf" + | "__bitAnd" + | "__bitOr" + | "__bitXor" + | "builtins" + | "__catAttrs" + | "__ceil" + | "__compareVersions" + | "__concatLists" + | "__concatMap" + | "__concatStringsSep" + | "__currentSystem" + | "__currentTime" + | "__deepSeq" + | "derivation" + | "derivationStrict" + | "dirOf" + | "__div" + | "__elem" + | "__elemAt" + | "fetchGit" + | "fetchMercurial" + | "fetchTarball" + | "fetchTree" + | "__fetchurl" + | "__filter" + | "__filterSource" + | "__findFile" + | "__floor" + | "__foldl'" + | "__fromJSON" + | "fromTOML" + | "__functionArgs" + | "__genericClosure" + | "__genList" + | "__getAttr" + | "__getContext" + | "__getEnv" + | "__groupBy" + | "__hasAttr" + | "__hasContext" + | "__hashFile" + | "__hashString" + | "__head" + | "import" + | "__intersectAttrs" + | "__isAttrs" + | "__isBool" + | "__isFloat" + | "__isFunction" + | "__isInt" + | "__isList" + | "isNull" + | "__isPath" + | "__isString" + | "__langVersion" + | "__length" + | "__lessThan" + | "__listToAttrs" + | "map" + | "__mapAttrs" + | "__match" + | "__mul" + | "__nixPath" + | "__nixVersion" + | "__parseDrvName" + | "__partition" + | "__path" + | "__pathExists" + | "placeholder" + | "__readDir" + | "__readFile" + | "removeAttrs" + | "__replaceStrings" + | "scopedImport" + | "__seq" + | "__sort" + | "__split" + | "__splitVersion" + | "__storeDir" + | "__storePath" + | "__stringLength" + | "__sub" + | "__substring" + | "__tail" + | "throw" + | "__toFile" + | "__toJSON" + | "__toPath" + | "toString" + | "__toXML" + | "__tryEval" + | "__typeOf" + | "__unsafeDiscardOutputDependency" + | "__unsafeDiscardStringContext" + | "__unsafeGetAttrPos" + | "__zipAttrsWith" => return Definition::Ambient, + _ => { + return if possibly_dynamic { + Definition::PossiblyDynamic + } else { + Definition::Unbound + } + } + }, }, + scope: Gc::new(Scope::None), + })), + Scope::Let { parent, contents } => match contents.borrow().get(name) { + Some(x) => Definition::Expression(x.clone()), + None => get_definition_rec(parent, name, possibly_dynamic), + }, + Scope::FunctionArguments { parent, names } => match names.borrow().get(name) { + Some(_) => Definition::Argument, + None => get_definition_rec(parent, name, possibly_dynamic), }, - scope: Gc::new(Scope::None), - })), - Scope::Let { parent, contents } => match contents.borrow().get(name) { - Some(x) => Some(x.clone()), - None => parent.get_let(name), - }, + Scope::With { + parent, + contents: _, + } => { + // trying to evaluate the content is not yet implemented + get_definition_rec(parent, name, true) + } + } } + get_definition_rec(&self, name, false) } pub fn root_path(&self) -> Option { @@ -79,6 +245,8 @@ impl Scope { Scope::None => None, Scope::Root(path) => Some(path.clone()), Scope::Let { parent, .. } => parent.root_path(), + Scope::With { parent, .. } => parent.root_path(), + Scope::FunctionArguments { parent, .. } => parent.root_path(), } } } diff --git a/src/static_analysis.rs b/src/static_analysis.rs index 2a1853e..69bfd7c 100644 --- a/src/static_analysis.rs +++ b/src/static_analysis.rs @@ -4,7 +4,7 @@ use rnix::TextRange; use crate::{ error::{EvalError, Located, ValueError}, - eval::{Expr, ExprSource}, + eval::{Expr, ExprSource, StringPartSource}, scope::Definition, }; /// If this node is an identifier, should it be a variable name ? @@ -50,6 +50,12 @@ fn visit(acc: &mut Vec>, node: &Expr, ident_ctx: Ident) { visit_result(acc, i, &node.range, IsVariable) } } + ExprSource::LetIn { definitions, body } => { + for i in definitions.iter() { + visit_result(acc, i, &node.range, IsVariable) + } + visit_result(acc, body, &node.range, IsVariable); + } ExprSource::KeyValuePair { key, value } => { visit_result(acc, key, &node.range, IsNotVariable); visit_result(acc, value, &node.range, IsVariable); @@ -58,11 +64,51 @@ fn visit(acc: &mut Vec>, node: &Expr, ident_ctx: Ident) { visit_result(acc, from, &node.range, IsVariable); visit_result(acc, index, &node.range, IsNotVariable); } + ExprSource::Lambda { arg, body } => { + visit_result(acc, body, &node.range, IsVariable); + visit_result(acc, arg, &node.range, IsNotVariable); + } + ExprSource::OrDefault { index, default } => { + visit_result(acc, index, &node.range, IsVariable); + visit_result(acc, default, &node.range, IsVariable); + } + ExprSource::With { inner, body } => { + visit_result(acc, body, &node.range, IsVariable); + visit_result(acc, inner, &node.range, IsVariable); + } + ExprSource::Assert { condition, body } => { + visit_result(acc, body, &node.range, IsVariable); + visit_result(acc, condition, &node.range, IsVariable); + } + ExprSource::IfElse { condition, body, else_body } => { + visit_result(acc, body, &node.range, IsVariable); + visit_result(acc, else_body, &node.range, IsVariable); + visit_result(acc, condition, &node.range, IsVariable); + } + ExprSource::Pattern { entries, .. } => { + for i in entries.values() { + if let Some(default) = i { + visit_result(acc, default, &node.range, IsVariable); + } + } + }, + ExprSource::String { parts } => { + for i in parts.iter() { + if let StringPartSource::Expression(expr) = i { + visit_result(acc, expr, &node.range, IsVariable); + } + } + }, + ExprSource::List { elements } => { + for i in elements.iter() { + visit_result(acc, i, &node.range, IsVariable); + } + }, ExprSource::Ident { name } => { if ident_ctx == IsVariable { if let Some(range) = &node.range { // check that the variable is bound - if node.scope.get_let(name).is_none() { + if matches!(node.scope.get_definition(name), Definition::Unbound) { acc.push(Located { range: range.clone(), kind: ValueError::UnboundIdentifier(name.clone()), @@ -78,6 +124,7 @@ fn visit(acc: &mut Vec>, node: &Expr, ident_ctx: Ident) { | ExprSource::Paren { inner } => visit_result(acc, inner, &node.range, IsVariable), ExprSource::BinOp { left, right, .. } | ExprSource::BoolAnd { left, right } + | ExprSource::Apply { function: left, arg: right } | ExprSource::Implication { left, right } | ExprSource::BoolOr { left, right } => { visit_result(acc, left, &node.range, IsVariable); diff --git a/src/tests.rs b/src/tests.rs index 84b887c..ce8ae05 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -67,6 +67,18 @@ fn order_of_operations() { assert_eq!(eval(code).as_int().unwrap(), 7); } +#[test] +fn eval_let() { + let code = "let x = 1; in x"; + assert_eq!(eval(code).as_int().unwrap(), 1); +} + +#[test] +fn eval_let_sequential() { + let code = "let x = 1; y = x; in x + y"; + assert_eq!(eval(code).as_int().unwrap(), 2); +} + #[test] fn div_int_by_float() { let code = "1 / 2.0"; @@ -94,7 +106,221 @@ fn binding_analysis_ignores_attrset_selection() { #[test] fn unbound_attrset() { let code = "1 + rec { x = 1; y = x; z = t; }.y"; - assert_eq!(static_analysis(code), hashmap!{"t" => "identifier t is unbound".into()}); + assert_eq!( + static_analysis(code), + hashmap! {"t" => "identifier t is unbound".into()} + ); +} + +#[test] +fn unbound_list() { + let code = "[ 1 t ]"; + assert_eq!( + static_analysis(code), + hashmap! {"t" => "identifier t is unbound".into()} + ); +} + +#[test] +fn bound_let() { + let code = "let x = 1; y = x; in x + y"; + assert_eq!(static_analysis(code), hashmap! {}); +} + +#[test] +fn bound_builtins() { + let code = "map"; + assert_eq!(static_analysis(code), hashmap! {}); +} + +#[test] +fn unbound_let_body() { + let code = "let x = 1; in x + y"; + assert_eq!( + static_analysis(code), + hashmap! {"y" => "identifier y is unbound".into()} + ); +} + +#[test] +fn unbound_let_defs() { + let code = "let x = t; y = u; in x + y"; + assert_eq!( + static_analysis(code), + hashmap! {"t" => "identifier t is unbound".into(), "u" => "identifier u is unbound".into()} + ); +} + +#[test] +fn bound_let_fixpoint() { + let code = "let x = x; in x"; + assert_eq!(static_analysis(code), hashmap! {}); +} + +#[test] +fn bound_let_recursive() { + let code = "let y = x; x = 1; in x - y"; + assert_eq!(static_analysis(code), hashmap! {}); +} + +#[test] +fn unbound_function_body() { + let code = "let f = x: x + y; in f 1"; + assert_eq!(static_analysis(code), hashmap! {"y" => "identifier y is unbound".into()}); +} + +#[test] +fn unbound_function_body_pattern() { + let code = "let f = {x}: x + y; in f { x = 1; }"; + assert_eq!(static_analysis(code), hashmap! {"y" => "identifier y is unbound".into()}); +} + +#[test] +fn unbound_function_pattern_default() { + let code = "let x = 1; f = {y ? x + z }: x + y; in f {}"; + assert_eq!(static_analysis(code), hashmap! {"z" => "identifier z is unbound".into()}); +} + +#[test] +fn bound_function_pattern_default_seq() { + let code = "let x = 1; f = {y ? x, z ? y }: x + y + z; in f {}"; + assert_eq!(static_analysis(code), hashmap! {}); +} + +#[test] +fn bound_function_at() { + let code = "let f = {y ? args, ... }@args: { inherit args y; }; in f {}"; + assert_eq!(static_analysis(code), hashmap! {}); +} + +#[test] +fn bound_function_default_loop() { + let code = "let f = {y ? y, ... }: y; in f {}"; + assert_eq!(static_analysis(code), hashmap! {}); +} + +#[test] +fn bound_or_default() { + let code = "let s = {}; n = 1; in s.a or n"; + assert_eq!(static_analysis(code), hashmap! {}); +} + +#[test] +fn unbound_or_default() { + let code = "let s = {}; in s.a or n"; + assert_eq!(static_analysis(code), hashmap! {"n" => "identifier n is unbound".into()}); +} + +#[test] +fn unbound_or_default_indexed() { + let code = "in s.a or 1"; + assert_eq!(static_analysis(code), hashmap! {"s" => "identifier s is unbound".into()}); +} + +#[test] +fn unbound_assert_condition() { + let code = "assert f; 1"; + assert_eq!(static_analysis(code), hashmap! {"f" => "identifier f is unbound".into()}); +} + +#[test] +fn unbound_assert_body() { + let code = "assert true; f"; + assert_eq!(static_analysis(code), hashmap! {"f" => "identifier f is unbound".into()}); +} + +#[test] +fn unbound_if_condition() { + let code = "if foo then 1 else {}"; + assert_eq!(static_analysis(code), hashmap! {"foo" => "identifier foo is unbound".into()}); +} + +#[test] +fn unbound_if_body() { + let code = "if true then foo else {}"; + assert_eq!(static_analysis(code), hashmap! {"foo" => "identifier foo is unbound".into()}); +} + +#[test] +fn unbound_if_body_else() { + let code = "if true then 1 else foo"; + assert_eq!(static_analysis(code), hashmap! {"foo" => "identifier foo is unbound".into()}); +} + +#[test] +fn bound_let_inherit() { + let code = "let foo = {a = 1;}; in let inherit (foo) a; in a"; + assert_eq!(static_analysis(code), hashmap! {}); +} + +#[test] +fn unbound_let_inherit() { + let code = "let foo = {a = 1;}; in let inherit (bar) a; in a"; + assert_eq!(static_analysis(code), hashmap! {"bar" => "identifier bar is unbound".into()}); +} + +#[test] +fn unbound_let_inherit2() { + let code = "let foo = {a = 1;}; in let inherit (foo) a; in aaaa"; + assert_eq!(static_analysis(code), hashmap! {"aaaa" => "identifier aaaa is unbound".into()}); +} + +#[test] +fn unbound_let_inherit3() { + let code = "let foo = {a = 1;}; in let inherit (foo) a; in a + bar"; + assert_eq!(static_analysis(code), hashmap! {"bar" => "identifier bar is unbound".into()}); +} + +#[test] +fn unbound_with() { + let code = "with foo; 1"; + assert_eq!( + static_analysis(code), + hashmap! {"foo" => "identifier foo is unbound".into()} + ); +} + +#[test] +fn maybe_bound_with() { + let code = "let inf = x: inf x; in with inf {}; bar"; + assert_eq!(static_analysis(code), hashmap! {}); +} + +#[test] +fn unbound_string() { + let code = r#" "foo${bar}baz" "#; + assert_eq!(static_analysis(code), hashmap! {"bar" => "identifier bar is unbound".into()}); +} + +#[test] +fn unbound_multiline_string() { + let code = r#" '' + foo + ${bar} + baz'' +"#; + assert_eq!(static_analysis(code), hashmap! {"bar" => "identifier bar is unbound".into()}); +} + +#[test] +fn unbound_config() { + let code = "{config, pkgs, ...}: { config = { services.xserver.enable = lib.mkForce true; }; }"; + assert_eq!(static_analysis(code), hashmap! {"lib" => "identifier lib is unbound".into()}); +} + +#[test] +fn unbound_config2() { + let code = "{config, pkgs, ...}: { config = { environment.systemPackages = [ (lib.hiPrio pkgs.sl) firefox ]; }"; + assert_eq!( + static_analysis(code), + hashmap! {"lib" => "identifier lib is unbound".into(), "firefox" => "identifier firefox is unbound".into()} + ); +} + +#[test] +fn maybe_bound_config3() { + let code = "{config, pkgs, ...}: { config = { environment.systemPackages = with pkgs; [ (lib.hiPrio sl) firefox ]; }"; + assert_eq!(static_analysis(code), hashmap! {}); } #[cfg(test)]