Repository: tweag/nickel
Pull request #645 (2022-03-08 15:50:02)

Fix failing typechecking of enum contracts
Fix an issue unveiled during the implementation of #644, and which prevents it from passing the CI. When an enum type is used in a term context, like `let FooOrBar = contract.apply [| Foo, Bar |]`, it is elaborated to a contract and then typechecked as such.

It turns its elaboration used a primitive operation `UnaryOp::Switch`, but the typechecker bails out (concretely: panic) when encountering such a construct that is supposed to be a byproduct of the evaluation of `Term::Switch` AST node (the point is precisely that we need a distinguished `Term::Switch` node for typechecking, but it is then evaluated away to `UnaryOp::Switch` which isn't special among primops anymore).

This PR fixes it by making the conversion of an enum type to a contract to use `Term::Switch` instead of `UnaryOp::Switch`, and add a regression test. Incidentally, it fixes the documentation of the macro `mk_switch`, that was copy-pasted from the one of `mk_record` without proper adaptation.


Patch:
diff --git a/src/term.rs b/src/term.rs
index fd78f28d..00bc6243 100644
--- a/src/term.rs
+++ b/src/term.rs
@@ -1320,12 +1320,11 @@ pub mod make {
         };
     }
 
-    /// Multi field record for types implementing `Into<Ident>` (for the identifiers), and
-    /// `Into<RichTerm>` for the fields. Identifiers and corresponding content are specified as a
-    /// tuple: `mk_record!(("field1", t1), ("field2", t2))` corresponds to the record `{ field1 =
-    /// t1; field2 = t2 }`.
+    /// Switch for types implementing `Into<Ident>` (for patterns) and `Into<RichTerm>` for the
+    /// body of each case. Cases are specified as tuple, and the default case (optional) is separated by a `;`:
+    /// `mk_switch!(format, ("Json", json_case), ("Yaml", yaml_case) ; def)` corresponds to
+    /// ``switch { `Json => json_case, `Yaml => yaml_case, _ => def} format``.
     #[macro_export]
-    #[cfg(test)]
     macro_rules! mk_switch {
         ( $exp:expr, $( ($id:expr, $body:expr) ),* ; $default:expr ) => {
             {
diff --git a/src/types.rs b/src/types.rs
index 4bfe1dc0..2c8953d3 100644
--- a/src/types.rs
+++ b/src/types.rs
@@ -54,8 +54,8 @@
 use crate::error::{ParseError, ParseErrors, TypecheckError};
 use crate::identifier::Ident;
 use crate::term::make as mk_term;
-use crate::term::{RichTerm, Term, UnaryOp};
-use crate::{mk_app, mk_fun};
+use crate::term::{RichTerm, Term};
+use crate::{mk_app, mk_fun, mk_switch};
 use std::collections::HashMap;
 use std::fmt;
 
@@ -256,17 +256,15 @@ impl Types {
                         }
                         AbsType::RowExtend(id, None, rest) => {
                             let rest_contract = form(*rest, h)?;
-                            let mut map = HashMap::new();
-                            map.insert(id, Term::Bool(true).into());
 
                             mk_app!(
                                 contract::row_extend(),
                                 rest_contract,
                                 mk_fun!(
                                     "x",
-                                    mk_app!(
-                                        mk_term::op1(UnaryOp::Switch(true), mk_term::var("x")),
-                                        Term::Record(map, Default::default()),
+                                    mk_switch!(
+                                        mk_term::var("x"),
+                                        (id, Term::Bool(true)) ;
                                         Term::Bool(false)
                                     )
                                 )


Test patch:
diff --git a/tests/pass/contracts.ncl b/tests/pass/contracts.ncl
index baa79b12..a4599653 100644
--- a/tests/pass/contracts.ncl
+++ b/tests/pass/contracts.ncl
@@ -52,6 +52,12 @@ let Assert = fun l x => x || %blame% l in
     fun x => switch { `foo => 1, `"bar:baz" => 2, _ => 3, } x in
   f `"boo,grr" == 3,
 
+  # enums_applied
+  # Regression test for enum types converted to contracts outside of annotations
+  # causing issues wrt typechecking
+  let Wrapper = contract.apply [| Foo, Bar |] in
+  (`Foo | Wrapper) == `Foo,
+
   # records_simple
   ({} | {}) == {},
   let x | {a: Num, s: Str} = {a = 1, s = "a"} in
@@ -96,7 +102,7 @@ let Assert = fun l x => x || %blame% l in
   let Contract = {a | Num, ..} & {b | Num, ..} in
   ({a = 0, b = 0, c = 0} | Contract) == {a = 0, b = 0, c = 0},
 
-  # arrays 
+  # arrays
   ([1, "2", false] | Array Dyn) == [1, "2", false],
   ([1, 2, 3] | Array Num) == [1, 2, 3],
   (["1", "2", "false"] | Array Str) == ["1", "2", "false"],