Repository: tweag/nickel
Pull request #912 (2022-11-08 11:12:16)

implementation of the "has fieldpath" operator of nix
This PR add support for `{<...>} ? <field.path>` syntax.

It depends on #1062 because it fixes an issue comming from `rnix v0.10` by updating `rnix` to the v0.11

yannham: @francois-caddet The target branch is master, and the diff looks big. Did you mean to target the nix branch instead?
dpulls[bot]: :tada: All dependencies have been resolved !

Patch:
diff --git a/src/nix.rs b/src/nix.rs
index fca5e9c4..4a1de4f3 100644
--- a/src/nix.rs
+++ b/src/nix.rs
@@ -3,14 +3,47 @@ use crate::conversion::State;
 pub use crate::conversion::ToNickel;
 use crate::identifier::Ident;
 use crate::mk_app;
-use crate::parser::utils::mk_span;
+use crate::parser::utils::{mk_span, FieldPathElem};
 use crate::term::make::{self, if_then_else};
 use crate::term::{record::RecordData, BinaryOp, UnaryOp};
 use crate::term::{MergePriority, MetaValue, RichTerm, Term};
 use codespan::FileId;
-use rnix::ast::{BinOp as NixBinOp, Str as NixStr, UnaryOp as NixUniOp};
+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;
 
+// kept for future use.
+//fn path_elem_from_nix(attr: NixAttr, state: &State) -> FieldPathElem {
+//    match attr {
+//        NixAttr::Ident(id) => FieldPathElem::Ident(id.to_string().into()),
+//        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<T>(n: rnix::ast::Attrpath, state: &State) -> T
+where
+    T: FromIterator<RichTerm>,
+{
+    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;
@@ -72,7 +105,6 @@ impl ToNickel for NixBinOp {
 impl ToNickel for rnix::ast::Expr {
     fn translate(self, state: &State) -> RichTerm {
         use rnix::ast::Expr;
-        use rowan::ast::AstNode;
         let pos = self.syntax().text_range();
         let file_id = state.file_id;
         let span = mk_span(file_id, pos.start().into(), pos.end().into());
@@ -101,7 +133,7 @@ impl ToNickel for rnix::ast::Expr {
             )
             .into(),
             Expr::AttrSet(n) => {
-                use crate::parser::utils::{build_record, elaborate_field_path, FieldPathElem};
+                use crate::parser::utils::{build_record, elaborate_field_path};
                 use rnix::ast::HasEntry;
                 let fields: Vec<(_, _)> = n
                     .attrpath_values()
@@ -113,7 +145,7 @@ impl ToNickel for rnix::ast::Expr {
                             .attrs()
                             .map(|p| match p {
                                 rnix::ast::Attr::Ident(id) => {
-                                    FieldPathElem::Ident(id.to_string().into())
+                                    FieldPathElem::Ident(id_from_nix(id, state))
                                 }
                                 rnix::ast::Attr::Dynamic(d) => {
                                     FieldPathElem::Expr(d.expr().unwrap().translate(state))
@@ -134,13 +166,13 @@ impl ToNickel for rnix::ast::Expr {
                 "true" => Term::Bool(true),
                 "false" => Term::Bool(false),
                 "null" => Term::Null,
-                id => {
-                    if state.env.contains(id) || state.with.is_empty() {
-                        Term::Var(id.into())
+                id_str => {
+                    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.into()).into(),
+                            Term::Str(id.to_string()).into(),
                         )
                     }
                 }
@@ -267,8 +299,16 @@ impl ToNickel for rnix::ast::Expr {
                         .into()
                     })
             }
-            Expr::Path(_) | Expr::HasAttr(_) => unimplemented!(), // TODO: What is the diff with
-                                                                  // Select / operator HasField?
+            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!(), // TODO: What is the diff with select?
         }
         .with_pos(crate::position::TermPos::Original(span))
     }
diff --git a/src/stdlib.rs b/src/stdlib.rs
index e46f32d0..d6f896c9 100644
--- a/src/stdlib.rs
+++ b/src/stdlib.rs
@@ -150,6 +150,14 @@ pub mod compat {
     use crate::term::make::op1;
     use crate::term::{array::Array, Term, UnaryOp};
 
+    /// 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 {
diff --git a/stdlib/compat.ncl b/stdlib/compat.ncl
index 6e443b12..05f08f11 100644
--- a/stdlib/compat.ncl
+++ b/stdlib/compat.ncl
@@ -9,6 +9,13 @@ compat | doc "Nix compatibility layer. This library should not be used by Nickel
     then a ++ b
     else a + b,
 
+  has_field_path = fun fields record =>
+      %length% fields == 0 || # Because it's only used by generated code, this length will never be initialy 0. So if it's 0, it mean the end of the path.
+      (
+        %has_field% (%head% fields) record &&
+	has_field_path (%tail% fields) record."%{%head% fields}"
+      ),
+
   with =
     let AssertFound = fun l val => if val.found 
       then val.value


Test patch:
diff --git a/tests/nix.rs b/tests/nix.rs
index c154ff13..ed0c2516 100644
--- a/tests/nix.rs
+++ b/tests/nix.rs
@@ -17,3 +17,8 @@ fn run(path: &str) {
 fn basics_nix() {
     run("basics.nix");
 }
+
+#[test]
+fn records_nix() {
+    run("records.nix");
+}
diff --git a/tests/nix/records.nix b/tests/nix/records.nix
new file mode 100644
index 00000000..ec597a7e
--- /dev/null
+++ b/tests/nix/records.nix
@@ -0,0 +1,12 @@
+# 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)
+]