From fe0d8dc2aa497887918be6b4f08026858ef30bd7 Mon Sep 17 00:00:00 2001 From: Aaron Janse Date: Wed, 30 Jun 2021 21:44:06 -0700 Subject: [PATCH 1/8] evaluate arithmetic --- Cargo.lock | 36 ++++++++ Cargo.toml | 1 + playground/math.nix | 1 + src/eval.rs | 216 ++++++++++++++++++++++++++++++++++++++++++++ src/lookup.rs | 14 +-- src/main.rs | 119 +++++++++++++++++++++--- src/parse.rs | 117 ++++++++++++++++++++++++ src/scope.rs | 48 ++++++++++ src/tests.rs | 43 +++++++++ src/value.rs | 74 +++++++++++++++ 10 files changed, 648 insertions(+), 21 deletions(-) create mode 100644 playground/math.nix create mode 100644 src/eval.rs create mode 100644 src/parse.rs create mode 100644 src/scope.rs create mode 100644 src/tests.rs create mode 100644 src/value.rs diff --git a/Cargo.lock b/Cargo.lock index 3e5587d..9b7e863 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "aho-corasick" version = "0.7.15" @@ -234,6 +236,27 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "gc" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3edaac0f5832202ebc99520cb77c932248010c4645d20be1dc62d6579f5b3752" +dependencies = [ + "gc_derive", +] + +[[package]] +name = "gc_derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60df8444f094ff7885631d80e78eb7d88c3c2361a98daaabb06256e4500db941" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "getrandom" version = "0.1.16" @@ -503,6 +526,7 @@ version = "0.1.0" dependencies = [ "dirs", "env_logger", + "gc", "lazy_static", "libc", "log", @@ -628,6 +652,18 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "synstructure" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + [[package]] name = "termcolor" version = "1.1.2" diff --git a/Cargo.toml b/Cargo.toml index ebd71f3..2417eec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ version = "0.1.0" [dependencies] dirs = "2.0.2" env_logger = "0.7.1" +gc = { version = "0.4", features = ["derive"] } lazy_static = "1.4" libc = "0.2.66" log = "0.4.8" diff --git a/playground/math.nix b/playground/math.nix new file mode 100644 index 0000000..d6d2e85 --- /dev/null +++ b/playground/math.nix @@ -0,0 +1 @@ +3 + (4 * 5) diff --git a/src/eval.rs b/src/eval.rs new file mode 100644 index 0000000..ae98723 --- /dev/null +++ b/src/eval.rs @@ -0,0 +1,216 @@ +use crate::parse::BinOpKind; +use crate::scope::*; +use crate::value::*; +use crate::EvalError; +use gc::{Finalize, Gc, GcCell, Trace}; +use rnix::TextRange; +use std::borrow::Borrow; + +type TreeResult = Result, EvalError>; + +/// Used to lazily calculate the value of a Tree. This should be +/// tolerant of parsing and evaluation errors from child Trees. +#[derive(Debug, Clone, Trace, Finalize)] +pub enum TreeSource { + Literal { + value: NixValue, + }, + Paren { + inner: TreeResult, + }, + BinOp { + op: BinOpKind, + left: TreeResult, + right: TreeResult, + }, + BoolAnd { + left: TreeResult, + right: TreeResult, + }, + BoolOr { + left: TreeResult, + right: TreeResult, + }, + Implication { + left: TreeResult, + right: TreeResult, + }, + UnaryInvert { + value: TreeResult, + }, + UnaryNegate { + value: TreeResult, + }, +} + +/// Syntax node that has context and can be lazily evaluated. +#[derive(Clone, Trace, Finalize)] +pub struct Tree { + #[unsafe_ignore_trace] + pub range: Option, + pub value: GcCell>>, + pub source: TreeSource, + pub scope: Gc, +} + +impl std::fmt::Debug for Tree { + // The scope can be recursive, so we don't want to print it by default + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Tree") + .field("value", &self.value) + .field("source", &self.source) + .field("range", &self.range) + .finish() + } +} + +impl Tree { + /// Lazily evaluate a Tree, caching its value + pub fn eval(&self) -> Result, EvalError> { + use std::ops::Deref; + let value_borrow = self.value.borrow(); + if let Some(ref value) = value_borrow.deref() { + Ok(value.clone()) + } else { + drop(value_borrow); + // We can later build a stack trace by wrapping errors here + let value = self.eval_uncached()?; + *self.value.borrow_mut() = Some(value.clone()); + Ok(value) + } + } + + fn eval_uncached(&self) -> Result, EvalError> { + match &self.source { + TreeSource::Paren { inner } => inner.as_ref()?.eval(), + TreeSource::Literal { value } => Ok(Gc::new(value.clone())), + TreeSource::BoolAnd { left, right } => { + if left.as_ref()?.eval()?.as_bool()? { + right.as_ref()?.eval() + } else { + Ok(Gc::new(NixValue::Bool(false))) + } + } + TreeSource::BoolOr { left, right } => { + if !left.as_ref()?.eval()?.as_bool()? { + right.as_ref()?.eval() + } else { + Ok(Gc::new(NixValue::Bool(true))) + } + } + TreeSource::Implication { left, right } => { + if !left.as_ref()?.eval()?.as_bool()? { + Ok(Gc::new(NixValue::Bool(true))) + } else { + right.as_ref()?.eval() + } + } + TreeSource::BinOp { op, left, right } => { + use BinOpKind::*; + use NixValue::*; + let tmp1 = left.as_ref()?.eval()?; + let tmp2 = right.as_ref()?.eval()?; + let left = tmp1.borrow(); + let right = tmp2.borrow(); + let out = match (op, left, right) { + (Add, Integer(x), Integer(y)) => Integer(x + y), + (Add, Float(x), Float(y)) => Float(x + y), + (Add, Integer(x), Float(y)) => Float(*x as f64 + y), + (Add, Float(x), Integer(y)) => Float(x + *y as f64), + + (Sub, Integer(x), Integer(y)) => Integer(x - y), + (Sub, Float(x), Float(y)) => Float(x - y), + (Sub, Integer(x), Float(y)) => Float(*x as f64 - y), + (Sub, Float(x), Integer(y)) => Float(x - *y as f64), + + (Mul, Integer(x), Integer(y)) => Integer(x * y), + (Mul, Float(x), Float(y)) => Float(x * y), + (Mul, Integer(x), Float(y)) => Float(*x as f64 * y), + (Mul, Float(x), Integer(y)) => Float(x * *y as f64), + + (Div, Integer(x), Integer(y)) => Integer( + x.checked_div(*y) + .ok_or_else(|| EvalError::Unexpected("division by zero".to_string()))?, + ), + (Div, Float(x), Float(y)) => Float(x / y), + (Div, Integer(x), Float(y)) => Float(*x as f64 / y), + (Div, Float(x), Integer(y)) => Float(x / *y as f64), + + // It seems like the Nix reference implementation compares floats exactly + // https://github.com/NixOS/nix/blob/4a5aa1dbf6/src/libutil/comparator.hh#L26 + (Equal, Integer(x), Integer(y)) => Bool(x == y), + (Equal, Float(x), Float(y)) => Bool(x == y), + (Equal, Integer(x), Float(y)) => Bool(*x as f64 == *y), + (Equal, Float(x), Integer(y)) => Bool(*x == *y as f64), + (Equal, Bool(x), Bool(y)) => Bool(x == y), + + (NotEqual, Integer(x), Integer(y)) => Bool(x != y), + (NotEqual, Float(x), Float(y)) => Bool(x != y), + (NotEqual, Integer(x), Float(y)) => Bool(*x as f64 != *y), + (NotEqual, Float(x), Integer(y)) => Bool(*x != *y as f64), + (NotEqual, Bool(x), Bool(y)) => Bool(x != y), + + (Less, Integer(x), Integer(y)) => Bool(x < y), + (Less, Float(x), Float(y)) => Bool(x < y), + (Less, Integer(x), Float(y)) => Bool((*x as f64) < *y), + (Less, Float(x), Integer(y)) => Bool(*x < *y as f64), + + (LessOrEq, Integer(x), Integer(y)) => Bool(x <= y), + (LessOrEq, Float(x), Float(y)) => Bool(x <= y), + (LessOrEq, Integer(x), Float(y)) => Bool(*x as f64 <= *y), + (LessOrEq, Float(x), Integer(y)) => Bool(*x <= *y as f64), + + (Greater, Integer(x), Integer(y)) => Bool(x > y), + (Greater, Float(x), Float(y)) => Bool(x > y), + (Greater, Integer(x), Float(y)) => Bool(*x as f64 > *y), + (Greater, Float(x), Integer(y)) => Bool(*x > (*y as f64)), + + (GreaterOrEq, Integer(x), Integer(y)) => Bool(x >= y), + (GreaterOrEq, Float(x), Float(y)) => Bool(x >= y), + (GreaterOrEq, Integer(x), Float(y)) => Bool(*x as f64 >= *y), + (GreaterOrEq, Float(x), Integer(y)) => Bool(*x >= (*y as f64)), + + _ => { + return Err(EvalError::Unexpected(format!( + "{:?} {:?} {:?} unsupported", + left, op, right + ))) + } + }; + Ok(Gc::new(out)) + } + TreeSource::UnaryInvert { value } => { + Ok(Gc::new(NixValue::Bool(!value.as_ref()?.eval()?.as_bool()?))) + } + TreeSource::UnaryNegate { value } => { + Ok(Gc::new(match value.as_ref()?.eval()?.borrow() { + NixValue::Integer(x) => NixValue::Integer(-x), + NixValue::Float(x) => NixValue::Float(-x), + _ => { + return Err(EvalError::TypeError( + "cannot negate a non-number".to_string(), + )) + } + })) + } + } + } + + /// Used for recursing to find the Tree at a cursor position + pub fn children(&self) -> Vec<&Box> { + match &self.source { + TreeSource::Paren { inner } => vec![inner], + TreeSource::Literal { value: _ } => vec![], + TreeSource::BinOp { op: _, left, right } => vec![left, right], + TreeSource::BoolAnd { left, right } => vec![left, right], + TreeSource::BoolOr { left, right } => vec![left, right], + TreeSource::Implication { left, right } => vec![left, right], + TreeSource::UnaryInvert { value } => vec![value], + TreeSource::UnaryNegate { value } => vec![value], + } + .into_iter() + .map(|x| x.as_ref()) + .filter_map(Result::ok) + .collect() + } +} diff --git a/src/lookup.rs b/src/lookup.rs index 3474389..782fe44 100644 --- a/src/lookup.rs +++ b/src/lookup.rs @@ -1,7 +1,4 @@ -use crate::{ - utils::{self, Datatype, Var}, - App, -}; +use crate::{App, eval::Tree, utils::{self, Datatype, Var}}; use lsp_types::Url; use rnix::{types::*, value::Value as ParsedValue, SyntaxNode}; use std::{ @@ -14,6 +11,8 @@ use lazy_static::lazy_static; use std::{process, str}; use regex; +use gc::Gc; +use crate::scope::Scope; lazy_static! { static ref BUILTINS: Vec = vec![ @@ -114,6 +113,7 @@ impl App { info.name, )) } + pub fn scope_from_node( &mut self, file: &mut Rc, @@ -147,14 +147,16 @@ impl App { let path = utils::uri_path(&file)?; node = match self.files.entry((**file).clone()) { Entry::Occupied(entry) => { - let (ast, _code) = entry.get(); + let (ast, _code, _) = entry.get(); ast.root().inner()?.clone() } Entry::Vacant(placeholder) => { let content = fs::read_to_string(&path).ok()?; let ast = rnix::parse(&content); let node = ast.root().inner()?.clone(); - placeholder.insert((ast, content)); + let gc_root = Gc::new(Scope::Root(path)); + let evaluated = Tree::parse(node.clone(), gc_root); + placeholder.insert((ast, content, evaluated)); node } }; diff --git a/src/main.rs b/src/main.rs index 8354dd5..e94fec2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,10 +21,17 @@ clippy::integer_arithmetic, )] +mod eval; mod lookup; +mod parse; +mod scope; +mod tests; mod utils; +mod value; use dirs::home_dir; +use eval::Tree; +use gc::{Finalize, Gc, Trace}; use log::{error, trace, warn}; use lsp_server::{Connection, ErrorCode, Message, Notification, Request, RequestId, Response}; use lsp_types::{ @@ -38,6 +45,7 @@ use rnix::{ value::{Anchor as RAnchor, Value as RValue}, SyntaxNode, TextRange, TextSize, }; +use scope::Scope; use std::{ borrow::Cow, collections::HashMap, @@ -45,9 +53,38 @@ use std::{ path::{Path, PathBuf}, process, rc::Rc, + str::FromStr, }; type Error = Box; +#[derive(Debug, Clone, Trace, Finalize)] +pub enum EvalError { + Unimplemented(String), + Unexpected(String), + TypeError(String), + Parsing, + Unknown, +} + +impl std::error::Error for EvalError {} + +impl std::fmt::Display for EvalError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + EvalError::Unimplemented(msg) => write!(f, "unimplemented: {}", msg), + EvalError::Unexpected(msg) => write!(f, "unexpected: {}", msg), + EvalError::TypeError(msg) => write!(f, "type error: {}", msg), + EvalError::Parsing => write!(f, "parsing error"), + EvalError::Unknown => write!(f, "unknown value"), + } + } +} + +impl From<&EvalError> for EvalError { + fn from(x: &EvalError) -> Self { + x.clone() + } +} fn main() { if let Err(err) = real_main() { @@ -82,6 +119,7 @@ fn real_main() -> Result<(), Error> { resolve_provider: Some(false), work_done_progress_options: WorkDoneProgressOptions::default(), }), + hover_provider: Some(true), rename_provider: Some(RenameProviderCapability::Simple(true)), selection_range_provider: Some(SelectionRangeProviderCapability::Simple(true)), ..ServerCapabilities::default() @@ -102,7 +140,7 @@ fn real_main() -> Result<(), Error> { } struct App { - files: HashMap, + files: HashMap)>, conn: Connection, } impl App { @@ -193,7 +231,7 @@ impl App { let document_links = self.document_links(¶ms).unwrap_or_default(); self.reply(Response::new_ok(id, document_links)); } else if let Some((id, params)) = cast::(&mut req) { - let changes = if let Some((ast, code)) = self.files.get(¶ms.text_document.uri) { + let changes = if let Some((ast, code, _)) = self.files.get(¶ms.text_document.uri) { let fmt = nixpkgs_fmt::reformat_node(&ast.node()); vec![TextEdit { range: utils::range(&code, TextRange::up_to(ast.node().text().len())), @@ -203,9 +241,24 @@ impl App { Vec::new() }; self.reply(Response::new_ok(id, changes)); + } else if let Some((id, params)) = cast::(&mut req) { + if let Some((range, markdown)) = self.hover(params) { + self.reply(Response::new_ok( + id, + Hover { + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value: markdown, + }), + range, + }, + )); + } else { + self.reply(Response::new_ok(id, ())); + } } else if let Some((id, params)) = cast::(&mut req) { let mut selections = Vec::new(); - if let Some((ast, code)) = self.files.get(¶ms.text_document.uri) { + if let Some((ast, code, _)) = self.files.get(¶ms.text_document.uri) { for pos in params.positions { selections.push(utils::selection_ranges(&ast.node(), code, pos)); } @@ -228,7 +281,13 @@ impl App { let text = params.text_document.text; let parsed = rnix::parse(&text); self.send_diagnostics(params.text_document.uri.clone(), &text, &parsed)?; - self.files.insert(params.text_document.uri, (parsed, text)); + if let Ok(path) = PathBuf::from_str(params.text_document.uri.path()) { + let gc_root = Gc::new(Scope::Root(path)); + let parsed_root = parsed.root().inner().ok_or(EvalError::Parsing); + let evaluated = parsed_root.and_then(|x| Tree::parse(x, gc_root)); + self.files + .insert(params.text_document.uri, (parsed, text, evaluated)); + } } DidChangeTextDocument::METHOD => { let params: DidChangeTextDocumentParams = serde_json::from_value(req.params)?; @@ -237,8 +296,8 @@ impl App { let mut content = Cow::from(&change.text); if let Some(range) = &change.range { if self.files.contains_key(&uri) { - let original = self.files.get(&uri) - .unwrap().1.lines().collect::>(); + let original = + self.files.get(&uri).unwrap().1.lines().collect::>(); let start_line = range.start.line; let start_char = range.start.character; let end_line = range.end.line; @@ -284,8 +343,13 @@ impl App { } let parsed = rnix::parse(&content); self.send_diagnostics(uri.clone(), &content, &parsed)?; - self.files - .insert(uri, (parsed, content.to_owned().to_string())); + if let Ok(path) = PathBuf::from_str(uri.path()) { + let gc_root = Gc::new(Scope::Root(path)); + let parsed_root = parsed.root().inner().ok_or(EvalError::Parsing); + let evaluated = parsed_root.and_then(|x| Tree::parse(x, gc_root)); + self.files + .insert(uri, (parsed, content.to_owned().to_string(), evaluated)); + } } } _ => (), @@ -293,14 +357,14 @@ impl App { Ok(()) } fn lookup_definition(&mut self, params: TextDocumentPositionParams) -> Option { - let (current_ast, current_content) = self.files.get(¶ms.text_document.uri)?; + let (current_ast, current_content, _) = self.files.get(¶ms.text_document.uri)?; let offset = utils::lookup_pos(current_content, params.position)?; let node = current_ast.node(); let (name, scope, _) = self.scope_for_ident(params.text_document.uri, &node, offset)?; let var_e = scope.get(name.as_str())?; if let Some(var) = &var_e.var { - let (_definition_ast, definition_content) = self.files.get(&var.file)?; + let (_definition_ast, definition_content, _) = self.files.get(&var.file)?; Some(Location { uri: (*var.file).clone(), range: utils::range(definition_content, var.key.text_range()), @@ -309,9 +373,17 @@ impl App { None } } + fn hover(&self, params: TextDocumentPositionParams) -> Option<(Option, String)> { + let (_, content, tree) = self.files.get(¶ms.text_document.uri)?; + let offset = utils::lookup_pos(content, params.position)?; + let child_tree = climb_tree(tree.as_ref().ok()?, offset).clone(); + let range = utils::range(content, child_tree.range?); + let val = child_tree.eval().ok()?.format_markdown(); + Some((Some(range), val)) + } #[allow(clippy::shadow_unrelated)] // false positive fn completions(&mut self, params: &TextDocumentPositionParams) -> Option> { - let (ast, content) = self.files.get(¶ms.text_document.uri)?; + let (ast, content, _) = self.files.get(¶ms.text_document.uri)?; let offset = utils::lookup_pos(content, params.position)?; let node = ast.node(); @@ -319,7 +391,7 @@ impl App { self.scope_for_ident(params.text_document.uri.clone(), &node, offset)?; // Re-open, because scope_for_ident may mutably borrow - let (_, content) = self.files.get(¶ms.text_document.uri)?; + let (_, content, _) = self.files.get(¶ms.text_document.uri)?; let mut completions = Vec::new(); for (var, data) in scope { @@ -327,7 +399,9 @@ impl App { let det = data.render_detail(); completions.push(CompletionItem { label: var.clone(), - documentation: data.documentation.map(|x| lsp_types::Documentation::String(x)), + documentation: data + .documentation + .map(|x| lsp_types::Documentation::String(x)), deprecated: Some(data.deprecated), text_edit: Some(TextEdit { range: utils::range(content, node.node().text_range()), @@ -371,7 +445,7 @@ impl App { } let uri = params.text_document_position.text_document.uri; - let (ast, code) = self.files.get(&uri)?; + let (ast, code, _) = self.files.get(&uri)?; let offset = utils::lookup_pos(code, params.text_document_position.position)?; let info = utils::ident_at(&ast.node(), offset)?; if !info.path.is_empty() { @@ -395,7 +469,7 @@ impl App { Some(changes) } fn document_links(&mut self, params: &DocumentLinkParams) -> Option> { - let (current_ast, current_content) = self.files.get(¶ms.text_document.uri)?; + let (current_ast, current_content, _) = self.files.get(¶ms.text_document.uri)?; let parent_dir = Path::new(params.text_document.uri.path()).parent(); let home_dir = home_dir(); let home_dir = home_dir.as_ref(); @@ -457,3 +531,18 @@ impl App { Ok(()) } } + +fn climb_tree(here: &Tree, offset: usize) -> &Tree { + for child in here.children().clone() { + let range = match child.range { + Some(x) => x, + None => continue, + }; + let start: usize = range.start().into(); + let end: usize = range.end().into(); + if start <= offset && offset <= end { + return climb_tree(child, offset); + } + } + here +} diff --git a/src/parse.rs b/src/parse.rs new file mode 100644 index 0000000..346214e --- /dev/null +++ b/src/parse.rs @@ -0,0 +1,117 @@ +use std::convert::TryFrom; + +use crate::value::*; +use crate::{ + eval::{Tree, TreeSource}, + scope::Scope, + EvalError, +}; +use gc::{Finalize, Gc, GcCell, Trace}; +use rnix::{ + types::{ParsedType, Wrapper}, + SyntaxNode, +}; + +/// Unlike `and`, `or`, and `->`, this subset of binops +/// does not need special handling for lazy evaluation. +#[derive(Debug, Clone, Trace, Finalize)] +pub enum BinOpKind { + Concat, + Update, + Add, + Sub, + Mul, + Div, + Equal, + Less, + LessOrEq, + Greater, + GreaterOrEq, + NotEqual, +} + +impl Tree { + /// 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 = |node| Tree::parse(node, scope.clone()).map(|x| Box::new(x)); + let source = match ParsedType::try_from(node.clone()).map_err(|_| EvalError::Parsing)? { + ParsedType::Paren(paren) => { + let inner = paren.inner().ok_or(EvalError::Parsing)?; + TreeSource::Paren { + inner: recurse(inner), + } + } + ParsedType::BinOp(binop) => { + let left = recurse(binop.lhs().ok_or(EvalError::Parsing)?); + let right = recurse(binop.rhs().ok_or(EvalError::Parsing)?); + use rnix::types::BinOpKind::*; + match binop.operator() { + And => TreeSource::BoolAnd { left, right }, + Or => TreeSource::BoolOr { left, right }, + Implication => TreeSource::Implication { left, right }, + _ => { + let op = match binop.operator() { + And | Or | IsSet | Implication => unreachable!(), + Concat => BinOpKind::Concat, + Update => BinOpKind::Update, + Add => BinOpKind::Add, + Sub => BinOpKind::Sub, + Mul => BinOpKind::Mul, + Div => BinOpKind::Div, + Equal => BinOpKind::Equal, + NotEqual => BinOpKind::NotEqual, + Less => BinOpKind::Less, + LessOrEq => BinOpKind::LessOrEq, + More => BinOpKind::Greater, + MoreOrEq => BinOpKind::GreaterOrEq, + }; + TreeSource::BinOp { op, left, right } + } + } + } + ParsedType::UnaryOp(unary) => { + use rnix::types::UnaryOpKind; + match unary.operator() { + UnaryOpKind::Invert => TreeSource::UnaryInvert { + value: recurse(unary.value().ok_or(EvalError::Parsing)?), + }, + UnaryOpKind::Negate => TreeSource::UnaryNegate { + value: recurse(unary.value().ok_or(EvalError::Parsing)?), + }, + } + } + ParsedType::Value(literal) => { + use rnix::value::Value::*; + // Booleans `true` and `false` are global variables, not literals + TreeSource::Literal { + value: match literal.to_value().map_err(|_| EvalError::Parsing)? { + Float(x) => NixValue::Float(x), + Integer(x) => NixValue::Integer(x), + String(_) => { + return Err(EvalError::Unimplemented("string literal".to_string())) + } + Path(_, _) => { + return Err(EvalError::Unimplemented("path literal".to_string())) + } + }, + } + } + node => { + return Err(EvalError::Unimplemented(format!( + "rnix-parser node {:?}", + node + ))) + } + }; + Ok(Self { + value: GcCell::new(None), + source, + range, + scope, + }) + } +} diff --git a/src/scope.rs b/src/scope.rs new file mode 100644 index 0000000..f71a7f3 --- /dev/null +++ b/src/scope.rs @@ -0,0 +1,48 @@ +use crate::eval::Tree; +use gc::{Finalize, Gc, Trace}; +use std::path::PathBuf; + +/// A parent Tree's scope is used to provide tooling for its child Trees. +/// This enum would provide four scope types: +/// - None: Used for calculated literals. For example, for the string +/// interpolation `"prefix${suffix}"`, the literal `prefix` by itself +/// cannot be referenced anywhere except at its definition, so we don't +/// need context-aware tooling for it. For `${suffix}`, however, we would +/// inherit the parent's scope. +/// - Root: Provided for each file. Used for providing global variables +/// and tracking the `import` dependency tree. Also used for detecting +/// which file an expression is defined in. +/// - Normal: Created by `let in` and `rec { }`. All the variable names +/// can be derived using static analysis with rnix-parser; we don't need +/// to evaluate anything to detect if a variable name is in this scope +/// - With: Created by `with $VAR` expressions. We need to evaluate $VAR +/// to determine whether a variable name is in scope. +#[derive(Trace, Finalize)] +pub enum Scope { + Root(PathBuf), +} + +impl Scope { + /// Finds the Tree of an identifier in the scope. + /// + /// This would do two passes up the tree: + /// 1. Check Scope::Normal and Scope::Root + /// 2. Check Scope::With, which requires evaluation + /// + /// See https://github.com/NixOS/nix/issues/490 for an explanation + /// of why Nix works this way. + /// + /// Examples: + /// ```plain + /// nix-repl> let import = 1; in import + /// 1 # found in Scope::Normal, which we reach before Scope::Root + /// nix-repl> import + /// «primop» # found in Scope::Root + /// nix-repl> with { import = 1; }; import + /// «primop» # found in Scope::Root, which we reach before Scope::With + /// ``` + #[allow(dead_code)] // this function will be implemented later + pub fn get(&self, _name: &str) -> Option> { + None + } +} diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..1880eba --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,43 @@ +use eval::Tree; +use gc::Gc; +use rnix::types::Wrapper; +use scope::Scope; +use std::borrow::Borrow; +use value::NixValue; + +#[allow(dead_code)] +fn eval(code: &str) -> NixValue { + let ast = rnix::parse(&code); + let root = ast.root().inner().unwrap(); + let path = std::env::current_dir().unwrap(); + let out = Tree::parse(root, Gc::new(Scope::Root(path))).unwrap(); + let tmp = out.eval(); + let val: &NixValue = tmp.as_ref().unwrap().borrow(); + val.clone() +} + +use super::*; + +#[test] +fn integer_division() { + let code = "1 / 2"; + assert_eq!(eval(code).as_int().unwrap(), 0); +} + +#[test] +fn float_division() { + let code = "1.0 / 2.0"; + assert_eq!(eval(code).as_float().unwrap(), 0.5); +} + +#[test] +fn order_of_operations() { + let code = "1 + 2 * 3"; + assert_eq!(eval(code).as_int().unwrap(), 7); +} + +#[test] +fn div_int_by_float() { + let code = "1 / 2.0"; + assert_eq!(eval(code).as_float().unwrap(), 0.5); +} diff --git a/src/value.rs b/src/value.rs new file mode 100644 index 0000000..a0a7777 --- /dev/null +++ b/src/value.rs @@ -0,0 +1,74 @@ +use crate::EvalError; +use gc::{Finalize, Trace}; +use std::fmt::Debug; + +#[derive(Clone, Trace, Finalize)] +pub enum NixValue { + Bool(bool), + Float(f64), + Integer(i64), +} + +impl NixValue { + fn type_name(&self) -> String { + match self { + NixValue::Bool(_) => "bool", + NixValue::Float(_) => "float", + NixValue::Integer(_) => "integer", + } + .to_string() + } + + pub fn as_bool(&self) -> Result { + match self { + NixValue::Bool(x) => Ok(*x), + _ => Err(EvalError::TypeError(format!( + "expected bool, got {}", + self.type_name() + ))), + } + } + + #[allow(dead_code)] // this function is used by tests + pub fn as_int(&self) -> Result { + match self { + NixValue::Integer(x) => Ok(*x), + _ => Err(EvalError::TypeError(format!( + "expected int, got {}", + self.type_name() + ))), + } + } + + #[allow(dead_code)] // this function is used by tests + pub fn as_float(&self) -> Result { + match self { + NixValue::Float(x) => Ok(*x), + _ => Err(EvalError::TypeError(format!( + "expected float, got {}", + self.type_name() + ))), + } + } + + pub fn format_markdown(&self) -> String { + use NixValue::*; + match self { + Bool(x) => format!("```nix\n{}\n```", x), + Float(x) => format!("```nix\n{}\n```", x), + Integer(x) => format!("```nix\n{}\n```", x), + } + } +} + +impl Debug for NixValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + NixValue::Bool(x) => write!(f, "{}", x), + // TODO: format nicely like `nix rep` + NixValue::Float(x) => write!(f, "{}", x), + NixValue::Integer(x) => write!(f, "{}", x), + } + } +} + From fd068a8eb4723d54ce4b41b5837fb3b4c7584f08 Mon Sep 17 00:00:00 2001 From: Aaron Janse Date: Sat, 3 Jul 2021 18:02:33 -0700 Subject: [PATCH 2/8] simplify cache borrowing --- src/eval.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/eval.rs b/src/eval.rs index ae98723..dff2c74 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -67,15 +67,13 @@ impl std::fmt::Debug for Tree { impl Tree { /// Lazily evaluate a Tree, caching its value pub fn eval(&self) -> Result, EvalError> { - use std::ops::Deref; - let value_borrow = self.value.borrow(); - if let Some(ref value) = value_borrow.deref() { + let mut value_borrow = self.value.borrow_mut(); + if let Some(ref value) = *value_borrow { Ok(value.clone()) } else { - drop(value_borrow); // We can later build a stack trace by wrapping errors here let value = self.eval_uncached()?; - *self.value.borrow_mut() = Some(value.clone()); + *value_borrow = Some(value.clone()); Ok(value) } } From a40d0e0e9efb7278ce322397e97a324a8ddc973f Mon Sep 17 00:00:00 2001 From: Aaron Janse Date: Sat, 3 Jul 2021 18:45:17 -0700 Subject: [PATCH 3/8] use macro for binops --- src/eval.rs | 104 +++++++++++++++++++++++----------------------------- 1 file changed, 46 insertions(+), 58 deletions(-) diff --git a/src/eval.rs b/src/eval.rs index dff2c74..f10d53b 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -103,71 +103,58 @@ impl Tree { right.as_ref()?.eval() } } + + #[allow(clippy::enum_glob_use)] + #[allow(clippy::float_cmp)] // We want to match the Nix reference implementation TreeSource::BinOp { op, left, right } => { use BinOpKind::*; use NixValue::*; - let tmp1 = left.as_ref()?.eval()?; - let tmp2 = right.as_ref()?.eval()?; - let left = tmp1.borrow(); - let right = tmp2.borrow(); - let out = match (op, left, right) { - (Add, Integer(x), Integer(y)) => Integer(x + y), - (Add, Float(x), Float(y)) => Float(x + y), - (Add, Integer(x), Float(y)) => Float(*x as f64 + y), - (Add, Float(x), Integer(y)) => Float(x + *y as f64), - - (Sub, Integer(x), Integer(y)) => Integer(x - y), - (Sub, Float(x), Float(y)) => Float(x - y), - (Sub, Integer(x), Float(y)) => Float(*x as f64 - y), - (Sub, Float(x), Integer(y)) => Float(x - *y as f64), - - (Mul, Integer(x), Integer(y)) => Integer(x * y), - (Mul, Float(x), Float(y)) => Float(x * y), - (Mul, Integer(x), Float(y)) => Float(*x as f64 * y), - (Mul, Float(x), Integer(y)) => Float(x * *y as f64), - - (Div, Integer(x), Integer(y)) => Integer( - x.checked_div(*y) - .ok_or_else(|| EvalError::Unexpected("division by zero".to_string()))?, - ), - (Div, Float(x), Float(y)) => Float(x / y), - (Div, Integer(x), Float(y)) => Float(*x as f64 / y), - (Div, Float(x), Integer(y)) => Float(x / *y as f64), - - // It seems like the Nix reference implementation compares floats exactly - // https://github.com/NixOS/nix/blob/4a5aa1dbf6/src/libutil/comparator.hh#L26 - (Equal, Integer(x), Integer(y)) => Bool(x == y), - (Equal, Float(x), Float(y)) => Bool(x == y), - (Equal, Integer(x), Float(y)) => Bool(*x as f64 == *y), - (Equal, Float(x), Integer(y)) => Bool(*x == *y as f64), - (Equal, Bool(x), Bool(y)) => Bool(x == y), - (NotEqual, Integer(x), Integer(y)) => Bool(x != y), - (NotEqual, Float(x), Float(y)) => Bool(x != y), - (NotEqual, Integer(x), Float(y)) => Bool(*x as f64 != *y), - (NotEqual, Float(x), Integer(y)) => Bool(*x != *y as f64), - (NotEqual, Bool(x), Bool(y)) => Bool(x != y), + // Workaround for "temporary value dropped while borrowed" + // https://doc.rust-lang.org/error-index.html#E0716 + let left_tmp = left.as_ref()?.eval()?; + let left_val = left_tmp.borrow(); + let right_tmp = right.as_ref()?.eval()?; + let right_val = right_tmp.borrow(); - (Less, Integer(x), Integer(y)) => Bool(x < y), - (Less, Float(x), Float(y)) => Bool(x < y), - (Less, Integer(x), Float(y)) => Bool((*x as f64) < *y), - (Less, Float(x), Integer(y)) => Bool(*x < *y as f64), - - (LessOrEq, Integer(x), Integer(y)) => Bool(x <= y), - (LessOrEq, Float(x), Float(y)) => Bool(x <= y), - (LessOrEq, Integer(x), Float(y)) => Bool(*x as f64 <= *y), - (LessOrEq, Float(x), Integer(y)) => Bool(*x <= *y as f64), - - (Greater, Integer(x), Integer(y)) => Bool(x > y), - (Greater, Float(x), Float(y)) => Bool(x > y), - (Greater, Integer(x), Float(y)) => Bool(*x as f64 > *y), - (Greater, Float(x), Integer(y)) => Bool(*x > (*y as f64)), + // Specially handle integer division by zero + if let (Div, Integer(_), Integer(0)) = (op, left_val, right_val) { + return Err(EvalError::Unexpected("division by zero".to_string())); + } - (GreaterOrEq, Integer(x), Integer(y)) => Bool(x >= y), - (GreaterOrEq, Float(x), Float(y)) => Bool(x >= y), - (GreaterOrEq, Integer(x), Float(y)) => Bool(*x as f64 >= *y), - (GreaterOrEq, Float(x), Integer(y)) => Bool(*x >= (*y as f64)), + macro_rules! match_binops { + ( arithmetic [ $( $arith_kind:pat => $arith_oper:tt, )+ ], + comparisons [ $( $comp_kind:pat => $comp_oper:tt, )+ ], + $( $pattern:pat => $expr:expr ),* ) => { + match (op, left_val, right_val) { + $( + ($arith_kind, Integer(x), Integer(y)) => Integer(x $arith_oper y), + ($arith_kind, Float(x), Float(y)) => Float(x $arith_oper y), + ($arith_kind, Integer(x), Float(y)) => Float((*x as f64) $arith_oper y), + ($arith_kind, Float(x), Integer(y)) => Float(x $arith_oper (*y as f64)), + )* + $( + ($comp_kind, Integer(x), Integer(y)) => Bool(x $comp_oper y), + ($comp_kind, Float(x), Float(y)) => Bool(x $comp_oper y), + ($comp_kind, Integer(x), Float(y)) => Bool((*x as f64) $comp_oper *y), + ($comp_kind, Float(x), Integer(y)) => Bool(*x $comp_oper (*y as f64)), + )* + $( + $pattern => $expr, + )* + } + }; + } + let out = match_binops! { + arithmetic [ + Add => +, Sub => -, Mul => *, Div => /, + ], + comparisons [ + Equal => ==, NotEqual => !=, + Greater => >, GreaterOrEq => >=, + Less => <, LessOrEq => <=, + ], _ => { return Err(EvalError::Unexpected(format!( "{:?} {:?} {:?} unsupported", @@ -175,6 +162,7 @@ impl Tree { ))) } }; + Ok(Gc::new(out)) } TreeSource::UnaryInvert { value } => { From 31257851304d7c8caffd23343c735d5705c60552 Mon Sep 17 00:00:00 2001 From: Aaron Janse Date: Sat, 3 Jul 2021 19:01:33 -0700 Subject: [PATCH 4/8] refactor binop parsing --- src/parse.rs | 47 ++++++++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/src/parse.rs b/src/parse.rs index 346214e..5e1c5d7 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -46,32 +46,37 @@ impl Tree { } } ParsedType::BinOp(binop) => { + use rnix::types::BinOpKind::*; let left = recurse(binop.lhs().ok_or(EvalError::Parsing)?); let right = recurse(binop.rhs().ok_or(EvalError::Parsing)?); - use rnix::types::BinOpKind::*; - match binop.operator() { + macro_rules! binop_source { + ( $op:expr ) => { + TreeSource::BinOp { + op: $op, + left, + right, + } + }; + } + let op = match binop.operator() { And => TreeSource::BoolAnd { left, right }, Or => TreeSource::BoolOr { left, right }, Implication => TreeSource::Implication { left, right }, - _ => { - let op = match binop.operator() { - And | Or | IsSet | Implication => unreachable!(), - Concat => BinOpKind::Concat, - Update => BinOpKind::Update, - Add => BinOpKind::Add, - Sub => BinOpKind::Sub, - Mul => BinOpKind::Mul, - Div => BinOpKind::Div, - Equal => BinOpKind::Equal, - NotEqual => BinOpKind::NotEqual, - Less => BinOpKind::Less, - LessOrEq => BinOpKind::LessOrEq, - More => BinOpKind::Greater, - MoreOrEq => BinOpKind::GreaterOrEq, - }; - TreeSource::BinOp { op, left, right } - } - } + IsSet => return Err(EvalError::Unimplemented("IsSet".to_string())), + Concat => binop_source!(BinOpKind::Concat), + Update => binop_source!(BinOpKind::Update), + Add => binop_source!(BinOpKind::Add), + Sub => binop_source!(BinOpKind::Sub), + Mul => binop_source!(BinOpKind::Mul), + Div => binop_source!(BinOpKind::Div), + Equal => binop_source!(BinOpKind::Equal), + NotEqual => binop_source!(BinOpKind::NotEqual), + Less => binop_source!(BinOpKind::Less), + LessOrEq => binop_source!(BinOpKind::LessOrEq), + More => binop_source!(BinOpKind::Greater), + MoreOrEq => binop_source!(BinOpKind::GreaterOrEq), + }; + TreeSource::BinOp { op, left, right } } ParsedType::UnaryOp(unary) => { use rnix::types::UnaryOpKind; From 2b321da096c77589c2a1b93aafdc72ad6b4c37de Mon Sep 17 00:00:00 2001 From: Aaron Janse Date: Thu, 19 Aug 2021 10:44:21 -0700 Subject: [PATCH 5/8] update error handling --- src/error.rs | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/eval.rs | 18 +++++++++------- src/main.rs | 43 ++++++++++---------------------------- src/parse.rs | 39 ++++++++++++++++++++--------------- src/value.rs | 16 +++++++-------- 5 files changed, 111 insertions(+), 63 deletions(-) create mode 100644 src/error.rs diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..51abc6e --- /dev/null +++ b/src/error.rs @@ -0,0 +1,58 @@ +use gc::{Finalize, Gc, GcCell, Trace}; + +pub const ERR_PARSING: EvalError = EvalError::Internal(InternalError::Parsing); + + +#[derive(Debug, Clone, Trace, Finalize)] +pub enum EvalError { + Internal(InternalError), + /// Used for Nix errors such as division by zero + Value(ValueError), +} + +impl From<&EvalError> for EvalError { + fn from(x: &EvalError) -> Self { + x.clone() + } +} + +#[derive(Debug, Clone, Trace, Finalize)] +pub enum InternalError { + Unimplemented(String), + Parsing, +} + +#[derive(Debug, Clone, Trace, Finalize)] +pub enum ValueError { + DivisionByZero, + TypeError(String), +} + +impl std::error::Error for EvalError {} + +impl std::fmt::Display for EvalError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + EvalError::Internal(x) => x.fmt(f), + EvalError::Value(x) => x.fmt(f), + } + } +} + +impl std::fmt::Display for InternalError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + InternalError::Parsing => write!(f, "parsing"), + InternalError::Unimplemented(msg) => write!(f, "{}", msg), + } + } +} + +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::TypeError(msg) => write!(f, "{}", msg), + } + } +} diff --git a/src/eval.rs b/src/eval.rs index f10d53b..d77abfd 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -1,3 +1,4 @@ +use crate::error::{InternalError, ValueError}; use crate::parse::BinOpKind; use crate::scope::*; use crate::value::*; @@ -105,7 +106,8 @@ impl Tree { } #[allow(clippy::enum_glob_use)] - #[allow(clippy::float_cmp)] // We want to match the Nix reference implementation + #[allow(clippy::float_cmp)] + // We want to match the Nix reference implementation TreeSource::BinOp { op, left, right } => { use BinOpKind::*; use NixValue::*; @@ -119,13 +121,13 @@ impl Tree { // Specially handle integer division by zero if let (Div, Integer(_), Integer(0)) = (op, left_val, right_val) { - return Err(EvalError::Unexpected("division by zero".to_string())); + return Err(EvalError::Value(ValueError::DivisionByZero)); } macro_rules! match_binops { ( arithmetic [ $( $arith_kind:pat => $arith_oper:tt, )+ ], comparisons [ $( $comp_kind:pat => $comp_oper:tt, )+ ], - $( $pattern:pat => $expr:expr ),* ) => { + $( $pattern:pat => $expr:expr ),* ) => { match (op, left_val, right_val) { $( ($arith_kind, Integer(x), Integer(y)) => Integer(x $arith_oper y), @@ -156,10 +158,12 @@ impl Tree { Less => <, LessOrEq => <=, ], _ => { - return Err(EvalError::Unexpected(format!( + // We assume that it's our fault if an operation is unsupported. + // Over time, we can rewrite common cases into type errors. + return Err(EvalError::Internal(InternalError::Unimplemented(format!( "{:?} {:?} {:?} unsupported", left, op, right - ))) + )))) } }; @@ -173,9 +177,9 @@ impl Tree { NixValue::Integer(x) => NixValue::Integer(-x), NixValue::Float(x) => NixValue::Float(-x), _ => { - return Err(EvalError::TypeError( + return Err(EvalError::Value(ValueError::TypeError( "cannot negate a non-number".to_string(), - )) + ))) } })) } diff --git a/src/main.rs b/src/main.rs index e94fec2..a587e93 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,6 +21,7 @@ clippy::integer_arithmetic, )] +mod error; mod eval; mod lookup; mod parse; @@ -29,6 +30,8 @@ mod tests; mod utils; mod value; +use error::{EvalError, ERR_PARSING}; + use dirs::home_dir; use eval::Tree; use gc::{Finalize, Gc, Trace}; @@ -57,34 +60,6 @@ use std::{ }; type Error = Box; -#[derive(Debug, Clone, Trace, Finalize)] -pub enum EvalError { - Unimplemented(String), - Unexpected(String), - TypeError(String), - Parsing, - Unknown, -} - -impl std::error::Error for EvalError {} - -impl std::fmt::Display for EvalError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - EvalError::Unimplemented(msg) => write!(f, "unimplemented: {}", msg), - EvalError::Unexpected(msg) => write!(f, "unexpected: {}", msg), - EvalError::TypeError(msg) => write!(f, "type error: {}", msg), - EvalError::Parsing => write!(f, "parsing error"), - EvalError::Unknown => write!(f, "unknown value"), - } - } -} - -impl From<&EvalError> for EvalError { - fn from(x: &EvalError) -> Self { - x.clone() - } -} fn main() { if let Err(err) = real_main() { @@ -283,7 +258,7 @@ impl App { self.send_diagnostics(params.text_document.uri.clone(), &text, &parsed)?; if let Ok(path) = PathBuf::from_str(params.text_document.uri.path()) { let gc_root = Gc::new(Scope::Root(path)); - let parsed_root = parsed.root().inner().ok_or(EvalError::Parsing); + let parsed_root = parsed.root().inner().ok_or(ERR_PARSING); let evaluated = parsed_root.and_then(|x| Tree::parse(x, gc_root)); self.files .insert(params.text_document.uri, (parsed, text, evaluated)); @@ -345,7 +320,7 @@ impl App { self.send_diagnostics(uri.clone(), &content, &parsed)?; if let Ok(path) = PathBuf::from_str(uri.path()) { let gc_root = Gc::new(Scope::Root(path)); - let parsed_root = parsed.root().inner().ok_or(EvalError::Parsing); + let parsed_root = parsed.root().inner().ok_or(ERR_PARSING); let evaluated = parsed_root.and_then(|x| Tree::parse(x, gc_root)); self.files .insert(uri, (parsed, content.to_owned().to_string(), evaluated)); @@ -378,8 +353,12 @@ impl App { let offset = utils::lookup_pos(content, params.position)?; let child_tree = climb_tree(tree.as_ref().ok()?, offset).clone(); let range = utils::range(content, child_tree.range?); - let val = child_tree.eval().ok()?.format_markdown(); - Some((Some(range), val)) + let msg = match child_tree.eval() { + Ok(value) => value.format_markdown(), + Err(EvalError::Value(ref err)) => format!("{}", err), + Err(EvalError::Internal(_)) => return None, + }; + Some((Some(range), msg)) } #[allow(clippy::shadow_unrelated)] // false positive fn completions(&mut self, params: &TextDocumentPositionParams) -> Option> { diff --git a/src/parse.rs b/src/parse.rs index 5e1c5d7..2c006f3 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -1,10 +1,10 @@ use std::convert::TryFrom; +use crate::error::{EvalError, InternalError, ERR_PARSING}; use crate::value::*; use crate::{ eval::{Tree, TreeSource}, scope::Scope, - EvalError, }; use gc::{Finalize, Gc, GcCell, Trace}; use rnix::{ @@ -38,17 +38,17 @@ impl Tree { pub fn parse(node: SyntaxNode, scope: Gc) -> Result { let range = Some(node.text_range()); let recurse = |node| Tree::parse(node, scope.clone()).map(|x| Box::new(x)); - let source = match ParsedType::try_from(node.clone()).map_err(|_| EvalError::Parsing)? { + let source = match ParsedType::try_from(node.clone()).map_err(|_| ERR_PARSING)? { ParsedType::Paren(paren) => { - let inner = paren.inner().ok_or(EvalError::Parsing)?; + let inner = paren.inner().ok_or(ERR_PARSING)?; TreeSource::Paren { inner: recurse(inner), } } ParsedType::BinOp(binop) => { use rnix::types::BinOpKind::*; - let left = recurse(binop.lhs().ok_or(EvalError::Parsing)?); - let right = recurse(binop.rhs().ok_or(EvalError::Parsing)?); + let left = recurse(binop.lhs().ok_or(ERR_PARSING)?); + let right = recurse(binop.rhs().ok_or(ERR_PARSING)?); macro_rules! binop_source { ( $op:expr ) => { TreeSource::BinOp { @@ -58,11 +58,15 @@ impl Tree { } }; } - let op = match binop.operator() { + match binop.operator() { And => TreeSource::BoolAnd { left, right }, Or => TreeSource::BoolOr { left, right }, Implication => TreeSource::Implication { left, right }, - IsSet => return Err(EvalError::Unimplemented("IsSet".to_string())), + IsSet => { + return Err(EvalError::Internal(InternalError::Unimplemented( + "IsSet".to_string(), + ))) + } Concat => binop_source!(BinOpKind::Concat), Update => binop_source!(BinOpKind::Update), Add => binop_source!(BinOpKind::Add), @@ -75,17 +79,16 @@ impl Tree { LessOrEq => binop_source!(BinOpKind::LessOrEq), More => binop_source!(BinOpKind::Greater), MoreOrEq => binop_source!(BinOpKind::GreaterOrEq), - }; - TreeSource::BinOp { op, left, right } + } } ParsedType::UnaryOp(unary) => { use rnix::types::UnaryOpKind; match unary.operator() { UnaryOpKind::Invert => TreeSource::UnaryInvert { - value: recurse(unary.value().ok_or(EvalError::Parsing)?), + value: recurse(unary.value().ok_or(ERR_PARSING)?), }, UnaryOpKind::Negate => TreeSource::UnaryNegate { - value: recurse(unary.value().ok_or(EvalError::Parsing)?), + value: recurse(unary.value().ok_or(ERR_PARSING)?), }, } } @@ -93,23 +96,27 @@ impl Tree { use rnix::value::Value::*; // Booleans `true` and `false` are global variables, not literals TreeSource::Literal { - value: match literal.to_value().map_err(|_| EvalError::Parsing)? { + value: match literal.to_value().map_err(|_| ERR_PARSING)? { Float(x) => NixValue::Float(x), Integer(x) => NixValue::Integer(x), String(_) => { - return Err(EvalError::Unimplemented("string literal".to_string())) + return Err(EvalError::Internal(InternalError::Unimplemented( + "string literal".to_string(), + ))) } Path(_, _) => { - return Err(EvalError::Unimplemented("path literal".to_string())) + return Err(EvalError::Internal(InternalError::Unimplemented( + "path literal".to_string(), + ))) } }, } } node => { - return Err(EvalError::Unimplemented(format!( + return Err(EvalError::Internal(InternalError::Unimplemented(format!( "rnix-parser node {:?}", node - ))) + )))) } }; Ok(Self { diff --git a/src/value.rs b/src/value.rs index a0a7777..7879a49 100644 --- a/src/value.rs +++ b/src/value.rs @@ -1,4 +1,4 @@ -use crate::EvalError; +use crate::{EvalError, error::ValueError}; use gc::{Finalize, Trace}; use std::fmt::Debug; @@ -10,7 +10,7 @@ pub enum NixValue { } impl NixValue { - fn type_name(&self) -> String { + pub fn type_name(&self) -> String { match self { NixValue::Bool(_) => "bool", NixValue::Float(_) => "float", @@ -22,10 +22,10 @@ impl NixValue { pub fn as_bool(&self) -> Result { match self { NixValue::Bool(x) => Ok(*x), - _ => Err(EvalError::TypeError(format!( + _ => Err(EvalError::Value(ValueError::TypeError(format!( "expected bool, got {}", self.type_name() - ))), + )))), } } @@ -33,10 +33,10 @@ impl NixValue { pub fn as_int(&self) -> Result { match self { NixValue::Integer(x) => Ok(*x), - _ => Err(EvalError::TypeError(format!( + _ => Err(EvalError::Value(ValueError::TypeError(format!( "expected int, got {}", self.type_name() - ))), + )))), } } @@ -44,10 +44,10 @@ impl NixValue { pub fn as_float(&self) -> Result { match self { NixValue::Float(x) => Ok(*x), - _ => Err(EvalError::TypeError(format!( + _ => Err(EvalError::Value(ValueError::TypeError(format!( "expected float, got {}", self.type_name() - ))), + )))), } } From 27a28dcfd5767391b668708bdcca8df4d526b7f1 Mon Sep 17 00:00:00 2001 From: Aaron Janse Date: Sun, 19 Sep 2021 16:41:54 -0700 Subject: [PATCH 6/8] rename Tree to Expr --- src/error.rs | 2 +- src/eval.rs | 78 +++++++++++++++++++++++++-------------------------- src/lookup.rs | 4 +-- src/main.rs | 22 +++++++-------- src/parse.rs | 22 +++++++-------- src/scope.rs | 8 +++--- src/tests.rs | 4 +-- 7 files changed, 70 insertions(+), 70 deletions(-) diff --git a/src/error.rs b/src/error.rs index 51abc6e..e40c4ff 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,4 @@ -use gc::{Finalize, Gc, GcCell, Trace}; +use gc::{Finalize, Trace}; pub const ERR_PARSING: EvalError = EvalError::Internal(InternalError::Parsing); diff --git a/src/eval.rs b/src/eval.rs index d77abfd..0a7d097 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -7,57 +7,57 @@ use gc::{Finalize, Gc, GcCell, Trace}; use rnix::TextRange; use std::borrow::Borrow; -type TreeResult = Result, EvalError>; +type ExprResult = Result, EvalError>; -/// Used to lazily calculate the value of a Tree. This should be -/// tolerant of parsing and evaluation errors from child Trees. +/// Used to lazily calculate the value of a Expr. This should be +/// tolerant of parsing and evaluation errors from child Exprs. #[derive(Debug, Clone, Trace, Finalize)] -pub enum TreeSource { +pub enum ExprSource { Literal { value: NixValue, }, Paren { - inner: TreeResult, + inner: ExprResult, }, BinOp { op: BinOpKind, - left: TreeResult, - right: TreeResult, + left: ExprResult, + right: ExprResult, }, BoolAnd { - left: TreeResult, - right: TreeResult, + left: ExprResult, + right: ExprResult, }, BoolOr { - left: TreeResult, - right: TreeResult, + left: ExprResult, + right: ExprResult, }, Implication { - left: TreeResult, - right: TreeResult, + left: ExprResult, + right: ExprResult, }, UnaryInvert { - value: TreeResult, + value: ExprResult, }, UnaryNegate { - value: TreeResult, + value: ExprResult, }, } /// Syntax node that has context and can be lazily evaluated. #[derive(Clone, Trace, Finalize)] -pub struct Tree { +pub struct Expr { #[unsafe_ignore_trace] pub range: Option, pub value: GcCell>>, - pub source: TreeSource, + pub source: ExprSource, pub scope: Gc, } -impl std::fmt::Debug for Tree { +impl std::fmt::Debug for Expr { // The scope can be recursive, so we don't want to print it by default fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Tree") + f.debug_struct("Expr") .field("value", &self.value) .field("source", &self.source) .field("range", &self.range) @@ -65,8 +65,8 @@ impl std::fmt::Debug for Tree { } } -impl Tree { - /// Lazily evaluate a Tree, caching its value +impl Expr { + /// Lazily evaluate a Expr, caching its value pub fn eval(&self) -> Result, EvalError> { let mut value_borrow = self.value.borrow_mut(); if let Some(ref value) = *value_borrow { @@ -81,23 +81,23 @@ impl Tree { fn eval_uncached(&self) -> Result, EvalError> { match &self.source { - TreeSource::Paren { inner } => inner.as_ref()?.eval(), - TreeSource::Literal { value } => Ok(Gc::new(value.clone())), - TreeSource::BoolAnd { left, right } => { + ExprSource::Paren { inner } => inner.as_ref()?.eval(), + ExprSource::Literal { value } => Ok(Gc::new(value.clone())), + ExprSource::BoolAnd { left, right } => { if left.as_ref()?.eval()?.as_bool()? { right.as_ref()?.eval() } else { Ok(Gc::new(NixValue::Bool(false))) } } - TreeSource::BoolOr { left, right } => { + ExprSource::BoolOr { left, right } => { if !left.as_ref()?.eval()?.as_bool()? { right.as_ref()?.eval() } else { Ok(Gc::new(NixValue::Bool(true))) } } - TreeSource::Implication { left, right } => { + ExprSource::Implication { left, right } => { if !left.as_ref()?.eval()?.as_bool()? { Ok(Gc::new(NixValue::Bool(true))) } else { @@ -108,7 +108,7 @@ impl Tree { #[allow(clippy::enum_glob_use)] #[allow(clippy::float_cmp)] // We want to match the Nix reference implementation - TreeSource::BinOp { op, left, right } => { + ExprSource::BinOp { op, left, right } => { use BinOpKind::*; use NixValue::*; @@ -169,10 +169,10 @@ impl Tree { Ok(Gc::new(out)) } - TreeSource::UnaryInvert { value } => { + ExprSource::UnaryInvert { value } => { Ok(Gc::new(NixValue::Bool(!value.as_ref()?.eval()?.as_bool()?))) } - TreeSource::UnaryNegate { value } => { + ExprSource::UnaryNegate { value } => { Ok(Gc::new(match value.as_ref()?.eval()?.borrow() { NixValue::Integer(x) => NixValue::Integer(-x), NixValue::Float(x) => NixValue::Float(-x), @@ -186,17 +186,17 @@ impl Tree { } } - /// Used for recursing to find the Tree at a cursor position - pub fn children(&self) -> Vec<&Box> { + /// Used for recursing to find the Expr at a cursor position + pub fn children(&self) -> Vec<&Box> { match &self.source { - TreeSource::Paren { inner } => vec![inner], - TreeSource::Literal { value: _ } => vec![], - TreeSource::BinOp { op: _, left, right } => vec![left, right], - TreeSource::BoolAnd { left, right } => vec![left, right], - TreeSource::BoolOr { left, right } => vec![left, right], - TreeSource::Implication { left, right } => vec![left, right], - TreeSource::UnaryInvert { value } => vec![value], - TreeSource::UnaryNegate { value } => vec![value], + ExprSource::Paren { inner } => vec![inner], + ExprSource::Literal { value: _ } => vec![], + ExprSource::BinOp { op: _, left, right } => vec![left, right], + ExprSource::BoolAnd { left, right } => vec![left, right], + ExprSource::BoolOr { left, right } => vec![left, right], + ExprSource::Implication { left, right } => vec![left, right], + ExprSource::UnaryInvert { value } => vec![value], + ExprSource::UnaryNegate { value } => vec![value], } .into_iter() .map(|x| x.as_ref()) diff --git a/src/lookup.rs b/src/lookup.rs index 782fe44..00c2944 100644 --- a/src/lookup.rs +++ b/src/lookup.rs @@ -1,4 +1,4 @@ -use crate::{App, eval::Tree, utils::{self, Datatype, Var}}; +use crate::{App, eval::Expr, utils::{self, Datatype, Var}}; use lsp_types::Url; use rnix::{types::*, value::Value as ParsedValue, SyntaxNode}; use std::{ @@ -155,7 +155,7 @@ impl App { let ast = rnix::parse(&content); let node = ast.root().inner()?.clone(); let gc_root = Gc::new(Scope::Root(path)); - let evaluated = Tree::parse(node.clone(), gc_root); + let evaluated = Expr::parse(node.clone(), gc_root); placeholder.insert((ast, content, evaluated)); node } diff --git a/src/main.rs b/src/main.rs index a587e93..644d348 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,8 +33,8 @@ mod value; use error::{EvalError, ERR_PARSING}; use dirs::home_dir; -use eval::Tree; -use gc::{Finalize, Gc, Trace}; +use eval::Expr; +use gc::Gc; use log::{error, trace, warn}; use lsp_server::{Connection, ErrorCode, Message, Notification, Request, RequestId, Response}; use lsp_types::{ @@ -115,7 +115,7 @@ fn real_main() -> Result<(), Error> { } struct App { - files: HashMap)>, + files: HashMap)>, conn: Connection, } impl App { @@ -259,7 +259,7 @@ impl App { if let Ok(path) = PathBuf::from_str(params.text_document.uri.path()) { let gc_root = Gc::new(Scope::Root(path)); let parsed_root = parsed.root().inner().ok_or(ERR_PARSING); - let evaluated = parsed_root.and_then(|x| Tree::parse(x, gc_root)); + let evaluated = parsed_root.and_then(|x| Expr::parse(x, gc_root)); self.files .insert(params.text_document.uri, (parsed, text, evaluated)); } @@ -321,7 +321,7 @@ impl App { if let Ok(path) = PathBuf::from_str(uri.path()) { let gc_root = Gc::new(Scope::Root(path)); let parsed_root = parsed.root().inner().ok_or(ERR_PARSING); - let evaluated = parsed_root.and_then(|x| Tree::parse(x, gc_root)); + let evaluated = parsed_root.and_then(|x| Expr::parse(x, gc_root)); self.files .insert(uri, (parsed, content.to_owned().to_string(), evaluated)); } @@ -349,11 +349,11 @@ impl App { } } fn hover(&self, params: TextDocumentPositionParams) -> Option<(Option, String)> { - let (_, content, tree) = self.files.get(¶ms.text_document.uri)?; + let (_, content, expr) = self.files.get(¶ms.text_document.uri)?; let offset = utils::lookup_pos(content, params.position)?; - let child_tree = climb_tree(tree.as_ref().ok()?, offset).clone(); - let range = utils::range(content, child_tree.range?); - let msg = match child_tree.eval() { + let child_expr = climb_expr(expr.as_ref().ok()?, offset).clone(); + let range = utils::range(content, child_expr.range?); + let msg = match child_expr.eval() { Ok(value) => value.format_markdown(), Err(EvalError::Value(ref err)) => format!("{}", err), Err(EvalError::Internal(_)) => return None, @@ -511,7 +511,7 @@ impl App { } } -fn climb_tree(here: &Tree, offset: usize) -> &Tree { +fn climb_expr(here: &Expr, offset: usize) -> &Expr { for child in here.children().clone() { let range = match child.range { Some(x) => x, @@ -520,7 +520,7 @@ fn climb_tree(here: &Tree, offset: usize) -> &Tree { let start: usize = range.start().into(); let end: usize = range.end().into(); if start <= offset && offset <= end { - return climb_tree(child, offset); + return climb_expr(child, offset); } } here diff --git a/src/parse.rs b/src/parse.rs index 2c006f3..a70ac03 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -3,7 +3,7 @@ use std::convert::TryFrom; use crate::error::{EvalError, InternalError, ERR_PARSING}; use crate::value::*; use crate::{ - eval::{Tree, TreeSource}, + eval::{Expr, ExprSource}, scope::Scope, }; use gc::{Finalize, Gc, GcCell, Trace}; @@ -30,18 +30,18 @@ pub enum BinOpKind { NotEqual, } -impl Tree { +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 = |node| Tree::parse(node, scope.clone()).map(|x| Box::new(x)); + let recurse = |node| Expr::parse(node, scope.clone()).map(|x| Box::new(x)); let source = match ParsedType::try_from(node.clone()).map_err(|_| ERR_PARSING)? { ParsedType::Paren(paren) => { let inner = paren.inner().ok_or(ERR_PARSING)?; - TreeSource::Paren { + ExprSource::Paren { inner: recurse(inner), } } @@ -51,7 +51,7 @@ impl Tree { let right = recurse(binop.rhs().ok_or(ERR_PARSING)?); macro_rules! binop_source { ( $op:expr ) => { - TreeSource::BinOp { + ExprSource::BinOp { op: $op, left, right, @@ -59,9 +59,9 @@ impl Tree { }; } match binop.operator() { - And => TreeSource::BoolAnd { left, right }, - Or => TreeSource::BoolOr { left, right }, - Implication => TreeSource::Implication { left, right }, + And => ExprSource::BoolAnd { left, right }, + Or => ExprSource::BoolOr { left, right }, + Implication => ExprSource::Implication { left, right }, IsSet => { return Err(EvalError::Internal(InternalError::Unimplemented( "IsSet".to_string(), @@ -84,10 +84,10 @@ impl Tree { ParsedType::UnaryOp(unary) => { use rnix::types::UnaryOpKind; match unary.operator() { - UnaryOpKind::Invert => TreeSource::UnaryInvert { + UnaryOpKind::Invert => ExprSource::UnaryInvert { value: recurse(unary.value().ok_or(ERR_PARSING)?), }, - UnaryOpKind::Negate => TreeSource::UnaryNegate { + UnaryOpKind::Negate => ExprSource::UnaryNegate { value: recurse(unary.value().ok_or(ERR_PARSING)?), }, } @@ -95,7 +95,7 @@ impl Tree { ParsedType::Value(literal) => { use rnix::value::Value::*; // Booleans `true` and `false` are global variables, not literals - TreeSource::Literal { + ExprSource::Literal { value: match literal.to_value().map_err(|_| ERR_PARSING)? { Float(x) => NixValue::Float(x), Integer(x) => NixValue::Integer(x), diff --git a/src/scope.rs b/src/scope.rs index f71a7f3..4b486a8 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -1,8 +1,8 @@ -use crate::eval::Tree; +use crate::eval::Expr; use gc::{Finalize, Gc, Trace}; use std::path::PathBuf; -/// A parent Tree's scope is used to provide tooling for its child Trees. +/// A parent Expr's scope is used to provide tooling for its child Exprs. /// This enum would provide four scope types: /// - None: Used for calculated literals. For example, for the string /// interpolation `"prefix${suffix}"`, the literal `prefix` by itself @@ -23,7 +23,7 @@ pub enum Scope { } impl Scope { - /// Finds the Tree of an identifier in the scope. + /// Finds the Expr of an identifier in the scope. /// /// This would do two passes up the tree: /// 1. Check Scope::Normal and Scope::Root @@ -42,7 +42,7 @@ impl Scope { /// «primop» # found in Scope::Root, which we reach before Scope::With /// ``` #[allow(dead_code)] // this function will be implemented later - pub fn get(&self, _name: &str) -> Option> { + pub fn get(&self, _name: &str) -> Option> { None } } diff --git a/src/tests.rs b/src/tests.rs index 1880eba..c412d7a 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,4 +1,4 @@ -use eval::Tree; +use eval::Expr; use gc::Gc; use rnix::types::Wrapper; use scope::Scope; @@ -10,7 +10,7 @@ fn eval(code: &str) -> NixValue { let ast = rnix::parse(&code); let root = ast.root().inner().unwrap(); let path = std::env::current_dir().unwrap(); - let out = Tree::parse(root, Gc::new(Scope::Root(path))).unwrap(); + let out = Expr::parse(root, Gc::new(Scope::Root(path))).unwrap(); let tmp = out.eval(); let val: &NixValue = tmp.as_ref().unwrap().borrow(); val.clone() From c3a4b254bf610c4c25d5a4ca1ae7a4d020acea4c Mon Sep 17 00:00:00 2001 From: Aaron Janse Date: Fri, 24 Sep 2021 14:56:01 -0700 Subject: [PATCH 7/8] fix variable name --- src/lookup.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lookup.rs b/src/lookup.rs index 6b9fde9..2b6c01f 100644 --- a/src/lookup.rs +++ b/src/lookup.rs @@ -155,8 +155,8 @@ impl App { let ast = rnix::parse(&content); let node = ast.root().inner()?.clone(); let gc_root = Gc::new(Scope::Root(path)); - let evaluated = Expr::parse(node.clone(), gc_root); - placeholder.insert((ast, content, evaluated)); + let parsed = Expr::parse(node.clone(), gc_root); + placeholder.insert((ast, content, parsed)); node } }; From 5127f5b74ad4ee9cd133b33514e2293e3f49c5df Mon Sep 17 00:00:00 2001 From: Aaron Janse Date: Fri, 24 Sep 2021 14:58:14 -0700 Subject: [PATCH 8/8] fix off-by-one error --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 80c7bd4..8bfa1d1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -567,7 +567,7 @@ fn climb_expr(here: &Expr, offset: usize) -> &Expr { }; let start: usize = range.start().into(); let end: usize = range.end().into(); - if start <= offset && offset <= end { + if start <= offset && offset < end { return climb_expr(child, offset); } }