diff --git a/Cargo.lock b/Cargo.lock index 05ad0a9..31fda41 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.18" @@ -180,6 +182,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.2.3" @@ -434,6 +457,7 @@ version = "0.1.0" dependencies = [ "dirs", "env_logger", + "gc", "lazy_static", "libc", "log", @@ -547,6 +571,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 258c9e9..7fed5e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ version = "0.1.0" [dependencies] dirs = "3.0.2" env_logger = "0.9.0" +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/error.rs b/src/error.rs new file mode 100644 index 0000000..e40c4ff --- /dev/null +++ b/src/error.rs @@ -0,0 +1,58 @@ +use gc::{Finalize, 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 new file mode 100644 index 0000000..0a7d097 --- /dev/null +++ b/src/eval.rs @@ -0,0 +1,206 @@ +use crate::error::{InternalError, ValueError}; +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 ExprResult = Result, EvalError>; + +/// 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 ExprSource { + Literal { + value: NixValue, + }, + Paren { + inner: ExprResult, + }, + BinOp { + op: BinOpKind, + left: ExprResult, + right: ExprResult, + }, + BoolAnd { + left: ExprResult, + right: ExprResult, + }, + BoolOr { + left: ExprResult, + right: ExprResult, + }, + Implication { + left: ExprResult, + right: ExprResult, + }, + UnaryInvert { + value: ExprResult, + }, + UnaryNegate { + value: ExprResult, + }, +} + +/// Syntax node that has context and can be lazily evaluated. +#[derive(Clone, Trace, Finalize)] +pub struct Expr { + #[unsafe_ignore_trace] + pub range: Option, + pub value: GcCell>>, + pub source: ExprSource, + pub scope: Gc, +} + +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("Expr") + .field("value", &self.value) + .field("source", &self.source) + .field("range", &self.range) + .finish() + } +} + +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 { + Ok(value.clone()) + } else { + // We can later build a stack trace by wrapping errors here + let value = self.eval_uncached()?; + *value_borrow = Some(value.clone()); + Ok(value) + } + } + + fn eval_uncached(&self) -> Result, EvalError> { + match &self.source { + 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))) + } + } + ExprSource::BoolOr { left, right } => { + if !left.as_ref()?.eval()?.as_bool()? { + right.as_ref()?.eval() + } else { + Ok(Gc::new(NixValue::Bool(true))) + } + } + ExprSource::Implication { left, right } => { + if !left.as_ref()?.eval()?.as_bool()? { + Ok(Gc::new(NixValue::Bool(true))) + } else { + right.as_ref()?.eval() + } + } + + #[allow(clippy::enum_glob_use)] + #[allow(clippy::float_cmp)] + // We want to match the Nix reference implementation + ExprSource::BinOp { op, left, right } => { + use BinOpKind::*; + use NixValue::*; + + // 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(); + + // Specially handle integer division by zero + if let (Div, Integer(_), Integer(0)) = (op, left_val, right_val) { + 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 ),* ) => { + 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 => <=, + ], + _ => { + // 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 + )))) + } + }; + + Ok(Gc::new(out)) + } + ExprSource::UnaryInvert { value } => { + Ok(Gc::new(NixValue::Bool(!value.as_ref()?.eval()?.as_bool()?))) + } + ExprSource::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::Value(ValueError::TypeError( + "cannot negate a non-number".to_string(), + ))) + } + })) + } + } + } + + /// Used for recursing to find the Expr at a cursor position + pub fn children(&self) -> Vec<&Box> { + match &self.source { + 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()) + .filter_map(Result::ok) + .collect() + } +} diff --git a/src/lookup.rs b/src/lookup.rs index 30291b6..2b6c01f 100644 --- a/src/lookup.rs +++ b/src/lookup.rs @@ -1,7 +1,4 @@ -use crate::{ - utils::{self, Datatype, Var}, - App, -}; +use crate::{App, eval::Expr, 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 parsed = Expr::parse(node.clone(), gc_root); + placeholder.insert((ast, content, parsed)); node } }; diff --git a/src/main.rs b/src/main.rs index df4d95a..8bfa1d1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,10 +21,20 @@ clippy::integer_arithmetic, )] +mod error; +mod eval; mod lookup; +mod parse; +mod scope; +mod tests; mod utils; +mod value; + +use error::{EvalError, ERR_PARSING}; use dirs::home_dir; +use eval::Expr; +use gc::Gc; use log::{error, trace, warn}; use lsp_server::{Connection, ErrorCode, Message, Notification, Request, RequestId, Response}; use lsp_types::{ @@ -39,12 +49,14 @@ use rnix::{ value::{Anchor as RAnchor, Value as RValue}, SyntaxNode, TextRange, TextSize, }; +use scope::Scope; use std::{ collections::{HashMap, VecDeque}, panic, path::{Path, PathBuf}, process, rc::Rc, + str::FromStr, }; type Error = Box; @@ -82,6 +94,7 @@ fn real_main() -> Result<(), Error> { resolve_provider: Some(false), work_done_progress_options: WorkDoneProgressOptions::default(), }), + hover_provider: Some(HoverProviderCapability::Simple(true)), rename_provider: Some(OneOf::Left(true)), selection_range_provider: Some(SelectionRangeProviderCapability::Simple(true)), ..ServerCapabilities::default() @@ -102,7 +115,7 @@ fn real_main() -> Result<(), Error> { } struct App { - files: HashMap, + files: HashMap)>, conn: Connection, } impl App { @@ -193,7 +206,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 +216,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)); } @@ -230,7 +258,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(ERR_PARSING); + let evaluated = parsed_root.and_then(|x| Expr::parse(x, gc_root)); + self.files + .insert(params.text_document.uri, (parsed, text, evaluated)); + } } DidChangeTextDocument::METHOD => { // Per the language server spec (https://git.io/JcrvY), we should apply changes @@ -286,22 +320,27 @@ 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(ERR_PARSING); + let evaluated = parsed_root.and_then(|x| Expr::parse(x, gc_root)); + self.files + .insert(uri, (parsed, content.to_owned().to_string(), evaluated)); + } } _ => (), } 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()), @@ -310,9 +349,22 @@ impl App { None } } + fn hover(&self, params: HoverParams) -> Option<(Option, String)> { + let pos_params = params.text_document_position_params; + let (_, content, expr) = self.files.get(&pos_params.text_document.uri)?; + let offset = utils::lookup_pos(content, pos_params.position)?; + 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, + }; + Some((Some(range), msg)) + } #[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(); @@ -320,7 +372,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 { @@ -374,7 +426,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() { @@ -398,7 +450,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(); @@ -506,3 +558,18 @@ impl App { Ok(()) } } + +fn climb_expr(here: &Expr, offset: usize) -> &Expr { + 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_expr(child, offset); + } + } + here +} diff --git a/src/parse.rs b/src/parse.rs new file mode 100644 index 0000000..a70ac03 --- /dev/null +++ b/src/parse.rs @@ -0,0 +1,129 @@ +use std::convert::TryFrom; + +use crate::error::{EvalError, InternalError, ERR_PARSING}; +use crate::value::*; +use crate::{ + eval::{Expr, ExprSource}, + scope::Scope, +}; +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 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| 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)?; + ExprSource::Paren { + inner: recurse(inner), + } + } + ParsedType::BinOp(binop) => { + use rnix::types::BinOpKind::*; + let left = recurse(binop.lhs().ok_or(ERR_PARSING)?); + let right = recurse(binop.rhs().ok_or(ERR_PARSING)?); + macro_rules! binop_source { + ( $op:expr ) => { + ExprSource::BinOp { + op: $op, + left, + right, + } + }; + } + match binop.operator() { + 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(), + ))) + } + 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), + } + } + ParsedType::UnaryOp(unary) => { + use rnix::types::UnaryOpKind; + match unary.operator() { + UnaryOpKind::Invert => ExprSource::UnaryInvert { + value: recurse(unary.value().ok_or(ERR_PARSING)?), + }, + UnaryOpKind::Negate => ExprSource::UnaryNegate { + value: recurse(unary.value().ok_or(ERR_PARSING)?), + }, + } + } + ParsedType::Value(literal) => { + use rnix::value::Value::*; + // Booleans `true` and `false` are global variables, not literals + ExprSource::Literal { + value: match literal.to_value().map_err(|_| ERR_PARSING)? { + Float(x) => NixValue::Float(x), + Integer(x) => NixValue::Integer(x), + String(_) => { + return Err(EvalError::Internal(InternalError::Unimplemented( + "string literal".to_string(), + ))) + } + Path(_, _) => { + return Err(EvalError::Internal(InternalError::Unimplemented( + "path literal".to_string(), + ))) + } + }, + } + } + node => { + return Err(EvalError::Internal(InternalError::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..4b486a8 --- /dev/null +++ b/src/scope.rs @@ -0,0 +1,48 @@ +use crate::eval::Expr; +use gc::{Finalize, Gc, Trace}; +use std::path::PathBuf; + +/// 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 +/// 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 Expr 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..c412d7a --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,43 @@ +use eval::Expr; +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 = Expr::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..7879a49 --- /dev/null +++ b/src/value.rs @@ -0,0 +1,74 @@ +use crate::{EvalError, error::ValueError}; +use gc::{Finalize, Trace}; +use std::fmt::Debug; + +#[derive(Clone, Trace, Finalize)] +pub enum NixValue { + Bool(bool), + Float(f64), + Integer(i64), +} + +impl NixValue { + pub 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::Value(ValueError::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::Value(ValueError::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::Value(ValueError::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), + } + } +} +