diff --git a/Cargo.lock b/Cargo.lock index 69319d6201..1b54f4dab1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -346,6 +346,12 @@ dependencies = [ "crossterm", ] +[[package]] +name = "countme" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" + [[package]] name = "cpp_demangle" version = "0.3.5" @@ -849,6 +855,12 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "heck" version = "0.3.3" @@ -891,7 +903,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6012d540c5baa3589337a98ce73408de9b5a25ec9fc2c6fd6be8f0d39e0ca5a" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.11.2", ] [[package]] @@ -1239,6 +1251,8 @@ dependencies = [ "pretty", "pretty_assertions", "regex", + "rnix", + "rowan", "rustyline", "rustyline-derive", "serde", @@ -1892,12 +1906,40 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "rnix" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb35cedbeb70e0ccabef2a31bcff0aebd114f19566086300b8f42c725fc2cb5f" +dependencies = [ + "rowan", +] + +[[package]] +name = "rowan" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5811547e7ba31e903fe48c8ceab10d40d70a101f3d15523c847cce91aa71f332" +dependencies = [ + "countme", + "hashbrown 0.12.3", + "memoffset", + "rustc-hash", + "text-size", +] + [[package]] name = "rustc-demangle" version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc_version" version = "0.4.0" @@ -2357,6 +2399,12 @@ dependencies = [ "syn 0.15.44", ] +[[package]] +name = "text-size" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "288cb548dbe72b652243ea797201f3d481a0609a967980fcc5b2315ea811560a" + [[package]] name = "textwrap" version = "0.11.0" diff --git a/Cargo.toml b/Cargo.toml index d68dc0d934..31679c7f0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ markdown = ["termimad"] repl = ["rustyline", "rustyline-derive", "ansi_term"] repl-wasm = ["wasm-bindgen", "js-sys", "serde_repr"] doc = [ "comrak" ] +nix = [ "rnix", "rowan" ] [build-dependencies] lalrpop = "0.19.6" @@ -64,6 +65,10 @@ comrak = { version = "0.12.1", optional = true, features = [] } once_cell = "1.14.0" typed-arena = "2.0.1" +# needed dependencies to be able to interpret nix expr with nickel +rnix = { version = "0.11", optional = true } +rowan = { version = "0.15.10", optional = true } + [dev-dependencies] pretty_assertions = "1.2.1" assert_matches = "1.5.0" diff --git a/flake.nix b/flake.nix index 64621227c4..17e1602be3 100644 --- a/flake.nix +++ b/flake.nix @@ -162,6 +162,7 @@ mkFilter = regexp: path: _type: builtins.match regexp path != null; lalrpopFilter = mkFilter ".*lalrpop$"; nclFilter = mkFilter ".*ncl$"; + nixFilter = mkFilter ".*/tests/nix/.+\\.nix$"; txtFilter = mkFilter ".*txt$"; snapFilter = mkFilter ".*snap$"; in @@ -174,6 +175,7 @@ builtins.any (filter: filter path type) [ lalrpopFilter nclFilter + nixFilter txtFilter snapFilter filterCargoSources diff --git a/lsp/nls/src/requests/completion.rs b/lsp/nls/src/requests/completion.rs index bcbe5e33ba..fa0017351a 100644 --- a/lsp/nls/src/requests/completion.rs +++ b/lsp/nls/src/requests/completion.rs @@ -90,7 +90,7 @@ impl IdentWithType { if name.is_ascii() { String::from(name) } else { - format!("\"{}\"", name) + format!("\"{name}\"") } } let doc = || { @@ -814,7 +814,7 @@ mod tests { let actual = get_identifier_path(input); let expected: Option> = expected.map(|path| path.iter().map(|s| String::from(*s)).collect()); - assert_eq!(actual, expected, "test failed: {}", case_name) + assert_eq!(actual, expected, "test failed: {case_name}") } } diff --git a/src/bin/nickel.rs b/src/bin/nickel.rs index 07151d3784..438bb74db8 100644 --- a/src/bin/nickel.rs +++ b/src/bin/nickel.rs @@ -7,6 +7,8 @@ use nickel_lang::repl::query_print; use nickel_lang::repl::rustyline_frontend; use nickel_lang::term::{RichTerm, Term}; use nickel_lang::{serialize, serialize::ExportFormat}; +#[cfg(feature = "nix")] +use std::io::Read; use std::path::{Path, PathBuf}; use std::{ fs::{self, File}, @@ -40,6 +42,10 @@ struct Opt { /// Available subcommands. #[derive(StructOpt, Debug)] enum Command { + /// translate Nix input to Nickel code. + /// Only a POC, main target is to be able to run Nix code on nickel. + /// May never be a complet source to source transformation. + Nixin, /// Converts the parsed representation (AST) back to Nickel source code and prints it. Used for /// debugging purpose PprintAst { @@ -108,6 +114,34 @@ fn main() { #[cfg(not(feature = "repl"))] eprintln!("error: this executable was not compiled with REPL support"); + } else if let Some(Command::Nixin) = opts.command { + #[cfg(feature = "nix")] + { + use nickel_lang::cache::Cache; + use nickel_lang::cache::ErrorTolerance; + use nickel_lang::pretty::*; + use pretty::BoxAllocator; + + let mut buf = String::new(); + let mut cache = Cache::new(ErrorTolerance::Strict); + let mut out: Vec = Vec::new(); + opts.file + .map(std::fs::File::open) + .map(|f| f.and_then(|mut f| f.read_to_string(&mut buf))) + .unwrap_or_else(|| std::io::stdin().read_to_string(&mut buf)) + .unwrap_or_else(|err| { + eprintln!("Error when reading input: {err}"); + process::exit(1) + }); + let allocator = BoxAllocator; + let file_id = cache.add_source(".nix", buf.as_bytes()).unwrap(); + let rt = nickel_lang::nix::parse(&cache, file_id).unwrap(); + let doc: DocBuilder<_, ()> = rt.pretty(&allocator); + doc.render(80, &mut out).unwrap(); + println!("{}", String::from_utf8_lossy(&out).as_ref()); + } + #[cfg(not(feature = "nix"))] + eprintln!("error: this executable was not compiled with Nix evaluation support"); } else { let mut program = opts .file @@ -158,7 +192,7 @@ fn main() { }) } Some(Command::Typecheck) => program.typecheck(), - Some(Command::Repl { .. }) => unreachable!(), + Some(Command::Repl { .. }) | Some(Command::Nixin) => unreachable!(), #[cfg(feature = "doc")] Some(Command::Doc { ref output }) => output .as_ref() diff --git a/src/cache.rs b/src/cache.rs index 4fbb713f8f..5d2e458f81 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -28,6 +28,7 @@ use void::Void; #[derive(Clone, Copy, Eq, Debug, PartialEq)] pub enum InputFormat { Nickel, + Nix, Json, Yaml, Toml, @@ -37,6 +38,19 @@ impl InputFormat { fn from_path_buf(path_buf: &Path) -> Option { match path_buf.extension().and_then(OsStr::to_str) { Some("ncl") => Some(InputFormat::Nickel), + Some("nix") => { + #[cfg(feature = "nix")] + { + Some(InputFormat::Nix) + } + #[cfg(not(feature = "nix"))] + { + eprintln!( + "error: this executable was not compiled with Nix evaluation support" + ); + None + } + } Some("json") => Some(InputFormat::Json), Some("yaml") | Some("yml") => Some(InputFormat::Yaml), Some("toml") => Some(InputFormat::Toml), @@ -457,6 +471,22 @@ impl Cache { Ok((t, parse_errs)) } + // TODO: Error management for parse errors. + // May be better to throw an error instead of panicing if nickel has been compiled + // without Nix support + InputFormat::Nix => { + #[cfg(feature = "nix")] + { + Ok(( + crate::nix::parse(self, file_id).unwrap(), + ParseErrors::default(), + )) + } + #[cfg(not(feature = "nix"))] + { + panic!("error: this executable was not compiled with Nix evaluation support") + } + } InputFormat::Json => serde_json::from_str(self.files.source(file_id)) .map(|t| (t, ParseErrors::default())) .map_err(|err| ParseError::from_serde_json(err, file_id, &self.files)), diff --git a/src/conversion.rs b/src/conversion.rs new file mode 100644 index 0000000000..26512f1bf1 --- /dev/null +++ b/src/conversion.rs @@ -0,0 +1,38 @@ +//! This module contains a trait to implement on every thing you want to be convertible into nickel +//! AST. +//! It should be used mostly for AST to AST convertion (e.g.: nix to nickel). Effectively the +//! `translate` function require to pass a `codespan::FileId` so it's not wors to use it for +//! convertions which not imply a file/stream input. For these convertions, prefer `Into`/`From` +//! standart traits. + +use crate::term::RichTerm; +use codespan::FileId; +use std::collections::HashSet; + +/// State of the conversion. It contains the definitions in scope of the currently converted node (e.g.: +/// `with` environments, declared variables, current file id...), required for elaborate compilation (`with`). +#[derive(Clone)] +pub struct State { + /// The current transformation file ID. + pub file_id: FileId, + /// Variables in scope. + pub env: HashSet, + /// With scope. List of the records on which has been applied a with in the current scope. + pub with: Vec, +} + +pub trait ToNickel: Sized { + /// Used when converting a full file. Actually call `translate` with an initial `State`. + fn to_nickel(self, file_id: FileId) -> RichTerm { + let state = State { + file_id, + env: HashSet::new(), + with: Vec::new(), + }; + self.translate(&state) + } + + /// Use to convert an expression or a term of the source language to nickel. + /// For a complet exemple, you can see `crate::nix`, the convertion from Nix to Nickel. + fn translate(self, state: &State) -> RichTerm; +} diff --git a/src/error.rs b/src/error.rs index 3dc4b8f1f6..fabda766d2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -314,6 +314,11 @@ impl IntoDiagnostics for ParseErrors { /// An error occurring during parsing. #[derive(Debug, PartialEq, Eq, Clone)] pub enum ParseError { + #[cfg(feature = "nix")] + /// Temporary Nix error variant. + /// TODO: because parsing errors are almost the same between Nix and Nickel, Have a seamless + /// convertion from Nix errors to Nickel ones could be a good improvement. + NixParseError(FileId), /// Unexpected end of file. UnexpectedEOF(FileId, /* tokens expected by the parser */ Vec), /// Unexpected token. @@ -1399,6 +1404,22 @@ impl IntoDiagnostics for ParseError { _stdlib_ids: Option<&Vec>, ) -> Vec> { let diagnostic = match self { + // TODO: improve error management for nix parser. + #[cfg(feature = "nix")] + ParseError::NixParseError(file_id) => { + let end = files.source_span(file_id).end(); + let start = files.source_span(file_id).start(); + Diagnostic::error() + .with_message(format!( + "error parsing nix file {}", + files.name(file_id).to_string_lossy() + )) + .with_labels(vec![primary(&RawSpan { + start, + end, + src_id: file_id, + })]) + } ParseError::UnexpectedEOF(file_id, _expected) => { let end = files.source_span(file_id).end(); Diagnostic::error() diff --git a/src/lib.rs b/src/lib.rs index eda28e2c6f..2a07d4cddb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod cache; +pub mod conversion; pub mod deserialize; pub mod destructuring; pub mod environment; @@ -6,6 +7,8 @@ pub mod error; pub mod eval; pub mod identifier; pub mod label; +#[cfg(feature = "nix")] +pub mod nix; pub mod parser; pub mod position; pub mod pretty; diff --git a/src/nix.rs b/src/nix.rs new file mode 100644 index 0000000000..b6d3a2cff8 --- /dev/null +++ b/src/nix.rs @@ -0,0 +1,415 @@ +use crate::cache::Cache; +use crate::conversion::State; +pub use crate::conversion::ToNickel; +use crate::identifier::Ident; +use crate::mk_app; +use crate::parser::utils::{mk_span, FieldPathElem}; +use crate::position::TermPos; +use crate::term::make::{self, if_then_else}; +use crate::term::{record::RecordData, BinaryOp, UnaryOp}; +use crate::term::{ + record::{Field, FieldMetadata}, + MergePriority, RichTerm, Term, +}; +use codespan::FileId; +use rnix::ast::{ + Attr as NixAttr, BinOp as NixBinOp, Ident as NixIdent, Str as NixStr, UnaryOp as NixUniOp, +}; +use rowan::ast::AstNode; +use std::collections::HashMap; + +fn path_elem_from_nix(attr: NixAttr, state: &State) -> FieldPathElem { + match attr { + NixAttr::Ident(id) => FieldPathElem::Ident(id_from_nix(id, state)), + NixAttr::Str(s) => FieldPathElem::Expr(s.translate(state)), + NixAttr::Dynamic(d) => FieldPathElem::Expr(d.expr().unwrap().translate(state)), + } +} + +fn path_elem_rt(attr: NixAttr, state: &State) -> RichTerm { + match attr { + NixAttr::Ident(id) => Term::Str(id.to_string()).into(), + NixAttr::Str(s) => s.translate(state), + NixAttr::Dynamic(d) => d.expr().unwrap().translate(state), + } +} + +fn path_rts_from_nix(n: rnix::ast::Attrpath, state: &State) -> T +where + T: FromIterator, +{ + n.attrs().map(|a| path_elem_rt(a, state)).collect() +} + +fn id_from_nix(id: NixIdent, state: &State) -> Ident { + let pos = id.syntax().text_range(); + let span = mk_span(state.file_id, pos.start().into(), pos.end().into()); + Ident::new_with_pos(id.to_string(), crate::position::TermPos::Original(span)) +} + +impl ToNickel for NixStr { + fn translate(self, state: &State) -> RichTerm { + use rnix::ast::InterpolPart; + Term::StrChunks( + self.parts() + .enumerate() + .map(|(i, c)| match c { + InterpolPart::Literal(s) => crate::term::StrChunk::Literal(s.to_string()), + InterpolPart::Interpolation(interp) => { + crate::term::StrChunk::Expr(interp.expr().unwrap().translate(state), i) + } + }) + .collect(), + ) + .into() + } +} + +impl ToNickel for NixUniOp { + fn translate(self, state: &State) -> RichTerm { + use rnix::ast::UnaryOpKind::*; + let value = self.expr().unwrap().translate(state); + match self.operator().unwrap() { + Negate => make::op2(BinaryOp::Sub(), Term::Num(0.), value), + Invert => make::op1(UnaryOp::BoolNot(), value), + } + } +} + +impl ToNickel for NixBinOp { + fn translate(self, state: &State) -> RichTerm { + use rnix::ast::BinOpKind::*; + let lhs = self.lhs().unwrap().translate(state); + let rhs = self.rhs().unwrap().translate(state); + match self.operator().unwrap() { + Concat => make::op2(BinaryOp::ArrayConcat(), lhs, rhs), + // TODO: the Nix `//` operator. + Update => mk_app!(crate::stdlib::compat::update(), lhs, rhs), + + // Use a compatibility function to be able to merge strings with the same operator used + // for addition. + Add => mk_app!(crate::stdlib::compat::add(), lhs, rhs), + Sub => make::op2(BinaryOp::Sub(), lhs, rhs), + Mul => make::op2(BinaryOp::Mult(), lhs, rhs), + Div => make::op2(BinaryOp::Div(), lhs, rhs), + + Equal => make::op2(BinaryOp::Eq(), lhs, rhs), + Less => make::op2(BinaryOp::LessThan(), lhs, rhs), + More => make::op2(BinaryOp::GreaterThan(), lhs, rhs), + LessOrEq => make::op2(BinaryOp::LessOrEq(), lhs, rhs), + MoreOrEq => make::op2(BinaryOp::GreaterOrEq(), lhs, rhs), + NotEqual => make::op1(UnaryOp::BoolNot(), make::op2(BinaryOp::Eq(), lhs, rhs)), + + // the Nix `->` operator. + // if the lhs is true, then it return the boolean value of rhs. If lhs is false, the + // implication is alwais true. + Implication => if_then_else(lhs, rhs, Term::Bool(true)), + + // In Nickel as oposit to Nix, the `&&` and `||` operators are unary operators. + And => mk_app!(Term::Op1(UnaryOp::BoolAnd(), lhs), rhs), + Or => mk_app!(Term::Op1(UnaryOp::BoolOr(), lhs), rhs), + } + } +} + +impl ToNickel for rnix::ast::Expr { + fn translate(self, state: &State) -> RichTerm { + use rnix::ast::Expr; + let pos = self.syntax().text_range(); + let file_id = state.file_id; + let span = mk_span(file_id, pos.start().into(), pos.end().into()); + println!("{self:?}: {self}"); + match self { + // This is a parse error of the nix code. + // it's translated to a Nickel internal error specific for nix code (`NixParseError`) + // May not be the better way to do, but this version of the code does not realy have + // error management for the nix side. + Expr::Error(_) => { + Term::ParseError(crate::error::ParseError::NixParseError(file_id)).into() + // TODO: Improve error management + } + // The Root of a file. generaly, this field is not matched because the common way to + // translate is as we do in `parse` function below. Like this, we pass a actual `Expr` + // to this function and not the `Root` wrapper. + // Anyway we prefer to manage it, in case the caller pass a `Expr` casted from + // `rowan::AstNode`. + Expr::Root(n) => n.expr().unwrap().translate(state), + Expr::Paren(n) => n.expr().unwrap().translate(state), + + // TODO: Will we have an `Assert` contract at some point in the stdlib or do we implement it + // in `stdlib::compat`? + Expr::Assert(_) => unimplemented!(), + + // Some specificity around Nix literals or better said, on how `rnix` parse the + // literals: + // - It differenciate floats and integers. We then convertboth to floats. + // - For some reason, `Uri`s are concidered literals, but `Str` and `Path` are not. + Expr::Literal(n) => match n.kind() { + rnix::ast::LiteralKind::Float(v) => Term::Num(v.value().unwrap()), + rnix::ast::LiteralKind::Integer(v) => Term::Num(v.value().unwrap() as f64), + // TODO: How to manage Uris in nickel? + // What should be the nickel internal representation? + // String could be ok, but what if we give it back to a Nix expr? + // Apologise, not sure of the output of `Uri::to_string` + rnix::ast::LiteralKind::Uri(v) => Term::Str(v.to_string()), + } + .into(), + // That's what we call a multiline string in Nickel. Nix don't have the concept of + // string literal (e.g.: `Term::Str` of Nickel) + Expr::Str(n) => n.translate(state), + Expr::List(n) => Term::Array( + n.items().map(|elm| elm.translate(state)).collect(), + Default::default(), + ) + .into(), + Expr::AttrSet(n) => { + use crate::parser::utils::{build_record, FieldDef}; + use rnix::ast::HasEntry; + // TODO: Before that, we have to fill `state.env` when the attrset is recursive. + // As it is now, a with brought value take precedence over the fields of the + // recursive record which is incorrect. + let fields: Vec<(_, _)> = n + .attrpath_values() + .map(|kv| { + let val = kv.value().unwrap().translate(state); + let path: Vec<_> = kv + .attrpath() + .unwrap() + .attrs() + .map(|e| path_elem_from_nix(e, state)) + .collect(); + let field_def = FieldDef { + path, + field: Field::from(val), + pos: TermPos::None, + }; + field_def.elaborate() + }) + .collect(); + build_record(fields, Default::default()).into() + } + + // In nix it's allowed to define vars named `true`, `false` or `null`. + // But we prefer to not support it. If we try to redefine one of these builtins, nickel + // will panic (see below in the `LetIn` arm). + Expr::Ident(id) => match id.to_string().as_str() { + "true" => Term::Bool(true), + "false" => Term::Bool(false), + "null" => Term::Null, + id_str => { + // Compatibility with the Nix `with` construct. It look if the identifier has + // been staticaly defined and if not, it look for it in the `with` broughts + // identifiers. + if state.env.contains(id_str) || state.with.is_empty() { + Term::Var(id_from_nix(id, state)) + } else { + Term::App( + crate::stdlib::compat::with(state.with.clone().into_iter().collect()), + Term::Str(id.to_string()).into(), + ) + } + } + } + .into(), + Expr::LegacyLet(_) => panic!("Legacy let form is not supported"), // Probably useless to support it in a short term. + // `let ... in` blocks are recursive in Nix and not in Nickel. To emulate this, we use + // a `let = in`. The record provide recursivity then the values + // are destructured by the pattern. + Expr::LetIn(n) => { + use crate::destructuring; + use rnix::ast::HasEntry; + let mut destruct_vec = Vec::new(); + let mut fields = HashMap::new(); + let mut state = state.clone(); + state.env.extend(n.attrpath_values().map(|kv| { + kv.attrpath().unwrap().attrs().next().unwrap().to_string() // TODO: does not work if the let contains Dynamic or Str. Is + // it possible in Nix? + })); + for kv in n.attrpath_values() { + // In `let` blocks, the key is supposed to be a single ident so `Path` exactly one + // element. TODO: check nix really don't support attrpath on the lhs in a let + // block. + let id = kv.attrpath().unwrap().attrs().next().unwrap(); + // Check we don't try to redefine builtin values. Even if it's possible in Nix, + // we don't suport it. + let id: Ident = match id.to_string().as_str() { + "true" | "false" | "null" => panic!( + "`let {id}` is forbidden. Can not redefine `true`, `false` or `null`" + ), + s => { + let pos = id.syntax().text_range(); + let span = mk_span(state.file_id, pos.start().into(), pos.end().into()); + // give a position to the identifier. + Ident::new_with_pos(s, crate::position::TermPos::Original(span)) + } + }; + let rt = kv.value().unwrap().translate(&state); + destruct_vec.push(destructuring::Match::Simple(id, Default::default())); + fields.insert(id, rt); + } + make::let_pat::( + None, + destructuring::RecordPattern { + matches: destruct_vec, + open: false, + rest: None, + span, + }, + Term::RecRecord(RecordData::with_field_values(fields), Vec::new(), None), + n.body().unwrap().translate(&state), + ) + } + Expr::With(n) => { + let mut state = state.clone(); + // we push in a vec the term passed to the with (e.g.: `with t; ...` we push the + // term `t`) we push a term because it does not to have a variable, it can be any + // expretion evaluated to a record. + state.with.push(n.namespace().unwrap().translate(&state)); + // In the Nickel AST, a with don't realy exist. It's translated to its body. That's + // only when we will parse a variable access that we will take care of the `with`s. + // See the `Expr::Identifier` of the current `match`. + n.body().unwrap().translate(&state) + } + + // a lambda or a function definition. + Expr::Lambda(n) => { + match n.param().unwrap() { + // the simple case in which the param of the lambda is an identifier as in + // `f = x: ...` x is an identifier. + rnix::ast::Param::IdentParam(idp) => Term::Fun( + id_from_nix(idp.ident().unwrap(), state), + // TODO: add the `id` to `state.env` + n.body().unwrap().translate(state), + ), + // the param is a pattern as we generaly see in NixOS modules (`{pkgs, lib, + // ...}:` + rnix::ast::Param::Pattern(pat) => { + // TODO: add the matched identifiers to `state.env` + use crate::destructuring::*; + let at = pat + .pat_bind() + .map(|id| id_from_nix(id.ident().unwrap(), state)); + let matches = pat + .pat_entries() + .map(|e| { + // manage default values: + let field = if let Some(def) = e.default() { + Field { + value: Some(def.translate(state)), + metadata: FieldMetadata { + priority: MergePriority::Bottom, + ..Default::default() + }, + pending_contracts: Vec::new(), + } + } else { + // the value does not has default. So we construct an empty + // metavalue without any priority annotation. + Default::default() + }; + Match::Simple(id_from_nix(e.ident().unwrap(), state), field) + }) + .collect(); + let dest = RecordPattern { + matches, + open: pat.ellipsis_token().is_some(), + rest: None, + span, + }; + Term::FunPattern(at, dest, n.body().unwrap().translate(state)) + } + } + } + .into(), + + // function application. + Expr::Apply(n) => Term::App( + n.lambda().unwrap().translate(state), + n.argument().unwrap().translate(state), + ) + .into(), + Expr::IfElse(n) => if_then_else( + n.condition().unwrap().translate(state), + n.body().unwrap().translate(state), + n.else_body().unwrap().translate(state), + ), + Expr::BinOp(n) => n.translate(state), + Expr::UnaryOp(n) => n.translate(state), + + // static or dynamic records field access. + Expr::Select(n) => { + let select = n + .attrpath() + .unwrap() + .attrs() + // a nested access is an iterator on attrs from left to right. + .fold( + n.expr().unwrap().translate(state), // the fold is initialized with the + // record accessed. + |acc, i| { + match i { + rnix::ast::Attr::Ident(id) => { + Term::Op1(UnaryOp::StaticAccess(id_from_nix(id, state)), acc) + } + rnix::ast::Attr::Dynamic(d) => Term::Op2( + BinaryOp::DynAccess(), + d.expr().unwrap().translate(state), + acc, + ), + rnix::ast::Attr::Str(s) => { + Term::Op2(BinaryOp::DynAccess(), s.translate(state), acc) + } + } + .into() + }, + ); + // if the selection contains a `... or ` suffix + if let Some(def) = n.default_expr() { + let path = path_rts_from_nix(n.attrpath().unwrap(), state); + let path = Term::Array(path, Default::default()); + // we transform it to something like the following pseudo code: + // + // ``` + // if has_field_path + // then . + // else + // ``` + if_then_else( + mk_app!( + crate::stdlib::compat::has_field_path(), + path, + n.expr().unwrap().translate(state) + ), + select, + def.translate(state), + ) + } else { + select + } + } + + // The Nix `?` operator. + Expr::HasAttr(n) => { + let path = path_rts_from_nix(n.attrpath().unwrap(), state); + let path = Term::Array(path, Default::default()); + mk_app!( + crate::stdlib::compat::has_field_path(), + path, + n.expr().unwrap().translate(state) + ) + } + Expr::Path(_) => unimplemented!(), + } + // set the position in the AST to try to have some sort of debuging support. + .with_pos(crate::position::TermPos::Original(span)) + } +} + +/// the main entry of this module. It parse a Nix file pointed by `file_id` into a Nickel +/// AST/Richterm. +pub fn parse(cache: &Cache, file_id: FileId) -> Result { + let source = cache.files().source(file_id); + let root = rnix::Root::parse(source).ok()?; // TODO: we could return a list of errors calling + // `errors()` to improve error management. + Ok(root.expr().unwrap().to_nickel(file_id)) +} diff --git a/src/stdlib.rs b/src/stdlib.rs index 545c849538..5b252aacd0 100644 --- a/src/stdlib.rs +++ b/src/stdlib.rs @@ -5,7 +5,7 @@ use crate::term::make as mk_term; use crate::term::RichTerm; /// This is an array containing all the Nickel standard library modules. -pub fn modules() -> [StdlibModule; 8] { +pub fn modules() -> [StdlibModule; 9] { [ StdlibModule::Builtin, StdlibModule::Contract, @@ -15,6 +15,7 @@ pub fn modules() -> [StdlibModule; 8] { StdlibModule::Num, StdlibModule::Function, StdlibModule::Internals, + StdlibModule::Compat, ] } @@ -29,6 +30,7 @@ pub enum StdlibModule { Num, Function, Internals, + Compat, } impl StdlibModule { @@ -42,6 +44,7 @@ impl StdlibModule { StdlibModule::Num => "", StdlibModule::Function => "", StdlibModule::Internals => "", + StdlibModule::Compat => "", } } @@ -55,6 +58,7 @@ impl StdlibModule { StdlibModule::Num => include_str!("../stdlib/num.ncl"), StdlibModule::Function => include_str!("../stdlib/function.ncl"), StdlibModule::Internals => include_str!("../stdlib/internals.ncl"), + StdlibModule::Compat => include_str!("../stdlib/compat.ncl"), } } } @@ -74,6 +78,7 @@ impl TryFrom for StdlibModule { "num" => StdlibModule::Num, "function" => StdlibModule::Function, "internals" => StdlibModule::Internals, + "compat" => StdlibModule::Compat, _ => return Err(UnknownStdlibModule), }; Ok(module) @@ -91,6 +96,7 @@ impl From for Ident { StdlibModule::Num => "num", StdlibModule::Function => "function", StdlibModule::Internals => "internals", + StdlibModule::Compat => "Compat", }; Ident::from(name) } @@ -136,3 +142,46 @@ pub mod internals { generate_accessor!(rec_default); generate_accessor!(rec_force); } + +/// Contains functions helper for Nix evaluation by Nickel. +pub mod compat { + use super::*; + use crate::mk_app; + use crate::term::make::op1; + use crate::term::{array::Array, Term, UnaryOp}; + + /// helper function to perform a Nix like update (`//` operator). + pub fn update() -> RichTerm { + op1( + UnaryOp::StaticAccess("update_all".into()), + Term::Var("compat".into()), + ) + } + + /// helper function to check if a record has a nested field. + pub fn has_field_path() -> RichTerm { + op1( + UnaryOp::StaticAccess("has_field_path".into()), + Term::Var("compat".into()), + ) + } + + /// Generate the `with` compatibility Nickel function which may be applied to an `Ident` + /// you have to pass a list of with records in ordered from outer-most to inner-most one. + pub fn with(array: Array) -> RichTerm { + mk_app!( + op1( + UnaryOp::StaticAccess("with".into()), + Term::Var("compat".into()), + ), + Term::Array(array, Default::default()) + ) + } + + pub fn add() -> RichTerm { + op1( + UnaryOp::StaticAccess("add".into()), + Term::Var("compat".into()), + ) + } +} diff --git a/src/term/mod.rs b/src/term/mod.rs index b804bd8445..6795e60328 100644 --- a/src/term/mod.rs +++ b/src/term/mod.rs @@ -1888,7 +1888,6 @@ pub mod make { Term::LetPattern(id.map(|i| i.into()), pat.into(), t1.into(), t2.into()).into() } - #[cfg(test)] pub fn if_then_else(cond: T1, t1: T2, t2: T3) -> RichTerm where T1: Into, diff --git a/stdlib/compat.ncl b/stdlib/compat.ncl new file mode 100644 index 0000000000..9baa694ba7 --- /dev/null +++ b/stdlib/compat.ncl @@ -0,0 +1,60 @@ +{ + compat + | doc m%" + Nix compatibility layer. + + This library is used by program transpiled from Nix code to Nickel. This + library should'nt usually be used directly by Nickel program. The API + isn't stable and no backward-compatibility guarantees exist at this + point + "% + = { + # Addition in Nix is overloaded to work both as number addition and string + # concatenation. There is no such operator in Nickel. This function + # implement the equivalent of the Nix primitive operator by dynamically + # dispatching between addition and concatenation, based on the runtime type + # of its arguments. + add = fun a b => + if %typeof% a == `Str && %typeof% b == `Str + then + a ++ b + else + a + b, + + # The update operator of Nix `//`. It's a "general form" of the + # `record.update` of Nickel. + # + # TODO: May be interesting to be adapted and integrated to the actual Nickel + # stdlib. + update_all = fun r1 r2 => + r2 + |> record.fields + |> array.fold_left (fun acc key => record.update key r2."%{key}" acc) r1, + + has_field_path = fun fields record => + # Because it's only used by generated code, this length will never be + # initially 0. So if it's 0, it mean the end of the path. + let head = %head% fields in + %length% fields == 0 || + ( + %has_field% head record && + has_field_path (%tail% fields) record."%{head}" + ), + + with = + let AssertFound = fun label value => + if value.found then + value.value + else + %blame% label + in + + fun envs field => ( + array.fold_right (fun current acc => + if !acc.found && record.has_field field current + then { value = current."%{field}", found = true} + else acc + ) {value = null, found = false} envs + ) | AssertFound + } +} diff --git a/tests/nix.rs b/tests/nix.rs new file mode 100644 index 0000000000..39ec9dcf23 --- /dev/null +++ b/tests/nix.rs @@ -0,0 +1,35 @@ +#![cfg(feature = "nix")] + +use nickel_lang::term::Term; +use nickel_lang_utilities::eval; + +fn run(path: &str) { + eval(format!( + "import \"{}/tests/nix/{path}\" |> array.all function.id", + env!("CARGO_MANIFEST_DIR"), + )) + .map(|term| { + assert_eq!(term, Term::Bool(true), "error in test {path}"); + }) + .unwrap(); +} + +#[test] +fn basics_nix() { + run("basics.nix"); +} + +#[test] +fn lets_nix() { + run("lets.nix"); +} + +#[test] +fn records_nix() { + run("records.nix"); +} + +#[test] +fn with_nix() { + run("with.nix"); +} diff --git a/tests/nix/basics.nix b/tests/nix/basics.nix new file mode 100644 index 0000000000..a0a17037f6 --- /dev/null +++ b/tests/nix/basics.nix @@ -0,0 +1,38 @@ +# Basics tests taken from basics.ncl and rewritten with Nix +[ + # basic arithmetic + (1 + 1 == 2) + (1 - 2 + 3 - 4 == -2) + (2 - 3 - 4 == -5) + (-1 - 2 == -3) + (2 * 2 + 2 * 3 - 2 * 4 == 2) + (1 / 2 + 1 / 4 - 1 / 8 == 5 / 8) + #((10 + 1/4) % 3 == 1.25) + #(10 + 1/4 % 3 == 10.25) + + # comparisons + (1 < 1 == false) + (1 <= 1 == true) + (1 > 1 == false) + (1 >= 1 == true) + + # booleans expr + (true && false == false) + (true || false == true) + (true -> true == true) + (true -> false == false) + (false -> true == true) + (false -> false == true) + (!false == true) + (false && true || true == true) + # (false && (true || true) == false) # TODO: what happen here? + + # lists concataination + ([ 1 2 ] ++ [ 1 2 ] == [ 1 2 1 2 ]) + + # strings concataination + ("hello" + " " + "world" == "hello world") + + # if then else + ((if true then 1 else 2) == 1) +] diff --git a/tests/nix/lets.nix b/tests/nix/lets.nix new file mode 100644 index 0000000000..a734fd3ae8 --- /dev/null +++ b/tests/nix/lets.nix @@ -0,0 +1,6 @@ +[ + (let a = "a"; in a == "a") + (let a = "a"; b = "b"; in [ a b ] == [ "a" "b" ]) + (let a = c; b = a + c; c = 1; in [ a b c ] == [ 1 2 1 ]) + (let a = 1; in let b = a; in a + b == 2) +] diff --git a/tests/nix/records.nix b/tests/nix/records.nix new file mode 100644 index 0000000000..ada318e437 --- /dev/null +++ b/tests/nix/records.nix @@ -0,0 +1,24 @@ +# has_field operator (`?`) +[ + ({ a = 1; } ? a == true) + ({ a = 1; } ? "a" == true) + ({ a = 1; } ? b == false) + ({ a = 1; } ? "b" == false) + ({ a.foo = 1; } ? a.foo == true) + ({ a.foo = 1; } ? a."foo" == true) + ({ a.foo = 1; } ? "a.foo" == false) + ({ a.foo = 1; } ? "a".foo == true) + ({ a.foo = 1; } ? a == true) + + # field access or default + ({ a = "a"; }.a or "x" == "a") + ({ a = "a"; }.b or "x" == "x") + ({ a.b = "ab"; }.a or "x" == { b = "ab"; }) + ({ a.b = "ab"; }.a.b or "x" == "ab") + ({ a.b = "ab"; }.a.c or "x" == "x") + + # the '//' update operator. + ({ a = 1; b = 2; } // { b = 3; c = 4; } == { a = 1; b = 3; c = 4; }) + (let r = { a = 1; }; in r // { } == r) + (let r = { a = 1; }; in { } // r == r) +] diff --git a/tests/nix/with.nix b/tests/nix/with.nix new file mode 100644 index 0000000000..ef5e4b7cc4 --- /dev/null +++ b/tests/nix/with.nix @@ -0,0 +1,66 @@ +let + r1 = { a = "a1"; b = "b1"; c = "c1"; }; + r2 = { a = "a2"; b = "b2"; }; + r3 = { a = { a = "raa"; }; }; +in +[ + # simple with + (with r1; [ a b c ] == [ r1.a r1.b r1.c ]) + + # with does not shadow staticaly defined vars + ( + let + a = 11; + in + with r1; a == 11 + ) + + # with shadow previous with + (with r1; with r2; [ b c ] == [ r2.b r1.c ]) + + # complex shadowing + ( + let + a = 11; + in + with r1; with r2; [ a b c ] == [ 11 r2.b r1.c ] + ) + ( + let + a = 11; + in + with r1; let + b = 22; + in + [ a b c ] == [ 11 22 r1.c ] + ) + + # with and nested records + ( + with r3; a == r3.a && (with a; a == r3.a.a) + ) + + # with and rec records + # TODO: this test is disabled because, now, when we are in a rec record, we don't populate the environment with it's fields. See how it's done for `let in` blocks in `src/nix.rs`. + # ( + # with r1; rec { + # a = 11; + # v = a + 11; + # s = b + c; + # } == { a = 11; v = 22; s = "b1c1";} + # ) + + # with inside a function body + # TODO: this test is disabled because, now, when we are in a function body, we don't populate the environment with it's fields. See how it's done for `let in` blocks in `src/nix.rs`. + # ( + # let + # f = a: with r1; [a b]; + # in f 11 == [11 r1.b] + # ) + # Don't forget to manage the patern matched arguments. + # ( + # let + # f = {a, b}: with r1; [a b c]; + # in f r2 == [r2.a r2.b r1.c] + # ) +]