Repository: tweag/nickel
Pull request #1914 (2024-05-11 04:09:14)

chore: add assert to the stdlib
Previously Nickel was using an Assert only for integration tests. This pr makes it officially part of the standard library.

Closes https://github.com/tweag/nickel/issues/1300

ajbt200128: Note the original issue recommends adding these to `std.test` . I can move `Assert` and `check` there, and maybe `Equal` belongs there too. Added it to `std.contract` for now since `Equal` is there already
ajbt200128: Looks like pre-commit is failing, must not have run properly on my machine
yannham: After some discussions, we chose to put it in a new dedicated module `std.test`.

Patch:
diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml
index fda50e1c..ba181c6e 100644
--- a/.github/workflows/continuous-integration.yml
+++ b/.github/workflows/continuous-integration.yml
@@ -65,7 +65,7 @@ jobs:
           accept-flake-config = true
 
     - name: Setup Cachix
-      uses: cachix/cachix-action@v14
+      uses: cachix/cachix-action@v15
       with:
         name: tweag-nickel
         authToken: '${{ secrets.CACHIX_TWEAG_NICKEL_AUTH_TOKEN }}'
diff --git a/HACKING.md b/HACKING.md
index 0a674501..66c8f053 100644
--- a/HACKING.md
+++ b/HACKING.md
@@ -147,14 +147,13 @@ directory. All `.ncl` files in this directory are automatically converted into
 Rust integration tests, which run the file and assert that no errors were
 raised during evaluation.
 
-Each of these `.ncl` files is structured as an array of `Bool` expressions, which
-is ultimately passed to a `check` function defined in
-`core/tests/integration/pass/lib/assert.ncl`.
-This function applies an `Assert` contract to each value in the array, which
-checks that the value it is applied to evaluates to `true`. The benefit of using
-a contract for this is that if a test fails we can simply run the file directly
-using Nickel, which gives better error messages than the ones we get by default
-from `cargo test`.
+Each of these `.ncl` files is structured as an array of `Bool` expressions,
+which is ultimately passed to `std.contract.check` function defined in the
+standard library. This function applies an `Assert` (`std.test.Assert`)
+contract to each value in the array, which checks that the value it is applied
+to evaluates to `true`. The benefit of using a contract for this is that if a
+test fails we can simply run the file directly using Nickel, which gives better
+error messages than the ones we get by default from `cargo test`.
 
 Tests which are expected to fail may be written in Rust in `core/tests/integration`.
 However, simple failure test cases can make use of the test annotation support
diff --git a/core/src/eval/mod.rs b/core/src/eval/mod.rs
index fec09cf7..f1f3017d 100644
--- a/core/src/eval/mod.rs
+++ b/core/src/eval/mod.rs
@@ -1157,6 +1157,7 @@ pub fn subst<C: Cache>(
         // loop. Although avoidable, this requires some care and is not currently needed.
         | v @ Term::Fun(..)
         | v @ Term::Lbl(_)
+        | v @ Term::ForeignId(_)
         | v @ Term::SealingKey(_)
         | v @ Term::Enum(_)
         | v @ Term::Import(_)
diff --git a/core/src/eval/operation.rs b/core/src/eval/operation.rs
index fa88a69e..ee435cab 100644
--- a/core/src/eval/operation.rs
+++ b/core/src/eval/operation.rs
@@ -235,6 +235,7 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
                     Term::Array(..) => "Array",
                     Term::Record(..) | Term::RecRecord(..) => "Record",
                     Term::Lbl(..) => "Label",
+                    Term::ForeignId(_) => "ForeignId",
                     _ => "Other",
                 };
                 Ok(Closure::atomic_closure(RichTerm::new(
@@ -3439,6 +3440,14 @@ fn eq<C: Cache>(
             eq_pos: pos_op,
             term: RichTerm::new(Term::Fun(i, rt), pos2),
         }),
+        (Term::ForeignId(v), _) => Err(EvalError::EqError {
+            eq_pos: pos_op,
+            term: RichTerm::new(Term::ForeignId(v), pos1),
+        }),
+        (_, Term::ForeignId(v)) => Err(EvalError::EqError {
+            eq_pos: pos_op,
+            term: RichTerm::new(Term::ForeignId(v), pos2),
+        }),
         (_, _) => Ok(EqResult::Bool(false)),
     }
 }
diff --git a/core/src/parser/uniterm.rs b/core/src/parser/uniterm.rs
index afa4d0c0..07b1bb8c 100644
--- a/core/src/parser/uniterm.rs
+++ b/core/src/parser/uniterm.rs
@@ -692,6 +692,7 @@ impl FixTypeVars for Type {
             | TypeF::Number
             | TypeF::Bool
             | TypeF::String
+            | TypeF::ForeignId
             | TypeF::Symbol
             | TypeF::Flat(_)
             // We don't fix type variables inside a dictionary contract. A dictionary contract
diff --git a/core/src/pretty.rs b/core/src/pretty.rs
index 5287eb99..000ba873 100644
--- a/core/src/pretty.rs
+++ b/core/src/pretty.rs
@@ -998,6 +998,7 @@ where
                 .nest(2)
             ]
             .group(),
+            ForeignId(_) => allocator.text("%<foreign>"),
             SealingKey(sym) => allocator.text(format!("%<sealing key: {sym}>")),
             Sealed(_i, _rt, _lbl) => allocator.text("%<sealed>"),
             Annotated(annot, rt) => allocator.atom(rt).append(annot.pretty(allocator)),
@@ -1133,6 +1134,7 @@ where
                 ]
             }
             .group(),
+            ForeignId => allocator.text("ForeignId"),
             Symbol => allocator.text("Symbol"),
             Flat(t) => t.pretty(allocator),
             Var(var) => allocator.as_string(var),
diff --git a/core/src/stdlib.rs b/core/src/stdlib.rs
index bcec7534..c2c1a540 100644
--- a/core/src/stdlib.rs
+++ b/core/src/stdlib.rs
@@ -71,6 +71,7 @@ pub mod internals {
 
     generate_accessor!(num);
     generate_accessor!(bool);
+    generate_accessor!(foreign_id);
     generate_accessor!(string);
     generate_accessor!(fail);
 
diff --git a/core/src/term/mod.rs b/core/src/term/mod.rs
index 2bce81ba..940b15fa 100644
--- a/core/src/term/mod.rs
+++ b/core/src/term/mod.rs
@@ -58,6 +58,9 @@ use std::{
     rc::Rc,
 };
 
+/// The payload of a `Term::ForeignId`.
+pub type ForeignIdPayload = u64;
+
 /// The AST of a Nickel expression.
 ///
 /// Parsed terms also need to store their position in the source for error reporting.  This is why
@@ -261,6 +264,13 @@ pub enum Term {
     ///
     /// This is a temporary solution, and will be removed in the future.
     Closure(CacheIndex),
+
+    #[serde(skip)]
+    /// An opaque value that cannot be constructed within Nickel code.
+    ///
+    /// This can be used by programs that embed Nickel, as they can inject these opaque
+    /// values into the AST.
+    ForeignId(ForeignIdPayload),
 }
 
 // PartialEq is mostly used for tests, when it's handy to compare something to an expected result.
@@ -884,6 +894,7 @@ impl Term {
             Term::SealingKey(_) => Some("SealingKey".to_owned()),
             Term::Sealed(..) => Some("Sealed".to_owned()),
             Term::Annotated(..) => Some("Annotated".to_owned()),
+            Term::ForeignId(_) => Some("ForeignId".to_owned()),
             Term::Let(..)
             | Term::LetPattern(..)
             | Term::App(_, _)
@@ -930,6 +941,7 @@ impl Term {
             | Term::EnumVariant {..}
             | Term::Record(..)
             | Term::Array(..)
+            | Term::ForeignId(_)
             | Term::SealingKey(_) => true,
             Term::Let(..)
             | Term::LetPattern(..)
@@ -987,6 +999,7 @@ impl Term {
             | Term::Str(_)
             | Term::Lbl(_)
             | Term::Enum(_)
+            | Term::ForeignId(_)
             | Term::SealingKey(_) => true,
             Term::Let(..)
             | Term::LetPattern(..)
@@ -1029,6 +1042,7 @@ impl Term {
             | Term::Array(..)
             | Term::Var(..)
             | Term::SealingKey(..)
+            | Term::ForeignId(..)
             | Term::Op1(UnaryOp::StaticAccess(_), _)
             | Term::Op2(BinaryOp::DynAccess(), _, _)
             // Those special cases aren't really atoms, but mustn't be parenthesized because they
@@ -2196,6 +2210,7 @@ impl Traverse<RichTerm> for RichTerm {
             | Term::Import(_)
             | Term::ResolvedImport(_)
             | Term::SealingKey(_)
+            | Term::ForeignId(_)
             | Term::ParseError(_)
             | Term::RuntimeError(_) => None,
             Term::StrChunks(chunks) => chunks.iter().find_map(|ch| {
diff --git a/core/src/transform/free_vars.rs b/core/src/transform/free_vars.rs
index f79d0732..78221ded 100644
--- a/core/src/transform/free_vars.rs
+++ b/core/src/transform/free_vars.rs
@@ -39,6 +39,7 @@ impl CollectFreeVars for RichTerm {
             | Term::Num(_)
             | Term::Str(_)
             | Term::Lbl(_)
+            | Term::ForeignId(_)
             | Term::SealingKey(_)
             | Term::Enum(_)
             | Term::Import(_)
@@ -195,6 +196,7 @@ impl CollectFreeVars for Type {
             | TypeF::Number
             | TypeF::Bool
             | TypeF::String
+            | TypeF::ForeignId
             | TypeF::Symbol
             | TypeF::Var(_)
             | TypeF::Wildcard(_) => (),
diff --git a/core/src/typ.rs b/core/src/typ.rs
index ae0f1069..6e012db9 100644
--- a/core/src/typ.rs
+++ b/core/src/typ.rs
@@ -269,6 +269,8 @@ pub enum TypeF<Ty, RRows, ERows> {
     ///
     /// See [`crate::term::Term::Sealed`].
     Symbol,
+    /// The type of `Term::ForeignId`.
+    ForeignId,
     /// A type created from a user-defined contract.
     Flat(RichTerm),
     /// A function.
@@ -543,6 +545,7 @@ impl<Ty, RRows, ERows> TypeF<Ty, RRows, ERows> {
             TypeF::Number => Ok(TypeF::Number),
             TypeF::Bool => Ok(TypeF::Bool),
             TypeF::String => Ok(TypeF::String),
+            TypeF::ForeignId => Ok(TypeF::ForeignId),
             TypeF::Symbol => Ok(TypeF::Symbol),
             TypeF::Flat(t) => Ok(TypeF::Flat(t)),
             TypeF::Arrow(dom, codom) => Ok(TypeF::Arrow(f(dom, state)?, f(codom, state)?)),
@@ -818,6 +821,7 @@ impl Subcontract for Type {
             TypeF::Number => internals::num(),
             TypeF::Bool => internals::bool(),
             TypeF::String => internals::string(),
+            TypeF::ForeignId => internals::foreign_id(),
             // Array Dyn is specialized to array_dyn, which is constant time
             TypeF::Array(ref ty) if matches!(ty.typ, TypeF::Dyn) => internals::array_dyn(),
             TypeF::Array(ref ty) => mk_app!(internals::array(), ty.subcontract(vars, pol, sy)?),
@@ -1407,6 +1411,7 @@ impl Traverse<Type> for Type {
             | TypeF::Number
             | TypeF::Bool
             | TypeF::String
+            | TypeF::ForeignId
             | TypeF::Symbol
             | TypeF::Var(_)
             | TypeF::Enum(_)
diff --git a/core/src/typecheck/mk_uniftype.rs b/core/src/typecheck/mk_uniftype.rs
index 8d55d762..5208be0d 100644
--- a/core/src/typecheck/mk_uniftype.rs
+++ b/core/src/typecheck/mk_uniftype.rs
@@ -151,3 +151,4 @@ generate_builder!(str, String);
 generate_builder!(num, Number);
 generate_builder!(bool, Bool);
 generate_builder!(sym, Symbol);
+generate_builder!(foreign_id, ForeignId);
diff --git a/core/src/typecheck/mod.rs b/core/src/typecheck/mod.rs
index 1f9c97dc..fafeaec7 100644
--- a/core/src/typecheck/mod.rs
+++ b/core/src/typecheck/mod.rs
@@ -224,9 +224,12 @@ impl<E: TermEnvironment> VarLevelUpperBound for GenericUnifType<E> {
 impl<E: TermEnvironment> VarLevelUpperBound for GenericUnifTypeUnrolling<E> {
     fn var_level_upper_bound(&self) -> VarLevel {
         match self {
-            TypeF::Dyn | TypeF::Bool | TypeF::Number | TypeF::String | TypeF::Symbol => {
-                VarLevel::NO_VAR
-            }
+            TypeF::Dyn
+            | TypeF::Bool
+            | TypeF::Number
+            | TypeF::String
+            | TypeF::ForeignId
+            | TypeF::Symbol => VarLevel::NO_VAR,
             TypeF::Arrow(domain, codomain) => max(
                 domain.var_level_upper_bound(),
                 codomain.var_level_upper_bound(),
@@ -1440,6 +1443,7 @@ fn walk<V: TypecheckVisitor>(
         | Term::Str(_)
         | Term::Lbl(_)
         | Term::Enum(_)
+        | Term::ForeignId(_)
         | Term::SealingKey(_)
         // This function doesn't recursively typecheck imports: this is the responsibility of the
         // caller.
@@ -1632,6 +1636,7 @@ fn walk_type<V: TypecheckVisitor>(
        | TypeF::Number
        | TypeF::Bool
        | TypeF::String
+       | TypeF::ForeignId
        | TypeF::Symbol
        // Currently, the parser can't generate unbound type variables by construction. Thus we
        // don't check here for unbound type variables again.
@@ -2296,6 +2301,9 @@ fn check<V: TypecheckVisitor>(
             }
         }
 
+        Term::ForeignId(_) => ty
+            .unify(mk_uniftype::foreign_id(), state, &ctxt)
+            .map_err(|err| err.into_typecheck_err(state, rt.pos)),
         Term::SealingKey(_) => ty
             .unify(mk_uniftype::sym(), state, &ctxt)
             .map_err(|err| err.into_typecheck_err(state, rt.pos)),
diff --git a/core/src/typecheck/operation.rs b/core/src/typecheck/operation.rs
index 6b3f23f0..46f068ef 100644
--- a/core/src/typecheck/operation.rs
+++ b/core/src/typecheck/operation.rs
@@ -28,7 +28,16 @@ pub fn get_uop_type(
         UnaryOp::Typeof() => (
             mk_uniftype::dynamic(),
             mk_uty_enum!(
-                "Number", "Bool", "String", "Enum", "Function", "Array", "Record", "Label", "Other"
+                "Number",
+                "Bool",
+                "String",
+                "Enum",
+                "Function",
+                "Array",
+                "Record",
+                "Label",
+                "ForeignId",
+                "Other"
             ),
         ),
         // Bool -> Bool -> Bool
diff --git a/core/stdlib/internals.ncl b/core/stdlib/internals.ncl
index 22539fba..0a633b82 100644
--- a/core/stdlib/internals.ncl
+++ b/core/stdlib/internals.ncl
@@ -11,6 +11,8 @@
 
   "$string" = fun label value => if %typeof% value == 'String then value else %blame% label,
 
+  "$foreign_id" = fun label value => if %typeof% value == 'ForeignId then value else %blame% label,
+
   "$fail" = fun label _value => %blame% label,
 
   "$array" = fun element_contract label value =>
diff --git a/core/stdlib/std.ncl b/core/stdlib/std.ncl
index fe3b1695..2b2bdd39 100644
--- a/core/stdlib/std.ncl
+++ b/core/stdlib/std.ncl
@@ -2147,6 +2147,27 @@
       "%%
       = fun field r => r."%{field}",
 
+    get_or
+      : forall a. String -> a -> { _ : a } -> a
+      | doc m%%"
+        Returns the field of a record with given name or default value,
+        if there is no such field.
+
+        # Examples
+
+        ```nickel
+        std.record.get_or "tree" 3 { one = 1, two = 2 }
+          => 3
+        std.record.get_or "one" 11 { one = 1, two = 2 }
+          => 1
+        ```
+      "%%
+      = fun field default_value record =>
+        if %has_field% field record then
+          record."%{field}"
+        else
+          default_value,
+
     insert
       : forall a. String -> a -> { _ : a } -> { _ : a }
       | doc m%%"
@@ -3064,6 +3085,44 @@
       = fun s => %enum_from_str% s,
   },
 
+  test = {
+    Assert
+      | doc m%"
+        A contract that checks if its argument is the boolean true.
+
+        # Examples
+
+        ```nickel
+        1 == 2 | std.test.Assert
+         => error: contract broken by a value
+
+        1 == 1 | std.test.Assert
+         => true
+        ```
+        "%
+      = fun label value => if value then true else %blame% label,
+
+    assert_all
+      | Array Assert -> Bool
+      | doc m%"
+        Asserts that all the elements of an array are equal to true. Applies the
+        `std.test.Assert` contract on each element of the given array.
+
+        Evalutes to `true` if all the elements of the array are equal to true,
+        or fails with a contract error otherwise.
+
+        # Example
+
+        ```nickel
+        [ (1 == 2), (1 == 1), (1 == 3) ] |> std.test.assert_all
+         => error: contract broken by a value
+        ```
+        "%
+      # We mostly rely on the contracts to do the work here. We just need to
+      # make sure each element of the array is properly forced.
+      = std.array.all (fun x => x),
+  },
+
   is_number
     : Dyn -> Bool
     | doc m%"
@@ -3186,6 +3245,7 @@
       'Function,
       'Array,
       'Record,
+      'ForeignId,
       'Other
     |]
     | doc m%"
diff --git a/flake.lock b/flake.lock
index 50d0fb8f..915dc227 100644
--- a/flake.lock
+++ b/flake.lock
@@ -3,11 +3,11 @@
     "advisory-db": {
       "flake": false,
       "locked": {
-        "lastModified": 1712168594,
-        "narHash": "sha256-1Yh+vafNq19JDfmpknkWq11AkcQLPmFZ8X6YJZT5r7o=",
+        "lastModified": 1714183630,
+        "narHash": "sha256-1BVft7ggSN2XXFeXQjazU3jN9wVECd9qp2mZx/8GDMk=",
         "owner": "rustsec",
         "repo": "advisory-db",
-        "rev": "0bc9a77248be5cb5f2b51fe6aba8ba451d74c6bb",
+        "rev": "35e7459a331d3e0c585e56dabd03006b9b354088",
         "type": "github"
       },
       "original": {
@@ -23,11 +23,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1715274763,
-        "narHash": "sha256-3Iv1PGHJn9sV3HO4FlOVaaztOxa9uGLfOmUWrH7v7+A=",
+        "lastModified": 1716156051,
+        "narHash": "sha256-TjUX7WWRcrhuUxDHsR8pDR2N7jitqZehgCVSy3kBeS8=",
         "owner": "ipetkov",
         "repo": "crane",
-        "rev": "27025ab71bdca30e7ed0a16c88fd74c5970fc7f5",
+        "rev": "7443df1c478947bf96a2e699209f53b2db26209d",
         "type": "github"
       },
       "original": {
@@ -44,11 +44,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1712180168,
-        "narHash": "sha256-sYe00cK+kKnQlVo1wUIZ5rZl9x8/r3djShUqNgfjnM4=",
+        "lastModified": 1715274763,
+        "narHash": "sha256-3Iv1PGHJn9sV3HO4FlOVaaztOxa9uGLfOmUWrH7v7+A=",
         "owner": "ipetkov",
         "repo": "crane",
-        "rev": "06a9ff255c1681299a87191c2725d9d579f28b82",
+        "rev": "27025ab71bdca30e7ed0a16c88fd74c5970fc7f5",
         "type": "github"
       },
       "original": {
@@ -241,11 +241,11 @@
         "pre-commit-hooks": "pre-commit-hooks"
       },
       "locked": {
-        "lastModified": 1715361977,
-        "narHash": "sha256-j/PLYYGs+Gjge4JGYxMjOhWQEp+GB4Fdicetbpmp6n0=",
+        "lastModified": 1716186515,
+        "narHash": "sha256-BSvldBxypiYGuVuYfrIZNoM+tlkDsgQdz9HKe9e57v8=",
         "owner": "nixos",
         "repo": "nix",
-        "rev": "87ab3c0ea4e6f85e7b902050365bb75cf2836fbb",
+        "rev": "209d75529caaaf82a760056fc60156b9153af5c8",
         "type": "github"
       },
       "original": {
@@ -304,11 +304,11 @@
     },
     "nixpkgs_2": {
       "locked": {
-        "lastModified": 1715787315,
-        "narHash": "sha256-cYApT0NXJfqBkKcci7D9Kr4CBYZKOQKDYA23q8XNuWg=",
+        "lastModified": 1715961556,
+        "narHash": "sha256-+NpbZRCRisUHKQJZF3CT+xn14ZZQO+KjxIIanH3Pvn4=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "33d1e753c82ffc557b4a585c77de43d4c922ebb5",
+        "rev": "4a6b83b05df1a8bd7d99095ec4b4d271f2956b64",
         "type": "github"
       },
       "original": {
@@ -409,11 +409,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1715393623,
-        "narHash": "sha256-nSUFcUqyTQQ/aYFIB05mpCzytcKvfKMy3ZQAe0fP26A=",
+        "lastModified": 1716171463,
+        "narHash": "sha256-lc7wOh5BjYUoxdhcPkeUY8BmuL2qtRaHlW1403RW48E=",
         "owner": "oxalica",
         "repo": "rust-overlay",
-        "rev": "8eb8671512cb0c72c748058506e50c54fb5d8e2b",
+        "rev": "04d61d14803854fd8453ec43c5c53a471e5407a8",
         "type": "github"
       },
       "original": {
@@ -428,11 +428,11 @@
         "nixpkgs": "nixpkgs_3"
       },
       "locked": {
-        "lastModified": 1712196778,
-        "narHash": "sha256-SOiwCr2HtmYpw8OvQQVRPtiCBWwndbIoPqtsamZK3J8=",
+        "lastModified": 1715739484,
+        "narHash": "sha256-5zlSuCM54jH6tXi8OILZ7opT+lBYUkGU9eOMEvJh9HU=",
         "owner": "oxalica",
         "repo": "rust-overlay",
-        "rev": "20e7895d1873cc64c14a9f024a8e04f5824bed28",
+        "rev": "3d27c65641a61d36f1c7616d6150524cd9a2a5f7",
         "type": "github"
       },
       "original": {
@@ -498,11 +498,11 @@
         "rust-overlay": "rust-overlay_2"
       },
       "locked": {
-        "lastModified": 1712658781,
-        "narHash": "sha256-KWfgbVFV2zbCuNNFp9yeSgAa0Cc7cT090KK2J1ynfKg=",
+        "lastModified": 1715781565,
+        "narHash": "sha256-5o5DvwUYAmODYlFCuDJwRvEkitgq2SFD9Wy0DRMjKsY=",
         "owner": "tweag",
         "repo": "topiary",
-        "rev": "82a94a9f57104b89d4316afaeeab271cc51f7698",
+        "rev": "ba4f44062299cfc45d3c383e9697c325bedd0078",
         "type": "github"
       },
       "original": {


Test patch:
diff --git a/core/src/eval/tests.rs b/core/src/eval/tests.rs
index 09cf5614..d749feb6 100644
--- a/core/src/eval/tests.rs
+++ b/core/src/eval/tests.rs
@@ -8,7 +8,8 @@ use crate::term::make as mk_term;
 use crate::term::Number;
 use crate::term::{BinaryOp, StrChunk, UnaryOp};
 use crate::transform::import_resolution::strict::resolve_imports;
-use crate::{mk_app, mk_fun};
+use crate::{mk_app, mk_fun, mk_record};
+use assert_matches::assert_matches;
 use codespan::Files;
 
 /// Evaluate a term without import support.
@@ -18,6 +19,13 @@ fn eval_no_import(t: RichTerm) -> Result<Term, EvalError> {
         .map(Term::from)
 }
 
+/// Fully evaluate a term without import support.
+fn eval_full_no_import(t: RichTerm) -> Result<Term, EvalError> {
+    VirtualMachine::<_, CacheImpl>::new(DummyResolver {}, std::io::sink())
+        .eval_full(t)
+        .map(Term::from)
+}
+
 fn parse(s: &str) -> Option<RichTerm> {
     let id = Files::new().add("<test>", String::from(s));
 
@@ -347,3 +355,45 @@ fn substitution() {
         .to_string()
     );
 }
+
+#[test]
+fn foreign_id() {
+    let t = mk_term::op2(
+        BinaryOp::Merge(Label::default().into()),
+        mk_record!(("a", RichTerm::from(Term::Num(Number::from(1))))),
+        mk_record!(("b", RichTerm::from(Term::ForeignId(42)))),
+    );
+
+    // Terms that include foreign ids can be manipulated like normal, and the ids
+    // are passed through.
+    let Term::Record(data) = eval_no_import(t.clone()).unwrap() else {
+        panic!();
+    };
+    let b = LocIdent::from(Ident::new("b"));
+    let field = data.fields.get(&b).unwrap();
+    assert_matches!(field.value.as_ref().unwrap().as_ref(), Term::ForeignId(42));
+
+    // Foreign ids cannot be compared for equality.
+    let t_eq = mk_term::op2(
+        BinaryOp::Eq(),
+        RichTerm::from(Term::ForeignId(43)),
+        RichTerm::from(Term::ForeignId(42)),
+    );
+    assert_matches!(eval_no_import(t_eq), Err(EvalError::EqError { .. }));
+
+    // Opaque values cannot be merged (even if they're equal, since they can't get compared for equality).
+    let t_merge = mk_term::op2(
+        BinaryOp::Merge(Label::default().into()),
+        t.clone(),
+        t.clone(),
+    );
+    assert_matches!(
+        eval_full_no_import(t_merge),
+        Err(EvalError::MergeIncompatibleArgs { .. })
+    );
+
+    let t_typeof = mk_term::op1(UnaryOp::Typeof(), Term::ForeignId(42));
+    let ty = eval_no_import(t_typeof).unwrap();
+    let fid = LocIdent::from(Ident::new("ForeignId"));
+    assert_matches!(ty, Term::Enum(f) if f == fid);
+}
diff --git a/core/tests/integration/inputs/adts/enum_primops.ncl b/core/tests/integration/inputs/adts/enum_primops.ncl
index 9ff67836..eb7e261b 100644
--- a/core/tests/integration/inputs/adts/enum_primops.ncl
+++ b/core/tests/integration/inputs/adts/enum_primops.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let {check, ..} = import "../lib/assert.ncl" in
+
 
 [
   %enum_unwrap_variant% ('Left (1+1)) == 2,
@@ -8,4 +8,4 @@ let {check, ..} = import "../lib/assert.ncl" in
   %enum_get_tag% 'Right == 'Right,
   %enum_get_tag% ('Right "stuff") == 'Right,
 ]
-|> check
+|> std.test.assert_all
diff --git a/core/tests/integration/inputs/contracts/contracts.ncl b/core/tests/integration/inputs/contracts/contracts.ncl
index 8c4959a4..7dc66a6f 100644
--- a/core/tests/integration/inputs/contracts/contracts.ncl
+++ b/core/tests/integration/inputs/contracts/contracts.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let {check, Assert, ..} = import "../lib/assert.ncl" in
+let Assert = std.test.Assert in
 
 [
   let AlwaysTrue = fun l t =>
@@ -179,4 +179,4 @@ let {check, Assert, ..} = import "../lib/assert.ncl" in
   in
   tag == 'some_tag,
 ]
-|> check
+|> std.test.assert_all
diff --git a/core/tests/integration/inputs/contracts/let_order.ncl b/core/tests/integration/inputs/contracts/let_order.ncl
index 3812b30d..39f014ec 100644
--- a/core/tests/integration/inputs/contracts/let_order.ncl
+++ b/core/tests/integration/inputs/contracts/let_order.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let { check, Assert, .. } = import "../lib/assert.ncl" in
+
 let ConstantTrue = fun _label value => std.seq value true in
 [
   let foo | ConstantTrue | Bool = "not a bool" in
@@ -12,4 +12,4 @@ let ConstantTrue = fun _label value => std.seq value true in
       = "still not a bool"
   }.foo
 ]
-|> check
\ No newline at end of file
+|> std.test.assert_all
diff --git a/core/tests/integration/inputs/contracts/types_dont_propagate.ncl b/core/tests/integration/inputs/contracts/types_dont_propagate.ncl
index 874be213..def12e0f 100644
--- a/core/tests/integration/inputs/contracts/types_dont_propagate.ncl
+++ b/core/tests/integration/inputs/contracts/types_dont_propagate.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let {check, ..} = import "../lib/assert.ncl" in
+
 
 [
   # check that a record type literal is indeed converted to the corresponding
@@ -21,4 +21,4 @@ let {check, ..} = import "../lib/assert.ncl" in
   & {foo | force = false, bar | force = true})
   == {foo = false, bar = true},
 ]
-|> check
+|> std.test.assert_all
diff --git a/core/tests/integration/inputs/core/arrays.ncl b/core/tests/integration/inputs/core/arrays.ncl
index fc99dee7..262f5684 100644
--- a/core/tests/integration/inputs/core/arrays.ncl
+++ b/core/tests/integration/inputs/core/arrays.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let {check, ..} = import "../lib/assert.ncl" in
+
 
 [
   # accesses
@@ -49,4 +49,4 @@ let {check, ..} = import "../lib/assert.ncl" in
 
   std.array.map_with_index (+) [1, 2, 3] == [1, 3, 5],
 ]
-|> check
+|> std.test.assert_all
diff --git a/core/tests/integration/inputs/core/basics.ncl b/core/tests/integration/inputs/core/basics.ncl
index eb332f86..9fe46df8 100644
--- a/core/tests/integration/inputs/core/basics.ncl
+++ b/core/tests/integration/inputs/core/basics.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let {check, ..} = import "../lib/assert.ncl" in
+
 
 [
   # basic arithmetic
@@ -35,4 +35,4 @@ let {check, ..} = import "../lib/assert.ncl" in
   # This test checks that the terms of a match are closured
   let x = 3 in ((3 + 2) |> match { 'foo => 1, _ => x}) == 3,
 ]
-|> check
+|> std.test.assert_all
diff --git a/core/tests/integration/inputs/core/builtins.ncl b/core/tests/integration/inputs/core/builtins.ncl
index 9fef0b99..dbf15127 100644
--- a/core/tests/integration/inputs/core/builtins.ncl
+++ b/core/tests/integration/inputs/core/builtins.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let {check, ..} = import "../lib/assert.ncl" in
+
 
 [
   # is_record
@@ -28,4 +28,4 @@ let {check, ..} = import "../lib/assert.ncl" in
    |> std.deserialize 'Json
    == [3,4],
 ]
-|> check
+|> std.test.assert_all
diff --git a/core/tests/integration/inputs/core/curried_dot.ncl b/core/tests/integration/inputs/core/curried_dot.ncl
index 073b0bce..83e04a94 100644
--- a/core/tests/integration/inputs/core/curried_dot.ncl
+++ b/core/tests/integration/inputs/core/curried_dot.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let {check, ..} = import "../lib/assert.ncl" in
+
 
 let record = {foo = 1, bar = { baz = 2 }} in
 let field = "bar" in
@@ -9,4 +9,4 @@ let part = "ba" in
   (.) record field == { baz = 2 },
   let res = (.) record field == {baz = 2} in res,
   (.) record "%{part}r" == { baz = 2 },
-] |> check
+] |> std.test.assert_all
diff --git a/core/tests/integration/inputs/core/eq.ncl b/core/tests/integration/inputs/core/eq.ncl
index f74dff94..f721373c 100644
--- a/core/tests/integration/inputs/core/eq.ncl
+++ b/core/tests/integration/inputs/core/eq.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let {check, ..} = import "../lib/assert.ncl" in
+
 
 [
   # basic
@@ -84,4 +84,4 @@ let {check, ..} = import "../lib/assert.ncl" in
   'Left 2 != 'Right 2,
   'Up [1,2,3] == 'Up [0+1,1+1,2+1],
 ]
-|> check
+|> std.test.assert_all
diff --git a/core/tests/integration/inputs/core/fibo.ncl b/core/tests/integration/inputs/core/fibo.ncl
index 7d16f764..771aa18f 100644
--- a/core/tests/integration/inputs/core/fibo.ncl
+++ b/core/tests/integration/inputs/core/fibo.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let {Assert, ..} = import "../lib/assert.ncl" in
+let Assert = std.test.Assert in
 
 let Y | ((Number -> Number) -> Number -> Number) -> Number -> Number = fun f => (fun x => f (x x)) (fun x => f (x x)) in
 let dec : Number -> Number = fun x => x + (-1) in
diff --git a/core/tests/integration/inputs/core/functions.ncl b/core/tests/integration/inputs/core/functions.ncl
index 2e48a300..0c26f9c6 100644
--- a/core/tests/integration/inputs/core/functions.ncl
+++ b/core/tests/integration/inputs/core/functions.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let {check, ..} = import "../lib/assert.ncl" in
+
 
 [
   (fun x => x) 3 == 3,
@@ -16,4 +16,4 @@ let {check, ..} = import "../lib/assert.ncl" in
     g true
     == 4,
 ]
-|> check
+|> std.test.assert_all
diff --git a/core/tests/integration/inputs/core/import.ncl b/core/tests/integration/inputs/core/import.ncl
index 1c37d0e0..acbb3536 100644
--- a/core/tests/integration/inputs/core/import.ncl
+++ b/core/tests/integration/inputs/core/import.ncl
@@ -1,3 +1,3 @@
 # test.type = 'pass'
-let { Assert, .. } = import "../lib/assert.ncl" in
+let Assert = std.test.Assert in
 ((import "../lib/imported.ncl") 3 == 3 | Assert)
diff --git a/core/tests/integration/inputs/core/numbers.ncl b/core/tests/integration/inputs/core/numbers.ncl
index 8b0a59f8..d7a60e80 100644
--- a/core/tests/integration/inputs/core/numbers.ncl
+++ b/core/tests/integration/inputs/core/numbers.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let {check, ..} = import "../lib/assert.ncl" in
+
 
 [
   5 + 5 == 10,
@@ -7,4 +7,4 @@ let {check, ..} = import "../lib/assert.ncl" in
   1e6 == 1000000,
   1e+3 / 2e-3 == 0.5e6,
 ]
-|> check
\ No newline at end of file
+|> std.test.assert_all
diff --git a/core/tests/integration/inputs/core/records.ncl b/core/tests/integration/inputs/core/records.ncl
index a53cb4e2..a131e1c1 100644
--- a/core/tests/integration/inputs/core/records.ncl
+++ b/core/tests/integration/inputs/core/records.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let { check, .. } = import "../lib/assert.ncl" in
+
 
 [
   # accesses
@@ -171,4 +171,4 @@ let { check, .. } = import "../lib/assert.ncl" in
     && r.force == "no"
   )
 ]
-|> check
+|> std.test.assert_all
diff --git a/core/tests/integration/inputs/core/recursive_let.ncl b/core/tests/integration/inputs/core/recursive_let.ncl
index d3715acc..21428503 100644
--- a/core/tests/integration/inputs/core/recursive_let.ncl
+++ b/core/tests/integration/inputs/core/recursive_let.ncl
@@ -1,8 +1,8 @@
 # test.type = 'pass'
-let {check, ..} = import "../lib/assert.ncl" in
+
 
 [
   let rec f = fun n => if n == 0 then n else f (n - 1) in f 10 == 0,
   let rec fib = fun n => if n == 0 || n == 1 then 1 else fib (n - 1) + fib (n - 2) in fib 5 == 8,
 ]
-|> check
+|> std.test.assert_all
diff --git a/core/tests/integration/inputs/imports/yaml_import.ncl b/core/tests/integration/inputs/imports/yaml_import.ncl
index 93e4af8f..474b2384 100644
--- a/core/tests/integration/inputs/imports/yaml_import.ncl
+++ b/core/tests/integration/inputs/imports/yaml_import.ncl
@@ -1,6 +1,6 @@
 # test.type = 'pass'
 
-let {check, ..} = import "../lib/assert.ncl" in
+
 [
   (import "imported/empty.yaml") == null,
 
@@ -9,4 +9,4 @@ let {check, ..} = import "../lib/assert.ncl" in
     { type = "event", id = 2 }
   ],
 ]
-|> check
+|> std.test.assert_all
diff --git a/core/tests/integration/inputs/lib/assert.ncl b/core/tests/integration/inputs/lib/assert.ncl
deleted file mode 100644
index 54148ba8..00000000
--- a/core/tests/integration/inputs/lib/assert.ncl
+++ /dev/null
@@ -1,31 +0,0 @@
-# test.type = 'skip'
-{
-  Assert | doc m%"
-    A contract that checks if its argument is the boolean true.
-
-    We could get by just using arrays of boolean tests and `array.all`.
-
-    However, we apply this contract to each test instead because it provides
-    fine-grained error reporting, pinpointoing the exact expression that failed
-    in an array of tests, as opposed to the pure boolean solution.
-    "%
-    = fun l x => x || %blame% l,
-  # We can't use a static type because x | Assert is not of type bool.
-  # We could use additional contracts to make it work but it's not worth the
-  # hassle.
-  check | doc m%"
-    unchecked type: Array Bool -> Bool
-
-    Check an array of tests. Apply the `Assert` contract on each element of the
-    given array.
-
-    `check tests` It is semantically equivalent to
-
-    ```nickel
-    (array.all function.id tests) | Assert
-    ```
-
-    But will give more precise error reporting when one test fails.
-    "%
-    = std.array.fold_left (fun acc test => (test | Assert) && acc) true,
-}
diff --git a/core/tests/integration/inputs/merging/adts.ncl b/core/tests/integration/inputs/merging/adts.ncl
index fa7ec127..6d266257 100644
--- a/core/tests/integration/inputs/merging/adts.ncl
+++ b/core/tests/integration/inputs/merging/adts.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let { check, .. } = import "../lib/assert.ncl" in
+
 
 [
   'Foo true & 'Foo true == 'Foo true,
@@ -13,4 +13,4 @@ let { check, .. } = import "../lib/assert.ncl" in
   & { two.b = 1, one = 'Qux {c = 0} }
   == { one = 'Qux { a = 1, b = 1, c = 0 }, two = { a = 1, b = 1 } },
 ]
-|> check
+|> std.test.assert_all
diff --git a/core/tests/integration/inputs/merging/array-merge.ncl b/core/tests/integration/inputs/merging/array-merge.ncl
index 514e4307..17a93d48 100644
--- a/core/tests/integration/inputs/merging/array-merge.ncl
+++ b/core/tests/integration/inputs/merging/array-merge.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let {check, ..} = import "../lib/assert.ncl" in
+
 
 [
   # array pointwise merging
@@ -25,4 +25,4 @@ let {check, ..} = import "../lib/assert.ncl" in
   & ([0 + 0, 1 + 0, 2 + 0] | Array AddOne)
   == [1, 2, 3],
 ]
-|> check
+|> std.test.assert_all
diff --git a/core/tests/integration/inputs/merging/lazy-propagation.ncl b/core/tests/integration/inputs/merging/lazy-propagation.ncl
index 75bde1bd..6bb6789a 100644
--- a/core/tests/integration/inputs/merging/lazy-propagation.ncl
+++ b/core/tests/integration/inputs/merging/lazy-propagation.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let {check, Assert, ..} = import "../lib/assert.ncl" in
+
 
 # tests related to RFC005. Check in particular that the motivating examples of
 # RFC005 are indeed fixed by lazy propagation.
@@ -44,4 +44,4 @@ let {check, Assert, ..} = import "../lib/assert.ncl" in
   & {bar = "bar"})
   == {foo = 5, bar = "bar"},
 ]
-|> check
+|> std.test.assert_all
diff --git a/core/tests/integration/inputs/merging/metavalues.ncl b/core/tests/integration/inputs/merging/metavalues.ncl
index 3b282211..8820d9ce 100644
--- a/core/tests/integration/inputs/merging/metavalues.ncl
+++ b/core/tests/integration/inputs/merging/metavalues.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let {check, Assert, ..} = import "../lib/assert.ncl" in
+let Assert = std.test.Assert in
 
 [
   {val | default | Number = 10}.val == 10,
@@ -132,4 +132,4 @@ let {check, Assert, ..} = import "../lib/assert.ncl" in
     (%fields% value == ["foo", "opt"] | Assert)
   ),
 ]
-|> check
+|> std.test.assert_all
diff --git a/core/tests/integration/inputs/merging/multiple_overrides.ncl b/core/tests/integration/inputs/merging/multiple_overrides.ncl
index 8ebe7adb..3ef75e29 100644
--- a/core/tests/integration/inputs/merging/multiple_overrides.ncl
+++ b/core/tests/integration/inputs/merging/multiple_overrides.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let {check, ..} = import "../lib/assert.ncl" in
+
 
 [
   # regression test for issue #908 (https://github.com/tweag/nickel/issues/908)
@@ -121,6 +121,6 @@ let {check, ..} = import "../lib/assert.ncl" in
       fst_data = "override",
       snd_data = "override",
     }
-  ] |> check,
+  ] |> std.test.assert_all,
 ]
-|> check
+|> std.test.assert_all
diff --git a/core/tests/integration/inputs/merging/overriding.ncl b/core/tests/integration/inputs/merging/overriding.ncl
index b2170664..4a63cdee 100644
--- a/core/tests/integration/inputs/merging/overriding.ncl
+++ b/core/tests/integration/inputs/merging/overriding.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let {check, ..} = import "../lib/assert.ncl" in
+
 
 [
   {foo | default = 1} & {foo = 2} == {foo = 2},
@@ -72,4 +72,4 @@ let {check, ..} = import "../lib/assert.ncl" in
   ({foo | default = 1, bar = foo} & {foo = 2}).bar == 2,
   ({foo | default = 1, bar = foo, baz = bar} & {foo = 2}).baz == 2,
 ]
-|> check
+|> std.test.assert_all
diff --git a/core/tests/integration/inputs/merging/priorities.ncl b/core/tests/integration/inputs/merging/priorities.ncl
index b9899633..5d3d21da 100644
--- a/core/tests/integration/inputs/merging/priorities.ncl
+++ b/core/tests/integration/inputs/merging/priorities.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let {Assert, check, ..} =  import "../lib/assert.ncl" in
+let Assert = std.test.Assert in
 
 [
   let block1 = {
@@ -83,4 +83,4 @@ let {Assert, check, ..} =  import "../lib/assert.ncl" in
   # } in
   # {val | rec force = x } & {val = {foo = 0, bar = 10}}
   #   == {val = {bar = 10, foo = 1 + 2 + 10}} | Assert,
-] |> check
+] |> std.test.assert_all
diff --git a/core/tests/integration/inputs/parsing/annotations.ncl b/core/tests/integration/inputs/parsing/annotations.ncl
index ec86a45b..67e4d2f5 100644
--- a/core/tests/integration/inputs/parsing/annotations.ncl
+++ b/core/tests/integration/inputs/parsing/annotations.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let {check, Assert, ..} = import "../lib/assert.ncl" in
+let Assert = std.test.Assert in
 
 [
   # left_annot_precedence
@@ -23,4 +23,4 @@ let {check, Assert, ..} = import "../lib/assert.ncl" in
   # others_precedence
   ((fun x => x | Assert) true),
 ]
-|> check
+|> std.test.assert_all
diff --git a/core/tests/integration/inputs/parsing/identifiers.ncl b/core/tests/integration/inputs/parsing/identifiers.ncl
index 22ff98bf..61448bed 100644
--- a/core/tests/integration/inputs/parsing/identifiers.ncl
+++ b/core/tests/integration/inputs/parsing/identifiers.ncl
@@ -1,8 +1,8 @@
 # test.type = 'pass'
-let {Assert, check, ..} = import "../lib/assert.ncl" in
+
 
 [
   let this-isn't-invalid = true in this-isn't-invalid,
   let ___multi_underscore_start = true in ___multi_underscore_start,
 ]
-|> check
+|> std.test.assert_all
diff --git a/core/tests/integration/inputs/pattern-matching/arrays.ncl b/core/tests/integration/inputs/pattern-matching/arrays.ncl
index f0d6f3fe..b797f499 100644
--- a/core/tests/integration/inputs/pattern-matching/arrays.ncl
+++ b/core/tests/integration/inputs/pattern-matching/arrays.ncl
@@ -1,6 +1,4 @@
 # test.type = 'pass'
-let {check, ..} = import "../lib/assert.ncl" in
-
 [
   [] |> match {
     [x] => false,
@@ -51,4 +49,4 @@ let {check, ..} = import "../lib/assert.ncl" in
     _ => false,
   }
 ]
-|> check
+|> std.test.assert_all
diff --git a/core/tests/integration/inputs/pattern-matching/basics.ncl b/core/tests/integration/inputs/pattern-matching/basics.ncl
index 0f0ff908..dda27ebe 100644
--- a/core/tests/integration/inputs/pattern-matching/basics.ncl
+++ b/core/tests/integration/inputs/pattern-matching/basics.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let {check, ..} = import "../lib/assert.ncl" in
+
 
 [
   {foo = 1, bar = 2} |> match {
@@ -59,4 +59,4 @@ let {check, ..} = import "../lib/assert.ncl" in
   |> eval {}
   |> (==) 11,
 ]
-|> check
+|> std.test.assert_all
diff --git a/core/tests/integration/inputs/pattern-matching/contracts.ncl b/core/tests/integration/inputs/pattern-matching/contracts.ncl
index d31afef7..bc6811db 100644
--- a/core/tests/integration/inputs/pattern-matching/contracts.ncl
+++ b/core/tests/integration/inputs/pattern-matching/contracts.ncl
@@ -1,9 +1,9 @@
 # test.type = 'pass'
 
-let {check, ..} = import "../lib/assert.ncl" in
+
 
 [
   {foo = {}} |> match { {foo = {bar ? 5}} => true},
   {foo = {}} |> match { {foo = {bar | String }} => false, {foo} => true},
 ]
-|> check
+|> std.test.assert_all
diff --git a/core/tests/integration/inputs/pattern-matching/default_value.ncl b/core/tests/integration/inputs/pattern-matching/default_value.ncl
index 66aa899b..ea45bb8f 100644
--- a/core/tests/integration/inputs/pattern-matching/default_value.ncl
+++ b/core/tests/integration/inputs/pattern-matching/default_value.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let {check, ..} = import "../lib/assert.ncl" in
+
 
 [
   let {x ? 1, y ? 1, z} = {z = 1} in (x + y + z) == 3,
@@ -13,4 +13,4 @@ let {check, ..} = import "../lib/assert.ncl" in
     _ => false,
   },
 ]
-|> check
+|> std.test.assert_all
diff --git a/core/tests/integration/inputs/pattern-matching/enum_after_wildcard.ncl b/core/tests/integration/inputs/pattern-matching/enum_after_wildcard.ncl
index c9efa320..bbc17df2 100644
--- a/core/tests/integration/inputs/pattern-matching/enum_after_wildcard.ncl
+++ b/core/tests/integration/inputs/pattern-matching/enum_after_wildcard.ncl
@@ -8,7 +8,7 @@
 # This test ensure it correctly handle a wildcard being used before the end of
 # the match expression, which was mishandled previously (branches after the
 # wildcard would be selected if they matched).
-let {check, ..} = import "../lib/assert.ncl" in
+
 
 [
   'Foo |> match {
@@ -28,4 +28,4 @@ let {check, ..} = import "../lib/assert.ncl" in
     _ => false,
   },
 ]
-|> check
+|> std.test.assert_all
diff --git a/core/tests/integration/inputs/pattern-matching/guards.ncl b/core/tests/integration/inputs/pattern-matching/guards.ncl
index f37b1bcc..e9289201 100644
--- a/core/tests/integration/inputs/pattern-matching/guards.ncl
+++ b/core/tests/integration/inputs/pattern-matching/guards.ncl
@@ -1,6 +1,4 @@
 # test.type = 'pass'
-let {check, ..} = import "../lib/assert.ncl" in
-
 [
   'Ok |> match {
     'Ok if false => false,
@@ -29,4 +27,4 @@ let {check, ..} = import "../lib/assert.ncl" in
     _ => false
   }
 ]
-|> check
+|> std.test.assert_all
diff --git a/core/tests/integration/inputs/pattern-matching/wildcards.ncl b/core/tests/integration/inputs/pattern-matching/wildcards.ncl
index 7f849bbc..baba74af 100644
--- a/core/tests/integration/inputs/pattern-matching/wildcards.ncl
+++ b/core/tests/integration/inputs/pattern-matching/wildcards.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let {check, ..} = import "../lib/assert.ncl" in
+
 
 [
   {foo = 1, bar = 2} |> match {
@@ -21,4 +21,4 @@ let {check, ..} = import "../lib/assert.ncl" in
     'Point _ => true,
   },
 ]
-|> check
+|> std.test.assert_all
diff --git a/core/tests/integration/inputs/records/record_defs.ncl b/core/tests/integration/inputs/records/record_defs.ncl
index dd7fe59d..d22913c5 100644
--- a/core/tests/integration/inputs/records/record_defs.ncl
+++ b/core/tests/integration/inputs/records/record_defs.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let {check, ..} = import "../lib/assert.ncl" in
+
 
 [
   # piecewise_definitions
@@ -41,4 +41,4 @@ let {check, ..} = import "../lib/assert.ncl" in
   let x = "foo" in {"%{x}" = bar, bar = 1} == {foo = 1, bar = 1},
   ({"%foo"."%bar".baz = other + 1, other = 0}."%foo"."%bar".baz == 1),
 ]
-|> check
+|> std.test.assert_all
diff --git a/core/tests/integration/inputs/serialization/serialize.ncl b/core/tests/integration/inputs/serialization/serialize.ncl
index adfa0d1c..6a285d1d 100644
--- a/core/tests/integration/inputs/serialization/serialize.ncl
+++ b/core/tests/integration/inputs/serialization/serialize.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let {check, ..} = import "../lib/assert.ncl" in
+
 
 let assertSerInv = fun x =>
     let assertAux = fun format x =>
@@ -63,4 +63,4 @@ let assertDeserInv = fun x =>
   let ext = {foo = {some = {}}} in
   assertSerInv (base & ext),
 ]
-|> check
+|> std.test.assert_all
diff --git a/core/tests/integration/inputs/stdlib/record_empty_optional_ops.ncl b/core/tests/integration/inputs/stdlib/record_empty_optional_ops.ncl
index 01cc0781..31a31a1a 100644
--- a/core/tests/integration/inputs/stdlib/record_empty_optional_ops.ncl
+++ b/core/tests/integration/inputs/stdlib/record_empty_optional_ops.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let { check, .. } = import "../lib/assert.ncl" in
+
 let { record, .. } = std in
 
 [
@@ -23,4 +23,4 @@ let { record, .. } = std in
   std.record.has_field_with_opts "c" { a = 1, b = 2, c | optional } == true,
   std.record.has_field_with_opts "c" ({ a = 1, b = 2 } | { a, b, c | optional }) == true,
 ]
-|> check
+|> std.test.assert_all
diff --git a/core/tests/integration/inputs/stdlib/string_contains_find_replace.ncl b/core/tests/integration/inputs/stdlib/string_contains_find_replace.ncl
index b26cbbfd..a9c3cca7 100644
--- a/core/tests/integration/inputs/stdlib/string_contains_find_replace.ncl
+++ b/core/tests/integration/inputs/stdlib/string_contains_find_replace.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let {check, ..} = import "../lib/assert.ncl" in
+
 let {string, ..} = std in
 
 [
@@ -44,4 +44,4 @@ let {string, ..} = std in
   string.find "\\d" "no numeral" == { matched = "", index = -1, groups = []},
   string.find "❤️" "👨‍❤️‍💋‍👨" == { matched = "", index = -1, groups = []},
   string.find "❤️" "👨‍❤️‍💋‍👨❤️" == { matched = "❤️", index = 1, groups = [] },
-  ] |> check
+  ] |> std.test.assert_all
diff --git a/core/tests/integration/inputs/stdlib/string_contracts.ncl b/core/tests/integration/inputs/stdlib/string_contracts.ncl
index 051edc88..3d8eeb10 100644
--- a/core/tests/integration/inputs/stdlib/string_contracts.ncl
+++ b/core/tests/integration/inputs/stdlib/string_contracts.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let {check, ..} = import "../lib/assert.ncl" in
+
 let {string, ..} = std in
 
 [
@@ -37,4 +37,4 @@ let {string, ..} = std in
   ("_" | string.NonEmpty) == "_",
   ("a" | string.NonEmpty) == "a",
   (" " | string.NonEmpty) == " ",
-] |> check
+] |> std.test.assert_all
diff --git a/core/tests/integration/inputs/stdlib/string_conversions.ncl b/core/tests/integration/inputs/stdlib/string_conversions.ncl
index 694384d7..76dabfcd 100644
--- a/core/tests/integration/inputs/stdlib/string_conversions.ncl
+++ b/core/tests/integration/inputs/stdlib/string_conversions.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let {check, ..} = import "../lib/assert.ncl" in
+
 let {string, ..} = std in
 
 [
@@ -35,4 +35,4 @@ let {string, ..} = std in
   string.to_enum "X" == 'X,
   string.to_enum "X" == string.to_enum "X",
   string.to_enum "タグ" == '"タグ",
-] |> check
+] |> std.test.assert_all
diff --git a/core/tests/integration/inputs/stdlib/string_find.ncl b/core/tests/integration/inputs/stdlib/string_find.ncl
index bef00b66..2dd0a7a6 100644
--- a/core/tests/integration/inputs/stdlib/string_find.ncl
+++ b/core/tests/integration/inputs/stdlib/string_find.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let { check, .. } = import "../lib/assert.ncl" in
+
 let { string, .. } = std in
 
 [
@@ -8,4 +8,4 @@ let { string, .. } = std in
   std.string.find "(\\d+)\\.(\\d+)\\.(\\d+)" "1.2.3" == { groups = ["1", "2", "3"], index = 0, matched = "1.2.3" },
   std.string.find "(\\p{Emoji})=(\\w+)" "😀=smiling" == { groups = ["😀", "smiling"], index = 0, matched = "😀=smiling" }
 ]
-|> check
+|> std.test.assert_all
diff --git a/core/tests/integration/inputs/stdlib/string_find_all.ncl b/core/tests/integration/inputs/stdlib/string_find_all.ncl
index 0a2c563b..61245481 100644
--- a/core/tests/integration/inputs/stdlib/string_find_all.ncl
+++ b/core/tests/integration/inputs/stdlib/string_find_all.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let { check, .. } = import "../lib/assert.ncl" in
+
 let { string, .. } = std in
 
 [
@@ -27,4 +27,4 @@ let { string, .. } = std in
     { groups = ["🫠", "melting"], index = 22, matched = "🫠=melting", }
   ]
 ]
-|> check
+|> std.test.assert_all
diff --git a/core/tests/integration/inputs/stdlib/string_primitives.ncl b/core/tests/integration/inputs/stdlib/string_primitives.ncl
index 8ca7fe42..65e6d3b9 100644
--- a/core/tests/integration/inputs/stdlib/string_primitives.ncl
+++ b/core/tests/integration/inputs/stdlib/string_primitives.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let {check, ..} = import "../lib/assert.ncl" in
+
 let {string, ..} = std in
 
 [
@@ -34,4 +34,4 @@ let {string, ..} = std in
   string.substring 0 1 "👩🏻‍🦲" == "👩🏻‍🦲",
   string.substring 1 6 "👨‍❤️‍💋‍👨hello" == "hello",
   string.substring 2 4 "ab👨‍❤️‍💋‍👨👨‍❤️‍💋‍👨cd" == "👨‍❤️‍💋‍👨👨‍❤️‍💋‍👨",
-] |> check
+] |> std.test.assert_all
diff --git a/core/tests/integration/inputs/stdlib/string_split_join.ncl b/core/tests/integration/inputs/stdlib/string_split_join.ncl
index 93b33814..574bb3b5 100644
--- a/core/tests/integration/inputs/stdlib/string_split_join.ncl
+++ b/core/tests/integration/inputs/stdlib/string_split_join.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let {check, ..} = import "../lib/assert.ncl" in
+
 let {string, ..} = std in
 
 [
@@ -31,4 +31,4 @@ let {string, ..} = std in
     { sep = " ", str = "this is a sentence."},
     { sep = "👩‍❤️‍💋‍👨", str = "-👩‍❤️‍💋‍👨👩‍❤️‍💋‍👨👩‍❤️‍💋‍👨-" },
   ],
-] |> check
+] |> std.test.assert_all
diff --git a/core/tests/integration/inputs/stdlib/string_trim.ncl b/core/tests/integration/inputs/stdlib/string_trim.ncl
index b817b58e..025f752e 100644
--- a/core/tests/integration/inputs/stdlib/string_trim.ncl
+++ b/core/tests/integration/inputs/stdlib/string_trim.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let {check, ..} = import "../lib/assert.ncl" in
+
 let {string, ..} = std in
 
 [
@@ -19,4 +19,4 @@ let {string, ..} = std in
 
 
   "% == "a multiline string",
-] |> check
+] |> std.test.assert_all
diff --git a/core/tests/integration/inputs/stdlib/string_uppercase_lowercase.ncl b/core/tests/integration/inputs/stdlib/string_uppercase_lowercase.ncl
index b2f50210..231ba5dc 100644
--- a/core/tests/integration/inputs/stdlib/string_uppercase_lowercase.ncl
+++ b/core/tests/integration/inputs/stdlib/string_uppercase_lowercase.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let {check, ..} = import "../lib/assert.ncl" in
+
 let {string, ..} = std in
 
 [
@@ -32,4 +32,4 @@ let {string, ..} = std in
     "XXXXYYYYZZZZ",
     "THIS IS A TEST"
   ],
-] |> check
+] |> std.test.assert_all
diff --git a/core/tests/integration/inputs/strings/string_interpolation.ncl b/core/tests/integration/inputs/strings/string_interpolation.ncl
index b9f4f3ba..24896e20 100644
--- a/core/tests/integration/inputs/strings/string_interpolation.ncl
+++ b/core/tests/integration/inputs/strings/string_interpolation.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let {check, ..} = import "../lib/assert.ncl" in
+
 
 [
   # Simple interpolation tests
@@ -54,4 +54,4 @@ let {check, ..} = import "../lib/assert.ncl" in
   let actual = m%%"XYZ "%%%{"CBA"}""%% in
   let expected = "XYZ \"%CBA\"" in
   actual == expected,
-] |> check
+] |> std.test.assert_all
diff --git a/core/tests/integration/inputs/strings/symbolic_strings.ncl b/core/tests/integration/inputs/strings/symbolic_strings.ncl
index 7bf7c4e1..4ab7727c 100644
--- a/core/tests/integration/inputs/strings/symbolic_strings.ncl
+++ b/core/tests/integration/inputs/strings/symbolic_strings.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let {check, ..} = import "../lib/assert.ncl" in
+
 let sym = fun prefix_ fragments_ => { tag = 'SymbolicString, prefix = prefix_, fragments = fragments_ } in
 
 [
@@ -43,4 +43,4 @@ let sym = fun prefix_ fragments_ => { tag = 'SymbolicString, prefix = prefix_, f
   in
   actual == expected,
 ]
-|> check
+|> std.test.assert_all
diff --git a/core/tests/integration/inputs/typecheck/basic_typechecking.ncl b/core/tests/integration/inputs/typecheck/basic_typechecking.ncl
index 83c48cfb..ae5d6d7c 100644
--- a/core/tests/integration/inputs/typecheck/basic_typechecking.ncl
+++ b/core/tests/integration/inputs/typecheck/basic_typechecking.ncl
@@ -1,5 +1,5 @@
 # test.type = 'pass'
-let {Assert, ..} = import "../lib/assert.ncl" in
+let Assert = std.test.Assert in
 
 (let plus : Number -> Number -> Number = fun x => fun y => x + y in
  plus (54 : Number) (6 : Number) == 60 | Assert)