Repository: tweag/nickel
Pull request #751 (2022-06-24 16:39:57)

[Refactor] Get rid of is_xxx primops in favor of typeof
I needed to add a missing `builtin.is_enum` function, and the workflow is quite heavy:

- add a `is_enum` token in the lexer
- add corresponding cases in the grammar
- add an `IsEnum()` unary op and its implementation
- add the type of `IsEnum()` to `typecheck::operation` so that we can typecheck it
- add a case in `builtin.typeof`

It turns out our primops are all the `is_xxx` functions, and `builtin.typeof` is defined as a big if-then-else applying those functions in order. This is not really optimal.

This PR instead implements only one primop `typeof` once and for all. The `builtin.is_xxx` are just defined as standard functions using `typeof`.

Incidentally improve the macros `mk_ty_enum_rows/mk_ty_enum` slightly by making the tail optional.


Patch:
diff --git a/src/eval/operation.rs b/src/eval/operation.rs
index e39557d8..c3dfe168 100644
--- a/src/eval/operation.rs
+++ b/src/eval/operation.rs
@@ -180,30 +180,23 @@ fn process_unary_operation(
                 ))
             }
         }
-        UnaryOp::IsNum() => Ok(Closure::atomic_closure(RichTerm::new(
-            Term::Bool(matches!(*t, Term::Num(..))),
-            pos_op_inh,
-        ))),
-        UnaryOp::IsBool() => Ok(Closure::atomic_closure(RichTerm::new(
-            Term::Bool(matches!(*t, Term::Bool(..))),
-            pos_op_inh,
-        ))),
-        UnaryOp::IsStr() => Ok(Closure::atomic_closure(RichTerm::new(
-            Term::Bool(matches!(*t, Term::Str(..))),
-            pos_op_inh,
-        ))),
-        UnaryOp::IsFun() => Ok(Closure::atomic_closure(RichTerm::new(
-            Term::Bool(matches!(*t, Term::Fun(..))),
-            pos_op_inh,
-        ))),
-        UnaryOp::IsArray() => Ok(Closure::atomic_closure(RichTerm::new(
-            Term::Bool(matches!(*t, Term::Array(..))),
-            pos_op_inh,
-        ))),
-        UnaryOp::IsRecord() => Ok(Closure::atomic_closure(RichTerm::new(
-            Term::Bool(matches!(*t, Term::Record(..) | Term::RecRecord(..))),
-            pos_op_inh,
-        ))),
+        UnaryOp::Typeof() => {
+            let result = match *t {
+                Term::Num(_) => "Num",
+                Term::Bool(_) => "Bool",
+                Term::Str(_) => "Str",
+                Term::Enum(_) => "Enum",
+                Term::Fun(..) => "Fun",
+                Term::Array(..) => "Array",
+                Term::Record(..) | Term::RecRecord(..) => "Record",
+                Term::Lbl(..) => "Lbl",
+                _ => "Other",
+            };
+            Ok(Closure::atomic_closure(RichTerm::new(
+                Term::Enum(Ident::from(result)),
+                pos_op_inh,
+            )))
+        }
         UnaryOp::BoolAnd() =>
         // The syntax should not allow partially applied boolean operators.
         {
diff --git a/src/eval/stack.rs b/src/eval/stack.rs
index bb7588cb..a3043df7 100644
--- a/src/eval/stack.rs
+++ b/src/eval/stack.rs
@@ -346,7 +346,7 @@ mod tests {
     }
 
     fn some_cont() -> OperationCont {
-        OperationCont::Op1(UnaryOp::IsNum(), TermPos::None)
+        OperationCont::Op1(UnaryOp::Typeof(), TermPos::None)
     }
 
     fn some_arg_marker() -> Marker {
diff --git a/src/grammar.lalrpop b/src/grammar.lalrpop
index 79bbb71d..da72053f 100644
--- a/src/grammar.lalrpop
+++ b/src/grammar.lalrpop
@@ -502,6 +502,15 @@ StaticString: String = StringStart <s: ChunkLiteral?> StringEnd => s.unwrap_or_d
 EnumTag: Ident = {
     <Ident>,
     <StaticString> => <>.into(),
+    // Allow tags of the form `Str, `Bool, etc. This is ad-hoc and should
+    // eventually be replaced with a proper lexing of enum tags, but this is not
+    // a trivial task. For now, to make builtin.typeof usable, we just
+    // special-case builtin types.
+    "Dyn" => "Dyn".into(),
+    "Num" => "Num".into(),
+    "Bool" => "Bool".into(),
+    "Str" => "Str".into(),
+    "Array" => "Array".into(),
 }
 
 ChunkLiteralPart: ChunkLiteralPart<'input> = {
@@ -511,12 +520,7 @@ ChunkLiteralPart: ChunkLiteralPart<'input> = {
 };
 
 UOp: UnaryOp = {
-    "is_num" => UnaryOp::IsNum(),
-    "is_bool" => UnaryOp::IsBool(),
-    "is_str" => UnaryOp::IsStr(),
-    "is_fun" => UnaryOp::IsFun(),
-    "is_array" => UnaryOp::IsArray(),
-    "is_record" => UnaryOp::IsRecord(),
+    "typeof" => UnaryOp::Typeof(),
     "blame" => UnaryOp::Blame(),
     "chng_pol" => UnaryOp::ChangePolarity(),
     "polarity" => UnaryOp::Pol(),
@@ -841,12 +845,7 @@ extern {
         "Array" => Token::Normal(NormalToken::Array),
 
         "tag" => Token::Normal(NormalToken::Tag),
-        "is_num" => Token::Normal(NormalToken::IsNum),
-        "is_bool" => Token::Normal(NormalToken::IsBool),
-        "is_str" => Token::Normal(NormalToken::IsStr),
-        "is_fun" => Token::Normal(NormalToken::IsFun),
-        "is_array" => Token::Normal(NormalToken::IsArray),
-        "is_record" => Token::Normal(NormalToken::IsRecord),
+        "typeof" => Token::Normal(NormalToken::Typeof),
         "assume" => Token::Normal(NormalToken::Assume),
         "blame" => Token::Normal(NormalToken::Blame),
         "chng_pol" => Token::Normal(NormalToken::ChangePol),
diff --git a/src/parser/lexer.rs b/src/parser/lexer.rs
index 17e9e857..5ef17e70 100644
--- a/src/parser/lexer.rs
+++ b/src/parser/lexer.rs
@@ -147,18 +147,8 @@ pub enum NormalToken<'input> {
 
     #[token("%tag%")]
     Tag,
-    #[token("%is_num%")]
-    IsNum,
-    #[token("%is_bool%")]
-    IsBool,
-    #[token("%is_str%")]
-    IsStr,
-    #[token("%is_fun%")]
-    IsFun,
-    #[token("%is_array%")]
-    IsArray,
-    #[token("%is_record%")]
-    IsRecord,
+    #[token("%typeof%")]
+    Typeof,
 
     #[token("%assume%")]
     Assume,
diff --git a/src/pretty.rs b/src/pretty.rs
index 176d24c4..8707a9d1 100644
--- a/src/pretty.rs
+++ b/src/pretty.rs
@@ -126,11 +126,7 @@ where
             ArrayTail() => allocator.text("%tail%").append(allocator.space()),
             ArrayHead() => allocator.text("%head%").append(allocator.space()),
             DeepSeq(_) => allocator.text("%deep_seq%").append(allocator.space()),
-            IsNum() => allocator.text("%is_num%").append(allocator.space()),
-            IsFun() => allocator.text("%is_fun%").append(allocator.space()),
-            IsStr() => allocator.text("%is_str%").append(allocator.space()),
-            IsArray() => allocator.text("%is_array%").append(allocator.space()),
-            IsRecord() => allocator.text("%is_record%").append(allocator.space()),
+            Typeof() => allocator.text("%typeof%").append(allocator.space()),
             BoolNot() => allocator.text("!"),
             BoolAnd() => allocator.space().append(allocator.text("&&")),
             BoolOr() => allocator.space().append(allocator.text("||")),
diff --git a/src/term.rs b/src/term.rs
index 598580f5..96ce7bbb 100644
--- a/src/term.rs
+++ b/src/term.rs
@@ -709,18 +709,8 @@ pub enum UnaryOp {
     /// If-then-else.
     Ite(),
 
-    /// Test if a term is a numeral.
-    IsNum(),
-    /// Test if a term is a boolean.
-    IsBool(),
-    /// Test if a term is string literal.
-    IsStr(),
-    /// Test if a term is a function.
-    IsFun(),
-    /// Test if a term is an array.
-    IsArray(),
-    /// Test if a term is a record.
-    IsRecord(),
+    /// Return an enum tag representing the type of the term.
+    Typeof(),
 
     // Boolean AND and OR operator are encoded as unary operators so that they can be lazy in their
     // second argument.
diff --git a/src/typecheck/mk_typewrapper.rs b/src/typecheck/mk_typewrapper.rs
index 38a367ac..0bdb30b8 100644
--- a/src/typecheck/mk_typewrapper.rs
+++ b/src/typecheck/mk_typewrapper.rs
@@ -18,21 +18,24 @@ macro_rules! mk_tyw_arrow {
 }
 
 /// Multi-ary enum row constructor for types implementing `Into<TypeWrapper>`.
-/// `mk_tyw_enum_row!(id1, .., idn, tail)` correspond to `<id1, .., idn | tail>.
+/// `mk_tyw_enum_row!(id1, .., idn; tail)` correspond to `<id1, .., idn | tail>.
 #[macro_export]
 macro_rules! mk_tyw_enum_row {
-    ($id:expr, $tail:expr) => {
+    () => {
+        $crate::typecheck::TypeWrapper::from(AbsType::RowEmpty())
+    };
+    (; $tail:expr) => {
+        $crate::typecheck::TypeWrapper::from($tail)
+    };
+    ( $id:expr $(, $ids:expr )* $(; $tail:expr)?) => {
         $crate::typecheck::TypeWrapper::Concrete(
             $crate::types::AbsType::RowExtend(
                 Ident::from($id),
                 None,
-                Box::new($crate::typecheck::TypeWrapper::from($tail))
+                Box::new(mk_tyw_enum_row!($( $ids ),* $(; $tail)?))
             )
         )
     };
-    ( $fst:expr, $snd:expr , $( $rest:expr ),+ ) => {
-        mk_tyw_enum_row!($fst, mk_tyw_enum_row!($snd, $( $rest),+))
-    };
 }
 
 /// Multi-ary record row constructor for types implementing `Into<TypeWrapper>`.
@@ -60,16 +63,13 @@ macro_rules! mk_tyw_row {
 /// Wrapper around `mk_tyw_enum_row!` to build an enum type from an enum row.
 #[macro_export]
 macro_rules! mk_tyw_enum {
-    ( $rows:expr ) => {
+    ($( $ids:expr ),* $(; $tail:expr)?) => {
         $crate::typecheck::TypeWrapper::Concrete(
             $crate::types::AbsType::Enum(
-                Box::new($rows.into())
+                Box::new(mk_tyw_enum_row!($( $ids ),* $(; $tail)?))
             )
         )
     };
-    ( $fst:expr, $( $rest:expr ),+ ) => {
-        mk_tyw_enum!(mk_tyw_enum_row!($fst, $( $rest),+))
-    };
 }
 
 /// Wrapper around `mk_tyw_record!` to build a record type from a record row.
diff --git a/src/typecheck/mod.rs b/src/typecheck/mod.rs
index 2e6dfded..a37fe306 100644
--- a/src/typecheck/mod.rs
+++ b/src/typecheck/mod.rs
@@ -670,13 +670,13 @@ fn type_check_<L: Linearizer>(
                 None => cases.iter().try_fold(
                     mk_typewrapper::row_empty(),
                     |acc, x| -> Result<TypeWrapper, TypecheckError> {
-                        Ok(mk_tyw_enum_row!(x.0.clone(), acc))
+                        Ok(mk_tyw_enum_row!(x.0.clone(); acc))
                     },
                 )?,
             };
 
             unify(state, ty, res).map_err(|err| err.into_typecheck_err(state, rt.pos))?;
-            type_check_(state, envs, lin, linearizer, exp, mk_tyw_enum!(row))
+            type_check_(state, envs, lin, linearizer, exp, mk_tyw_enum!(; row))
         }
         Term::Var(x) => {
             let x_ty = envs
@@ -688,7 +688,7 @@ fn type_check_<L: Linearizer>(
         }
         Term::Enum(id) => {
             let row = state.table.fresh_unif_var();
-            unify(state, ty, mk_tyw_enum!(id.clone(), row))
+            unify(state, ty, mk_tyw_enum!(id.clone(); row))
                 .map_err(|err| err.into_typecheck_err(state, rt.pos))
         }
         // If some fields are defined dynamically, the only potential type that works is `{_ : a}`
@@ -1277,13 +1277,13 @@ pub fn unify(state: &mut State, mut t1: TypeWrapper, mut t2: TypeWrapper) -> Res
                     if r1.is_row_type() && r2.is_row_type() =>
                 {
                     unify_rows(state, r1.clone(), r2.clone())
-                        .map_err(|err| err.into_unif_err(mk_tyw_enum!(r1), mk_tyw_enum!(r2)))
+                        .map_err(|err| err.into_unif_err(mk_tyw_enum!(; r1), mk_tyw_enum!(; r2)))
                 }
                 (TypeWrapper::Concrete(r), _) if !r.is_row_type() => {
-                    Err(UnifError::IllformedType(mk_tyw_enum!(r)))
+                    Err(UnifError::IllformedType(mk_tyw_enum!(; r)))
                 }
                 (_, TypeWrapper::Concrete(r)) if !r.is_row_type() => {
-                    Err(UnifError::IllformedType(mk_tyw_enum!(r)))
+                    Err(UnifError::IllformedType(mk_tyw_enum!(; r)))
                 }
                 (tyw1, tyw2) => unify(state, tyw1, tyw2),
             },
diff --git a/src/typecheck/operation.rs b/src/typecheck/operation.rs
index 0d67b94b..816d424f 100644
--- a/src/typecheck/operation.rs
+++ b/src/typecheck/operation.rs
@@ -22,16 +22,11 @@ pub fn get_uop_type(
                 mk_tyw_arrow!(branches.clone(), branches.clone(), branches),
             )
         }
-        // forall a. a -> Bool
-        UnaryOp::IsNum()
-        | UnaryOp::IsBool()
-        | UnaryOp::IsStr()
-        | UnaryOp::IsFun()
-        | UnaryOp::IsArray()
-        | UnaryOp::IsRecord() => {
-            let inp = TypeWrapper::Ptr(state.table.fresh_var());
-            (inp, mk_typewrapper::bool())
-        }
+        // 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"),
+        ),
         // Bool -> Bool -> Bool
         UnaryOp::BoolAnd() | UnaryOp::BoolOr() => (
             mk_typewrapper::bool(),
@@ -52,7 +47,7 @@ pub fn get_uop_type(
             let row = TypeWrapper::Ptr(state.table.fresh_var());
             // Constraining a freshly created variable should never fail.
             constraint(state, row.clone(), id.clone()).unwrap();
-            (mk_tyw_enum!(row.clone()), mk_tyw_enum!(id.clone(), row))
+            (mk_tyw_enum!(; row.clone()), mk_tyw_enum!(id.clone(); row))
         }
         // This should not happen, as Switch() is only produced during evaluation.
         UnaryOp::Switch(_) => panic!("cannot typecheck Switch()"),
@@ -170,7 +165,7 @@ pub fn get_uop_type(
         // Str -> < | Dyn>
         UnaryOp::EnumFromStr() => (
             mk_typewrapper::str(),
-            mk_tyw_enum!(mk_typewrapper::dynamic()),
+            mk_tyw_enum!(; mk_typewrapper::dynamic()),
         ),
         // Str -> Str -> Bool
         UnaryOp::StrIsMatch() => (
@@ -325,13 +320,7 @@ pub fn get_bop_type(
         ),
         // <Md5, Sha1, Sha256, Sha512> -> Str -> Str
         BinaryOp::Hash() => (
-            mk_tyw_enum!(
-                "Md5",
-                "Sha1",
-                "Sha256",
-                "Sha512",
-                mk_typewrapper::row_empty()
-            ),
+            mk_tyw_enum!("Md5", "Sha1", "Sha256", "Sha512"),
             mk_typewrapper::str(),
             mk_typewrapper::str(),
         ),
@@ -339,14 +328,14 @@ pub fn get_bop_type(
         BinaryOp::Serialize() => {
             let ty_input = TypeWrapper::Ptr(state.table.fresh_var());
             (
-                mk_tyw_enum!("Json", "Yaml", "Toml", mk_typewrapper::row_empty()),
+                mk_tyw_enum!("Json", "Yaml", "Toml"),
                 ty_input,
                 mk_typewrapper::str(),
             )
         }
         // <Json, Yaml, Toml> -> Str -> Dyn
         BinaryOp::Deserialize() => (
-            mk_tyw_enum!("Json", "Yaml", "Toml", mk_typewrapper::row_empty()),
+            mk_tyw_enum!("Json", "Yaml", "Toml"),
             mk_typewrapper::str(),
             mk_typewrapper::dynamic(),
         ),
diff --git a/stdlib/array.ncl b/stdlib/array.ncl
index dd4971d0..0a79d06e 100644
--- a/stdlib/array.ncl
+++ b/stdlib/array.ncl
@@ -30,7 +30,7 @@
         ```
         "%m
       = fun label value =>
-       if %is_array% value then
+       if %typeof% value == `Array then
          if %length% value != 0 then
            value
          else
diff --git a/stdlib/builtin.ncl b/stdlib/builtin.ncl
index 6fada500..f90cb7d5 100644
--- a/stdlib/builtin.ncl
+++ b/stdlib/builtin.ncl
@@ -5,29 +5,33 @@
         Possible dynamic type for a value. Must be one of the tags:
 
         ```
-         `TypeNum
-         `TypeBool
-         `TypeStr
-         `TypeFun
-         `TypeArray
-         `TypeRecord
+         `Num
+         `Bool
+         `Str
+         `Enum
+         `Fun
+         `Array
+         `Record
+         `Lbl
          `Other
         ```
         "%m
       = fun label =>
         label
         |> contract.tag m%"
-          must be one of `TypeNum, `TypeBool, `TypeStr, `TypeFun, `TypeArray`, `TypeRecord or `Other
+          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 [|
-            TypeNum,
-            TypeBool,
-            TypeStr,
-            TypeFun,
-            TypeArray,
-            TypeRecord,
+        |> contract.apply [|
+            Num,
+            Bool,
+            Str,
+            Enum,
+            Lbl,
+            Fun,
+            Array,
+            Record,
             Other
           |],
 
@@ -78,7 +82,7 @@
           false
       ```
       "%m
-    = fun x => %is_num% x,
+    = fun x => %typeof% x == `Num,
 
     is_bool : Dyn -> Bool
     | doc m%"
@@ -92,7 +96,7 @@
           false
       ```
       "%m
-    = fun x => %is_bool% x,
+    = fun x => %typeof% x == `Bool,
 
     is_str : Dyn -> Bool
     | doc m%"
@@ -100,13 +104,13 @@
 
       For example:
       ```nickel
-        is_bool true =>
+        is_str true =>
           false
-        is_bool "Hello, World!" =>
+        is_str "Hello, World!" =>
           true
       ```
       "%m
-    = fun x => %is_str% x,
+    = fun x => %typeof% x == `Str,
 
     is_fun : Dyn -> Bool
     | doc m%"
@@ -120,7 +124,7 @@
           false
       ```
       "%m
-    = fun x => %is_fun% x,
+    = fun x => %typeof% x == `Fun,
 
     is_array : Dyn -> Bool
     | doc m%"
@@ -134,7 +138,7 @@
           false
       ```
       "%m
-    = fun x => %is_array% x,
+    = fun x => %typeof% x == `Array,
 
     is_record : Dyn -> Bool
     | doc m%"
@@ -148,7 +152,7 @@
           true
       ```
       "%m
-    = fun x => %is_record% x,
+    = fun x => %typeof% x == `Record,
 
     typeof | Dyn -> ValueType
     | doc m%"
@@ -157,19 +161,12 @@
       For example:
       ```nickel
         typeof [ 1, 2 ] =>
-          `TypeArray
+          `Array
         typeof (fun x => x) =>
-          `TypeFun
+          `Fun
       ```
       "%m
-    = fun x =>
-      if %is_num% x then `TypeNum
-      else if %is_bool% x then `TypeBool
-      else if %is_str% x then `TypeStr
-      else if %is_fun% x then `TypeFun
-      else if %is_array% x then `TypeArray
-      else if %is_record% x then `TypeRecord
-      else `Other,
+    = fun x => %typeof% x,
 
     seq : forall a. Dyn -> a -> a
     | doc m%"
diff --git a/stdlib/contract.ncl b/stdlib/contract.ncl
index f5ab54fd..14835f18 100644
--- a/stdlib/contract.ncl
+++ b/stdlib/contract.ncl
@@ -1,21 +1,21 @@
 {
   "$dyn" = fun l t => t,
 
-  "$num" = fun l t => if %is_num% t then t else %blame% l,
+  "$num" = fun l t => if %typeof% t == `Num then t else %blame% l,
 
-  "$bool" = fun l t => if %is_bool% t then t else %blame% l,
+  "$bool" = fun l t => if %typeof% t == `Bool then t else %blame% l,
 
-  "$string" = fun l t => if %is_str% t then t else %blame% l,
+  "$string" = fun l t => if %typeof% t == `Str then t else %blame% l,
 
   "$fail" = fun l t => %blame% l,
 
   "$array" = fun elt l t =>
-    if %is_array% t then
+    if %typeof% t == `Array then
       %map% t (fun value => %assume% elt (%go_array% l) value)
     else %blame% l,
 
   "$func" = fun s t l e =>
-      if %is_fun% e then
+      if %typeof% e == `Fun then
           (fun x => %assume% t (%go_codom% l) (e (%assume% s (%chng_pol% (%go_dom% l)) x)))
       else
           %blame% l,
@@ -34,13 +34,13 @@
           %assume% contr l t,
 
   "$record" = fun cont l t =>
-      if %is_record% t then
+      if %typeof% t == `Record then
           %assume% (cont {}) l t
       else
           %blame% (%tag% "not a record" l),
 
   "$dyn_record" = fun contr l t =>
-      if %is_record% t then
+      if %typeof% t == `Record then
           %record_map% t (fun _field value => %assume% contr l value)
       else
           %blame% (%tag% "not a record" l),
diff --git a/stdlib/num.ncl b/stdlib/num.ncl
index ddd71612..fc22f50a 100644
--- a/stdlib/num.ncl
+++ b/stdlib/num.ncl
@@ -13,7 +13,7 @@
       ```
       "%m
     = fun label value =>
-      if %is_num% value then
+      if %typeof% value == `Num then
         if value % 1 == 0 then
           value
         else
@@ -36,7 +36,7 @@
       ```
       "%m
     = fun label value =>
-      if %is_num% value then
+      if %typeof% value == `Num then
         if value % 1 == 0 && value >= 0 then
           value
         else
@@ -59,7 +59,7 @@
       ```
       "%m
     = fun label value =>
-      if %is_num% value then
+      if %typeof% value == `Num then
         if value % 1 == 0 && value > 0 then
           value
         else
@@ -80,7 +80,7 @@
       ```
       "%m
     = fun label value =>
-      if %is_num% value then
+      if %typeof% value == `Num then
         if value != 0 then
           value
         else
@@ -100,7 +100,7 @@
           false
       ```
       "%m
-    = fun x => %is_num% x && (x % 1 == 0),
+    = fun x => x % 1 == 0,
 
     min : Num -> Num -> Num
     | doc m%"
diff --git a/stdlib/string.ncl b/stdlib/string.ncl
index 526eaa7e..1d83d957 100644
--- a/stdlib/string.ncl
+++ b/stdlib/string.ncl
@@ -16,7 +16,7 @@
       ```
       "%m
     = fun l s =>
-      if %is_str% s then
+      if %typeof% s == `Str then
         if s == "true" || s == "True" then
           "true"
         else if s == "false" || s == "False" then
@@ -43,7 +43,7 @@
     = let pattern = m%"^[+-]?(\d+(\.\d*)?(e[+-]?\d+)?|\.\d+(e[+-]?\d+)?)$"%m in
       let is_num_literal = %str_is_match% pattern in
       fun l s =>
-        if %is_str% s then
+        if %typeof% s == `Str then
           if is_num_literal s then
             s
           else
@@ -68,7 +68,7 @@
       ```
       "%m
     = fun l s =>
-      if %is_str% s then
+      if %typeof% s == `Str then
         if length s == 1 then
           s
         else
@@ -91,7 +91,7 @@
       ```
       "%m
     = fun l s =>
-      if %is_str% s then
+      if %typeof% s == `Str then
         if %str_length% s > 0 then
           s
         else


Test patch:
diff --git a/src/eval/tests.rs b/src/eval/tests.rs
index b0338b3f..8f86e747 100644
--- a/src/eval/tests.rs
+++ b/src/eval/tests.rs
@@ -85,17 +85,17 @@ fn simple_plus() {
 
 #[test]
 fn asking_for_various_types() {
-    let num = mk_term::op1(UnaryOp::IsNum(), Term::Num(45.3));
-    assert_eq!(Ok(Term::Bool(true)), eval_no_import(num));
+    let num = mk_term::op1(UnaryOp::Typeof(), Term::Num(45.3));
+    assert_eq!(Ok(Term::Enum("Num".into())), eval_no_import(num));
 
-    let boolean = mk_term::op1(UnaryOp::IsBool(), Term::Bool(true));
-    assert_eq!(Ok(Term::Bool(true)), eval_no_import(boolean));
+    let boolean = mk_term::op1(UnaryOp::Typeof(), Term::Bool(true));
+    assert_eq!(Ok(Term::Enum("Bool".into())), eval_no_import(boolean));
 
     let lambda = mk_term::op1(
-        UnaryOp::IsFun(),
+        UnaryOp::Typeof(),
         mk_fun!("x", mk_app!(mk_term::var("x"), mk_term::var("x"))),
     );
-    assert_eq!(Ok(Term::Bool(true)), eval_no_import(lambda));
+    assert_eq!(Ok(Term::Enum("Fun".into())), eval_no_import(lambda));
 }
 
 fn mk_default(t: RichTerm) -> Term {
diff --git a/src/parser/tests.rs b/src/parser/tests.rs
index 1f63e4fc..9cc61100 100644
--- a/src/parser/tests.rs
+++ b/src/parser/tests.rs
@@ -140,13 +140,13 @@ fn lets() {
 #[test]
 fn unary_op() {
     assert_eq!(
-        parse_without_pos("%is_num% x"),
-        mk_term::op1(UnaryOp::IsNum(), mk_term::var("x"))
+        parse_without_pos("%typeof% x"),
+        mk_term::op1(UnaryOp::Typeof(), mk_term::var("x"))
     );
     assert_eq!(
-        parse_without_pos("%is_num% x y"),
+        parse_without_pos("%typeof% x y"),
         mk_app!(
-            mk_term::op1(UnaryOp::IsNum(), mk_term::var("x")),
+            mk_term::op1(UnaryOp::Typeof(), mk_term::var("x")),
             mk_term::var("y")
         ),
     );
diff --git a/tests/pass/records.ncl b/tests/pass/records.ncl
index 67785eb1..c1a1b0c7 100644
--- a/tests/pass/records.ncl
+++ b/tests/pass/records.ncl
@@ -29,7 +29,7 @@ let Assert = fun l x => x || %blame% l in
     == 2,
 
   let r = record.map
-      (fun y x => if %is_num% x then x + 1 else 0)
+      (fun y x => if %typeof% x == `Num then x + 1 else 0)
       {foo = 1, bar = "it's lazy"} in
     (r.foo) + (r.bar) == 2,