Repository: tweag/nickel
Pull request #983 (2022-12-08 14:35:34)

[Forwardport 0.3] Change switch to match
Forwardport (if that makes any sense) the introduction of the `match` keyword from 0.3 in #970. We had to first merge it in the 0.3 branch, because master had diverged too much after #916, and this direction was simpler.


Patch:
diff --git a/doc/manual/typing.md b/doc/manual/typing.md
index ca032045..5b35734c 100644
--- a/doc/manual/typing.md
+++ b/doc/manual/typing.md
@@ -211,11 +211,11 @@ The following type constructors are available:
 
   ```nickel
   let protocol : [| `http, `ftp, `sftp |] = `http in
-  (switch {
+  (protocol |> match {
     `http => 1,
     `ftp => 2,
     `sftp => 3
-  } protocol) : Num
+  }) : Num
   ```
 
 - **Arrow (function)**: `S -> T`. A function taking arguments of type `S` and
diff --git a/rfcs/001-overriding.md b/rfcs/001-overriding.md
index 8b451c20..d71dfb82 100644
--- a/rfcs/001-overriding.md
+++ b/rfcs/001-overriding.md
@@ -79,7 +79,7 @@ rather than data. Take the following example:
 ```nickel
 let record = {
   protocol | default = `Http,
-  port | default = switch protocol {
+  port | default = protocol |> match {
     `Http => 80,
     `Ftp => 21,
     _ => 8181,
diff --git a/rfcs/002-merge-types-terms-syntax.md b/rfcs/002-merge-types-terms-syntax.md
index 9bc91690..40c312c8 100644
--- a/rfcs/002-merge-types-terms-syntax.md
+++ b/rfcs/002-merge-types-terms-syntax.md
@@ -216,8 +216,8 @@ look-ahead to distinguish).
 
 With the first release in mind, we propose to just disable support for enum
 altogether. This is a handy but hardly fundamental feature, and this lets us
-more time to find a good syntax replacement. We can also disable `switch`
-temporarily, as currently its only usage is for enums.
+more time to find a good syntax replacement. We can also disable `switch` (edit
+from 05.12.22: now `match`) temporarily, as currently its only usage is for enums.
 
 ### Translation
 
diff --git a/src/eval/mod.rs b/src/eval/mod.rs
index bbd84e67..b3db913f 100644
--- a/src/eval/mod.rs
+++ b/src/eval/mod.rs
@@ -386,40 +386,7 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
                         env,
                     }
                 }
-                Term::Switch(exp, cases, default) => {
-                    self.set_mode(EvalMode::UnwrapMeta);
-
-                    let has_default = default.is_some();
-
-                    if let Some(t) = default {
-                        self.stack.push_arg(
-                            Closure {
-                                body: t.clone(),
-                                env: env.clone(),
-                            },
-                            pos,
-                        );
-                    }
 
-                    self.stack.push_arg(
-                        Closure {
-                            body: RichTerm::new(
-                                Term::Record(RecordData::with_fields(cases.clone())),
-                                pos,
-                            ),
-                            env: env.clone(),
-                        },
-                        pos,
-                    );
-
-                    Closure {
-                        body: RichTerm::new(
-                            Term::Op1(UnaryOp::Switch(has_default), exp.clone()),
-                            pos,
-                        ),
-                        env,
-                    }
-                }
                 Term::Op1(op, t) => {
                     self.set_mode(op.eval_mode());
 
@@ -686,6 +653,64 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
                         return Ok((RichTerm::new(Term::Fun(*x, t.clone()), pos), env));
                     }
                 }
+                // A match expression acts as a function (in Nickel, a match expression corresponds
+                // to the cases, and doesn't include the examined value).
+                //
+                // The behavior is the same as for a function: we look for an argument on the
+                // stack, and proceed to the evaluation of the match, or stop here otherwise. If
+                // found (let's call it `arg`), we evaluate `%match% arg cases default`, where
+                // `%match%` is the primitive operation `UnaryOp::Match` taking care of forcing the
+                // argument `arg` and doing the actual matching operation.
+                Term::Match { cases, default } => {
+                    if let Some((arg, pos_app)) = self.stack.pop_arg(&self.cache) {
+                        // Setting the stack to be as if we would have evaluated an application
+                        // `_ cases default`, where `default` is optional, and `_` is not relevant.
+
+                        let has_default = default.is_some();
+
+                        if let Some(t) = default {
+                            self.stack.push_arg(
+                                Closure {
+                                    body: t.clone(),
+                                    env: env.clone(),
+                                },
+                                pos,
+                            );
+                        }
+
+                        self.stack.push_arg(
+                            Closure {
+                                body: RichTerm::new(
+                                    Term::Record(RecordData::with_fields(cases.clone())),
+                                    pos,
+                                ),
+                                env: env.clone(),
+                            },
+                            pos,
+                        );
+
+                        // Now evaluating `%match% arg`, the left-most part of the application `%match%
+                        // arg cases default`, which is in fact a primop application.
+                        self.stack.push_op_cont(
+                            OperationCont::Op1(UnaryOp::Match { has_default }, pos_app),
+                            self.call_stack.len(),
+                            pos,
+                        );
+
+                        arg
+                    } else {
+                        return Ok((
+                            RichTerm::new(
+                                Term::Match {
+                                    cases: cases.clone(),
+                                    default: default.clone(),
+                                },
+                                pos,
+                            ),
+                            env,
+                        ));
+                    }
+                }
                 // Otherwise, this is either an ill-formed application, or we are done
                 t => {
                     if let Some((arg, pos_app)) = self.stack.pop_arg(&self.cache) {
@@ -850,7 +875,7 @@ pub fn subst<C: Cache>(
 
             RichTerm::new(Term::App(t1, t2), pos)
         }
-        Term::Switch(t, cases, default) => {
+        Term::Match {cases, default} => {
             let default =
                 default.map(|d| subst(cache, d, initial_env, env));
             let cases = cases
@@ -862,9 +887,8 @@ pub fn subst<C: Cache>(
                     )
                 })
                 .collect();
-            let t = subst(cache, t, initial_env, env);
 
-            RichTerm::new(Term::Switch(t, cases, default), pos)
+            RichTerm::new(Term::Match {cases, default}, pos)
         }
         Term::Op1(op, t) => {
             let t = subst(cache, t, initial_env, env);
diff --git a/src/eval/operation.rs b/src/eval/operation.rs
index d735e113..60b79d97 100644
--- a/src/eval/operation.rs
+++ b/src/eval/operation.rs
@@ -199,7 +199,7 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
                     Term::Bool(_) => "Bool",
                     Term::Str(_) => "Str",
                     Term::Enum(_) => "Enum",
-                    Term::Fun(..) => "Fun",
+                    Term::Fun(..) | Term::Match { .. } => "Fun",
                     Term::Array(..) => "Array",
                     Term::Record(..) | Term::RecRecord(..) => "Record",
                     Term::Lbl(..) => "Lbl",
@@ -304,17 +304,17 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
                     ))
                 }
             }
-            UnaryOp::Switch(has_default) => {
+            UnaryOp::Match { has_default } => {
                 let (cases_closure, ..) = self
                     .stack
                     .pop_arg(&self.cache)
-                    .expect("missing arg for switch");
+                    .expect("missing arg for match");
                 let default = if has_default {
                     Some(
                         self.stack
                             .pop_arg(&self.cache)
                             .map(|(clos, ..)| clos)
-                            .expect("missing default case for switch"),
+                            .expect("missing default case for match"),
                     )
                 } else {
                     None
@@ -331,7 +331,7 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
 
                     let mut cases = match cases_term.into_owned() {
                         Term::Record(r) => r.fields,
-                        _ => panic!("invalid argument for switch"),
+                        _ => panic!("invalid argument for match"),
                     };
 
                     cases
@@ -345,7 +345,7 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
                         // ? We should have a dedicated error for unmatched pattern
                         EvalError::TypeError(
                             String::from("Enum"),
-                            String::from("switch"),
+                            String::from("match"),
                             arg_pos,
                             RichTerm {
                                 term: t,
@@ -357,7 +357,7 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
                 } else {
                     Err(EvalError::TypeError(
                         String::from("Enum"),
-                        String::from("switch"),
+                        String::from("match"),
                         arg_pos,
                         RichTerm { term: t, pos },
                     ))
@@ -1077,7 +1077,7 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
                     ))
                 }
             }
-            UnaryOp::StrMatch() => {
+            UnaryOp::StrFind() => {
                 if let Term::Str(s) = &*t {
                     let re = regex::Regex::new(s)
                         .map_err(|err| EvalError::Other(err.to_string(), pos_op))?;
@@ -1087,7 +1087,7 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
                         param,
                         RichTerm::new(
                             Term::Op1(
-                                UnaryOp::StrMatchCompiled(re.into()),
+                                UnaryOp::StrFindCompiled(re.into()),
                                 RichTerm::new(Term::Var(param), pos_op_inh),
                             ),
                             pos_op_inh,
@@ -1119,7 +1119,7 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
                     ))
                 }
             }
-            UnaryOp::StrMatchCompiled(regex) => {
+            UnaryOp::StrFindCompiled(regex) => {
                 if let Term::Str(s) = &*t {
                     let capt = regex.captures(s);
                     let result = if let Some(capt) = capt {
@@ -1551,7 +1551,7 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
                     );
 
                     match *t1 {
-                        Term::Fun(..) => Ok(Closure {
+                        Term::Fun(..) | Term::Match { .. } => Ok(Closure {
                             body: RichTerm {
                                 term: t1,
                                 pos: pos1,
diff --git a/src/parser/grammar.lalrpop b/src/parser/grammar.lalrpop
index aec2c5f5..16d0a177 100644
--- a/src/parser/grammar.lalrpop
+++ b/src/parser/grammar.lalrpop
@@ -236,28 +236,6 @@ UniTerm: UniTerm = {
 
         UniTerm::from(rt)
     },
-    "switch" "{" <cases: (SwitchCase ",")*> <last: SwitchCase?> "}"
-        <exp: Term> => {
-        let mut acc = HashMap::with_capacity(cases.len());
-        let mut default = None;
-
-        for case in cases.into_iter().map(|x| x.0).chain(last.into_iter()) {
-            match case {
-                SwitchCase::Normal(id, t) => acc.insert(id, t),
-                // If there are multiple default cases, the last one silently
-                // erases the others. We should have a dedicated error for that
-                SwitchCase::Default(t) => default.replace(t),
-            };
-        }
-
-        UniTerm::from(
-            Term::Switch(
-                exp,
-                acc,
-                default,
-            )
-        )
-    },
     "if" <cond: Term> "then" <t1: Term> "else" <t2: Term> =>
         UniTerm::from(mk_app!(Term::Op1(UnaryOp::Ite(), cond), t1, t2)),
     <l: @L> <t: !> <r: @R> => {
@@ -300,6 +278,26 @@ Applicative: UniTerm = {
         => UniTerm::from(mk_term::op2(op, t1, t2)),
     NOpPre<AsTerm<RecordOperand>>,
     RecordOperand,
+    "match" "{" <cases: (MatchCase ",")*> <last: MatchCase?> "}" => {
+        let mut acc = HashMap::with_capacity(cases.len());
+        let mut default = None;
+
+        for case in cases.into_iter().map(|x| x.0).chain(last.into_iter()) {
+            match case {
+                MatchCase::Normal(id, t) => acc.insert(id, t),
+                // If there are multiple default cases, the last one silently
+                // erases the others. We should have a dedicated error for that
+                MatchCase::Default(t) => default.replace(t),
+            };
+        }
+
+        UniTerm::from(
+            Term::Match {
+                cases: acc,
+                default,
+            }
+        )
+    },
 };
 
 // The parametrized array type.
@@ -580,15 +578,15 @@ UOp: UnaryOp = {
     "num_from" => UnaryOp::NumFromStr(),
     "enum_from" => UnaryOp::EnumFromStr(),
     "str_is_match" => UnaryOp::StrIsMatch(),
-    "str_match" => UnaryOp::StrMatch(),
+    "str_find" => UnaryOp::StrFind(),
     "rec_force_op" => UnaryOp::RecForce(),
     "rec_default_op" => UnaryOp::RecDefault(),
     "record_empty_with_tail" => UnaryOp::RecordEmptyWithTail(),
 };
 
-SwitchCase: SwitchCase = {
-    <id: EnumTag> "=>" <t: Term> => SwitchCase::Normal(id, t),
-    "_" "=>" <t: Term> => SwitchCase::Default(<>),
+MatchCase: MatchCase = {
+    <id: EnumTag> "=>" <t: Term> => MatchCase::Normal(id, t),
+    "_" "=>" <t: Term> => MatchCase::Default(<>),
 }
 
 // Infix operators by precedence levels. Lowest levels take precedence over
@@ -845,7 +843,7 @@ extern {
         "in" => Token::Normal(NormalToken::In),
         "let" => Token::Normal(NormalToken::Let),
         "rec" => Token::Normal(NormalToken::Rec),
-        "switch" => Token::Normal(NormalToken::Switch),
+        "match" => Token::Normal(NormalToken::Match),
 
         "null" => Token::Normal(NormalToken::Null),
         "true" => Token::Normal(NormalToken::True),
@@ -951,7 +949,7 @@ extern {
         "str_replace" => Token::Normal(NormalToken::StrReplace),
         "str_replace_regex" => Token::Normal(NormalToken::StrReplaceRegex),
         "str_is_match" => Token::Normal(NormalToken::StrIsMatch),
-        "str_match" => Token::Normal(NormalToken::StrMatch),
+        "str_find" => Token::Normal(NormalToken::StrFind),
         "str_length" => Token::Normal(NormalToken::StrLength),
         "str_substr" => Token::Normal(NormalToken::StrSubstr),
         "str_from" => Token::Normal(NormalToken::ToStr),
diff --git a/src/parser/lexer.rs b/src/parser/lexer.rs
index 14fdefc3..e0d021c4 100644
--- a/src/parser/lexer.rs
+++ b/src/parser/lexer.rs
@@ -88,8 +88,8 @@ pub enum NormalToken<'input> {
     Let,
     #[token("rec")]
     Rec,
-    #[token("switch")]
-    Switch,
+    #[token("match")]
+    Match,
 
     #[token("null")]
     Null,
@@ -276,8 +276,8 @@ pub enum NormalToken<'input> {
     StrReplaceRegex,
     #[token("%str_is_match%")]
     StrIsMatch,
-    #[token("%str_match%")]
-    StrMatch,
+    #[token("%str_find%")]
+    StrFind,
     #[token("%str_length%")]
     StrLength,
     #[token("%str_substr%")]
@@ -319,7 +319,7 @@ pub enum NormalToken<'input> {
 
 pub const KEYWORDS: &[&str] = &[
     "Dyn", "Num", "Bool", "Str", "Array", "if", "then", "else", "forall", "in", "let", "rec",
-    "switch", "null", "true", "false", "fun", "import", "merge", "default", "doc", "optional",
+    "match", "null", "true", "false", "fun", "import", "merge", "default", "doc", "optional",
     "priority", "force",
 ];
 
diff --git a/src/parser/utils.rs b/src/parser/utils.rs
index bdf08ac1..52ff87b7 100644
--- a/src/parser/utils.rs
+++ b/src/parser/utils.rs
@@ -33,7 +33,7 @@ pub enum StringKind {
 
 /// Distinguish between a normal case `id => exp` and a default case `_ => exp`.
 #[derive(Clone, Debug)]
-pub enum SwitchCase {
+pub enum MatchCase {
     Normal(Ident, RichTerm),
     Default(RichTerm),
 }
diff --git a/src/pretty.rs b/src/pretty.rs
index f3c8228e..3753c607 100644
--- a/src/pretty.rs
+++ b/src/pretty.rs
@@ -581,40 +581,35 @@ where
                 .append(allocator.line())
                 .group()
                 .braces(),
-            Switch(tst, cases, def) => allocator
-                .text("switch")
-                .append(allocator.space())
-                .append(
-                    allocator
-                        .intersperse(
-                            sorted_map(cases).iter().map(|&(id, t)| {
-                                allocator
-                                    .text("`")
-                                    .append(allocator.quote_if_needed(id))
-                                    .append(allocator.space())
-                                    .append(allocator.text("=>"))
-                                    .append(allocator.space())
-                                    .append(t.to_owned().pretty(allocator))
-                                    .append(allocator.text(","))
-                            }),
-                            allocator.line(),
-                        )
-                        .append(def.clone().map_or(allocator.nil(), |d| {
+            Match { cases, default } => allocator.text("match").append(allocator.space()).append(
+                allocator
+                    .intersperse(
+                        sorted_map(cases).iter().map(|&(id, t)| {
                             allocator
-                                .line()
-                                .append(allocator.text("_"))
+                                .text("`")
+                                .append(allocator.quote_if_needed(id))
                                 .append(allocator.space())
                                 .append(allocator.text("=>"))
                                 .append(allocator.space())
-                                .append(d.pretty(allocator))
-                        }))
-                        .nest(2)
-                        .append(allocator.line_())
-                        .braces()
-                        .group(),
-                )
-                .append(allocator.space())
-                .append(allocator.atom(tst)),
+                                .append(t.to_owned().pretty(allocator))
+                                .append(allocator.text(","))
+                        }),
+                        allocator.line(),
+                    )
+                    .append(default.clone().map_or(allocator.nil(), |d| {
+                        allocator
+                            .line()
+                            .append(allocator.text("_"))
+                            .append(allocator.space())
+                            .append(allocator.text("=>"))
+                            .append(allocator.space())
+                            .append(d.pretty(allocator))
+                    }))
+                    .nest(2)
+                    .append(allocator.line_())
+                    .braces()
+                    .group(),
+            ),
             Array(fields, _) => allocator
                 // NOTE: the Array attributes are ignored here.
                 .line()
diff --git a/src/term.rs b/src/term.rs
index fc5d862b..d0331100 100644
--- a/src/term.rs
+++ b/src/term.rs
@@ -108,14 +108,15 @@ pub enum Term {
         Vec<(RichTerm, RichTerm)>, /* field whose name is defined by interpolation */
         Option<RecordDeps>, /* dependency tracking between fields. None before the free var pass */
     ),
-    /// A switch construct. The evaluation is done by the corresponding unary operator, but we
-    /// still need this one for typechecking.
+    /// A match construct. Correspond only to the match cases: this expression is still to be
+    /// applied to an argument to match on. Once applied, the evaluation is done by the
+    /// corresponding primitive operator. Still, we need this construct for typechecking and being
+    /// able to handle yet unapplied match expressions.
     #[serde(skip)]
-    Switch(
-        RichTerm,                 /* tested expression */
-        HashMap<Ident, RichTerm>, /* cases */
-        Option<RichTerm>,         /* default */
-    ),
+    Match {
+        cases: HashMap<Ident, RichTerm>,
+        default: Option<RichTerm>,
+    },
 
     /// An array.
     #[serde(serialize_with = "crate::serialize::serialize_array")]
@@ -830,7 +831,6 @@ impl<E> StrChunk<E> {
 }
 
 impl Term {
-    //#[cfg(test)]
     /// Recursively apply a function to all `Term`s contained in a `RichTerm`.
     pub fn apply_to_rich_terms<F>(&mut self, func: F)
     where
@@ -839,14 +839,16 @@ impl Term {
         use self::Term::*;
         match self {
             Null | ParseError(_) => (),
-            Switch(ref mut t, ref mut cases, ref mut def) => {
+            Match {
+                ref mut cases,
+                ref mut default,
+            } => {
                 cases.iter_mut().for_each(|c| {
                     let (_, t) = c;
                     func(t);
                 });
-                func(t);
-                if let Some(def) = def {
-                    func(def)
+                if let Some(default) = default {
+                    func(default)
                 }
             }
             Record(ref mut r) => {
@@ -908,6 +910,7 @@ impl Term {
             Term::Num(_) => Some("Num"),
             Term::Str(_) => Some("Str"),
             Term::Fun(_, _) | Term::FunPattern(_, _, _) => Some("Fun"),
+            Term::Match { .. } => Some("MatchExpression"),
             Term::Lbl(_) => Some("Label"),
             Term::Enum(_) => Some("Enum"),
             Term::Record(..) | Term::RecRecord(..) => Some("Record"),
@@ -919,7 +922,6 @@ impl Term {
             | Term::LetPattern(..)
             | Term::App(_, _)
             | Term::Var(_)
-            | Term::Switch(..)
             | Term::Op1(_, _)
             | Term::Op2(_, _, _)
             | Term::OpN(..)
@@ -952,6 +954,7 @@ impl Term {
                 format!("\"{}\"", chunks_str.join(""))
             }
             Term::Fun(_, _) | Term::FunPattern(_, _, _) => String::from("<func>"),
+            Term::Match { .. } => String::from("<func (match expr)>"),
             Term::Lbl(_) => String::from("<label>"),
             Term::Enum(id) => {
                 let re = regex::Regex::new("_?[a-zA-Z][_a-zA-Z0-9]*").unwrap();
@@ -994,7 +997,6 @@ impl Term {
             Term::Let(..)
             | Term::LetPattern(..)
             | Term::App(_, _)
-            | Term::Switch(..)
             | Term::Op1(_, _)
             | Term::Op2(_, _, _)
             | Term::OpN(..)
@@ -1039,6 +1041,8 @@ impl Term {
             | Term::Num(_)
             | Term::Str(_)
             | Term::Fun(_, _)
+            // match expressions are function
+            | Term::Match {..}
             | Term::Lbl(_)
             | Term::Enum(_)
             | Term::Record(..)
@@ -1049,7 +1053,6 @@ impl Term {
             | Term::FunPattern(..)
             | Term::App(_, _)
             | Term::Var(_)
-            | Term::Switch(..)
             | Term::Op1(_, _)
             | Term::Op2(_, _, _)
             | Term::OpN(..)
@@ -1088,7 +1091,7 @@ impl Term {
             | Term::Fun(_, _)
             | Term::FunPattern(_, _, _)
             | Term::App(_, _)
-            | Term::Switch(..)
+            | Term::Match { .. }
             | Term::Var(_)
             | Term::Op1(_, _)
             | Term::Op2(_, _, _)
@@ -1117,14 +1120,25 @@ impl Term {
             | Term::RecRecord(..)
             | Term::Array(..)
             | Term::Var(..)
-            | Term::Op1(..)
-            | Term::SealingKey(..) => true,
+            | Term::SealingKey(..)
+            // Those special cases aren't really atoms, but mustn't be parenthesized because they
+            // are really functions taking additional non-strict arguments and printed as "partial"
+            // infix operators.
+            //
+            // For example, `Op1(BoolOr, Var("x"))` is currently printed as `x ||`. Such operators
+            // must never parenthesized, such as in `(x ||)`.
+            //
+            // We might want a more robust mechanism for pretty printing such operators.
+            | Term::Op1(UnaryOp::BoolAnd(), _)
+            | Term::Op1(UnaryOp::BoolOr(), _)
+            => true,
             Term::Let(..)
-            | Term::Switch(..)
+            | Term::Match { .. }
             | Term::LetPattern(..)
             | Term::Fun(..)
             | Term::FunPattern(..)
             | Term::App(..)
+            | Term::Op1(..)
             | Term::Op2(..)
             | Term::OpN(..)
             | Term::Sealed(..)
@@ -1215,8 +1229,8 @@ pub enum UnaryOp {
     /// `embed c x` will have enum type `a | b | c`. It only affects typechecking as at runtime
     /// `embed someId` act like the identity.
     Embed(Ident),
-    /// A switch block. Used to match on a enumeration.
-    Switch(bool /* presence of a default case */), //HashMap<Ident, CapturedTerm>, Option<CapturedTerm>),
+    /// Evaluate a match block applied to an argument.
+    Match { has_default: bool },
 
     /// Static access to a record field.
     ///
@@ -1318,17 +1332,17 @@ pub enum UnaryOp {
     /// Transform a string to an enum.
     EnumFromStr(),
     /// Test if a regex matches a string.
-    /// Like [`UnaryOp::StrMatch`], this is a unary operator because we would like a way to share the
+    /// Like [`UnaryOp::StrFind`], this is a unary operator because we would like a way to share the
     /// same "compiled regex" for many matching calls. This is done by returning functions
-    /// wrapping [`UnaryOp::StrIsMatchCompiled`] and [`UnaryOp::StrMatchCompiled`]
+    /// wrapping [`UnaryOp::StrIsMatchCompiled`] and [`UnaryOp::StrFindCompiled`]
     StrIsMatch(),
     /// Match a regex on a string, and returns the captured groups together, the index of the
     /// match, etc.
-    StrMatch(),
+    StrFind(),
     /// Version of [`UnaryOp::StrIsMatch`] which remembers the compiled regex.
     StrIsMatchCompiled(CompiledRegex),
-    /// Version of [`UnaryOp::StrMatch`] which remembers the compiled regex.
-    StrMatchCompiled(CompiledRegex),
+    /// Version of [`UnaryOp::StrFind`] which remembers the compiled regex.
+    StrFindCompiled(CompiledRegex),
     /// Force full evaluation of a term and return it.
     ///
     /// This was added in the context of [`BinaryOp::ArrayLazyAssume`],
@@ -1707,10 +1721,10 @@ impl RichTerm {
                     pos,
                 )
             },
-            Term::Switch(t, cases, default) => {
+            Term::Match { cases, default } => {
                 // The annotation on `map_res` use Result's corresponding trait to convert from
                 // Iterator<Result> to a Result<Iterator>
-                let cases_res: Result<HashMap<Ident, RichTerm>, E> = cases
+                let cases_result : Result<HashMap<Ident, RichTerm>, E> = cases
                     .into_iter()
                     // For the conversion to work, note that we need a Result<(Ident,RichTerm), E>
                     .map(|(id, t)| t.traverse(f, state, order).map(|t_ok| (id, t_ok)))
@@ -1718,10 +1732,8 @@ impl RichTerm {
 
                 let default = default.map(|t| t.traverse(f, state, order)).transpose()?;
 
-                let t = t.traverse(f, state, order)?;
-
                 RichTerm::new(
-                    Term::Switch(t, cases_res?, default),
+                    Term::Match {cases: cases_result?, default },
                     pos,
                 )
             },
@@ -2033,22 +2045,22 @@ pub mod make {
     /// `mk_switch!(format, ("Json", json_case), ("Yaml", yaml_case) ; def)` corresponds to
     /// ``switch { `Json => json_case, `Yaml => yaml_case, _ => def} format``.
     #[macro_export]
-    macro_rules! mk_switch {
-        ( $exp:expr, $( ($id:expr, $body:expr) ),* ; $default:expr ) => {
+    macro_rules! mk_match {
+        ( $( ($id:expr, $body:expr) ),* ; $default:expr ) => {
             {
-                let mut map = std::collections::HashMap::new();
+                let mut cases = std::collections::HashMap::new();
                 $(
-                    map.insert($id.into(), $body.into());
+                    cases.insert($id.into(), $body.into());
                 )*
-                $crate::term::RichTerm::from($crate::term::Term::Switch($crate::term::RichTerm::from($exp), map, Some($crate::term::RichTerm::from($default))))
+                $crate::term::RichTerm::from($crate::term::Term::Match {cases, default: Some($crate::term::RichTerm::from($default)) })
             }
         };
-        ( $exp:expr, $( ($id:expr, $body:expr) ),*) => {
-                let mut map = std::collections::HashMap::new();
+        ( $( ($id:expr, $body:expr) ),*) => {
+                let mut cases = std::collections::HashMap::new();
                 $(
-                    map.insert($id.into(), $body.into());
+                    cases.insert($id.into(), $body.into());
                 )*
-                $crate::term::RichTerm::from($crate::term::Term::Switch($crate::term::RichTerm::from($exp), map, None))
+                $crate::term::RichTerm::from($crate::term::Term::Match {cases, default: None})
         };
     }
 
diff --git a/src/transform/free_vars.rs b/src/transform/free_vars.rs
index 05c89550..51587281 100644
--- a/src/transform/free_vars.rs
+++ b/src/transform/free_vars.rs
@@ -88,8 +88,7 @@ impl CollectFreeVars for RichTerm {
                 t1.collect_free_vars(free_vars);
                 t2.collect_free_vars(free_vars);
             }
-            Term::Switch(t, cases, default) => {
-                t.collect_free_vars(free_vars);
+            Term::Match { cases, default } => {
                 for t in cases.values_mut().chain(default.iter_mut()) {
                     t.collect_free_vars(free_vars);
                 }
diff --git a/src/transform/share_normal_form.rs b/src/transform/share_normal_form.rs
index 07b81011..a14ff803 100644
--- a/src/transform/share_normal_form.rs
+++ b/src/transform/share_normal_form.rs
@@ -181,7 +181,9 @@ pub fn should_share(t: &Term) -> bool {
         | Term::SealingKey(_)
         | Term::Var(_)
         | Term::Enum(_)
-        | Term::Fun(_, _) => false,
+        | Term::Fun(_, _)
+        // match acts like a function, and is a WHNF
+        | Term::Match {..} => false,
         _ => true,
     }
 }
diff --git a/src/typecheck/eq.rs b/src/typecheck/eq.rs
index 804dce68..1ccf2f6f 100644
--- a/src/typecheck/eq.rs
+++ b/src/typecheck/eq.rs
@@ -32,7 +32,7 @@
 //!
 //! ## Equality on terms
 //!
-//! The terms inside a type may be arbitrarily complex. Primops applications, `switch`, and the
+//! The terms inside a type may be arbitrarily complex. Primops applications, `match`, and the
 //! like are quite unlikely to appear inside an annotation (they surely appear inside contract
 //! definitions). We don't want to compare functions syntactically either. The spirit of this
 //! implementation is to equate aliases or simple constructs that may appear inlined inside an
diff --git a/src/typecheck/mod.rs b/src/typecheck/mod.rs
index ccfcc0c2..64475fc6 100644
--- a/src/typecheck/mod.rs
+++ b/src/typecheck/mod.rs
@@ -869,12 +869,10 @@ fn walk<L: Linearizer>(
             walk(state, ctxt.clone(), lin, linearizer.scope(), e)?;
             walk(state, ctxt, lin, linearizer, t)
         }
-        Term::Switch(exp, cases, default) => {
+        Term::Match {cases, default} => {
             cases.values().chain(default.iter()).try_for_each(|case| {
                 walk(state, ctxt.clone(), lin, linearizer.scope(), case)
-            })?;
-
-            walk(state, ctxt, lin, linearizer, exp)
+            })
         }
         Term::RecRecord(record, dynamic, ..) => {
             for (id, field) in record.fields.iter() {
@@ -1182,10 +1180,14 @@ fn type_check_<L: Linearizer>(
             type_check_(state, ctxt.clone(), lin, linearizer.scope(), e, arr)?;
             type_check_(state, ctxt, lin, linearizer, t, src)
         }
-        Term::Switch(exp, cases, default) => {
+        Term::Match { cases, default } => {
             // Currently, if it has a default value, we typecheck the whole thing as
-            // taking ANY enum, since it's more permissive and there's no loss of information
-            let res = state.table.fresh_type_uvar();
+            // taking ANY enum, since it's more permissive and there's no loss of information.
+
+            // A match expression is a special kind of function. Thus it's typed as `a -> b`, where
+            // `a` is a enum type determined by the matched tags and `b` is the type of each match
+            // arm.
+            let return_type = state.table.fresh_type_uvar();
 
             for case in cases.values() {
                 type_check_(
@@ -1194,13 +1196,20 @@ fn type_check_<L: Linearizer>(
                     lin,
                     linearizer.scope(),
                     case,
-                    res.clone(),
+                    return_type.clone(),
                 )?;
             }
 
             let erows = match default {
                 Some(t) => {
-                    type_check_(state, ctxt.clone(), lin, linearizer.scope(), t, res.clone())?;
+                    type_check_(
+                        state,
+                        ctxt.clone(),
+                        lin,
+                        linearizer.scope(),
+                        t,
+                        return_type.clone(),
+                    )?;
                     state.table.fresh_erows_uvar()
                 }
                 None => cases.iter().try_fold(
@@ -1211,8 +1220,13 @@ fn type_check_<L: Linearizer>(
                 )?,
             };
 
-            unify(state, &ctxt, ty, res).map_err(|err| err.into_typecheck_err(state, rt.pos))?;
-            type_check_(state, ctxt, lin, linearizer, exp, mk_uty_enum!(; erows))
+            // `arg_type` represents the type of arguments that the match expression can be applied
+            // to.
+            let arg_type = mk_uty_enum!(; erows);
+
+            // we unify the expected type of the match expression with `arg_type -> return_type`
+            unify(state, &ctxt, ty, mk_uty_arrow!(arg_type, return_type))
+                .map_err(|err| err.into_typecheck_err(state, rt.pos))
         }
         Term::Var(x) => {
             let x_ty = ctxt
diff --git a/src/typecheck/operation.rs b/src/typecheck/operation.rs
index e33d8895..a3a751ef 100644
--- a/src/typecheck/operation.rs
+++ b/src/typecheck/operation.rs
@@ -54,8 +54,8 @@ pub fn get_uop_type(
             codomain.constrain_fresh_erows_var(state, row_var_id);
             (domain, codomain)
         }
-        // This should not happen, as Switch() is only produced during evaluation.
-        UnaryOp::Switch(_) => panic!("cannot typecheck Switch()"),
+        // This should not happen, as a match primop is only produced during evaluation.
+        UnaryOp::Match { .. } => panic!("cannot typecheck match primop"),
         // Dyn -> Dyn
         UnaryOp::ChangePolarity() | UnaryOp::GoDom() | UnaryOp::GoCodom() | UnaryOp::GoArray() => {
             (mk_uniftype::dynamic(), mk_uniftype::dynamic())
@@ -166,13 +166,13 @@ pub fn get_uop_type(
             mk_uniftype::str(),
             mk_uty_arrow!(mk_uniftype::str(), mk_uniftype::bool()),
         ),
-        // Str -> Str -> {match: Str, index: Num, groups: Array Str}
-        UnaryOp::StrMatch() => (
+        // Str -> Str -> {matched: Str, index: Num, groups: Array Str}
+        UnaryOp::StrFind() => (
             mk_uniftype::str(),
             mk_uty_arrow!(
                 mk_uniftype::str(),
                 mk_uty_record!(
-                    ("match", TypeF::Str),
+                    ("matched", TypeF::Str),
                     ("index", TypeF::Num),
                     ("groups", mk_uniftype::array(TypeF::Str))
                 )
@@ -180,8 +180,8 @@ pub fn get_uop_type(
         ),
         // Str -> Bool
         UnaryOp::StrIsMatchCompiled(_) => (mk_uniftype::str(), mk_uniftype::bool()),
-        // Str -> {match: Str, index: Num, groups: Array Str}
-        UnaryOp::StrMatchCompiled(_) => (
+        // Str -> {matched: Str, index: Num, groups: Array Str}
+        UnaryOp::StrFindCompiled(_) => (
             mk_uniftype::str(),
             mk_uty_record!(
                 ("match", TypeF::Str),
@@ -398,7 +398,7 @@ pub fn get_nop_type(
             ],
             mk_uniftype::dynamic(),
         ),
-        // This should not happen, as Switch() is only produced during evaluation.
+        // This should not happen, as MergeContract() is only produced during evaluation.
         NAryOp::MergeContract() => panic!("cannot typecheck MergeContract()"),
     })
 }
diff --git a/src/types.rs b/src/types.rs
index a62e10c3..2260361e 100644
--- a/src/types.rs
+++ b/src/types.rs
@@ -703,7 +703,7 @@ impl EnumRows {
         let case_body = if has_tail {
             mk_term::var(value_arg)
         }
-        // Otherwise, we build a switch with all the tags as cases, which just returns the
+        // Otherwise, we build a match with all the tags as cases, which just returns the
         // original argument, and a default case that blames.
         //
         // For example, for an enum type [| `foo, `bar, `baz |], the `case` function looks
@@ -711,7 +711,7 @@ impl EnumRows {
         //
         // ```
         // fun l x =>
-        //   switch {
+        //   match {
         //     `foo => x,
         //     `bar => x,
         //     `baz => x,
@@ -719,11 +719,13 @@ impl EnumRows {
         //   } x
         // ```
         else {
-            RichTerm::from(Term::Switch(
-                mk_term::var(value_arg),
-                cases,
-                Some(mk_app!(contract::enum_fail(), mk_term::var(label_arg))),
-            ))
+            mk_app!(
+                Term::Match {
+                    cases,
+                    default: Some(mk_app!(contract::enum_fail(), mk_term::var(label_arg))),
+                },
+                mk_term::var(value_arg)
+            )
         };
         let case = mk_fun!(label_arg, value_arg, case_body);
 
diff --git a/stdlib/string.ncl b/stdlib/string.ncl
index de5ca0bc..9b96b374 100644
--- a/stdlib/string.ncl
+++ b/stdlib/string.ncl
@@ -349,24 +349,24 @@
       "%
     = fun regex => %str_is_match% regex,
 
-    match : Str -> Str -> {match: Str, index: Num, groups: Array Str}
+    find : Str -> Str -> {matched: Str, index: Num, groups: Array Str}
     | doc m%"
-      `match regex str` matches `str` given `regex`. Results in the part of `str` that matched, the index of the
+      `find regex str` matches `str` given `regex`. Results in the part of `str` that matched, the index of the
       first character that was part of the match in `str`, and a arrays of all capture groups if any.
 
       For example:
       ```nickel
-        match "^(\\d).*(\\d).*(\\d).*$" "5 apples, 6 pears and 0 grapes" =>
-          { match = "5 apples, 6 pears and 0 grapes", index = 0, groups = [ "5", "6", "0" ] }
-        match "3" "01234" =>
-          { match = "3", index = 3, groups = [ ] }
+        find "^(\\d).*(\\d).*(\\d).*$" "5 apples, 6 pears and 0 grapes" =>
+          { matched = "5 apples, 6 pears and 0 grapes", index = 0, groups = [ "5", "6", "0" ] }
+        find "3" "01234" =>
+          { matched = "3", index = 3, groups = [ ] }
       ```
 
       Note that this function may perform better by sharing its partial application between multiple calls,
       because in this case the underlying regular expression will only be compiled once (See the documentation
       of `string.is_match` for more details).
       "%
-    = fun regex => %str_match% regex,
+    = fun regex => %str_find% regex,
 
     length : Str -> Num
     | doc m%"


Test patch:
diff --git a/src/eval/tests.rs b/src/eval/tests.rs
index 8d09efd2..62d2d3ff 100644
--- a/src/eval/tests.rs
+++ b/src/eval/tests.rs
@@ -364,10 +364,10 @@ fn substitution() {
         parse("let x = 1 in if true then 1 + (if false then 1 else \"Glob2\") else false").unwrap()
     );
 
-    let t = parse("switch {`x => [1, glob1], `y => loc2, `z => {id = true, other = glob3}} loc1")
+    let t = parse("match{`x => [1, glob1], `y => loc2, `z => {id = true, other = glob3}} loc1")
         .unwrap();
     assert_eq!(
         subst(&eval_cache, t, &initial_env, &env),
-        parse("switch {`x => [1, 1], `y => (if false then 1 else \"Glob2\"), `z => {id = true, other = false}} true").unwrap()
+        parse("match {`x => [1, 1], `y => (if false then 1 else \"Glob2\"), `z => {id = true, other = false}} true").unwrap()
     );
 }
diff --git a/src/parser/tests.rs b/src/parser/tests.rs
index 7ad5cf4d..4590e330 100644
--- a/src/parser/tests.rs
+++ b/src/parser/tests.rs
@@ -5,7 +5,7 @@ use crate::parser::error::ParseError as InternalParseError;
 use crate::term::make as mk_term;
 use crate::term::Term::*;
 use crate::term::{record, BinaryOp, RichTerm, StrChunk, UnaryOp};
-use crate::{mk_app, mk_switch};
+use crate::{mk_app, mk_match};
 use assert_matches::assert_matches;
 use codespan::Files;
 
@@ -177,14 +177,20 @@ fn enum_terms() {
             Enum(Ident::from("this works!")).into(),
         ),
         (
-            "switch with raw tags",
-            "switch { `foo => true, `bar => false, _ => 456, } 123",
-            mk_switch!(Num(123.), ("foo", Bool(true)), ("bar", Bool(false)) ; Num(456.)),
+            "match with raw tags",
+            "match { `foo => true, `bar => false, _ => 456, } 123",
+            mk_app!(
+                mk_match!(("foo", Bool(true)), ("bar", Bool(false)) ; Num(456.)),
+                Num(123.)
+            ),
         ),
         (
-            "switch with string tags",
-            "switch { `\"one:two\" => true, `\"three four\" => false, _ => 13 } 1",
-            mk_switch!(Num(1.), ("one:two", Bool(true)), ("three four", Bool(false)) ; Num(13.)),
+            "match with string tags",
+            "match { `\"one:two\" => true, `\"three four\" => false, _ => 13 } 1",
+            mk_app!(
+                mk_match!(("one:two", Bool(true)), ("three four", Bool(false)) ; Num(13.)),
+                Num(1.)
+            ),
         ),
     ];
 
diff --git a/tests/integration/contracts_fail.rs b/tests/integration/contracts_fail.rs
index f1483908..a5e610a1 100644
--- a/tests/integration/contracts_fail.rs
+++ b/tests/integration/contracts_fail.rs
@@ -277,12 +277,11 @@ fn records_contracts_closed() {
     assert_raise_blame!("let Contract = {a | Num} & {b | Num} in ({a=1, b=2, c=3} | Contract)");
 }
 
-// #[test]
-// fn enum_complex() {
-//     eval(
-//         "let f : <foo, bar> -> Num =
-//             fun x => switch { `foo => 1, `bar => 2, } x in
-//             f `boo",
-//     )
-//     .unwrap_err();
-// }
+#[test]
+fn enum_complex() {
+    eval(
+        "let f : [| `foo, `bar |] -> Num = match { `foo => 1, `bar => 2, } in
+         f `boo",
+    )
+    .unwrap_err();
+}
diff --git a/tests/integration/pass/annotations.ncl b/tests/integration/pass/annotations.ncl
index 0bbf261c..4146d2d7 100644
--- a/tests/integration/pass/annotations.ncl
+++ b/tests/integration/pass/annotations.ncl
@@ -21,7 +21,5 @@ let {check, Assert, ..} = import "testlib.ncl" 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),
 ]
 |> check
diff --git a/tests/integration/pass/basics.ncl b/tests/integration/pass/basics.ncl
index 27fc35ca..efcc3161 100644
--- a/tests/integration/pass/basics.ncl
+++ b/tests/integration/pass/basics.ncl
@@ -31,7 +31,7 @@ let {check, ..} = import "testlib.ncl" in
   1/4 + 1/4 - 1/4 + 1/4 >= 1/2 == true,
   1/4 + 1/4 - 1/4 + 1/4 < 1/2 == false,
 
-  # This test checks that the terms of a switch are closured
-  let x = 3 in (switch { `foo => 1, _ => x} (3 + 2)) == 3,
+  # This test checks that the terms of a match are closured
+  let x = 3 in ((3 + 2) |> match { `foo => 1, _ => x}) == 3,
 ]
 |> check
diff --git a/tests/integration/pass/contracts.ncl b/tests/integration/pass/contracts.ncl
index e14c0d74..383e4ab4 100644
--- a/tests/integration/pass/contracts.ncl
+++ b/tests/integration/pass/contracts.ncl
@@ -37,19 +37,19 @@ let {check, Assert, ..} = import "testlib.ncl" in
 
   # enums_complex
   let f : forall r. [| `foo, `bar ; r |] -> Num =
-    fun x => switch { `foo => 1, `bar => 2, _ => 3, } x in
+    match { `foo => 1, `bar => 2, _ => 3, } in
   f `bar == 2,
 
   let f : forall r. [| `foo, `bar ; r |] -> Num =
-    fun x => switch { `foo => 1, `bar => 2, _ => 3, } x in
+    match { `foo => 1, `bar => 2, _ => 3, } in
   f `boo == 3,
 
   let f : forall r. [| `foo, `"bar:baz" ; r |] -> Num =
-    fun x => switch { `foo => 1, `"bar:baz" => 2, _ => 3, } x in
+    fun x => match { `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
+    fun x => match { `foo => 1, `"bar:baz" => 2, _ => 3, } x in
   f `"boo,grr" == 3,
 
   # enums_applied
diff --git a/tests/integration/pass/serialize.ncl b/tests/integration/pass/serialize.ncl
index f496a978..13acf4e9 100644
--- a/tests/integration/pass/serialize.ncl
+++ b/tests/integration/pass/serialize.ncl
@@ -21,9 +21,9 @@ let assertDeserInv = fun x =>
   assertSerInv {val = ["a", 3, []]},
   assertSerInv {a.foo.bar = "2", b = false, c = [{d = "e"}, {d = "f"}]},
 
-  # regression test for a previously missing `#[serde(skip)]` on the `Switch`
+  # regression test for a previously missing `#[serde(skip)]` on the `Match`
   # variant of `term::Term`. That was causing a list with the right size to be
-  # serialized as a `Switch` instead of an array. This test checks that lists of
+  # serialized as a `Match` instead of an array. This test checks that lists of
   # various sizes don't get misinterpreted again as a term construct that misses a
   # `#[serde(skip)]` annotations
   {
diff --git a/tests/integration/pass/typechecking.ncl b/tests/integration/pass/typechecking.ncl
index 1e192c4e..42b47240 100644
--- a/tests/integration/pass/typechecking.ncl
+++ b/tests/integration/pass/typechecking.ncl
@@ -54,25 +54,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),
+  ((`bla |> match {`bla => 3}) : Num),
+  ((`blo |> match {`bla => 3, _ => 2}) : 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 => x |> match {`bla => 1, `ble => 2}) : [|`bla, `ble |] -> Num),
+  ((fun x => %embed% bli x |> match {`bla => 1, `ble => 2, `bli => 4})
     : [|`bla, `ble |] -> Num),
   ((fun x =>
-      (switch {`bla => 3, `bli => 2} x)
-      + (switch {`bli => 6, `bla => 20} x))
+      (x |> match {`bla => 3, `bli => 2})
+      + (x |> match {`bli => 6, `bla => 20}))
     `bla
     : Num),
 
-  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. [|`blo, `ble ; r |] -> Num =
+    match {`blo => 1, `ble => 2, _ => 3} 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
+    match {`blo => `bla, `ble => `bli, _ => `bla} in
     f `bli,
 
   # recursive let bindings
diff --git a/tests/integration/typecheck_fail.rs b/tests/integration/typecheck_fail.rs
index 1a4c0e34..7799256e 100644
--- a/tests/integration/typecheck_fail.rs
+++ b/tests/integration/typecheck_fail.rs
@@ -79,30 +79,28 @@ 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!("match { `foo => 3} `bar : Num");
+    assert_typecheck_fails!("match { `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"
-    );
+    assert_typecheck_fails!("(match {`bla => 1, `ble => 2, `bli => 4}) : [| `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"
+            (x |> match {`bla => 3, `bli => 2}) +
+            (x |> match {`bla => 6, `blo => 20})) `bla : Num"
     );
     assert_typecheck_fails!(
         "let f : forall r. [| `blo, `ble ; r |] -> Num =
-            fun x => (switch {`blo => 1, `ble => 2, `bli => 3} x) in
+            match {`blo => 1, `ble => 2, `bli => 3} 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
+            match {`blo => `bla, `ble => `bli, _ => `blo} in
         f `bli"
     );
 }