Repository: tweag/nickel
Pull request #780 (2022-07-28 17:11:31)

Restore syntax for enum types
The enum types synax was temporary and somehow hidden. For version `0.2.0`, we introduce them again. This PR also changes the syntax such that alternatives must be prefixed with a backtick `, as for literal enum tags, for visual consistency. See #671 for more context.

This PR also updates the manual accordingly.


Patch:
diff --git a/benches/arrays.rs b/benches/arrays.rs
index 4427688c..06e36577 100644
--- a/benches/arrays.rs
+++ b/benches/arrays.rs
@@ -118,15 +118,6 @@ config = Criterion::default().with_profiler(PProfProfiler::new(100, Output::Flam
         path = "arrays/sum",
         args = (50),
         eval_mode = EvalMode::DeepSeq,
-    }, {
-        name = "primes normal",
-        path = "arrays/primes",
-        args = (30),
-    }, {
-        name = "primes deepseq",
-        path = "arrays/primes",
-        args = (30),
-        eval_mode = EvalMode::DeepSeq,
     }
 }
 criterion_main!(benches);
diff --git a/benches/arrays/sum.ncl b/benches/arrays/sum.ncl
index 55b2397b..d5cc1ff2 100644
--- a/benches/arrays/sum.ncl
+++ b/benches/arrays/sum.ncl
@@ -1,4 +1,4 @@
-let rec sum
+let sum
   | Array Num -> Num 
   = fun xs =>
     if array.length xs == 0 then 0
diff --git a/doc/manual/syntax.md b/doc/manual/syntax.md
index cd1f2283..7a44bc4c 100644
--- a/doc/manual/syntax.md
+++ b/doc/manual/syntax.md
@@ -214,8 +214,9 @@ While it's technically possible to just use strings in place of enum tags, using
 an enum tag insists on the fact that only a finite number of alternatives can be
 used for the corresponding value.
 
-Enum will also be handled more finely by the typechecker, however, as of the
-version 0.1 of Nickel, enum types are not yet supported.
+Additionally, the typechecker is aware of enums and can for example statically
+enforce that only valid tags are passed to a function within a typed block. See
+[the manual section on typing](./typing.md) for more details.
 
 ## Equality
 
diff --git a/doc/manual/typing.md b/doc/manual/typing.md
index 5f334dce..7b201815 100644
--- a/doc/manual/typing.md
+++ b/doc/manual/typing.md
@@ -193,21 +193,21 @@ The following type constructors are available:
   record.map (fun char count => count + 1) occurrences : {_ : Num}
   ```
 
-<!-- - **Enum**: `<tag1, .., tagn>`: an enumeration comprised of alternatives `tag1`, -->
-<!--   .., `tagn`. An enumeration literal is prefixed with a backtick and serialized -->
-<!--   as a string. It is useful to encode finite alternatives. The advantage over -->
-<!--   strings is that the typechecker handles them more finely: it is able to detect -->
-<!--   incomplete matches, for example. -->
-<!--  -->
-<!--   Example: -->
-<!--   ```nickel -->
-<!--   let protocol : <http, ftp, sftp> = `http in -->
-<!--   (switch { -->
-<!--     `http => 1, -->
-<!--     `ftp => 2, -->
-<!--     `sftp => 3 -->
-<!--   } protocol) : Num -->
-<!--   ``` -->
+- **Enum**: ``[| `tag1, .., `tagn |]``: an enumeration comprised of alternatives
+  `tag1`, .., `tagn`. An enumeration literal is prefixed with a backtick and
+  serialized as a string. It is useful to encode finite alternatives. The
+  advantage over strings is that the typechecker handles them more finely: it is
+  able to detect incomplete matches, for example.
+
+  Example:
+  ```nickel
+  let protocol : [| `http, `ftp, `sftp |] = `http in
+  (switch {
+    `http => 1,
+    `ftp => 2,
+    `sftp => 3
+  } protocol) : Num
+  ```
 
 - **Arrow (function)**: `S -> T`. A function taking arguments of type `S` and returning a value of
   type `T`. For multi-parameters functions, just iterate the arrow constructor.
diff --git a/examples/record-contract/record-contract.ncl b/examples/record-contract/record-contract.ncl
index bb69f087..cdcdd435 100644
--- a/examples/record-contract/record-contract.ncl
+++ b/examples/record-contract/record-contract.ncl
@@ -22,7 +22,7 @@ let Container = {
 } in
 
 let KubernetesConfig = {
-  kind | [| ReplicationController, ReplicaSet, Pod |]
+  kind | [| `ReplicationController, `ReplicaSet, `Pod |]
        | doc "The kind of the element being configured."
        | default = `Pod,
 
diff --git a/lsp/nls/src/linearization/mod.rs b/lsp/nls/src/linearization/mod.rs
index 1d1a843a..474f3806 100644
--- a/lsp/nls/src/linearization/mod.rs
+++ b/lsp/nls/src/linearization/mod.rs
@@ -293,7 +293,7 @@ impl Linearizer for AnalysisHost {
             Term::Record(fields, _) | Term::RecRecord(fields, ..) => {
                 lin.push(LinearizationItem {
                     id,
-                    pos,
+                    pos: pos,
                     ty,
                     kind: TermKind::Record(HashMap::new()),
                     scope: self.scope.clone(),
@@ -312,10 +312,77 @@ impl Linearizer for AnalysisHost {
                 x.push(ident.to_owned())
             }
             Term::MetaValue(meta) => {
+                // DF walk AST (of Contracts)
+                // adds usages of user defined contracts to the linearization
+                fn walk_terms(
+                    lin: &mut Linearization<Building>,
+                    mut host: AnalysisHost,
+                    RichTerm { term, pos }: &RichTerm,
+                ) {
+                    host.add_term(lin, term, *pos, TypeWrapper::Concrete(AbsType::Dyn()));
+                    match &**term {
+                        Term::Op1(_, rt) => walk_terms(lin, host, rt),
+                        Term::Op2(_, rt1, rt2) => {
+                            walk_terms(lin, host.scope(), rt1);
+                            walk_terms(lin, host.scope(), rt2);
+                        }
+                        Term::OpN(_, rts) => {
+                            rts.iter().for_each(|rt| walk_terms(lin, host.scope(), rt))
+                        }
+                        Term::StrChunks(chunks) => chunks
+                            .iter()
+                            .filter_map(|chunk| match chunk {
+                                nickel_lang::term::StrChunk::Literal(_) => None,
+                                nickel_lang::term::StrChunk::Expr(rt, _) => Some(rt),
+                            })
+                            .for_each(|rt| walk_terms(lin, host.scope(), rt)),
+                        Term::App(rt1, rt2) => {
+                            walk_terms(lin, host.scope(), rt1);
+                            walk_terms(lin, host.scope(), rt2);
+                        }
+                        Term::Record(rts, _) | Term::RecRecord(rts, ..) => rts
+                            .values()
+                            .for_each(|rt| walk_terms(lin, host.scope(), rt)),
+                        _ => {}
+                    }
+                }
+
+                // recursively linearize flat types
+                fn walk_types(
+                    lin: &mut Linearization<Building>,
+                    outer_host: &mut AnalysisHost,
+                    t: &nickel_lang::types::Types,
+                ) {
+                    let mut scope = outer_host.scope.clone();
+                    let (scope_id, next_scope_id) = outer_host.next_scope_id.next();
+                    outer_host.next_scope_id = next_scope_id;
+                    scope.push(scope_id);
+
+                    let inner_host = AnalysisHost {
+                        env: outer_host.env.clone(),
+                        scope,
+                        ..Default::default()
+                    };
+                    match &t.0 {
+                        AbsType::Flat(rt) => walk_terms(lin, inner_host, &rt),
+                        AbsType::Arrow(t1, t2) => {
+                            walk_types(lin, outer_host, &t1);
+                            walk_types(lin, outer_host, &t2);
+                        }
+                        _ => {}
+                    }
+                }
+
                 // Notice 1: No push to lin for the `MetaValue` itself
                 // Notice 2: we discard the encoded value as anything we
                 //           would do with the value will be handled in the following
                 //           call to [Self::add_term]
+                // Notice 3: we walk the inner contract types to resolve references
+
+                meta.contracts
+                    .iter()
+                    .for_each(|c| walk_types(lin, self, &c.types));
+
                 if meta.value.is_some() {
                     self.meta = Some(MetaValue {
                         value: None,
@@ -328,7 +395,7 @@ impl Linearizer for AnalysisHost {
 
                 lin.push(LinearizationItem {
                     id,
-                    pos,
+                    pos: pos,
                     ty,
                     scope: self.scope.clone(),
                     kind: TermKind::Structure,
@@ -428,7 +495,9 @@ impl Linearizer for AnalysisHost {
             scope,
             env: self.env.clone(),
             next_scope_id: ScopeId::default(),
-            meta: self.meta.clone(),
+            /// when opening a new scope `meta` is assumed to be `None` as meta data
+            /// is immediately followed by a term without opening a scope
+            meta: None,
             record_fields: self.record_fields.as_mut().and_then(|(record, fields)| {
                 Some(*record).zip(fields.pop().map(|field| vec![field]))
             }),
@@ -437,29 +506,6 @@ impl Linearizer for AnalysisHost {
         }
     }
 
-    fn scope_meta(&mut self) -> Self {
-        let mut scope = self.scope.clone();
-        let (scope_id, next_scope_id) = self.next_scope_id.next();
-        self.next_scope_id = next_scope_id;
-
-        scope.push(scope_id);
-
-        AnalysisHost {
-            scope,
-            env: self.env.clone(),
-            next_scope_id: ScopeId::default(),
-            // Metadata must be attached to the original scope of the value (`self`), while the new
-            // scope for metadata should be clean.
-            // In general, the scope for the metadata shouldn't interfere with any of the previous
-            // state (record fields, let binding, etc.), which is kept intact inside `self`, while
-            // this new scope gets a cleared stated.
-            meta: None,
-            record_fields: None,
-            let_binding: None,
-            access: None,
-        }
-    }
-
     fn retype_ident(
         &mut self,
         lin: &mut Linearization<Building>,
diff --git a/src/grammar.lalrpop b/src/grammar.lalrpop
index f3c0c55c..bee710ff 100644
--- a/src/grammar.lalrpop
+++ b/src/grammar.lalrpop
@@ -316,7 +316,7 @@ Atom: UniTerm = {
     AsUniTerm<StrChunks>,
     Ident => UniTerm::from(UniTermNode::Var(<>)),
     UniRecord => UniTerm::from(UniTermNode::Record(<>)),
-    "`" <EnumTag> => UniTerm::from(Term::Enum(<>)),
+    <EnumTag> => UniTerm::from(Term::Enum(<>)),
     "[" <terms: (<Term> ",")*> <last: Term?> "]" => {
         let terms : Vec<RichTerm> = terms.into_iter()
             .chain(last.into_iter()).collect();
@@ -384,47 +384,50 @@ Pattern: (Option<Ident>,Destruct) = {
 Destruct: Destruct = {
     <start: @L> "{" <mut matches: (<Match> ",")*> <last:LastMatch?> "}" <end: @R> => {
         let (open, rest) = match last {
-	    Some(LastMatch::Match(m)) => {
-	        matches.push(*m);
-	        (false,None)
-	    },
-	    Some(LastMatch::Ellipsis(rest)) => (true, rest),
-	    _ => (false, None),
-	};
-	let span = mk_span(src_id, start, end);
-	Destruct::Record{matches, open, rest, span}
+            Some(LastMatch::Match(m)) => {
+                matches.push(*m);
+                (false,None)
+            },
+            Some(LastMatch::Ellipsis(rest)) => (true, rest),
+            _ => (false, None),
+        };
+
+        let span = mk_span(src_id, start, end);
+        Destruct::Record{matches, open, rest, span}
     },
 };
 
 // A binding `ident = <pattern>` inside a destructuring pattern.
 Match: Match = {
     <left:Ident> <anns: Annot<FixedType>?> <default: DefaultAnnot?> "=" <right: Pattern> => {
-	let meta = match (default, anns) {
-	    (Some(d), Some(m)) => MetaValue::flatten(d,m),
-	    (Some(m),_) | (_,Some(m)) => m,
-  	    _ => MetaValue {
-	            contracts: vec![Contract{
-	                types: Types(AbsType::Dyn().into()),
-		        label: Label{span: left.pos.unwrap(), ..Default::default()},
-		    }],
-		    ..Default::default()
-	    },
-	};
-	Match::Assign(left, meta, right)
+        let meta = match (default, anns) {
+            (Some(d), Some(m)) => MetaValue::flatten(d,m),
+            (Some(m),_) | (_,Some(m)) => m,
+            _ => MetaValue {
+              contracts: vec![Contract{
+                  types: Types(AbsType::Dyn().into()),
+                  label: Label{span: left.pos.unwrap(), ..Default::default()},
+              }],
+              ..Default::default()
+            },
+        };
+
+        Match::Assign(left, meta, right)
     },
     <id:Ident> <anns: Annot<FixedType>?> <default: DefaultAnnot?> => {
-	let meta = match (default, anns) {
-	    (Some(d), Some(m)) => MetaValue::flatten(d,m),
-	    (Some(m),_) | (_,Some(m)) => m,
-  	    _ => MetaValue {
-	            contracts: vec![Contract{
-	                types: Types(AbsType::Dyn().into()),
-		        label: Label{span: id.pos.unwrap(), ..Default::default()},
-		    }],
-		    ..Default::default()
-	    },
-	};
-	Match::Simple(id, meta)
+        let meta = match (default, anns) {
+            (Some(d), Some(m)) => MetaValue::flatten(d,m),
+            (Some(m),_) | (_,Some(m)) => m,
+            _ => MetaValue {
+                  contracts: vec![Contract{
+                      types: Types(AbsType::Dyn().into()),
+                      label: Label{span: id.pos.unwrap(), ..Default::default()},
+                  }],
+                  ..Default::default()
+            },
+        };
+
+        Match::Simple(id, meta)
     },
 };
 
@@ -499,7 +502,9 @@ Interpolation = { "%{", "multstr %{" };
 
 StaticString: String = StringStart <s: ChunkLiteral?> StringEnd => s.unwrap_or_default();
 
-EnumTag: Ident = {
+EnumTag: Ident = "`" <EnumTagContent> => <>;
+
+EnumTagContent: Ident = {
     <Ident>,
     <StaticString> => <>.into(),
     // Allow tags of the form `Str, `Bool, etc. This is ad-hoc and should
@@ -554,7 +559,7 @@ UOp: UnaryOp = {
 };
 
 SwitchCase: SwitchCase = {
-    "`" <id: EnumTag> "=>" <t: Term> => SwitchCase::Normal(id, t),
+    "`" <id: EnumTagContent> "=>" <t: Term> => SwitchCase::Normal(id, t),
     "_" "=>" <t: Term> => SwitchCase::Default(<>),
 }
 
@@ -750,7 +755,7 @@ TypeBuiltin: Types = {
 
 TypeAtom: Types = {
     <TypeBuiltin>,
-    "[|" <rows:(<EnumTag> ",")*> <last: (<EnumTag>)?> <tail: (";" <EnumTag>)?> "|]" => {
+    "[|" <rows:(<EnumTag> ",")*> <last: (<EnumTag>)?> <tail: (";" <Ident>)?> "|]" => {
         let ty = rows.into_iter()
             .chain(last.into_iter())
             // As we build row types as a linked list via a fold on the original
@@ -794,7 +799,7 @@ extern {
         "forall" => Token::Normal(NormalToken::Forall),
         "in" => Token::Normal(NormalToken::In),
         "let" => Token::Normal(NormalToken::Let),
-	"rec" => Token::Normal(NormalToken::Rec),
+        "rec" => Token::Normal(NormalToken::Rec),
         "switch" => Token::Normal(NormalToken::Switch),
 
         "null" => Token::Normal(NormalToken::Null),
diff --git a/src/pretty.rs b/src/pretty.rs
index 0afe0e6a..fc8f29bf 100644
--- a/src/pretty.rs
+++ b/src/pretty.rs
@@ -772,15 +772,14 @@ where
                 .braces(),
             RowEmpty() => allocator.nil(),
             RowExtend(id, ty_opt, tail) => {
-                let mut builder = allocator.quote_if_needed(&id);
-
-                builder = if let Some(ty) = ty_opt {
-                    builder
+                let builder = if let Some(ty) = ty_opt {
+                    allocator
+                        .quote_if_needed(&id)
                         .append(allocator.text(":"))
                         .append(allocator.space())
                         .append(ty.pretty(allocator))
                 } else {
-                    builder
+                    allocator.text("`").append(allocator.quote_if_needed(&id))
                 };
 
                 match tail.0 {
diff --git a/src/typecheck/error.rs b/src/typecheck/error.rs
index 160792ac..9c1af99a 100644
--- a/src/typecheck/error.rs
+++ b/src/typecheck/error.rs
@@ -1,12 +1,8 @@
 //! Internal error types for typechecking.
 use super::{reporting, State, TypeWrapper};
 use crate::{
-    error::TypecheckError,
-    identifier::Ident,
-    label::ty_path,
-    position::TermPos,
-    term::RichTerm,
-    types::{AbsType, Types},
+    error::TypecheckError, identifier::Ident, label::ty_path, position::TermPos, term::RichTerm,
+    types::AbsType,
 };
 
 /// Error during the unification of two row types.
diff --git a/src/typecheck/linearization.rs b/src/typecheck/linearization.rs
index 8cfd49fa..3b563bce 100644
--- a/src/typecheck/linearization.rs
+++ b/src/typecheck/linearization.rs
@@ -129,12 +129,6 @@ pub trait Linearizer {
     /// required or produced in parallel instances should therefore be put
     /// into the Building State `L` which is passed
     fn scope(&mut self) -> Self;
-
-    /// Create a scope for the processing of terms potentially contained in metadata (contracts
-    /// expressions in the annotations). When walking such terms, scoping behaves differently with
-    /// respect to the previous state (pending record fields, let binding, metavalue, etc.).
-    /// Mostly, the `self` linearizer keeps the state while the returned one has a clean state.
-    fn scope_meta(&mut self) -> Self;
 }
 
 /// [Linearizer] that deliberately does not maintain any state or act
@@ -153,10 +147,6 @@ where
     fn scope(&mut self) -> Self {
         StubHost::new()
     }
-
-    fn scope_meta(&mut self) -> Self {
-        self.scope()
-    }
 }
 
 impl<B, C, E> StubHost<B, C, E> {
diff --git a/src/typecheck/mod.rs b/src/typecheck/mod.rs
index 73861e92..c9a11d32 100644
--- a/src/typecheck/mod.rs
+++ b/src/typecheck/mod.rs
@@ -417,7 +417,7 @@ fn walk<L: Linearizer>(
         }
         // An type annotation switches mode to check.
         Term::MetaValue(meta) => {
-            meta.contracts.iter().chain(meta.types.iter()).try_for_each(|ty| walk_type(state, envs.clone(), lin, linearizer.scope_meta(), &ty.types))?;
+            meta.contracts.iter().chain(meta.types.iter()).try_for_each(|ty| walk_type(state, envs.clone(), lin, linearizer.scope(), &ty.types))?;
 
             match meta {
                 MetaValue {
@@ -431,9 +431,6 @@ fn walk<L: Linearizer>(
                 }
                 MetaValue {value: Some(t), .. } =>  walk(state, envs, lin, linearizer, t),
                 // A metavalue without a body nor a type annotation is a record field without definition.
-                // TODO: we might have something to with the linearizer to clear the current
-                // metadata. It looks like it may be unduly attached to the next field definition,
-                // which is not critical, but still a bug.
                 _ => Ok(()),
             }
         }
@@ -817,7 +814,7 @@ fn type_check_<L: Linearizer>(
                 .iter()
                 .chain(meta.types.iter())
                 .try_for_each(|ty| {
-                    walk_type(state, envs.clone(), lin, linearizer.scope_meta(), &ty.types)
+                    walk_type(state, envs.clone(), lin, linearizer.scope(), &ty.types)
                 })?;
 
             match meta {
@@ -846,13 +843,10 @@ fn type_check_<L: Linearizer>(
                         .map_err(|err| err.into_typecheck_err(state, rt.pos))?;
 
                     // if there's an inner value, we still have to walk it, as it may contain
-                    // statically typed blocks.
+                    // statically typed block.
                     if let Some(t) = value {
                         walk(state, envs, lin, linearizer, t)
                     } else {
-                        // TODO: we might have something to with the linearizer to clear the current
-                        // metadata. It looks like it may be unduly attached to the next field definition,
-                        // which is not critical, but still a bug.
                         Ok(())
                     }
                 }
@@ -863,9 +857,6 @@ fn type_check_<L: Linearizer>(
                 }
                 // A metavalue without a body nor a type annotation is a record field without definition.
                 // We infer it to be of type `Dyn` for now.
-                // TODO: we might have something to with the linearizer to clear the current
-                // metadata. It looks like it may be unduly attached to the next field definition,
-                // which is not critical, but still a bug.
                 _ => unify(state, ty, mk_typewrapper::dynamic())
                     .map_err(|err| err.into_typecheck_err(state, rt.pos)),
             }
diff --git a/src/typecheck/operation.rs b/src/typecheck/operation.rs
index 816d424f..17919ff8 100644
--- a/src/typecheck/operation.rs
+++ b/src/typecheck/operation.rs
@@ -22,7 +22,7 @@ pub fn get_uop_type(
                 mk_tyw_arrow!(branches.clone(), branches.clone(), branches),
             )
         }
-        // Dyn -> [| Num, Bool, Str, Enum, Fun, Array, Record, Lbl, Other |]
+        // Dyn -> [| `Num, `Bool, `Str, `Enum, `Fun, `Array, `Record, `Lbl, `Other |]
         UnaryOp::Typeof() => (
             mk_typewrapper::dynamic(),
             mk_tyw_enum!("Num", "Bool", "Str", "Enum", "Fun", "Array", "Record", "Lbl", "Other"),
diff --git a/src/types.rs b/src/types.rs
index 2bed8147..85e61a5d 100644
--- a/src/types.rs
+++ b/src/types.rs
@@ -477,10 +477,10 @@ impl fmt::Display for Types {
             AbsType::DynRecord(ty) => write!(f, "{{_: {}}}", ty),
             AbsType::RowEmpty() => Ok(()),
             AbsType::RowExtend(id, ty_opt, tail) => {
-                write!(f, "{}", id)?;
-
                 if let Some(ty) = ty_opt {
-                    write!(f, ": {}", ty)?;
+                    write!(f, "{}: {}", id, ty)?;
+                } else {
+                    write!(f, "`{}", id)?;
                 }
 
                 match tail.0 {
@@ -553,8 +553,8 @@ mod test {
         assert_format_eq("forall r. {x: Bool, y: Bool, z: Bool ; r}");
         assert_format_eq("{x: Bool, y: Bool, z: Bool}");
 
-        assert_format_eq("[|a, b, c, d|]");
-        assert_format_eq("forall r. [|tag1, tag2, tag3 ; r|]");
+        assert_format_eq("[|`a, `b, `c, `d|]");
+        assert_format_eq("forall r. [|`tag1, `tag2, `tag3 ; r|]");
 
         assert_format_eq("Array Num");
         assert_format_eq("Array (Array Num)");
diff --git a/stdlib/array.ncl b/stdlib/array.ncl
index 0a79d06e..75b3788a 100644
--- a/stdlib/array.ncl
+++ b/stdlib/array.ncl
@@ -1,22 +1,5 @@
 {
   array = {
-    ComparisonResult
-      | doc m%"
-          The result of a comparison. Must be one of the tags:
-
-          ```
-            `Less
-            `Equal
-            `Greater
-          ```
-        "%m
-      = fun label =>
-        label
-        |> contract.tag "must be one of `Less, `Equal, or `Greater"
-        # The enum type syntax [| ... |] is not stable. Do not rely on it in
-        # your own Nickel programs.
-        |> contract.apply [| Less, Equal, Greater |],
-
     NonEmpty
       | doc m%"
         Contract to ensure the given array is not empty.
@@ -271,7 +254,7 @@
         "%m
       = fun f n => %generate% n f,
 
-    sort | forall a. (a -> a -> ComparisonResult) -> Array a -> Array a
+    sort | forall a. (a -> a -> [| `Lesser, `Equal, `Greater |]) -> Array a -> Array a
       | doc m%"
         Sorts the given arrays based on the provided comparison operator.
 
diff --git a/stdlib/builtin.ncl b/stdlib/builtin.ncl
index d151a8ed..0cd9150f 100644
--- a/stdlib/builtin.ncl
+++ b/stdlib/builtin.ncl
@@ -1,75 +1,5 @@
 {
   builtin = {
-    ValueType
-      | doc m%"
-        Possible dynamic type for a value. Must be one of the tags:
-
-        ```
-         `Num
-         `Bool
-         `Str
-         `Enum
-         `Fun
-         `Array
-         `Record
-         `Lbl
-         `Other
-        ```
-        "%m
-      = fun label =>
-        label
-        |> contract.tag m%"
-          must be one of `Num, `Bool, `Str, `Enum, `Fun, `Array`, `Record, `Lbl or `Other
-        "%m
-        # The enum type syntax [| ... |] is not stable. Do not rely on it in
-        # your own Nickel programs.
-        |> contract.apply [|
-            Num,
-            Bool,
-            Str,
-            Enum,
-            Lbl,
-            Fun,
-            Array,
-            Record,
-            Other
-          |],
-
-    HashAlgorithm
-      | doc m%"
-        Supportd hash algorithm. Must be one of the tag:
-
-        ```
-          `Md5
-          `Sha1
-          `Sha256
-          `Sha512
-        ```
-        "%m
-      = fun label =>
-        label
-        |> contract.tag "must be one of `Md5, `Sha1, `Sha256, `Sha512"
-        # The enum type syntax [| ... |] is not stable. Do not rely on it in
-        # your own Nickel programs.
-        |> contract.apply [| Md5, Sha1, Sha256, Sha512 |],
-
-    ExportFormat
-      | doc m%"
-        Supported import and export format. Must be one of the tag:
-
-        ```
-          `Json
-          `Toml
-          `Yaml
-        ```
-        "%m
-      = fun label =>
-        label
-        |> contract.tag "must be one of `Json, `Toml or `Yaml"
-        # The enum type syntax [| ... |] is not stable. Do not rely on it in
-        # your own Nickel programs.
-        |> contract.apply [| Json, Toml, Yaml |],
-
     is_num : Dyn -> Bool
     | doc m%"
       Checks if the given value is a number.
@@ -169,7 +99,17 @@
       "%m
     = fun x => %typeof% x == `Record,
 
-    typeof | Dyn -> ValueType
+    typeof : Dyn -> [|
+        `Num,
+        `Bool,
+        `Str,
+        `Enum,
+        `Lbl,
+        `Fun,
+        `Array,
+        `Record,
+        `Other
+      |]
     | doc m%"
       Results in a value representing the type of the typed value.
 
@@ -215,7 +155,7 @@
       "%m
     = fun x y => %deep_seq% x y,
 
-    hash | HashAlgorithm -> Str -> Str
+    hash : [| `Md5, `Sha1, `Sha256, `Sha512 |] -> Str -> Str
     | doc m%"
       Hashes the given string provided the desired hash algorithm.
 
@@ -227,7 +167,7 @@
       "%m
     = fun type s => %hash% type s,
 
-    serialize | ExportFormat -> Dyn -> Str
+    serialize : [| `Json, `Toml, `Yaml |] -> Dyn -> Str
     | doc m%"
       Serializes the given value to the desired representation.
 
@@ -242,7 +182,7 @@
       "%m
     = fun format x => %serialize% format (%deep_seq% x x),
 
-    deserialize | ExportFormat -> Str -> Dyn
+    deserialize : [| `Json, `Toml, `Yaml |] -> Str -> Dyn
     | doc m%"
       Deserializes the given string to a nickel value given the encoding of the string.
 


Test patch:
diff --git a/tests/contracts_fail.rs b/tests/contracts_fail.rs
index 39d97a5f..511e0e4b 100644
--- a/tests/contracts_fail.rs
+++ b/tests/contracts_fail.rs
@@ -32,8 +32,8 @@ fn id_fail() {
 
 #[test]
 fn enum_simple() {
-    assert_raise_blame!("`far | [|foo, bar|]");
-    assert_raise_blame!("123 | [|foo, bar|]");
+    assert_raise_blame!("`far | [|`foo, `bar|]");
+    assert_raise_blame!("123 | [|`foo, `bar|]");
     assert_raise_blame!("`foo | [| |]");
 }
 
diff --git a/tests/pass/contracts.ncl b/tests/pass/contracts.ncl
index 2cbab5c7..d581e7a6 100644
--- a/tests/pass/contracts.ncl
+++ b/tests/pass/contracts.ncl
@@ -28,34 +28,34 @@ let Assert = fun l x => x || %blame% l in
   ("hello" ++ " world" | Str) == "hello world",
 
   # enums_simple
-  (`foo | [| foo, bar |]) == `foo,
-  (`bar | forall r. [| foo, bar ; r|]) == `bar,
+  (`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",
+  (`"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 =
+  let f : forall r. [| `foo, `bar ; r |] -> Num =
     fun x => switch { `foo => 1, `bar => 2, _ => 3, } x in
   f `bar == 2,
 
-  let f : forall r. [| foo, bar ; r |] -> Num =
+  let f : forall r. [| `foo, `bar ; r |] -> Num =
     fun x => switch { `foo => 1, `bar => 2, _ => 3, } x in
   f `boo == 3,
 
-  let f : forall r. [| foo, "bar:baz" ; r |] -> Num =
+  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 =
+  let f : forall r. [| `foo, `"bar:baz" ; r |] -> Num =
     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
+  let Wrapper = contract.apply [| `Foo, `Bar |] in
   (`Foo | Wrapper) == `Foo,
 
   # records_simple
diff --git a/tests/pass/typechecking.ncl b/tests/pass/typechecking.ncl
index 3adf43b3..3d6d56aa 100644
--- a/tests/pass/typechecking.ncl
+++ b/tests/pass/typechecking.ncl
@@ -50,28 +50,28 @@ let typecheck = [
     ((if (f true) then (f 2) else 3) : Num),
 
   # enums_simple
-  (`bla : [|bla |]),
-  (`blo : [|bla, blo |]),
-  (`bla : forall r. [|bla ; r |]),
-  (`bla : forall r. [|bla, blo ; r |]),
+  (`bla : [|`bla |]),
+  (`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),
 
   # enums_complex
-  ((fun x => switch {`bla => 1, `ble => 2} x) : [|bla, ble |] -> Num),
+  ((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),
+    : [|`bla, `ble |] -> Num),
   ((fun 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 =>
+  let f : forall r. [|`blo, `ble ; r |] -> Num = fun x =>
     switch {`blo => 1, `ble => 2, _ => 3} x in
             (f `bli : Num),
 
-  let f : forall r. (forall p. [|blo, ble ; r |] -> [|bla, bli ; p |]) =
+  let f : forall r. (forall p. [|`blo, `ble ; r |] -> [|`bla, `bli ; p |]) =
     fun x => switch {`blo => `bla, `ble => `bli, _ => `bla} x in
     f `bli,
 
diff --git a/tests/typecheck_fail.rs b/tests/typecheck_fail.rs
index 4cab24e0..105ad46e 100644
--- a/tests/typecheck_fail.rs
+++ b/tests/typecheck_fail.rs
@@ -77,7 +77,7 @@ fn simple_forall() {
 
 #[test]
 fn enum_simple() {
-    assert_typecheck_fails!("`foo : [| bar |]");
+    assert_typecheck_fails!("`foo : [| `bar |]");
     assert_typecheck_fails!("switch { `foo => 3} `bar : Num");
     assert_typecheck_fails!("switch { `foo => 3, `bar => true} `bar : Num");
 }
@@ -85,7 +85,7 @@ fn enum_simple() {
 #[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
@@ -95,12 +95,12 @@ fn enum_complex() {
             (switch {`bla => 6, `blo => 20} x)) `bla : Num"
     );
     assert_typecheck_fails!(
-        "let f : forall r. [| blo, ble ; r |] -> Num =
+        "let f : forall r. [| `blo, `ble ; r |] -> Num =
             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 |]) =
+        "let f : forall r. (forall p. [| `blo, `ble ; r |] -> [| `bla, `bli ; p |]) =
             fun x => (switch {`blo => `bla, `ble => `bli, _ => `blo} x) in
         f `bli"
     );