Repository: tweag/nickel (rust)

Issue:
[Dogfooding] Escaping enums
**Is your feature request related to a problem? Please describe.**
While dogfooding on the [mantis repo](https://github.com/input-output-hk/mantis-ops), there are several fields with a finite say of strings as possible values (ex: [here](https://github.com/input-output-hk/mantis-ops/blob/81ad4cb0ec5ff9faeb9065a2ddac060cef6452ac/schemas/nomad/types.cue#L54)). It's nice to represent this type/contract with enums in Nickel, like:

```
{
  protocol | <http, ftp, https>
}
```

As the typechecker is then aware of those, can check match exhaustiveness, and so on. However it isn't possible when one of the alternative is not syntactically an identifier.

**Describe the solution you'd like**
Having a way of escaping non-identifier characters inside enum tags, e.g. by surrounding them by quotes:

```
{
  operator | <"Foo!bar", "Foo!baz", "!="> = `"Foo!bar",
}
```

**Describe alternatives you've considered**
We can also encode this using a custom contract (most probably, this kind of combinator will be in the stdlib at some point):

```
let IsIn = fun values label value =>
  if lists.elem value values then 
    value 
  else
    contracts.blame label
in
{
  operator | #(IsIn ["Foo!bar", "Foo!baz", "!="])
}
```

But:

1. It is less consistent (some enums will be defined with the builtin type, some others with this custom contracts, without beginners knowing why, maybe)
2. As mentioned, the typechecker don't know what to do with that (no match, no typechecking, no pattern matching exhaustiveness, etc.)

A counter-argument could be that enums are meant for things that resembles simple labels, and if one of the alternative for a field is `"!="`, then maybe it shouldn't be encoded as an enum to begin with.

Pull request description:
can use arbitrary Str in enums deffinitions
impl and close #460 

Patch:
diff --git a/src/grammar.lalrpop b/src/grammar.lalrpop
index 290aa072..8e623295 100644
--- a/src/grammar.lalrpop
+++ b/src/grammar.lalrpop
@@ -183,7 +183,7 @@ Atom: RichTerm = {
     Bool => RichTerm::from(Term::Bool(<>)),
     StrChunks,
     Ident => RichTerm::from(Term::Var(<>)),
-    "`" <Ident> => RichTerm::from(Term::Enum(<>)),
+    "`" <EnumTag> => RichTerm::from(Term::Enum(<>)),
     "{" <fields: (<RecordField> ",")*> <last: RecordLastField?> "}" => {
         let (last_field, attrs) = match last {
             Some(RecordLastField::Field(f)) => (Some(f), Default::default()),
@@ -318,6 +318,12 @@ HashBrace = { "#{", "multstr #{" };
 
 StaticString: String = StringStart <s: ChunkLiteral> StringEnd => s;
 
+EnumTag: Ident = {
+    <Ident>,
+    <StaticString> => <>.into(),
+}
+
+
 ChunkLiteralPart: ChunkLiteralPart<'input> = {
     "str literal" => ChunkLiteralPart::Str(<>),
     "multstr literal" => ChunkLiteralPart::Str(<>),
@@ -362,7 +368,7 @@ UOp: UnaryOp = {
 };
 
 switch_case: SwitchCase = {
-    <id: Ident> "=>" <t: Term> => SwitchCase::Normal(id, t),
+    "`" <id: EnumTag> "=>" <t: Term> => SwitchCase::Normal(id, t),
     "_" "=>" <t: Term> => SwitchCase::Default(<>),
 }
 
@@ -488,7 +494,7 @@ subType : Types = {
     <Ident> => Types(AbsType::Var(<>)),
     "#" <WithPos<RecordOperand>> => Types(AbsType::Flat(<>)),
     "(" <Types> ")",
-    "<" <rows:(<Ident> ",")*> <last: (<Ident>)?> <tail: ("|" <Ident>)?> ">" => {
+    "<" <rows:(<EnumTag> ",")*> <last: (<EnumTag>)?> <tail: ("|" <EnumTag>)?> ">" => {
         let ty = rows.into_iter()
             .chain(last.into_iter())
             // As we build row types as a linked list via a fold on the original
diff --git a/src/operation.rs b/src/operation.rs
index fb42f4fd..f2336e50 100644
--- a/src/operation.rs
+++ b/src/operation.rs
@@ -925,18 +925,10 @@ fn process_unary_operation(
         }
         UnaryOp::EnumFromStr() => {
             if let Term::Str(s) = *t {
-                let re = regex::Regex::new("_?[a-zA-Z][_a-zA-Z0-9]*").unwrap();
-                if re.is_match(&s) {
-                    Ok(Closure::atomic_closure(RichTerm::new(
-                        Term::Enum(s.into()),
-                        pos_op_inh,
-                    )))
-                } else {
-                    Err(EvalError::Other(
-                        format!("enumFrom: invalid enum tag `{}`", s),
-                        pos,
-                    ))
-                }
+                Ok(Closure::atomic_closure(RichTerm::new(
+                    Term::Enum(s.into()),
+                    pos_op_inh,
+                )))
             } else {
                 Err(EvalError::TypeError(
                     String::from("Str"),
diff --git a/src/term.rs b/src/term.rs
index 1b10761a..c69fadbb 100644
--- a/src/term.rs
+++ b/src/term.rs
@@ -397,7 +397,15 @@ impl Term {
             }
             Term::Fun(_, _) => String::from("<func>"),
             Term::Lbl(_) => String::from("<label>"),
-            Term::Enum(s) => format!("`{}", s),
+            Term::Enum(id) => {
+                let re = regex::Regex::new("_?[a-zA-Z][_a-zA-Z0-9]*").unwrap();
+                let s = id.to_string();
+                if re.is_match(&s) {
+                    format!("`{}", s)
+                } else {
+                    format!("`\"{}\"", s)
+                }
+            }
             Term::Record(..) | Term::RecRecord(..) => String::from("{ ... }"),
             Term::List(_) => String::from("[ ... ]"),
             Term::Sym(_) => String::from("<sym>"),


Test patch:
diff --git a/src/parser/tests.rs b/src/parser/tests.rs
index dd85a304..cd724296 100644
--- a/src/parser/tests.rs
+++ b/src/parser/tests.rs
@@ -154,9 +154,13 @@ fn unary_op() {
 #[test]
 fn enum_terms() {
     assert_eq!(parse_without_pos("`foo"), Enum(Ident::from("foo")).into(),);
+    assert_eq!(
+        parse_without_pos("`\"foo:bar\""),
+        Enum(Ident::from("foo:bar")).into(),
+    );
 
     assert_eq!(
-        parse_without_pos("switch { foo => true, bar => false, _ => 456, } 123"),
+        parse_without_pos("switch { `foo => true, `bar => false, _ => 456, } 123"),
         mk_switch!(Num(123.), ("foo", Bool(true)), ("bar", Bool(false)) ; Num(456.))
     )
 }
diff --git a/tests/contracts_fail.rs b/tests/contracts_fail.rs
index 738e77c7..87c59c26 100644
--- a/tests/contracts_fail.rs
+++ b/tests/contracts_fail.rs
@@ -182,7 +182,7 @@ fn records_contracts_closed() {
 // fn enum_complex() {
 //     eval(
 //         "let f : <foo, bar> -> Num =
-//             fun x => switch { foo => 1, bar => 2, } x in
+//             fun x => switch { `foo => 1, `bar => 2, } x in
 //             f `boo",
 //     )
 //     .unwrap_err();
diff --git a/tests/pass/annotations.ncl b/tests/pass/annotations.ncl
index d2def197..101555bc 100644
--- a/tests/pass/annotations.ncl
+++ b/tests/pass/annotations.ncl
@@ -21,6 +21,6 @@ let Assert = fun l x => x || %blame% l in
 // others_precedence
 ((fun x => x | #Assert) true) &&
 (let AssertOk = fun l t => if t == `Ok then t else %blame% l in
-  switch {Ok => true, Err => false} `Ok | #AssertOk) &&
+  switch {`Ok => true, `Err => false} `Ok | #AssertOk) &&
 
 true
diff --git a/tests/pass/basics.ncl b/tests/pass/basics.ncl
index d5b746b5..09d7af7f 100644
--- a/tests/pass/basics.ncl
+++ b/tests/pass/basics.ncl
@@ -32,6 +32,6 @@ let Assert = fun l x => x || %blame% l in
 (1/4 + 1/4 - 1/4 + 1/4 < 1/2 == false | #Assert) &&
 
 // This test checks that the terms of a switch are closured
-(let x = 3 in (switch { foo => 1, _ => x, } (3 + 2)) == 3) &&
+(let x = 3 in (switch { `foo => 1, _ => x} (3 + 2)) == 3) &&
 
 true
diff --git a/tests/pass/contracts.ncl b/tests/pass/contracts.ncl
index e3730e04..f2979fa5 100644
--- a/tests/pass/contracts.ncl
+++ b/tests/pass/contracts.ncl
@@ -30,15 +30,27 @@ twice (fun x => x + 1) 3 == 5) &&
 (`foo | <foo, bar>) == `foo &&
 (`bar | forall r. <foo, bar | r>) == `bar &&
 
+// enums_escaped
+(`"foo:baz" | <"foo:baz", "bar:baz">) == `"foo:baz" &&
+(`"bar:baz" | forall r. <"foo:baz", "bar:baz" | r>) == `"bar:baz" &&
+
 // enums_complex
 (let f : forall r. <foo, bar | r> -> Num =
-  fun x => switch { foo => 1, bar => 2, _ => 3, } x in
+  fun x => switch { `foo => 1, `bar => 2, _ => 3, } x in
 f `bar == 2) &&
 
 (let f : forall r. <foo, bar | r> -> Num =
-  fun x => switch { foo => 1, bar => 2, _ => 3, } x in
+  fun x => switch { `foo => 1, `bar => 2, _ => 3, } x in
 f `boo == 3) &&
 
+(let f : forall r. <foo, "bar:baz" | r> -> Num =
+  fun x => switch { `foo => 1, `"bar:baz" => 2, _ => 3, } x in
+f `"bar:baz" == 2) &&
+
+(let f : forall r. <foo, "bar:baz" | r> -> Num =
+  fun x => switch { `foo => 1, `"bar:baz" => 2, _ => 3, } x in
+f `"boo,grr" == 3) &&
+
 // records_simple
 (({} | {}) == {} | #Assert) &&
 (let x | {a: Num, s: Str} = {a = 1, s = "a"} in
diff --git a/tests/pass/typechecking.ncl b/tests/pass/typechecking.ncl
index b6debd92..6265d540 100644
--- a/tests/pass/typechecking.ncl
+++ b/tests/pass/typechecking.ncl
@@ -53,25 +53,25 @@ let typecheck = [
   (`blo : <bla, blo>),
   (`bla : forall r. <bla | r>),
   (`bla : forall r. <bla, blo | r>),
-  ((switch {bla => 3} `bla) : Num),
-  ((switch {bla => 3, _ => 2} `blo) : Num),
+  ((switch {`bla => 3} `bla) : Num),
+  ((switch {`bla => 3, _ => 2} `blo) : Num),
 
   // enums_complex
-  ((fun x => switch {bla => 1, ble => 2} x) : <bla, ble> -> Num),
-  ((fun x => switch {bla => 1, ble => 2, bli => 4} (%embed% bli x))
+  ((fun x => switch {`bla => 1, `ble => 2} x) : <bla, ble> -> Num),
+  ((fun x => switch {`bla => 1, `ble => 2, `bli => 4} (%embed% bli x))
     : <bla, ble> -> Num),
   ((fun x =>
-      (switch {bla => 3, bli => 2} x)
-      + (switch {bli => 6, bla => 20} x))
+      (switch {`bla => 3, `bli => 2} x)
+      + (switch {`bli => 6, `bla => 20} x))
     `bla
     : Num),
 
   let f : forall r. <blo, ble | r> -> Num = fun x =>
-    switch {blo => 1, ble => 2, _ => 3} x in
+    switch {`blo => 1, `ble => 2, _ => 3} x in
             (f `bli : Num),
 
   let f : forall r. (forall p. <blo, ble | r> -> <bla, bli | p>) =
-    fun x => switch {blo => `bla, ble => `bli, _ => `bla, } x in
+    fun x => switch {`blo => `bla, `ble => `bli, _ => `bla} x in
     f `bli,
 
   // static records
diff --git a/tests/typecheck_fail.rs b/tests/typecheck_fail.rs
index 9b7f7dbc..4d744b94 100644
--- a/tests/typecheck_fail.rs
+++ b/tests/typecheck_fail.rs
@@ -78,30 +78,30 @@ fn simple_forall() {
 #[test]
 fn enum_simple() {
     assert_typecheck_fails!("`foo : <bar>");
-    assert_typecheck_fails!("switch { foo => 3} `bar : Num");
-    assert_typecheck_fails!("switch { foo => 3, bar => true} `bar : Num");
+    assert_typecheck_fails!("switch { `foo => 3} `bar : Num");
+    assert_typecheck_fails!("switch { `foo => 3, `bar => true} `bar : Num");
 }
 
 #[test]
 fn enum_complex() {
     assert_typecheck_fails!(
-        "(fun x => switch {bla => 1, ble => 2, bli => 4} x) : <bla, ble> -> Num"
+        "(fun x => switch {`bla => 1, `ble => 2, `bli => 4} x) : <bla, ble> -> Num"
     );
     // TODO typecheck this, I'm not sure how to do it with row variables
     // LATER NOTE: this requires row subtyping, not easy
     assert_typecheck_fails!(
         "(fun x =>
-            (switch {bla => 3, bli => 2} x) +
-            (switch {bla => 6, blo => 20} x)) `bla : Num"
+            (switch {`bla => 3, `bli => 2} x) +
+            (switch {`bla => 6, `blo => 20} x)) `bla : Num"
     );
     assert_typecheck_fails!(
         "let f : forall r. <blo, ble | r> -> Num =
-            fun x => (switch {blo => 1, ble => 2, bli => 3} x) in
+            fun x => (switch {`blo => 1, `ble => 2, `bli => 3} x) in
         f"
     );
     assert_typecheck_fails!(
         "let f : forall r. (forall p. <blo, ble | r> -> <bla, bli | p>) =
-            fun x => (switch {blo => `bla, ble => `bli, _ => `blo} x) in
+            fun x => (switch {`blo => `bla, `ble => `bli, _ => `blo} x) in
         f `bli"
     );
 }