diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,5 +1,82 @@
 # Revision history for zeolite-lang
 
+## 0.21.0.0  -- 2021-11-19
+
+### Compiler CLI
+
+* **[breaking]** Combines params and args in function calls into a single
+  object. This breaks all C++ extensions.
+
+  - `Call_foo` for function `foo` must now take `const ParamsArgs& params_args`
+    as its only argument.
+    - Access param `n` using `params_args.GetParam(n)`.
+    - Access arg `n` using `params_args.GetArg(n)`.
+  - Function calls must group params and args with
+    `PassParamsArgs(params..., args...)` rather than with
+    `ParamTuple(params...)` and `ArgTuple(args...)`.
+  - To directly pass the returns from another function call, use that function
+    call in place of `args...`, e.g.
+    `PassParamsArgs(params..., TypeValue::Call(Var_foo, Function_Foo_bar, ...))`.
+  - Value initialization should be changed to take `ParamsArgs` instead of
+    `ValueTuple`, since the latter has been removed. (Alternatively, just pass a
+    custom set of arguments.)
+  - `ReturnTuple` usage for returns remains unchanged.
+
+* **[new]** Updates dependency check to skip checking the modification times of
+  `$ModuleOnly$` `.0rp` source files when determining if the target module needs
+  to be recompiled.
+
+* **[new]** Adds `--clean` mode to `zeolite` to clear cached data for a module.
+
+* **[new]** Allows specifying a root path with `-p` in special `zeolite`
+  execution modes, e.g., `--missed-lines`.
+
+### Libraries
+
+* **[breaking]** Makes `LinkedNode` in `lib/container` weak in the reverse
+  direction to avoid reference cycles.
+
+* **[new]** Adds `Token` to `lib/math`, for more efficient comparison of strings
+  when used in algorithms.
+
+* **[new]** Gives `Testing.checkEmpty` in `lib/testing` a parameter to allow
+  values that do not refine `Formatted`. (This is not breaking because of the
+  new language feature that infers params by default.)
+
+* **[new]** Adds `getAll` to `TypeMap` in `lib/container` to read all values of
+  a specified type.
+
+* **[new]** Adds `checkTrue` and `checkFalse` to `Testing` in `lib/testing`.
+
+### Language
+
+* **[new]** Makes param inference the default when no params are specified in a
+  function call. For example, `call<?>(foo)` can now be `call(foo)`.
+
+* **[new]** Adds syntax to select a single return value from a function that
+  returns multiple values.
+
+  ```text
+  // Previous method:
+  scoped {
+    Foo foo, _ <- call()
+  } in \ foo.bar()
+
+  // New method:
+  \ call(){0}.bar()
+  ```
+
+* **[new]** Adds the `$FlatCleanup[`*`memberName`*`]$` pragma to enable
+  non-recursive cleanup for deeply-recursive types like linked lists.
+
+* **[behavior]** Removes all dynamic allocation when passing params and
+  arguments in function calls.
+
+* **[behavior]** Improves efficiency of returning values from function calls.
+
+* **[behavior]** Improves efficiency of `Bool`, `Char`, `Float`, and `Int`
+  values when used in function calls and containers.
+
 ## 0.20.0.1  -- 2021-11-14
 
 ### Language
diff --git a/base/.zeolite-module b/base/.zeolite-module
--- a/base/.zeolite-module
+++ b/base/.zeolite-module
@@ -34,11 +34,12 @@
   "base/thread-capture.h"
   "base/thread-crosser.h"
   "base/pooled.hpp"
+  "base/returns.hpp"
   "base/types.hpp"
   "base/src/boxed.cpp"
   "base/src/category-source.cpp"
   "base/src/logging.cpp"
+  "base/src/returns.cpp"
   "base/src/thread-crosser.cc"
-  "base/src/types.cpp"
 ]
 mode: incremental {}
diff --git a/base/boxed.hpp b/base/boxed.hpp
--- a/base/boxed.hpp
+++ b/base/boxed.hpp
@@ -23,10 +23,12 @@
 #include <cstdlib>
 #include <atomic>
 
-#include "function.hpp"
 #include "types.hpp"
 
 
+class ParamsArgs;
+class ValueFunction;
+
 namespace zeolite_internal {
 
 struct UnionValue {
@@ -79,7 +81,9 @@
 
   inline BoxedValue& operator = (const BoxedValue& other) {
     if (&other != this) {
-      Cleanup();
+      if (union_.type_ == UnionValue::Type::BOXED) {
+        Cleanup();
+      }
       union_ = other.union_;
       switch (union_.type_) {
         case UnionValue::Type::BOXED:
@@ -100,7 +104,9 @@
 
   inline BoxedValue& operator = (BoxedValue&& other) {
     if (&other != this) {
-      Cleanup();
+      if (union_.type_ == UnionValue::Type::BOXED) {
+        Cleanup();
+      }
       union_ = other.union_;
       other.union_.type_  = UnionValue::Type::EMPTY;
       other.union_.value_.as_pointer_ = nullptr;
@@ -133,7 +139,9 @@
   }
 
   inline ~BoxedValue() {
-    Cleanup();
+    if (union_.type_ == UnionValue::Type::BOXED) {
+      Cleanup();
+    }
   }
 
   inline PrimBool AsBool() const {
@@ -228,8 +236,7 @@
     return value;
   }
 
-  ReturnTuple Dispatch(
-    const ValueFunction& label, const ParamTuple& params, const ValueTuple& args) const;
+  ReturnTuple Dispatch(const ::ValueFunction& label, const ::ParamsArgs& params_args) const;
 
   void Cleanup();
 
diff --git a/base/category-source.hpp b/base/category-source.hpp
--- a/base/category-source.hpp
+++ b/base/category-source.hpp
@@ -26,13 +26,14 @@
 
 #include "boxed.hpp"
 #include "cycle-check.hpp"
-#include "types.hpp"
 #include "function.hpp"
+#include "returns.hpp"
+#include "types.hpp"
 
 
 #define BUILTIN_FAIL(e) { \
   FAIL() << TypeValue::Call((e), Function_Formatted_formatted, \
-                            ParamTuple(), ArgTuple()).Only().AsString(); \
+                            PassParamsArgs()).At(0).AsString(); \
   __builtin_unreachable(); \
   }
 
@@ -62,9 +63,8 @@
 
 class TypeCategory {
  public:
-  inline ReturnTuple Call(const CategoryFunction& label,
-                          const ParamTuple& params, const ValueTuple& args) {
-    return Dispatch(label, params, args);
+  inline ReturnTuple Call(const CategoryFunction& label, const ParamsArgs& params_args) {
+    return Dispatch(label, params_args);
   }
 
   virtual std::string CategoryName() const = 0;
@@ -75,19 +75,17 @@
  protected:
   TypeCategory() = default;
 
-  virtual ReturnTuple Dispatch(const CategoryFunction& label,
-                               const ParamTuple& params, const ValueTuple& args);
+  virtual ReturnTuple Dispatch(const CategoryFunction& label, const ParamsArgs& params_args);
 };
 
 class TypeInstance {
  public:
   inline static ReturnTuple Call(const S<const TypeInstance>& target,
-                                 const TypeFunction& label,
-                                 const ParamTuple& params, const ValueTuple& args) {
+                                 const TypeFunction& label, const ParamsArgs& params_args) {
     if (target == nullptr) {
       FAIL() << "Function called on null value";
     }
-    return target->Dispatch(label, params, args);
+    return target->Dispatch(label, params_args);
   }
 
   virtual std::string CategoryName() const = 0;
@@ -117,8 +115,7 @@
  protected:
   TypeInstance() = default;
 
-  virtual ReturnTuple Dispatch(const TypeFunction& label,
-                               const ParamTuple& params, const ValueTuple& args) const;
+  virtual ReturnTuple Dispatch(const TypeFunction& label, const ParamsArgs& params_args) const;
 
   virtual bool CanConvertFrom(const S<const TypeInstance>& from) const
   { return false; }
@@ -161,8 +158,8 @@
 class TypeValue {
  public:
   inline static ReturnTuple Call(const BoxedValue& target, const ValueFunction& label,
-                                 const ParamTuple& params, const ValueTuple& args) {
-    return target.Dispatch(label, params, args);
+                                 const ParamsArgs& params_args) {
+    return target.Dispatch(label, params_args);
   }
 
   virtual const PrimString& AsString() const;
@@ -188,8 +185,9 @@
   // NOTE: For some reason, making this private causes a segfault.
   virtual std::string CategoryName() const = 0;
 
-  virtual ReturnTuple Dispatch(
-    const ValueFunction& label, const ParamTuple& params, const ValueTuple& args);
+  virtual BoxedValue FlatCleanup() { return BoxedValue(); }
+
+  virtual ReturnTuple Dispatch(const ValueFunction& label, const ParamsArgs& params_args);
 
  private:
   // Creating a BoxedValue from a TypeValue won't have the correct offset.
diff --git a/base/function.hpp b/base/function.hpp
--- a/base/function.hpp
+++ b/base/function.hpp
@@ -21,6 +21,7 @@
 
 #include <ostream>
 
+#include "returns.hpp"
 #include "types.hpp"
 
 
@@ -64,6 +65,148 @@
 
 inline std::ostream& operator << (std::ostream& output, const ValueFunction& func) {
   return output << func.category << "." << func.function;
+}
+
+struct ParamsArgs {
+  virtual int NumParams() const = 0;
+  virtual const S<const TypeInstance>& GetParam(int pos) const = 0;
+  virtual int NumArgs() const = 0;
+  virtual const BoxedValue& GetArg(int pos) const = 0;
+};
+
+template<int P, int A>
+struct PassArgs : public ParamsArgs {
+  template<class... Ps>
+  PassArgs(const Ps&... passed) {
+    Init<0, 0>(passed...);
+  }
+
+  template<int Pn, int An>
+  void Init() {}
+
+  template<int Pn, int An, class... Ps>
+  void Init(const S<const TypeInstance>& param, const Ps&... passed) {
+    params[Pn] = &param;
+    Init<Pn+1, An>(passed...);
+  }
+
+  template<int Pn, int An, class... Ps>
+  void Init(const BoxedValue& arg, const Ps&... passed) {
+    args[An] = &arg;
+    Init<Pn, An+1>(passed...);
+  }
+
+  int NumParams() const final { return P; }
+
+  const S<const TypeInstance>& GetParam(int pos) const final {
+    if (pos < 0 || pos >= P) {
+      FAIL() << "Bad param index";
+    }
+    return *params[pos];
+  }
+
+  int NumArgs() const final { return A; }
+
+  const BoxedValue& GetArg(int pos) const final {
+    if (pos < 0 || pos >= A) {
+      FAIL() << "Bad arg index";
+    }
+    return *args[pos];
+  }
+
+  const S<const TypeInstance>* params[P];
+  const BoxedValue*   args[A];
+};
+
+template<>
+struct PassArgs<0, 0> : public ParamsArgs {
+  constexpr PassArgs() {}
+
+  int NumParams() const final { return 0; }
+
+  const S<const TypeInstance>& GetParam(int pos) const final {
+    FAIL() << "Bad param index";
+    __builtin_unreachable();
+  }
+
+  int NumArgs() const final { return 0; }
+
+  const BoxedValue& GetArg(int pos) const final {
+    FAIL() << "Bad arg index";
+    __builtin_unreachable();
+  }
+};
+
+template<int P>
+struct PassReturns : public ParamsArgs {
+  template<class... Ps>
+  PassReturns(const Ps&... passed) {
+    Init<0>(passed...);
+  }
+
+  template<int Pn, class... Ps>
+  void Init(const S<const TypeInstance>& param, const Ps&... passed) {
+    params[Pn] = &param;
+    Init<Pn+1>(passed...);
+  }
+
+  template<int Pn>
+  void Init(const ReturnTuple& forward) {
+    returns = &forward;
+  }
+
+  int NumParams() const final { return P; }
+
+  const S<const TypeInstance>& GetParam(int pos) const final {
+    if (pos < 0 || pos >= P) {
+      FAIL() << "Bad param index";
+    }
+    return *params[pos];
+  }
+
+  int NumArgs() const final                     { return returns->Size(); }
+  const BoxedValue& GetArg(int pos) const final { return returns->At(pos); }
+
+  const S<const TypeInstance>* params[P];
+  const ReturnTuple* returns;
+};
+
+template<int P, int A, class... Ps>
+struct AutoArgs;
+
+template<int P, int A>
+struct AutoArgs<P, A> {
+  using Type = PassArgs<P, A>;
+};
+
+template<int P, int A, class... Ps>
+struct AutoArgs<P, A, BoxedValue, Ps...> {
+  using Type = typename AutoArgs<P, A+1, Ps...>::Type;
+};
+
+template<int P, class... Ps>
+struct AutoCall {
+  using Type = typename AutoArgs<P, 0, Ps...>::Type;
+};
+
+template<int P>
+struct AutoCall<P, ReturnTuple> {
+  using Type = PassReturns<P>;
+};
+
+template<int P, class... Ps>
+struct AutoCall<P, S<const TypeInstance>, Ps...> {
+  using Type = typename AutoCall<P+1, Ps...>::Type;
+};
+
+template<class... Ps>
+struct GetCall {
+  using Type = typename AutoCall<0, Ps...>::Type;
+};
+
+template<class... Ps>
+typename GetCall<Ps...>::Type PassParamsArgs(const Ps&... passed) {
+  return typename GetCall<Ps...>::Type(passed...);
 }
 
 #endif  // FUNCTION_HPP_
diff --git a/base/pooled.hpp b/base/pooled.hpp
--- a/base/pooled.hpp
+++ b/base/pooled.hpp
@@ -20,9 +20,7 @@
 #define POOLED_HPP_
 
 
-class ArgTuple;
 class ReturnTuple;
-class ParamTuple;
 
 namespace zeolite_internal {
 
@@ -39,6 +37,7 @@
   PoolStorage* next = nullptr;
 
 private:
+  template<class> friend class PoolCache;
   template<class> friend class PoolManager;
 
   inline PoolStorage(int s, PoolStorage* n) : size(s), next(n) {}
@@ -56,9 +55,7 @@
 
 template<class T>
 class PoolArray {
-  friend class ::ArgTuple;
   friend class ::ReturnTuple;
-  friend class ::ParamTuple;
 
   constexpr PoolArray() : size_(0), array_(nullptr) {}
 
@@ -114,6 +111,53 @@
 
   int size_;
   PoolStorage<T>* array_;
+};
+
+template<class T>
+class PoolCache {
+ public:
+  PoolCache(int max) : max_(max) {}
+
+  ~PoolCache() {
+    while (pool_) {
+      --size_;
+      auto* current = pool_;
+      pool_ = pool_->next;
+      current->~PoolStorage<T>();
+      delete[] (unsigned char*) current;
+    }
+  }
+
+ private:
+  template<class> friend class PoolManager;
+
+  inline PoolStorage<T>* Take() {
+    PoolStorage<T>* const storage = pool_;
+    if (storage == nullptr) {
+      return nullptr;
+    } else {
+      --size_;
+      pool_ = storage->next;
+      storage->next = nullptr;
+      return storage;
+    }
+  }
+
+  inline bool Return(PoolStorage<T>* storage) {
+    PoolStorage<T>* const head = pool_;
+    if (size_ < max_) {
+      ++size_;
+      storage->next = head;
+      pool_ = storage;
+      return true;
+    } else {
+      return false;
+    }
+  }
+
+  PoolStorage<T>* pool_;
+  int size_ = 0;
+  const int max_;
 };
 
 }  // namespace zeolite_internal
diff --git a/base/returns.hpp b/base/returns.hpp
new file mode 100644
--- /dev/null
+++ b/base/returns.hpp
@@ -0,0 +1,78 @@
+/* -----------------------------------------------------------------------------
+Copyright 2019-2021 Kevin P. Barry
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+----------------------------------------------------------------------------- */
+
+// Author: Kevin P. Barry [ta0kira@gmail.com]
+
+#ifndef RETURNS_HPP_
+#define RETURNS_HPP_
+
+#include "boxed.hpp"
+#include "pooled.hpp"
+
+
+namespace zeolite_internal {
+
+template<>
+class PoolManager<BoxedValue> {
+  using PoolEntry = PoolStorage<BoxedValue>;
+  using Managed = PoolEntry::Managed;
+
+  template<class> friend class PoolArray;
+  template<class> friend struct PoolCache;
+
+  static PoolEntry* Take(int size);
+  static void Return(PoolEntry* storage, int size);
+
+  static thread_local PoolCache<BoxedValue> cache3_;
+};
+
+}  // namespace zeolite_internal
+
+
+class ReturnTuple {
+ public:
+  constexpr ReturnTuple() : data_() {}
+
+  explicit ReturnTuple(int size) : data_(size-1) {}
+
+  explicit ReturnTuple(BoxedValue first) : first_(std::move(first)), data_() {}
+
+  template<class...Ts>
+  explicit ReturnTuple(BoxedValue first, Ts... returns)
+    : first_(std::move(first)), data_(sizeof...(Ts)) {
+    data_.Init(std::move(returns)...);
+  }
+
+  ReturnTuple(ReturnTuple&&) = default;
+
+  void TransposeFrom(ReturnTuple&& other);
+
+  // NOTE: This will never be 0.
+  int Size() const;
+  BoxedValue& At(int pos);
+  const BoxedValue& At(int pos) const;
+
+ private:
+  ReturnTuple(const ReturnTuple&) = delete;
+  ReturnTuple& operator =(ReturnTuple&&) = delete;
+  ReturnTuple& operator =(const ReturnTuple&) = delete;
+  void* operator new(std::size_t size) = delete;
+
+  BoxedValue first_;
+  zeolite_internal::PoolArray<BoxedValue> data_;
+};
+
+#endif  // RETURNS_HPP_
diff --git a/base/src/Extension_Bool.cpp b/base/src/Extension_Bool.cpp
--- a/base/src/Extension_Bool.cpp
+++ b/base/src/Extension_Bool.cpp
@@ -39,21 +39,21 @@
 struct ExtType_Bool : public Type_Bool {
   inline ExtType_Bool(Category_Bool& p, Params<0>::Type params) : Type_Bool(p, params) {}
 
-  ReturnTuple Call_default(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_default(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Bool.default")
     return ReturnTuple(Box_Bool(false));
   }
 
-  ReturnTuple Call_equals(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_equals(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Bool.equals")
-    const PrimBool Var_arg1 = (args.At(0)).AsBool();
-    const PrimBool Var_arg2 = (args.At(1)).AsBool();
+    const PrimBool Var_arg1 = (params_args.GetArg(0)).AsBool();
+    const PrimBool Var_arg2 = (params_args.GetArg(1)).AsBool();
     return ReturnTuple(Box_Bool(Var_arg1==Var_arg2));
   }
 };
 
 ReturnTuple DispatchBool(PrimBool value, const ValueFunction& label,
-                         const ParamTuple& params, const ValueTuple& args) {
+                         const ParamsArgs& params_args) {
   switch (label.collection) {
     case CategoryId_AsBool:
       return ReturnTuple(Box_Bool(value));
diff --git a/base/src/Extension_Char.cpp b/base/src/Extension_Char.cpp
--- a/base/src/Extension_Char.cpp
+++ b/base/src/Extension_Char.cpp
@@ -42,38 +42,38 @@
 struct ExtType_Char : public Type_Char {
   inline ExtType_Char(Category_Char& p, Params<0>::Type params) : Type_Char(p, params) {}
 
-  ReturnTuple Call_maxBound(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_maxBound(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Char.maxBound")
     return ReturnTuple(Box_Char('\xff'));
   }
 
-  ReturnTuple Call_minBound(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_minBound(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Char.minBound")
     return ReturnTuple(Box_Char('\0'));
   }
 
-  ReturnTuple Call_default(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_default(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Char.default")
     return ReturnTuple(Box_Char('\0'));
   }
 
-  ReturnTuple Call_equals(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_equals(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Char.equals")
-    const PrimChar Var_arg1 = (args.At(0)).AsChar();
-    const PrimChar Var_arg2 = (args.At(1)).AsChar();
+    const PrimChar Var_arg1 = (params_args.GetArg(0)).AsChar();
+    const PrimChar Var_arg2 = (params_args.GetArg(1)).AsChar();
     return ReturnTuple(Box_Bool(Var_arg1==Var_arg2));
   }
 
-  ReturnTuple Call_lessThan(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_lessThan(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Char.lessThan")
-    const PrimChar Var_arg1 = (args.At(0)).AsChar();
-    const PrimChar Var_arg2 = (args.At(1)).AsChar();
+    const PrimChar Var_arg1 = (params_args.GetArg(0)).AsChar();
+    const PrimChar Var_arg2 = (params_args.GetArg(1)).AsChar();
     return ReturnTuple(Box_Bool(Var_arg1<Var_arg2));
   }
 };
 
 ReturnTuple DispatchChar(PrimChar value, const ValueFunction& label,
-                         const ParamTuple& params, const ValueTuple& args) {
+                         const ParamsArgs& params_args) {
   switch (label.collection) {
     case CategoryId_AsBool:
       return ReturnTuple(Box_Bool(value != '\0'));
diff --git a/base/src/Extension_CharBuffer.cpp b/base/src/Extension_CharBuffer.cpp
--- a/base/src/Extension_CharBuffer.cpp
+++ b/base/src/Extension_CharBuffer.cpp
@@ -37,9 +37,9 @@
 struct ExtType_CharBuffer : public Type_CharBuffer {
   inline ExtType_CharBuffer(Category_CharBuffer& p, Params<0>::Type params) : Type_CharBuffer(p, params) {}
 
-  ReturnTuple Call_new(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_new(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("CharBuffer.new")
-    const PrimInt Var_arg1 = (args.At(0)).AsInt();
+    const PrimInt Var_arg1 = (params_args.GetArg(0)).AsInt();
     if (Var_arg1 < 0) {
       FAIL() << "Buffer size " << Var_arg1 << " is invalid";
     }
@@ -51,18 +51,18 @@
   inline ExtValue_CharBuffer(S<const Type_CharBuffer> p, PrimCharBuffer value)
     : Value_CharBuffer(std::move(p)), value_(std::move(value)) {}
 
-  ReturnTuple Call_readAt(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_readAt(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("CharBuffer.readAt")
-    const PrimInt Var_arg1 = (args.At(0)).AsInt();
+    const PrimInt Var_arg1 = (params_args.GetArg(0)).AsInt();
     if (Var_arg1 < 0 || Var_arg1 >= AsCharBuffer().size()) {
       FAIL() << "Read position " << Var_arg1 << " is out of bounds";
     }
     return ReturnTuple(Box_Char(AsCharBuffer()[Var_arg1]));
   }
 
-  ReturnTuple Call_resize(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_resize(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("CharBuffer.resize")
-    const PrimInt Var_arg1 = (args.At(0)).AsInt();
+    const PrimInt Var_arg1 = (params_args.GetArg(0)).AsInt();
     if (Var_arg1 < 0) {
       FAIL() << "Buffer size " << Var_arg1 << " is invalid";
     } else {
@@ -71,15 +71,15 @@
     return ReturnTuple(VAR_SELF);
   }
 
-  ReturnTuple Call_size(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_size(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("CharBuffer.size")
     return ReturnTuple(Box_Int(value_.size()));
   }
 
-  ReturnTuple Call_writeAt(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_writeAt(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("CharBuffer.writeAt")
-    const PrimInt Var_arg1 = (args.At(0)).AsInt();
-    const PrimChar Var_arg2 = (args.At(1)).AsChar();
+    const PrimInt Var_arg1 = (params_args.GetArg(0)).AsInt();
+    const PrimChar Var_arg2 = (params_args.GetArg(1)).AsChar();
     if (Var_arg1 < 0 || Var_arg1 >= value_.size()) {
       FAIL() << "Write position " << Var_arg1 << " is out of bounds";
     } else {
diff --git a/base/src/Extension_Float.cpp b/base/src/Extension_Float.cpp
--- a/base/src/Extension_Float.cpp
+++ b/base/src/Extension_Float.cpp
@@ -42,28 +42,28 @@
 struct ExtType_Float : public Type_Float {
   inline ExtType_Float(Category_Float& p, Params<0>::Type params) : Type_Float(p, params) {}
 
-  ReturnTuple Call_default(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_default(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Float.default")
     return ReturnTuple(Box_Float(0.0));
   }
 
-  ReturnTuple Call_equals(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_equals(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Float.equals")
-    const PrimFloat Var_arg1 = (args.At(0)).AsFloat();
-    const PrimFloat Var_arg2 = (args.At(1)).AsFloat();
+    const PrimFloat Var_arg1 = (params_args.GetArg(0)).AsFloat();
+    const PrimFloat Var_arg2 = (params_args.GetArg(1)).AsFloat();
     return ReturnTuple(Box_Bool(Var_arg1==Var_arg2));
   }
 
-  ReturnTuple Call_lessThan(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_lessThan(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Float.lessThan")
-    const PrimFloat Var_arg1 = (args.At(0)).AsFloat();
-    const PrimFloat Var_arg2 = (args.At(1)).AsFloat();
+    const PrimFloat Var_arg1 = (params_args.GetArg(0)).AsFloat();
+    const PrimFloat Var_arg2 = (params_args.GetArg(1)).AsFloat();
     return ReturnTuple(Box_Bool(Var_arg1<Var_arg2));
   }
 };
 
 ReturnTuple DispatchFloat(PrimFloat value, const ValueFunction& label,
-                          const ParamTuple& params, const ValueTuple& args) {
+                          const ParamsArgs& params_args) {
   switch (label.collection) {
     case CategoryId_AsBool:
       return ReturnTuple(Box_Bool(value != 0.0));
diff --git a/base/src/Extension_Int.cpp b/base/src/Extension_Int.cpp
--- a/base/src/Extension_Int.cpp
+++ b/base/src/Extension_Int.cpp
@@ -45,38 +45,38 @@
 struct ExtType_Int : public Type_Int {
   inline ExtType_Int(Category_Int& p, Params<0>::Type params) : Type_Int(p, params) {}
 
-  ReturnTuple Call_maxBound(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_maxBound(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Int.maxBound")
     return ReturnTuple(Box_Int(9223372036854775807LL));
   }
 
-  ReturnTuple Call_minBound(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_minBound(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Int.minBound")
     return ReturnTuple(Box_Int(-9223372036854775807LL - 1LL));
   }
 
-  ReturnTuple Call_default(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_default(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Int.default")
     return ReturnTuple(Box_Int(0));
   }
 
-  ReturnTuple Call_equals(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_equals(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Int.equals")
-    const PrimInt Var_arg1 = (args.At(0)).AsInt();
-    const PrimInt Var_arg2 = (args.At(1)).AsInt();
+    const PrimInt Var_arg1 = (params_args.GetArg(0)).AsInt();
+    const PrimInt Var_arg2 = (params_args.GetArg(1)).AsInt();
     return ReturnTuple(Box_Bool(Var_arg1==Var_arg2));
   }
 
-  ReturnTuple Call_lessThan(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_lessThan(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Int.lessThan")
-    const PrimInt Var_arg1 = (args.At(0)).AsInt();
-    const PrimInt Var_arg2 = (args.At(1)).AsInt();
+    const PrimInt Var_arg1 = (params_args.GetArg(0)).AsInt();
+    const PrimInt Var_arg2 = (params_args.GetArg(1)).AsInt();
     return ReturnTuple(Box_Bool(Var_arg1<Var_arg2));
   }
 };
 
 ReturnTuple DispatchInt(PrimInt value, const ValueFunction& label,
-                        const ParamTuple& params, const ValueTuple& args) {
+                        const ParamsArgs& params_args) {
   switch (label.collection) {
     case CategoryId_AsBool:
       return ReturnTuple(Box_Bool(value != 0));
diff --git a/base/src/Extension_String.cpp b/base/src/Extension_String.cpp
--- a/base/src/Extension_String.cpp
+++ b/base/src/Extension_String.cpp
@@ -48,17 +48,11 @@
   std::string CategoryName() const final { return "StringBuilder"; }
 
   ReturnTuple Dispatch(const ValueFunction& label,
-                       const ParamTuple& params, const ValueTuple& args) final {
-    if (args.Size() != label.arg_count) {
-      FAIL() << "Wrong number of args";
-    }
-    if (params.Size() != label.param_count){
-      FAIL() << "Wrong number of params";
-    }
+                       const ParamsArgs& params_args) final {
     if (&label == &Function_Append_append) {
       TRACE_FUNCTION("StringBuilder.append")
       std::lock_guard<std::mutex> lock(mutex);
-      output_ << TypeValue::Call(args.At(0), Function_Formatted_formatted, ParamTuple(), ArgTuple()).Only().AsString();
+      output_ << TypeValue::Call(params_args.GetArg(0), Function_Formatted_formatted, PassParamsArgs()).At(0).AsString();
       return ReturnTuple(VAR_SELF);
     }
     if (&label == &Function_Build_build) {
@@ -66,7 +60,7 @@
       std::lock_guard<std::mutex> lock(mutex);
       return ReturnTuple(Box_String(output_.str()));
     }
-    return TypeValue::Dispatch(label, params, args);
+    return TypeValue::Dispatch(label, params_args);
   }
 
  private:
@@ -77,33 +71,33 @@
 struct ExtType_String : public Type_String {
   inline ExtType_String(Category_String& p, Params<0>::Type params) : Type_String(p, params) {}
 
-  ReturnTuple Call_builder(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_builder(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("String.builder")
     return ReturnTuple(BoxedValue::New<Value_StringBuilder>());
   }
 
-  ReturnTuple Call_default(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_default(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("String.default")
     return ReturnTuple(Box_String(""));
   }
 
-  ReturnTuple Call_equals(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_equals(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("String.equals")
-    const BoxedValue& Var_arg1 = (args.At(0));
-    const BoxedValue& Var_arg2 = (args.At(1));
+    const BoxedValue& Var_arg1 = (params_args.GetArg(0));
+    const BoxedValue& Var_arg2 = (params_args.GetArg(1));
     return ReturnTuple(Box_Bool(Var_arg1.AsString()==Var_arg2.AsString()));
   }
 
-  ReturnTuple Call_fromCharBuffer(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_fromCharBuffer(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("String.fromCharBuffer")
-    const BoxedValue& Var_arg1 = (args.At(0));
+    const BoxedValue& Var_arg1 = (params_args.GetArg(0));
     return ReturnTuple(Box_String(PrimString(Var_arg1.AsCharBuffer())));
   }
 
-  ReturnTuple Call_lessThan(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_lessThan(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("String.lessThan")
-    const BoxedValue& Var_arg1 = (args.At(0));
-    const BoxedValue& Var_arg2 = (args.At(1));
+    const BoxedValue& Var_arg1 = (params_args.GetArg(0));
+    const BoxedValue& Var_arg2 = (params_args.GetArg(1));
     return ReturnTuple(Box_Bool(Var_arg1.AsString()<Var_arg2.AsString()));
   }
 };
@@ -115,9 +109,7 @@
 
   std::string CategoryName() const final { return "StringOrder"; }
 
-  ReturnTuple Dispatch(const ValueFunction& label,
-                       const ParamTuple& params,
-                       const ValueTuple& args) final {
+  ReturnTuple Dispatch(const ValueFunction& label, const ParamsArgs& params_args) final {
     if (&label == &Function_Order_next) {
       TRACE_FUNCTION("StringOrder.next")
       if (index_+1 >= value_.size()) {
@@ -134,7 +126,7 @@
       }
       return ReturnTuple(Box_Char(value_[index_]));
     }
-    return TypeValue::Dispatch(label, params, args);
+    return TypeValue::Dispatch(label, params_args);
   }
 
  private:
@@ -147,12 +139,12 @@
   inline ExtValue_String(S<const Type_String> p, const PrimString& value)
     : Value_String(std::move(p)), value_(value) {}
 
-  ReturnTuple Call_asBool(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_asBool(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("String.asBool")
     return ReturnTuple(Box_Bool(value_.size() != 0));
   }
 
-  ReturnTuple Call_defaultOrder(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_defaultOrder(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("String.defaultOrder")
     if (value_.empty()) {
       return ReturnTuple(Var_empty);
@@ -161,29 +153,29 @@
     }
   }
 
-  ReturnTuple Call_formatted(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_formatted(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("String.formatted")
     return ReturnTuple(VAR_SELF);
   }
 
-  ReturnTuple Call_readAt(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_readAt(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("String.readAt")
-    const PrimInt Var_arg1 = (args.At(0)).AsInt();
+    const PrimInt Var_arg1 = (params_args.GetArg(0)).AsInt();
     if (Var_arg1 < 0 || Var_arg1 >= value_.size()) {
       FAIL() << "Read position " << Var_arg1 << " is out of bounds";
     }
     return ReturnTuple(Box_Char(value_[Var_arg1]));
   }
 
-  ReturnTuple Call_size(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_size(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("String.size")
     return ReturnTuple(Box_Int(value_.size()));
   }
 
-  ReturnTuple Call_subSequence(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_subSequence(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("String.subSequence")
-    const PrimInt Var_arg1 = (args.At(0)).AsInt();
-    const PrimInt Var_arg2 = (args.At(1)).AsInt();
+    const PrimInt Var_arg1 = (params_args.GetArg(0)).AsInt();
+    const PrimInt Var_arg2 = (params_args.GetArg(1)).AsInt();
     if (Var_arg1 < 0 || Var_arg1 > value_.size()) {
       FAIL() << "Subsequence position " << Var_arg1 << " is out of bounds";
     }
diff --git a/base/src/boxed.cpp b/base/src/boxed.cpp
--- a/base/src/boxed.cpp
+++ b/base/src/boxed.cpp
@@ -28,16 +28,16 @@
 #endif  // ZEOLITE_PUBLIC_NAMESPACE
 
 ReturnTuple DispatchBool(PrimBool value, const ValueFunction& label,
-                         const ParamTuple& params, const ValueTuple& args);
+                         const ParamsArgs& params_args);
 
 ReturnTuple DispatchChar(PrimChar value, const ValueFunction& label,
-                         const ParamTuple& params, const ValueTuple& args);
+                         const ParamsArgs& params_args);
 
 ReturnTuple DispatchInt(PrimInt value, const ValueFunction& label,
-                        const ParamTuple& params, const ValueTuple& args);
+                        const ParamsArgs& params_args);
 
 ReturnTuple DispatchFloat(PrimFloat value, const ValueFunction& label,
-                          const ParamTuple& params, const ValueTuple& args);
+                          const ParamsArgs& params_args);
 
 #ifdef ZEOLITE_PUBLIC_NAMESPACE
 }  // namespace ZEOLITE_PUBLIC_NAMESPACE
@@ -145,49 +145,50 @@
 }
 
 ReturnTuple BoxedValue::Dispatch(
-  const ValueFunction& label, const ParamTuple& params, const ValueTuple& args) const {
+  const ValueFunction& label, const ParamsArgs& params_args) const {
   switch (union_.type_) {
     case UnionValue::Type::EMPTY:
       FAIL() << "Function called on empty value";
       __builtin_unreachable();
       break;
     case UnionValue::Type::BOOL:
-      return DispatchBool(union_.value_.as_bool_, label, params, args);
+      return DispatchBool(union_.value_.as_bool_, label, params_args);
     case UnionValue::Type::CHAR:
-      return DispatchChar(union_.value_.as_char_, label, params, args);
+      return DispatchChar(union_.value_.as_char_, label, params_args);
     case UnionValue::Type::INT:
-      return DispatchInt(union_.value_.as_int_, label, params, args);
+      return DispatchInt(union_.value_.as_int_, label, params_args);
     case UnionValue::Type::FLOAT:
-      return DispatchFloat(union_.value_.as_float_, label, params, args);
+      return DispatchFloat(union_.value_.as_float_, label, params_args);
     case UnionValue::Type::BOXED:
       if (!union_.value_.as_pointer_ || !union_.value_.as_pointer_->object_) {
         FAIL() << "Function called on null pointer";
       }
-      return union_.value_.as_pointer_->object_->Dispatch(label, params, args);
+      return union_.value_.as_pointer_->object_->Dispatch(label, params_args);
   }
 }
 
 void BoxedValue::Cleanup() {
-  switch (union_.type_) {
-    case UnionValue::Type::BOXED:
-      while (union_.value_.as_pointer_->lock_.test_and_set(std::memory_order_acquire));
-      if (--union_.value_.as_pointer_->strong_ == 0) {
-        TypeValue* const object = union_.value_.as_pointer_->object_;
-        union_.value_.as_pointer_->object_ = nullptr;
-        union_.value_.as_pointer_->lock_.clear(std::memory_order_release);
-        object->~TypeValue();
-        if (--union_.value_.as_pointer_->weak_ == 0) {
-          // NOTE: as_bytes_ contains object => ~TypeValue() must happen first.
-          free(union_.value_.as_bytes_);
-        }
-      } else {
-        union_.value_.as_pointer_->lock_.clear(std::memory_order_release);
+  while (union_.type_ == UnionValue::Type::BOXED) {
+    while (union_.value_.as_pointer_->lock_.test_and_set(std::memory_order_acquire));
+    if (--union_.value_.as_pointer_->strong_ == 0) {
+      TypeValue* const object = union_.value_.as_pointer_->object_;
+      union_.value_.as_pointer_->object_ = nullptr;
+      union_.value_.as_pointer_->lock_.clear(std::memory_order_release);
+      BoxedValue next = object->FlatCleanup();
+      object->~TypeValue();
+      if (--union_.value_.as_pointer_->weak_ == 0) {
+        // NOTE: as_bytes_ contains object => ~TypeValue() must happen first.
+        free(union_.value_.as_bytes_);
       }
-      break;
-    default:
+      union_ = next.union_;
+      // This is only to skip cleanup of next.
+      next.union_.type_ = UnionValue::Type::EMPTY;
+    } else {
+      union_.value_.as_pointer_->lock_.clear(std::memory_order_release);
       break;
+    }
   }
-  union_.type_  = UnionValue::Type::EMPTY;
+  union_.type_ = UnionValue::Type::EMPTY;
   union_.value_.as_pointer_ = nullptr;
 }
 
diff --git a/base/src/category-source.cpp b/base/src/category-source.cpp
--- a/base/src/category-source.cpp
+++ b/base/src/category-source.cpp
@@ -182,20 +182,17 @@
 const BoxedValue Var_empty;
 
 
-ReturnTuple TypeCategory::Dispatch(const CategoryFunction& label,
-                                   const ParamTuple& params, const ValueTuple& args) {
+ReturnTuple TypeCategory::Dispatch(const CategoryFunction& label, const ParamsArgs& params_args) {
   FAIL() << CategoryName() << " does not implement " << label;
   __builtin_unreachable();
 }
 
-ReturnTuple TypeInstance::Dispatch(const TypeFunction& label,
-                                   const ParamTuple& params, const ValueTuple& args) const {
+ReturnTuple TypeInstance::Dispatch(const TypeFunction& label, const ParamsArgs& params_args) const {
   FAIL() << CategoryName() << " does not implement " << label;
   __builtin_unreachable();
 }
 
-ReturnTuple TypeValue::Dispatch(const ValueFunction& label,
-                                const ParamTuple& params, const ValueTuple& args) {
+ReturnTuple TypeValue::Dispatch(const ValueFunction& label, const ParamsArgs& params_args) {
   FAIL() << CategoryName() << " does not implement " << label;
   __builtin_unreachable();
 }
diff --git a/base/src/returns.cpp b/base/src/returns.cpp
new file mode 100644
--- /dev/null
+++ b/base/src/returns.cpp
@@ -0,0 +1,93 @@
+/* -----------------------------------------------------------------------------
+Copyright 2019,2021 Kevin P. Barry
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+----------------------------------------------------------------------------- */
+
+// Author: Kevin P. Barry [ta0kira@gmail.com]
+
+#include "returns.hpp"
+
+#include "logging.hpp"
+
+
+void ReturnTuple::TransposeFrom(ReturnTuple&& other) {
+  if (Size() != other.Size()) {
+    FAIL() << "ReturnTuple size mismatch in assignment: " << Size()
+           << " (expected) " << other.Size() << " (actual)";
+  }
+  first_ = std::move(other.first_);
+  for (int i = 0; i < data_.Size(); ++i) {
+    data_[i] = std::move(other.data_[i]);
+  }
+}
+
+int ReturnTuple::Size() const {
+  return data_.Size()+1;
+}
+
+BoxedValue& ReturnTuple::At(int pos) {
+  if (pos == 0) {
+    return first_;
+  }
+  if (pos < 0 || pos >= Size()) {
+    FAIL() << "Bad return index";
+  }
+  return data_[pos-1];
+}
+
+const BoxedValue& ReturnTuple::At(int pos) const {
+  if (pos == 0) {
+    return first_;
+  }
+  if (pos < 0 || pos >= Size()) {
+    FAIL() << "Bad return index";
+  }
+  return data_[pos-1];
+}
+
+
+namespace zeolite_internal {
+
+thread_local PoolCache<BoxedValue> PoolManager<BoxedValue>::cache3_(256);
+
+// static
+typename PoolManager<BoxedValue>::PoolEntry* PoolManager<BoxedValue>::Take(int orig_size) {
+  int size = orig_size;
+  if (size < 1) return nullptr;
+  if (size < 3) {
+    size = 3;
+  }
+  PoolEntry* storage = nullptr;
+  if (size == 3 && (storage = cache3_.Take())) {
+  } else {
+    storage = new (new unsigned char[sizeof(PoolEntry) + size*sizeof(Managed)]) PoolEntry(size, nullptr);
+  }
+  new (storage->data()) Managed[orig_size];
+  return storage;
+}
+
+// static
+void PoolManager<BoxedValue>::Return(PoolEntry* storage, int orig_size) {
+  if (!storage) return;
+  for (int i = 0; i < orig_size; ++i) {
+    storage->data()[i].~Managed();
+  }
+  if (storage->size == 3 && cache3_.Return(storage)) {
+    return;
+  }
+  storage->~PoolEntry();
+  delete[] (unsigned char*) storage;
+}
+
+}  // namespace zeolite_internal
diff --git a/base/src/types.cpp b/base/src/types.cpp
deleted file mode 100644
--- a/base/src/types.cpp
+++ /dev/null
@@ -1,233 +0,0 @@
-/* -----------------------------------------------------------------------------
-Copyright 2019,2021 Kevin P. Barry
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
------------------------------------------------------------------------------ */
-
-// Author: Kevin P. Barry [ta0kira@gmail.com]
-
-#include "types.hpp"
-
-#include <atomic>
-
-#include "boxed.hpp"
-#include "logging.hpp"
-
-
-int ArgTuple::Size() const {
-  return data_.Size();
-}
-
-const BoxedValue& ArgTuple::At(int pos) const {
-  if (pos < 0 || pos >= data_.Size()) {
-    FAIL() << "Bad ArgTuple index";
-  }
-  return *data_[pos];
-}
-
-const BoxedValue& ArgTuple::Only() const {
-  if (data_.Size() != 1) {
-    FAIL() << "Bad ArgTuple index";
-  }
-  return *data_[0];
-}
-
-void ReturnTuple::TransposeFrom(ReturnTuple&& other) {
-  if (Size() != other.Size()) {
-    FAIL() << "ReturnTuple size mismatch in assignment: " << Size()
-           << " (expected) " << other.Size() << " (actual)";
-  }
-  for (int i = 0; i < Size(); ++i) {
-    At(i) = std::move(other.At(i));
-  }
-}
-
-int ReturnTuple::Size() const {
-  return data_.Size();
-}
-
-BoxedValue& ReturnTuple::At(int pos) {
-  if (pos < 0 || pos >= data_.Size()) {
-    FAIL() << "Bad ReturnTuple index";
-  }
-  return data_[pos];
-}
-
-const BoxedValue& ReturnTuple::At(int pos) const {
-  if (pos < 0 || pos >= data_.Size()) {
-    FAIL() << "Bad ReturnTuple index";
-  }
-  return data_[pos];
-}
-
-const BoxedValue& ReturnTuple::Only() const {
-  if (data_.Size() != 1) {
-    FAIL() << "Bad ReturnTuple index";
-  }
-  return data_[0];
-}
-
-int ParamTuple::Size() const {
-  return data_.Size();
-}
-
-const S<const TypeInstance>& ParamTuple::At(int pos) const {
-  if (pos < 0 || pos >= data_.Size()) {
-    FAIL() << "Bad ParamTuple index";
-  }
-  return data_[pos];
-}
-
-
-namespace {
-
-template<class P>
-static inline P* PoolTakeCommon(std::atomic_flag& flag,
-                                P*& pool, unsigned int& size) {
-  while (flag.test_and_set(std::memory_order_acquire));
-  P* const storage = pool;
-  if (storage == nullptr) {
-    flag.clear(std::memory_order_release);
-    return nullptr;
-  } else {
-    --size;
-    pool = storage->next;
-    flag.clear(std::memory_order_release);
-    storage->next = nullptr;
-    return storage;
-  }
-}
-
-template<class P>
-static inline bool PoolReturnCommon(P* storage, std::atomic_flag& flag,
-                                    P*& pool, unsigned int& size,
-                                    unsigned int limit) {
-  while (flag.test_and_set(std::memory_order_acquire));
-  P* const head = pool;
-  if (size < limit) {
-    ++size;
-    storage->next = head;
-    pool = storage;
-    flag.clear(std::memory_order_release);
-    return true;
-  } else {
-    flag.clear(std::memory_order_release);
-    return false;
-  }
-}
-
-}  // namespace
-
-
-namespace zeolite_internal {
-
-unsigned int PoolManager<BoxedValue>::pool4_size_ = 0;
-typename PoolManager<BoxedValue>::PoolEntry* PoolManager<BoxedValue>::pool4_{nullptr};
-std::atomic_flag PoolManager<BoxedValue>::pool4_flag_ = ATOMIC_FLAG_INIT;
-
-// static
-typename PoolManager<BoxedValue>::PoolEntry* PoolManager<BoxedValue>::Take(int orig_size) {
-  int size = orig_size;
-  if (size == 0) return nullptr;
-  if (size < 4) {
-    size = 4;
-  }
-  PoolEntry* storage = nullptr;
-  if (size == 4 && (storage = PoolTakeCommon(pool4_flag_, pool4_, pool4_size_))) {
-  } else {
-    storage = new (new unsigned char[sizeof(PoolEntry) + size*sizeof(Managed)]) PoolEntry(size, nullptr);
-  }
-  new (storage->data()) Managed[orig_size];
-  return storage;
-}
-
-// static
-void PoolManager<BoxedValue>::Return(PoolEntry* storage, int orig_size) {
-  if (!storage) return;
-  for (int i = 0; i < orig_size; ++i) {
-    storage->data()[i].~Managed();
-  }
-  if (storage->size == 4 && PoolReturnCommon(storage, pool4_flag_, pool4_, pool4_size_, pool_limit_)) {
-    return;
-  }
-  storage->~PoolEntry();
-  delete[] (unsigned char*) storage;
-}
-
-
-unsigned int PoolManager<const BoxedValue*>::pool4_size_ = 0;
-typename PoolManager<const BoxedValue*>::PoolEntry* PoolManager<const BoxedValue*>::pool4_{nullptr};
-std::atomic_flag PoolManager<const BoxedValue*>::pool4_flag_ = ATOMIC_FLAG_INIT;
-
-// static
-typename PoolManager<const BoxedValue*>::PoolEntry* PoolManager<const BoxedValue*>::Take(int orig_size) {
-  int size = orig_size;
-  if (size == 0) return nullptr;
-  if (size < 4) {
-    size = 4;
-  }
-  PoolEntry* storage = nullptr;
-  if (size == 4 && (storage = PoolTakeCommon(pool4_flag_, pool4_, pool4_size_))) {
-  } else {
-    storage = new (new unsigned char[sizeof(PoolEntry) + size*sizeof(Managed)]) PoolEntry(size, nullptr);
-  }
-  // NOTE: Skipping constructor calls since pointers are trivial.
-  return storage;
-}
-
-// static
-void PoolManager<const BoxedValue*>::Return(PoolEntry* storage, int orig_size) {
-  if (!storage) return;
-  // NOTE: Skipping destructor calls since pointers are trivial.
-  if (storage->size == 4 && PoolReturnCommon(storage, pool4_flag_, pool4_, pool4_size_, pool_limit_)) {
-    return;
-  }
-  storage->~PoolEntry();
-  delete[] (unsigned char*) storage;
-}
-
-
-unsigned int PoolManager<S<const TypeInstance>>::pool4_size_ = 0;
-typename PoolManager<S<const TypeInstance>>::PoolEntry* PoolManager<S<const TypeInstance>>::pool4_{nullptr};
-std::atomic_flag PoolManager<S<const TypeInstance>>::pool4_flag_ = ATOMIC_FLAG_INIT;
-
-// static
-typename PoolManager<S<const TypeInstance>>::PoolEntry* PoolManager<S<const TypeInstance>>::Take(int orig_size) {
-  int size = orig_size;
-  if (size == 0) return nullptr;
-  if (size < 4) {
-    size = 4;
-  }
-  PoolEntry* storage = nullptr;
-  if (size == 4 && (storage = PoolTakeCommon(pool4_flag_, pool4_, pool4_size_))) {
-  } else {
-    storage = new (new unsigned char[sizeof(PoolEntry) + size*sizeof(Managed)]) PoolEntry(size, nullptr);
-  }
-  new (storage->data()) Managed[orig_size];
-  return storage;
-}
-
-// static
-void PoolManager<S<const TypeInstance>>::Return(PoolEntry* storage, int orig_size) {
-  if (!storage) return;
-  for (int i = 0; i < orig_size; ++i) {
-    storage->data()[i].~Managed();
-  }
-  if (storage->size == 4 && PoolReturnCommon(storage, pool4_flag_, pool4_, pool4_size_, pool_limit_)) {
-    return;
-  }
-  storage->~PoolEntry();
-  delete[] (unsigned char*) storage;
-}
-
-}  // namespace zeolite_internal
diff --git a/base/types.hpp b/base/types.hpp
--- a/base/types.hpp
+++ b/base/types.hpp
@@ -19,7 +19,6 @@
 #ifndef TYPES_HPP_
 #define TYPES_HPP_
 
-#include <atomic>
 #include <cstdint>
 #include <functional>
 #include <memory>
@@ -168,148 +167,5 @@
 typename ParamsKey<N>::Type GetKeyFromParams(const typename Params<N>::Type& from) {
   return KeyFromParams<N, 0>::Get(from);
 }
-
-class ValueTuple {
- public:
-  virtual int Size() const = 0;
-  virtual const BoxedValue& At(int pos) const = 0;
-  virtual const BoxedValue& Only() const = 0;
-
- protected:
-  ValueTuple() = default;
-  virtual ~ValueTuple() = default;
-
- private:
-  void* operator new(std::size_t size) = delete;
-};
-
-
-namespace zeolite_internal {
-
-template<>
-class PoolManager<BoxedValue> {
-  using PoolEntry = PoolStorage<BoxedValue>;
-  using Managed = PoolEntry::Managed;
-
-  template<class> friend class PoolArray;
-
-  static PoolEntry* Take(int size);
-  static void Return(PoolEntry* storage, int size);
-
-  static constexpr unsigned int pool_limit_ = 256;
-  static unsigned int pool4_size_;
-  static PoolEntry* pool4_;
-  static std::atomic_flag pool4_flag_;
-};
-
-template<>
-class PoolManager<const BoxedValue*> {
-  using PoolEntry = PoolStorage<const BoxedValue*>;
-  using Managed = PoolEntry::Managed;
-
-  template<class> friend class PoolArray;
-
-  static PoolEntry* Take(int size);
-  static void Return(PoolEntry* storage, int size);
-
-  static constexpr unsigned int pool_limit_ = 256;
-  static unsigned int pool4_size_;
-  static PoolEntry* pool4_;
-  static std::atomic_flag pool4_flag_;
-};
-
-template<>
-class PoolManager<S<const TypeInstance>> {
-  using PoolEntry = PoolStorage<S<const TypeInstance>>;
-  using Managed = PoolEntry::Managed;
-
-  template<class> friend class PoolArray;
-
-  static PoolEntry* Take(int size);
-  static void Return(PoolEntry* storage, int size);
-
-  static constexpr unsigned int pool_limit_ = 256;
-  static unsigned int pool4_size_;
-  static PoolEntry* pool4_;
-  static std::atomic_flag pool4_flag_;
-};
-
-}  // namespace zeolite_internal
-
-
-class ReturnTuple : public ValueTuple {
- public:
-  constexpr ReturnTuple() : data_() {}
-
-  ReturnTuple(int size) : data_(size) {}
-
-  template<class...Ts>
-  explicit ReturnTuple(Ts... returns) : data_(sizeof...(Ts)) {
-    data_.Init(std::move(returns)...);
-  }
-
-  ReturnTuple(ReturnTuple&&) = default;
-
-  void TransposeFrom(ReturnTuple&& other);
-
-  int Size() const final;
-  BoxedValue& At(int pos);
-  const BoxedValue& At(int pos) const final;
-  const BoxedValue& Only() const final;
-
- private:
-  ReturnTuple(const ReturnTuple&) = delete;
-  ReturnTuple& operator =(ReturnTuple&&) = delete;
-  ReturnTuple& operator =(const ReturnTuple&) = delete;
-  void* operator new(std::size_t size) = delete;
-
-  zeolite_internal::PoolArray<BoxedValue> data_;
-};
-
-class ArgTuple : public ValueTuple {
- public:
-  constexpr ArgTuple() : data_() {}
-
-  template<class...Ts>
-  explicit ArgTuple(const Ts&... args) : data_(sizeof...(Ts)) {
-    data_.Init(&args...);
-  }
-
-  int Size() const final;
-  const BoxedValue& At(int pos) const final;
-  const BoxedValue& Only() const final;
-
- private:
-  ArgTuple(const ArgTuple&) = delete;
-  ArgTuple(ArgTuple&&) = delete;
-  ArgTuple& operator = (const ArgTuple&) = delete;
-  ArgTuple& operator = (ArgTuple&&) =  delete;
-  void* operator new(std::size_t size) = delete;
-
-  zeolite_internal::PoolArray<const BoxedValue*> data_;
-};
-
-class ParamTuple {
- public:
-  constexpr ParamTuple() : data_() {}
-
-  template<class...Ts>
-  explicit ParamTuple(const Ts&... params) : data_(sizeof...(Ts)) {
-    data_.Init(std::move(params)...);
-  }
-
-  ParamTuple(ParamTuple&& other) = default;
-
-  int Size() const;
-  const S<const TypeInstance>& At(int pos) const;
-
- private:
-  ParamTuple(const ParamTuple&) = delete;
-  ParamTuple& operator = (const ParamTuple&) = delete;
-  ParamTuple& operator = (ParamTuple&&) = delete;
-  void* operator new(std::size_t size) = delete;
-
-  zeolite_internal::PoolArray<S<const TypeInstance>> data_;
-};
 
 #endif  // TYPES_HPP_
diff --git a/bin/unit-tests.hs b/bin/unit-tests.hs
--- a/bin/unit-tests.hs
+++ b/bin/unit-tests.hs
@@ -1,5 +1,5 @@
 {- -----------------------------------------------------------------------------
-Copyright 2019-2020 Kevin P. Barry
+Copyright 2019-2021 Kevin P. Barry
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@
 import qualified Test.DefinedCategory as TestDefinedCategory
 import qualified Test.IntegrationTest as TestIntegrationTest
 import qualified Test.MergeTree       as TestMergeTree
+import qualified Test.ParseConfig     as TestParseConfig
 import qualified Test.ParseMetadata   as TestParseMetadata
 import qualified Test.Parser          as TestParser
 import qualified Test.Pragma          as TestPragma
@@ -38,6 +39,7 @@
     labelWith "DefinedCategory" TestDefinedCategory.tests,
     labelWith "IntegrationTest" TestIntegrationTest.tests,
     labelWith "MergeTree"       TestMergeTree.tests,
+    labelWith "ParseConfig"     TestParseConfig.tests,
     labelWith "ParseMetadata"   TestParseMetadata.tests,
     labelWith "Parser"          TestParser.tests,
     labelWith "Pragma"          TestPragma.tests,
diff --git a/bin/zeolite-setup.hs b/bin/zeolite-setup.hs
--- a/bin/zeolite-setup.hs
+++ b/bin/zeolite-setup.hs
@@ -1,5 +1,5 @@
 {- -----------------------------------------------------------------------------
-Copyright 2020 Kevin P. Barry
+Copyright 2020-2021 Kevin P. Barry
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
 -- Author: Kevin P. Barry [ta0kira@gmail.com]
 
 import Control.Monad (when)
+import Control.Monad.Trans
 import System.Directory
 import System.Environment
 import System.Exit
@@ -28,20 +29,26 @@
 import Cli.RunCompiler
 import Config.LoadConfig
 import Config.LocalConfig
+import Config.ParseConfig ()
 
 
 main :: IO ()
-main = do
-  (cxxSpec:arSpec:_) <- fmap ((++ repeat Nothing) . map Just) getArgs
-  f <- localConfigPath
-  isFile <- doesFileExist f
-  when isFile $ do
-    hPutStrLn stderr $ "*** WARNING: Local config " ++ f ++ " will be overwritten. ***"
-  config <- createConfig cxxSpec arSpec
-  hPutStrLn stderr $ "Writing local config to " ++ f ++ "."
-  writeFile f (show config ++ "\n")
-  initLibraries config
-  hPutStrLn stderr "Setup is now complete!"
+main = tryTrackedErrorsIO "" "Zeolite setup failed:" (lift getArgs >>= handle) where
+  handle ("--reuse":_) = do
+    config <- loadConfig
+    runWith config
+  handle args = do
+    let (cxxSpec:arSpec:_) = (map Just $ args) ++ repeat Nothing
+    f <- lift $ localConfigPath
+    isFile <- lift $ doesFileExist f
+    when isFile $
+      lift $ hPutStrLn stderr $ "*** WARNING: Local config " ++ f ++ " will be overwritten. ***"
+    config <- lift $ createConfig cxxSpec arSpec
+    saveConfig config
+    runWith config
+  runWith config = do
+    initLibraries config
+    lift $ hPutStrLn stderr "Setup is now complete!"
 
 clangBinary :: String
 clangBinary = "clang++"
@@ -85,27 +92,25 @@
   handle [(n,"")] = Left n
   handle _        = Right s
 
-createConfig :: Maybe String -> Maybe String -> IO LocalConfig
+createConfig :: Maybe String -> Maybe String -> IO (Resolver,Backend)
 createConfig cxxSpec arSpec = do
   clang <- findExecutables clangBinary
   gcc   <- findExecutables gccBinary
   ar    <- findExecutables arBinary
   compiler <- promptChoice "Which clang-compatible C++ compiler should be used?" cxxSpec (clang ++ gcc)
   archiver <- promptChoice "Which ar-compatible archiver should be used?"        arSpec  ar
-  let config = LocalConfig {
-      lcBackend = UnixBackend {
-        ucCxxBinary    = compiler,
-        ucCompileFlags = compileFlags,
-        ucLibraryFlags = libraryFlags,
-        ucBinaryFlags  = binaryFlags,
-        ucArBinary     = archiver
-      },
-      lcResolver = SimpleResolver {
-        srVisibleSystem = includePaths,
-        srExtraPaths = []
-      }
-    }
-  return config
+  return (
+    SimpleResolver {
+      srVisibleSystem = includePaths,
+      srExtraPaths = []
+    },
+    UnixBackend {
+      ucCxxBinary    = compiler,
+      ucCompileFlags = compileFlags,
+      ucLibraryFlags = libraryFlags,
+      ucBinaryFlags  = binaryFlags,
+      ucArBinary     = archiver
+    })
 
 promptChoice :: String -> Maybe String -> [String] -> IO String
 promptChoice _ (Just spec) cs = handle $ intOrString spec where
@@ -147,9 +152,9 @@
     exitFailure
   hGetLine stdin
 
-initLibraries :: LocalConfig -> IO ()
-initLibraries (LocalConfig backend resolver) = do
-  path <- rootPath >>= canonicalizePath
+initLibraries :: (Resolver,Backend) -> TrackedErrorsIO ()
+initLibraries (resolver,backend) = do
+  path <- lift $ rootPath >>= canonicalizePath
   let options = CompileOptions {
       coHelp = HelpNotNeeded,
       coPublicDeps = [],
@@ -161,7 +166,6 @@
       coMode = CompileRecompileRecursive,
       coForce = ForceAll
     }
-  tryTrackedErrorsIO "Warnings:" "Zeolite setup failed:" $ do
-    runCompiler resolver backend options
-    mapM_ optionalWarning optionalLibraries where
-    optionalWarning library = compilerWarningM $ "Optional library " ++ library ++ " must be built manually if needed"
+  runCompiler resolver backend options
+  mapM_ optionalWarning optionalLibraries where
+  optionalWarning library = compilerWarningM $ "Optional library " ++ library ++ " must be built manually if needed"
diff --git a/bin/zeolite.hs b/bin/zeolite.hs
--- a/bin/zeolite.hs
+++ b/bin/zeolite.hs
@@ -1,5 +1,5 @@
 {- -----------------------------------------------------------------------------
-Copyright 2020 Kevin P. Barry
+Copyright 2020-2021 Kevin P. Barry
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -18,10 +18,11 @@
 
 import Control.Monad (when)
 import Control.Monad.Trans
-import GHC.IO.Handle
+import System.FilePath
 import System.Directory
 import System.Environment
 import System.Exit
+import GHC.IO.Handle
 import System.IO
 import qualified Data.Map as Map
 import qualified Data.Set as Set
@@ -85,57 +86,74 @@
   if null os
      then exitSuccess
      else exitFailure
-tryFastModes ("--show-traces":ps) = do
-  tryZeoliteIO $ do
-    (_,backend) <- loadConfig
-    let h = getCompilerHash backend
-    mapM_ (showTraces h) ps
-  exitSuccess where
-    showTraces h p = do
-      p' <- errorFromIO $ canonicalizePath p
-      -- Not needed to show traces, but causes the error to be about the module
-      -- rather than the filename if the module hasn't been compiled.
-      _ <- loadModuleMetadata h ForceAll Map.empty p'
-      ts <- readPossibleTraces p
-      mapM_ (errorFromIO . hPutStrLn stdout) $ Set.toList ts
-tryFastModes ("--missed-lines":l:ps) = do
-  tryZeoliteIO $ do
-    (_,backend) <- loadConfig
-    let h = getCompilerHash backend
-    expected <- fmap Set.unions $ mapM (getTraces h) ps
-    actual <- loadTraces
-    mapM_ (errorFromIO . hPutStrLn stdout) $ Set.toList $ expected `Set.difference` actual
-  exitSuccess where
-    loadTraces = do
-      errorFromIO $ hPutStrLn stderr $ "Loading trace data from \"" ++ l ++ "\"."
-      lc <- errorFromIO $ readFile l
-      fmap (Set.fromList . map teContext) $ parseTracesFile (l,lc)
-    getTraces h p = do
-      p' <- errorFromIO $ canonicalizePath p
-      -- Not needed to show traces, but causes the error to be about the module
-      -- rather than the filename if the module hasn't been compiled.
-      _ <- loadModuleMetadata h ForceAll Map.empty p'
-      readPossibleTraces p
-tryFastModes ("--missed-lines":_) = do
-  hPutStrLn stderr $ "Pass a .csv generated by -t --log-traces."
-  exitFailure
-tryFastModes ("--show-deps":ps) = do
-  tryZeoliteIO $ do
-    (_,backend) <- loadConfig
-    let h = getCompilerHash backend
-    mapM_ (showDeps h) ps
-  exitSuccess where
-    showDeps h p = do
-      p' <- errorFromIO $ canonicalizePath p
-      m <- loadModuleMetadata h ForceAll Map.empty p'
-      errorFromIO $ hPutStrLn stdout $ show p'
-      errorFromIO $ mapM_ showDep (cmObjectFiles m)
-    showDep (CategoryObjectFile c ds _) = do
-      mapM_ (\d -> hPutStrLn stdout $ "  " ++ show (ciCategory c) ++
-                                      " -> " ++ show (ciCategory d) ++
-                                      " " ++ show (ciPath d)) ds
-    showDep _ = return ()
-tryFastModes _ = return ()
+tryFastModes os0 = maybePath os0 where
+  maybePath ("-p":root:os) = tryMode root os
+  maybePath os             = tryMode "" os
+  tryMode root ("--clean":ps) = do
+    tryZeoliteIO $ do
+      mapCompilerM_ loadModule ps
+      mapCompilerM_ clean ps
+    exitSuccess where
+      loadModule p = do
+        p' <- errorFromIO $ canonicalizePath (root </> p)
+        -- Not needed for clean, but forces the caller to only pass real modules.
+        loadRecompile p'
+      clean p = do
+        p' <- errorFromIO $ canonicalizePath (root </> p)
+        errorFromIO $ hPutStrLn stderr $ "Clearing cached data for module \"" ++ p' ++ "\"."
+        eraseCachedData p'
+  tryMode root ("--show-traces":ps) = do
+    tryZeoliteIO $ do
+      (_,backend) <- loadConfig
+      h <- getCompilerHash backend
+      mapCompilerM_ (showTraces h) ps
+    exitSuccess where
+      showTraces h p = do
+        p' <- errorFromIO $ canonicalizePath (root </> p)
+        -- Not needed to show traces, but causes the error to be about the
+        -- module rather than the filename if the module hasn't been compiled.
+        _ <- loadModuleMetadata h ForceAll Map.empty p'
+        ts <- readPossibleTraces p'
+        mapM_ (errorFromIO . hPutStrLn stdout) $ Set.toList ts
+  tryMode root ("--missed-lines":l:ps) = do
+    tryZeoliteIO $ do
+      (_,backend) <- loadConfig
+      h <- getCompilerHash backend
+      expected <- fmap Set.unions $ mapCompilerM (getTraces h) ps
+      actual <- loadTraces
+      mapM_ (errorFromIO . hPutStrLn stdout) $ Set.toList $ expected `Set.difference` actual
+    exitSuccess where
+      loadTraces = do
+        l' <- errorFromIO $ canonicalizePath (root </> l)
+        errorFromIO $ hPutStrLn stderr $ "Loading trace data from \"" ++ l' ++ "\"."
+        lc <- errorFromIO $ readFile l'
+        fmap (Set.fromList . map teContext) $ parseTracesFile (l,lc)
+      getTraces h p = do
+        p' <- errorFromIO $ canonicalizePath (root </> p)
+        -- Not needed to check traces, but causes the error to be about the
+        -- module rather than the filename if the module hasn't been compiled.
+        _ <- loadModuleMetadata h ForceAll Map.empty p'
+        readPossibleTraces p'
+  tryMode _ ("--missed-lines":_) = do
+    hPutStrLn stderr $ "Pass a .csv generated by -t --log-traces."
+    exitFailure
+  tryMode root ("--show-deps":ps) = do
+    tryZeoliteIO $ do
+      (_,backend) <- loadConfig
+      h <- getCompilerHash backend
+      mapCompilerM_ (showDeps h) ps
+    exitSuccess where
+      showDeps h p = do
+        p' <- errorFromIO $ canonicalizePath (root </> p)
+        m <- loadModuleMetadata h ForceAll Map.empty p'
+        errorFromIO $ hPutStrLn stdout $ show p'
+        errorFromIO $ mapM_ showDep (cmObjectFiles m)
+      showDep (CategoryObjectFile c ds _) = do
+        mapM_ (\d -> hPutStrLn stdout $ "  " ++ show (ciCategory c) ++
+                                        " -> " ++ show (ciCategory d) ++
+                                        " " ++ show (ciPath d)) ds
+      showDep _ = return ()
+  tryMode _ _ = return ()
 
 tryZeoliteIO :: TrackedErrorsIO a -> IO a
 tryZeoliteIO = tryTrackedErrorsIO "Warnings (ignored):" "Zeolite execution failed:"
diff --git a/example/parser/parse-text.0rx b/example/parser/parse-text.0rx
--- a/example/parser/parse-text.0rx
+++ b/example/parser/parse-text.0rx
@@ -16,12 +16,12 @@
           // Partial match => set error context.
           return context.setBrokenInput(message)
         } else {
-          return context.setValue<?>(ErrorOr:error(message))
+          return context.setValue(ErrorOr:error(message))
         }
       }
       context <- context.advance()
     }
-    return context.setValue<?>(ErrorOr:value<?>(match))
+    return context.setValue(ErrorOr:value(match))
   }
 
   create (match) {
@@ -74,12 +74,12 @@
       context <- context.advance()
     }
     if (count >= min) {
-      return context.setValue<?>(ErrorOr:value<?>(builder.build()))
+      return context.setValue(ErrorOr:value(builder.build()))
     } elif (count > 0) {
       // Partial match => set error context.
       return context.setBrokenInput(message)
     } else {
-      return context.setValue<?>(ErrorOr:error(message))
+      return context.setValue(ErrorOr:error(message))
     }
   }
 
@@ -99,9 +99,9 @@
     }
     if (contextOld.atEof() || contextOld.current() != match) {
       String message <- "Failed to match '" + match.formatted() + "' at " + contextOld.getPosition()
-      return contextOld.setValue<?>(ErrorOr:error(message))
+      return contextOld.setValue(ErrorOr:error(message))
     } else {
-      return contextOld.advance().setValue<?>(ErrorOr:value<?>(match))
+      return contextOld.advance().setValue(ErrorOr:value(match))
     }
   }
 
@@ -122,7 +122,7 @@
   @value #x value
 
   run (contextOld) {
-    return contextOld.setValue<?>(ErrorOr:value<?>(value))
+    return contextOld.setValue(ErrorOr:value(value))
   }
 
   create (value) {
@@ -140,7 +140,7 @@
   @value String message
 
   run (contextOld) {
-    return contextOld.setValue<?>(ErrorOr:error(message))
+    return contextOld.setValue(ErrorOr:error(message))
   }
 
   create (message) {
@@ -163,7 +163,7 @@
     }
     ParseContext<#x> context <- contextOld.run<#x>(parser)
     if (context.hasAnyError()) {
-      return contextOld.setValue<?>(context.getValue().convertError())
+      return contextOld.setValue(context.getValue().convertError())
     } else {
       return context.toState()
     }
@@ -220,11 +220,11 @@
     if (context.hasAnyError()) {
       return context.toState()
     } else {
-      ParseContext<any> context2 <- context.run<?>(parser2)
+      ParseContext<any> context2 <- context.run(parser2)
       if (context2.hasAnyError()) {
         return context2.convertError()
       } else {
-        return context2.setValue<?>(context.getValue())
+        return context2.setValue(context.getValue())
       }
     }
   }
@@ -248,7 +248,7 @@
     if (contextOld.hasAnyError()) {
       return contextOld.convertError()
     }
-    ParseContext<any> context <- contextOld.run<?>(parser1)
+    ParseContext<any> context <- contextOld.run(parser1)
     if (context.hasAnyError()) {
       return context.convertError()
     } else {
diff --git a/example/parser/parser-test.0rt b/example/parser/parser-test.0rt
--- a/example/parser/parser-test.0rt
+++ b/example/parser/parser-test.0rt
@@ -4,10 +4,10 @@
 
 unittest test {
   String raw <- FileTesting.forceReadFile($ExprLookup[MODULE_PATH]$ + "/test-data.txt")
-  ErrorOr<TestData> errorOrData <- TestDataParser.create() `ParseState:consumeAll<?>` raw
+  ErrorOr<TestData> errorOrData <- TestDataParser.create() `ParseState:consumeAll` raw
   TestData data <- errorOrData.getValue()
 
-  \ Testing.checkEquals<?>(data.getName(),"example data")
-  \ Testing.checkEquals<?>(data.getDescription(),"THIS_IS_A_TOKEN")
-  \ Testing.checkEquals<?>(data.getBoolean(),false)
+  \ Testing.checkEquals(data.getName(),"example data")
+  \ Testing.checkEquals(data.getDescription(),"THIS_IS_A_TOKEN")
+  \ Testing.checkFalse(data.getBoolean())
 }
diff --git a/example/parser/parser.0rx b/example/parser/parser.0rx
--- a/example/parser/parser.0rx
+++ b/example/parser/parser.0rx
@@ -94,7 +94,7 @@
 
   @category new (String) -> (ParseState<any>)
   new (data) {
-    return ParseState<any>{ data, 0, 1, 1, ErrorOr:value<?>(Void.void()), empty }
+    return ParseState<any>{ data, 0, 1, 1, ErrorOr:value(Void.void()), empty }
   }
 
   @value getError () -> (Formatted)
diff --git a/example/parser/test-data.0rx b/example/parser/test-data.0rx
--- a/example/parser/test-data.0rx
+++ b/example/parser/test-data.0rx
@@ -44,7 +44,7 @@
 
   refines Parser<TestData>
 
-  @category Parser<any> whitespace <- SequenceOfParser.create(" \n\t",1,0) `Parse.or<?>` Parse.error("Expected whitespace")
+  @category Parser<any> whitespace <- SequenceOfParser.create(" \n\t",1,0) `Parse.or` Parse.error("Expected whitespace")
 
   @category String sentenceChars <- "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
                                     "abcdefghijklmnopqrstuvwxyz" +
@@ -52,36 +52,36 @@
   @category Parser<String> sentence <- SequenceOfParser.create(sentenceChars,0,0)
   @category Parser<any>    quote    <- CharParser.create('"')
 
-  @category Parser<String> quotedSentence  <- quote `Parse.right<?>` sentence `Parse.left<?>` quote
+  @category Parser<String> quotedSentence  <- quote `Parse.right` sentence `Parse.left` quote
   @category Parser<String> token           <- SequenceOfParser.create("ABCDEFGHIJKLMNOPQRSTUVWXYZ_",1,0)
-  @category Parser<String> sentenceOrToken <- quotedSentence `Parse.or<?>` token `Parse.left<?>` whitespace
+  @category Parser<String> sentenceOrToken <- quotedSentence `Parse.or` token `Parse.left` whitespace
 
-  @category Parser<Bool> acronym           <- StringParser.create("acronym")  `Parse.right<?>` Parse.const<?>(true)
-  @category Parser<Bool> aardvark          <- StringParser.create("aardvark") `Parse.right<?>` Parse.const<?>(false)
-  @category Parser<Bool> acronymOrAardvark <- Parse.try<?>(acronym) `Parse.or<?>` aardvark `Parse.left<?>` whitespace
+  @category Parser<Bool> acronym           <- StringParser.create("acronym")  `Parse.right` Parse.const(true)
+  @category Parser<Bool> aardvark          <- StringParser.create("aardvark") `Parse.right` Parse.const(false)
+  @category Parser<Bool> acronymOrAardvark <- Parse.try(acronym) `Parse.or` aardvark `Parse.left` whitespace
 
-  @category Parser<any> fileStart      <- StringParser.create("file_start")   `Parse.left<?>` whitespace
-  @category Parser<any> fileEnd        <- StringParser.create("file_end")     `Parse.left<?>` whitespace
-  @category Parser<any> nameTag        <- StringParser.create("name:")        `Parse.left<?>` whitespace
-  @category Parser<any> descriptionTag <- StringParser.create("description:") `Parse.left<?>` whitespace
-  @category Parser<any> aWordTag       <- StringParser.create("a_word:")      `Parse.left<?>` whitespace
+  @category Parser<any> fileStart      <- StringParser.create("file_start")   `Parse.left` whitespace
+  @category Parser<any> fileEnd        <- StringParser.create("file_end")     `Parse.left` whitespace
+  @category Parser<any> nameTag        <- StringParser.create("name:")        `Parse.left` whitespace
+  @category Parser<any> descriptionTag <- StringParser.create("description:") `Parse.left` whitespace
+  @category Parser<any> aWordTag       <- StringParser.create("a_word:")      `Parse.left` whitespace
 
   run (contextOld) {
     ParseContext<any> context <- contextOld
-    context                              <- context.run<?>(fileStart)
-    context                              <- context.run<?>(nameTag)
-    context, ErrorOr<String> name        <- context.runAndGet<?>(sentenceOrToken)
-    context                              <- context.run<?>(descriptionTag)
-    context, ErrorOr<String> description <- context.runAndGet<?>(sentenceOrToken)
-    context                              <- context.run<?>(aWordTag)
-    context, ErrorOr<Bool> boolean       <- context.runAndGet<?>(acronymOrAardvark)
-    context                              <- context.run<?>(fileEnd)
+    context                              <- context.run(fileStart)
+    context                              <- context.run(nameTag)
+    context, ErrorOr<String> name        <- context.runAndGet(sentenceOrToken)
+    context                              <- context.run(descriptionTag)
+    context, ErrorOr<String> description <- context.runAndGet(sentenceOrToken)
+    context                              <- context.run(aWordTag)
+    context, ErrorOr<Bool> boolean       <- context.runAndGet(acronymOrAardvark)
+    context                              <- context.run(fileEnd)
 
     if (context.hasAnyError()) {
       return context.convertError()
     } else {
-      return context.setValue<?>(
-        ErrorOr:value<?>(TestData.create(name.getValue(),
+      return context.setValue(
+        ErrorOr:value(TestData.create(name.getValue(),
                                          description.getValue(),
                                          boolean.getValue())))
     }
diff --git a/lib/container/helpers.0rp b/lib/container/helpers.0rp
--- a/lib/container/helpers.0rp
+++ b/lib/container/helpers.0rp
@@ -29,7 +29,7 @@
   //
   // Example:
   //
-  //   Bool lt <- x `KeyValueH:lessThan<?,?>` y
+  //   Bool lt <- x `KeyValueH:lessThan` y
   @category lessThan<#k,#v>
     #k defines LessThan<#k>
     #v defines LessThan<#v>
@@ -63,7 +63,7 @@
   //
   // Example:
   //
-  //   Bool eq <- x `KeyValueH:equals<?,?>` y
+  //   Bool eq <- x `KeyValueH:equals` y
   @category equals<#k,#v>
     #k defines Equals<#k>
     #v defines Equals<#v>
diff --git a/lib/container/list.0rp b/lib/container/list.0rp
--- a/lib/container/list.0rp
+++ b/lib/container/list.0rp
@@ -55,6 +55,10 @@
 }
 
 // Node in a doubly-linked list.
+//
+// Notes:
+// - This list is weak in the reverse direction. In other words, prev only
+//   exists if there is another non-weak reference to it.
 concrete LinkedNode<#x> {
   defines NewNode<#x>
   // Duplication happens only in the forward direction.
@@ -93,6 +97,9 @@
 
   // Create a new builder.
   @type new () -> (#self)
+
+  // Append all of the elements from the Order.
+  @value appendAll (optional Order<#x>) -> (#self)
 
   // Build the list.
   //
diff --git a/lib/container/src/Extension_Vector.cpp b/lib/container/src/Extension_Vector.cpp
--- a/lib/container/src/Extension_Vector.cpp
+++ b/lib/container/src/Extension_Vector.cpp
@@ -41,31 +41,31 @@
 BoxedValue CreateValue_Vector(S<const Type_Vector> parent, VectorType values);
 
 struct ExtCategory_Vector : public Category_Vector {
-  ReturnTuple Call_copyFrom(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_copyFrom(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("Vector:copyFrom")
-    const S<const TypeInstance> Param_y = params.At(0);
-    const BoxedValue& Var_arg1 = (args.At(0));
+    const S<const TypeInstance> Param_y = params_args.GetParam(0);
+    const BoxedValue& Var_arg1 = (params_args.GetArg(0));
     VectorType values;
-    const PrimInt size = TypeValue::Call(Var_arg1, Function_Container_size, ParamTuple(), ArgTuple()).Only().AsInt();
+    const PrimInt size = TypeValue::Call(Var_arg1, Function_Container_size, PassParamsArgs()).At(0).AsInt();
     for (int i = 0; i < size; ++i) {
-      values.push_back(TypeValue::Call(Var_arg1, Function_ReadAt_readAt, ParamTuple(), ArgTuple(Box_Int(i))).Only());
+      values.push_back(TypeValue::Call(Var_arg1, Function_ReadAt_readAt, PassParamsArgs(Box_Int(i))).At(0));
     }
     return ReturnTuple(CreateValue_Vector(CreateType_Vector(Params<1>::Type(Param_y)), std::move(values)));
   }
 
-  ReturnTuple Call_create(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_create(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("Vector:create")
-    const S<const TypeInstance> Param_y = params.At(0);
+    const S<const TypeInstance> Param_y = params_args.GetParam(0);
     return ReturnTuple(CreateValue_Vector(CreateType_Vector(Params<1>::Type(Param_y)), VectorType()));
   }
 
-  ReturnTuple Call_createSize(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_createSize(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("Vector:createSize")
-    const S<const TypeInstance> Param_y = params.At(0);
-    const PrimInt Var_arg1 = (args.At(0)).AsInt();
+    const S<const TypeInstance> Param_y = params_args.GetParam(0);
+    const PrimInt Var_arg1 = (params_args.GetArg(0)).AsInt();
     VectorType values;
     for (int i = 0; i < Var_arg1; ++i) {
-      values.push_back(TypeInstance::Call(Param_y, Function_Default_default, ParamTuple(), ArgTuple()).Only());
+      values.push_back(TypeInstance::Call(Param_y, Function_Default_default, PassParamsArgs()).At(0));
     }
     return ReturnTuple(CreateValue_Vector(CreateType_Vector(Params<1>::Type(Param_y)), std::move(values)));
   }
@@ -82,9 +82,7 @@
 
   std::string CategoryName() const final { return "VectorOrder"; }
 
-  ReturnTuple Dispatch(const ValueFunction& label,
-                       const ParamTuple& params,
-                       const ValueTuple& args) final {
+  ReturnTuple Dispatch(const ValueFunction& label, const ParamsArgs& params_args) final {
     if (&label == &Function_Order_next) {
       TRACE_FUNCTION("VectorOrder.next")
       if (index_+1 >= values_.size()) {
@@ -101,7 +99,7 @@
       }
       return ReturnTuple(values_[index_]);
     }
-    return TypeValue::Dispatch(label, params, args);
+    return TypeValue::Dispatch(label, params_args);
   }
 
  private:
@@ -114,14 +112,14 @@
   inline ExtValue_Vector(S<const Type_Vector> p, VectorType v)
     : Value_Vector(std::move(p)), values(std::move(v)) {}
 
-  ReturnTuple Call_append(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_append(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("Vector.append")
-    const BoxedValue& Var_arg1 = (args.At(0));
+    const BoxedValue& Var_arg1 = (params_args.GetArg(0));
     values.push_back(Var_arg1);
     return ReturnTuple(VAR_SELF);
   }
 
-  ReturnTuple Call_defaultOrder(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_defaultOrder(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("Vector.defaultOrder")
     if (values.empty()) {
       return ReturnTuple(Var_empty);
@@ -130,12 +128,12 @@
     }
   }
 
-  ReturnTuple Call_duplicate(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_duplicate(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("Vector.duplicate")
     return ReturnTuple(BoxedValue::New<ExtValue_Vector>(parent, values));
   }
 
-  ReturnTuple Call_pop(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_pop(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("Vector.pop")
     if (values.empty()) {
       BUILTIN_FAIL(Box_String(PrimString_FromLiteral("no elements left to pop")))
@@ -145,31 +143,31 @@
     return ReturnTuple(value);
   }
 
-  ReturnTuple Call_push(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_push(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("Vector.push")
-    const BoxedValue& Var_arg1 = (args.At(0));
+    const BoxedValue& Var_arg1 = (params_args.GetArg(0));
     values.push_back(Var_arg1);
     return ReturnTuple(VAR_SELF);
   }
 
-  ReturnTuple Call_readAt(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_readAt(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("Vector.readAt")
-    const PrimInt Var_arg1 = (args.At(0)).AsInt();
+    const PrimInt Var_arg1 = (params_args.GetArg(0)).AsInt();
     if (Var_arg1 < 0 || Var_arg1 >= values.size()) {
       FAIL() << "index " << Var_arg1 << " is out of bounds";
     }
     return ReturnTuple(values[Var_arg1]);
   }
 
-  ReturnTuple Call_size(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_size(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("Vector.size")
     return ReturnTuple(Box_Int(values.size()));
   }
 
-  ReturnTuple Call_writeAt(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_writeAt(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("Vector.writeAt")
-    const PrimInt Var_arg1 = (args.At(0)).AsInt();
-    const BoxedValue& Var_arg2 = (args.At(1));
+    const PrimInt Var_arg1 = (params_args.GetArg(0)).AsInt();
+    const BoxedValue& Var_arg2 = (params_args.GetArg(1));
     if (Var_arg1 < 0 || Var_arg1 >= values.size()) {
       FAIL() << "index " << Var_arg1 << " is out of bounds";
     }
diff --git a/lib/container/src/auto-tree.0rx b/lib/container/src/auto-tree.0rx
--- a/lib/container/src/auto-tree.0rx
+++ b/lib/container/src/auto-tree.0rx
@@ -304,7 +304,7 @@
   }
 
   get () {
-    return TreeKeyValue:create<?,?>(node.getKey(),node.getValue())
+    return TreeKeyValue:create(node.getKey(),node.getValue())
   }
 }
 
@@ -353,6 +353,6 @@
   }
 
   get () {
-    return TreeKeyValue:create<?,?>(node.getKey(),node.getValue())
+    return TreeKeyValue:create(node.getKey(),node.getValue())
   }
 }
diff --git a/lib/container/src/list.0rx b/lib/container/src/list.0rx
--- a/lib/container/src/list.0rx
+++ b/lib/container/src/list.0rx
@@ -16,9 +16,9 @@
 
 // Author: Kevin P. Barry [ta0kira@gmail.com]
 
-define LinkedNode {
+define LinkedNode { $FlatCleanup[next]$
   @value #x value
-  @value optional #self prev
+  @value weak #self prev
   @value optional #self next
 
   newNode (value) {
@@ -43,43 +43,35 @@
   }
 
   prev () {
-    return prev
+    return strong(prev)
   }
 
   duplicate () {
     $Hidden[prev,next,value]$
-    ListBuilder<#x,#self> builder <- #self.builder()
-    traverse (self -> #x value2) {
-      \ builder.append(value2)
-    }
-    scoped {
-      optional #self head, _ <- builder.build()
-    } in return require(head)
+    return require(builder().appendAll(self).build(){0})
   }
 
   setNext (next2) (old) {
     old <- next
-    $Hidden[old]$
-    if (present(next)) {
-      \ require(next).setPrevBase(empty)
+    if (present(old)) {
+      \ require(old).setPrevBase(empty)
     }
     next <- next2
-    if (present(next)) {
-      \ require(next).setPrev(empty)
-      \ require(next).setPrevBase(self)
+    if (present(next2)) {
+      \ require(next2).setPrev(empty)
+      \ require(next2).setPrevBase(self)
     }
   }
 
   setPrev (prev2) (old) {
-    old <- prev
-    $Hidden[old]$
-    if (present(prev)) {
-      \ require(prev).setNextBase(empty)
+    old <- strong(prev)
+    if (present(old)) {
+      \ require(old).setNextBase(empty)
     }
     prev <- prev2
-    if (present(prev)) {
-      \ require(prev).setNext(empty)
-      \ require(prev).setNextBase(self)
+    if (present(prev2)) {
+      \ require(prev2).setNext(empty)
+      \ require(prev2).setNextBase(self)
     }
   }
 
@@ -94,7 +86,7 @@
   }
 }
 
-define ForwardNode {
+define ForwardNode { $FlatCleanup[next]$
   @value #x value
   @value optional #self next
 
@@ -121,13 +113,7 @@
 
   duplicate () {
     $Hidden[next,value]$
-    ListBuilder<#x,#self> builder <- #self.builder()
-    traverse (self -> #x value2) {
-      \ builder.append(value2)
-    }
-    scoped {
-      optional #self head, _ <- builder.build()
-    } in return require(head)
+    return require(builder().appendAll(self).build(){0})
   }
 
   setNext (next2) (old) {
@@ -152,6 +138,13 @@
       \ require(tail).setNext(node)
     }
     tail <- node
+    return self
+  }
+
+  appendAll (values) {
+    traverse (values -> #x value) {
+      \ append(value)
+    }
     return self
   }
 
diff --git a/lib/container/src/search-tree.0rx b/lib/container/src/search-tree.0rx
--- a/lib/container/src/search-tree.0rx
+++ b/lib/container/src/search-tree.0rx
@@ -46,19 +46,19 @@
   }
 
   defaultOrder () {
-    return ForwardTreeOrder:create<?,?>(tree.getRoot())
+    return ForwardTreeOrder:create(tree.getRoot())
   }
 
   reverseOrder () {
-    return ReverseTreeOrder:create<?,?>(tree.getRoot())
+    return ReverseTreeOrder:create(tree.getRoot())
   }
 
   getForward (k) {
-    return ForwardTreeOrder:seek<?,?>(k,tree.getRoot())
+    return ForwardTreeOrder:seek(k,tree.getRoot())
   }
 
   getReverse (k) {
-    return ReverseTreeOrder:seek<?,?>(k,tree.getRoot())
+    return ReverseTreeOrder:seek(k,tree.getRoot())
   }
 }
 
diff --git a/lib/container/src/tree-set.0rx b/lib/container/src/tree-set.0rx
--- a/lib/container/src/tree-set.0rx
+++ b/lib/container/src/tree-set.0rx
@@ -36,6 +36,10 @@
     return self
   }
 
+  append (k) {
+    return add(k)
+  }
+
   remove (k) {
     \ tree.remove(k)
     return self
diff --git a/lib/container/src/type-map.0rx b/lib/container/src/type-map.0rx
--- a/lib/container/src/type-map.0rx
+++ b/lib/container/src/type-map.0rx
@@ -28,7 +28,7 @@
   }
 
   set (k,v) {
-    \ tree.set(k,GenericValue:create<?>(v))
+    \ tree.set(k,GenericValue:create(v))
     return self
   }
 
@@ -45,6 +45,17 @@
     } else {
       return empty
     }
+  }
+
+  getAll (output) {
+    traverse (tree.defaultOrder() -> KeyValue<any,GenericValue<any>> pair) {
+      scoped {
+        optional #x value <- pair.getValue().check<#x>()
+      } in if (present(value)) {
+        \ output.append(require(value))
+      }
+    }
+    return output
   }
 }
 
diff --git a/lib/container/test/helpers.0rt b/lib/container/test/helpers.0rt
--- a/lib/container/test/helpers.0rt
+++ b/lib/container/test/helpers.0rt
@@ -21,41 +21,41 @@
 }
 
 unittest keyValueHLessThan {
-  \ Testing.checkEquals<?>(KV.new(1,"a") `KeyValueH:lessThan<?,?>` KV.new(1,"a"),false)
-  \ Testing.checkEquals<?>(KV.new(1,"a") `KeyValueH:lessThan<?,?>` KV.new(1,"b"),true)
-  \ Testing.checkEquals<?>(KV.new(2,"a") `KeyValueH:lessThan<?,?>` KV.new(1,"b"),false)
-  \ Testing.checkEquals<?>(KV.new(1,"b") `KeyValueH:lessThan<?,?>` KV.new(2,"a"),true)
+  \ Testing.checkFalse(KV.new(1,"a") `KeyValueH:lessThan` KV.new(1,"a"))
+  \ Testing.checkTrue(KV.new(1,"a") `KeyValueH:lessThan` KV.new(1,"b"))
+  \ Testing.checkFalse(KV.new(2,"a") `KeyValueH:lessThan` KV.new(1,"b"))
+  \ Testing.checkTrue(KV.new(1,"b") `KeyValueH:lessThan` KV.new(2,"a"))
 }
 
 unittest keyValueHLessThanWith {
-  \ Testing.checkEquals<?>(KV.new(1,"a") `KeyValueH:lessThanWith<?,?,Int,AlwaysEqual>` KV.new(1,"a"),false)
-  \ Testing.checkEquals<?>(KV.new(1,"a") `KeyValueH:lessThanWith<?,?,Int,AlwaysEqual>` KV.new(1,"b"),false)
-  \ Testing.checkEquals<?>(KV.new(2,"a") `KeyValueH:lessThanWith<?,?,Int,AlwaysEqual>` KV.new(1,"b"),false)
-  \ Testing.checkEquals<?>(KV.new(1,"b") `KeyValueH:lessThanWith<?,?,Int,AlwaysEqual>` KV.new(2,"a"),true)
+  \ Testing.checkFalse(KV.new(1,"a") `KeyValueH:lessThanWith<?,?,Int,AlwaysEqual>` KV.new(1,"a"))
+  \ Testing.checkFalse(KV.new(1,"a") `KeyValueH:lessThanWith<?,?,Int,AlwaysEqual>` KV.new(1,"b"))
+  \ Testing.checkFalse(KV.new(2,"a") `KeyValueH:lessThanWith<?,?,Int,AlwaysEqual>` KV.new(1,"b"))
+  \ Testing.checkTrue(KV.new(1,"b") `KeyValueH:lessThanWith<?,?,Int,AlwaysEqual>` KV.new(2,"a"))
 
-  \ Testing.checkEquals<?>(KV.new(1,"a") `KeyValueH:lessThanWith<?,?,AlwaysEqual,String>` KV.new(1,"a"),false)
-  \ Testing.checkEquals<?>(KV.new(1,"a") `KeyValueH:lessThanWith<?,?,AlwaysEqual,String>` KV.new(1,"b"),true)
-  \ Testing.checkEquals<?>(KV.new(2,"a") `KeyValueH:lessThanWith<?,?,AlwaysEqual,String>` KV.new(1,"b"),true)
-  \ Testing.checkEquals<?>(KV.new(1,"b") `KeyValueH:lessThanWith<?,?,AlwaysEqual,String>` KV.new(2,"a"),false)
+  \ Testing.checkFalse(KV.new(1,"a") `KeyValueH:lessThanWith<?,?,AlwaysEqual,String>` KV.new(1,"a"))
+  \ Testing.checkTrue(KV.new(1,"a") `KeyValueH:lessThanWith<?,?,AlwaysEqual,String>` KV.new(1,"b"))
+  \ Testing.checkTrue(KV.new(2,"a") `KeyValueH:lessThanWith<?,?,AlwaysEqual,String>` KV.new(1,"b"))
+  \ Testing.checkFalse(KV.new(1,"b") `KeyValueH:lessThanWith<?,?,AlwaysEqual,String>` KV.new(2,"a"))
 }
 
 unittest keyValueHEquals {
-  \ Testing.checkEquals<?>(KV.new(1,"a") `KeyValueH:equals<?,?>` KV.new(1,"a"),true)
-  \ Testing.checkEquals<?>(KV.new(1,"a") `KeyValueH:equals<?,?>` KV.new(1,"b"),false)
-  \ Testing.checkEquals<?>(KV.new(2,"a") `KeyValueH:equals<?,?>` KV.new(1,"b"),false)
-  \ Testing.checkEquals<?>(KV.new(1,"b") `KeyValueH:equals<?,?>` KV.new(2,"a"),false)
+  \ Testing.checkTrue(KV.new(1,"a") `KeyValueH:equals` KV.new(1,"a"))
+  \ Testing.checkFalse(KV.new(1,"a") `KeyValueH:equals` KV.new(1,"b"))
+  \ Testing.checkFalse(KV.new(2,"a") `KeyValueH:equals` KV.new(1,"b"))
+  \ Testing.checkFalse(KV.new(1,"b") `KeyValueH:equals` KV.new(2,"a"))
 }
 
 unittest keyValueHEqualsWith {
-  \ Testing.checkEquals<?>(KV.new(1,"a") `KeyValueH:equalsWith<?,?,Int,AlwaysEqual>` KV.new(1,"a"),true)
-  \ Testing.checkEquals<?>(KV.new(1,"a") `KeyValueH:equalsWith<?,?,Int,AlwaysEqual>` KV.new(1,"b"),true)
-  \ Testing.checkEquals<?>(KV.new(2,"a") `KeyValueH:equalsWith<?,?,Int,AlwaysEqual>` KV.new(1,"b"),false)
-  \ Testing.checkEquals<?>(KV.new(1,"b") `KeyValueH:equalsWith<?,?,Int,AlwaysEqual>` KV.new(2,"a"),false)
+  \ Testing.checkTrue(KV.new(1,"a") `KeyValueH:equalsWith<?,?,Int,AlwaysEqual>` KV.new(1,"a"))
+  \ Testing.checkTrue(KV.new(1,"a") `KeyValueH:equalsWith<?,?,Int,AlwaysEqual>` KV.new(1,"b"))
+  \ Testing.checkFalse(KV.new(2,"a") `KeyValueH:equalsWith<?,?,Int,AlwaysEqual>` KV.new(1,"b"))
+  \ Testing.checkFalse(KV.new(1,"b") `KeyValueH:equalsWith<?,?,Int,AlwaysEqual>` KV.new(2,"a"))
 
-  \ Testing.checkEquals<?>(KV.new(1,"a") `KeyValueH:equalsWith<?,?,AlwaysEqual,String>` KV.new(1,"a"),true)
-  \ Testing.checkEquals<?>(KV.new(1,"a") `KeyValueH:equalsWith<?,?,AlwaysEqual,String>` KV.new(1,"b"),false)
-  \ Testing.checkEquals<?>(KV.new(2,"a") `KeyValueH:equalsWith<?,?,AlwaysEqual,String>` KV.new(1,"b"),false)
-  \ Testing.checkEquals<?>(KV.new(1,"b") `KeyValueH:equalsWith<?,?,AlwaysEqual,String>` KV.new(2,"a"),false)
+  \ Testing.checkTrue(KV.new(1,"a") `KeyValueH:equalsWith<?,?,AlwaysEqual,String>` KV.new(1,"a"))
+  \ Testing.checkFalse(KV.new(1,"a") `KeyValueH:equalsWith<?,?,AlwaysEqual,String>` KV.new(1,"b"))
+  \ Testing.checkFalse(KV.new(2,"a") `KeyValueH:equalsWith<?,?,AlwaysEqual,String>` KV.new(1,"b"))
+  \ Testing.checkFalse(KV.new(1,"b") `KeyValueH:equalsWith<?,?,AlwaysEqual,String>` KV.new(2,"a"))
 }
 
 unittest integrationTest {
@@ -64,9 +64,9 @@
   SearchTree<Int,String> tree2 <- SearchTree<Int,String>.new()
       .set(1,"a").set(2,"b").set(3,"c").set(4,"d")
 
-  \ Testing.checkEquals<?>(tree1.defaultOrder() `OrderH:equalsWith<?,KVEquals<Int,AlwaysEqual>>`    tree2.defaultOrder(),true)
-  \ Testing.checkEquals<?>(tree1.defaultOrder() `OrderH:equalsWith<?,KVEquals<Int,String>>`         tree2.defaultOrder(),false)
-  \ Testing.checkEquals<?>(tree1.defaultOrder() `OrderH:equalsWith<?,KVEquals<AlwaysEqual,String>>` tree2.defaultOrder(),false)
+  \ Testing.checkTrue(tree1.defaultOrder() `OrderH:equalsWith<?,KVEquals<Int,AlwaysEqual>>`    tree2.defaultOrder())
+  \ Testing.checkFalse(tree1.defaultOrder() `OrderH:equalsWith<?,KVEquals<Int,String>>`         tree2.defaultOrder())
+  \ Testing.checkFalse(tree1.defaultOrder() `OrderH:equalsWith<?,KVEquals<AlwaysEqual,String>>` tree2.defaultOrder())
 }
 
 concrete KVEquals<#k,#v> {
diff --git a/lib/container/test/list.0rt b/lib/container/test/list.0rt
--- a/lib/container/test/list.0rt
+++ b/lib/container/test/list.0rt
@@ -40,15 +40,16 @@
       .append(4)
 
   traverse (expected.defaultOrder() -> Int value) {
-    \ Testing.checkEquals<?>(require(head).get(),value)
+    \ Testing.checkEquals(require(head).get(),value)
   } update {
     head <- require(head).next()
   }
-  \ Testing.checkEquals<?>(present(head),false)
+  \ Testing.checkFalse(present(head))
 }
 
 unittest reverseTraverse {
-  _, optional LinkedNode<Int> tail <- LinkedNode<Int>.builder()
+  // NOTE: Since the list is weak in reverse, we need to store head.
+  optional LinkedNode<Int> head, optional LinkedNode<Int> tail <- LinkedNode<Int>.builder()
       .append(3)
       .append(2)
       .append(5)
@@ -66,11 +67,11 @@
       .append(3)
 
   traverse (expected.defaultOrder() -> Int value) {
-    \ Testing.checkEquals<?>(require(tail).get(),value)
+    \ Testing.checkEquals(require(tail).get(),value)
   } update {
     tail <- require(tail).prev()
   }
-  \ Testing.checkEquals<?>(present(tail),false)
+  \ Testing.checkFalse(present(tail))
 }
 
 unittest setNext {
@@ -83,7 +84,7 @@
       .append(4)
       .build()
 
-  optional LinkedNode<Int> after1  <- Helper.jumpBy<?>(3,head1)
+  optional LinkedNode<Int> after1  <- Helper.jumpBy(3,head1)
   optional LinkedNode<Int> before1 <- require(after1).prev()
 
   optional LinkedNode<Int> head2, _ <- LinkedNode<Int>.builder()
@@ -95,15 +96,15 @@
       .append(14)
       .build()
 
-  optional LinkedNode<Int> after2  <- Helper.jumpBy<?>(3,head2)
+  optional LinkedNode<Int> after2  <- Helper.jumpBy(3,head2)
   optional LinkedNode<Int> before2 <- require(after2).prev()
 
-  \ Testing.checkEquals<?>(require(require(before1).setNext(after2)).get(),1)
+  \ Testing.checkEquals(require(require(before1).setNext(after2)).get(),1)
 
-  \ Testing.checkEquals<?>(present(require(after1).prev()),false)
-  \ Testing.checkEquals<?>(require(require(before1).next()).get(),11)
-  \ Testing.checkEquals<?>(require(require(after2).prev()).get(),5)
-  \ Testing.checkEquals<?>(present(require(before2).next()),false)
+  \ Testing.checkFalse(present(require(after1).prev()))
+  \ Testing.checkEquals(require(require(before1).next()).get(),11)
+  \ Testing.checkEquals(require(require(after2).prev()).get(),5)
+  \ Testing.checkFalse(present(require(before2).next()))
 }
 
 unittest setPrev {
@@ -116,7 +117,7 @@
       .append(4)
       .build()
 
-  optional LinkedNode<Int> after1  <- Helper.jumpBy<?>(3,head1)
+  optional LinkedNode<Int> after1  <- Helper.jumpBy(3,head1)
   optional LinkedNode<Int> before1 <- require(after1).prev()
 
   optional LinkedNode<Int> head2, _ <- LinkedNode<Int>.builder()
@@ -128,15 +129,15 @@
       .append(14)
       .build()
 
-  optional LinkedNode<Int> after2  <- Helper.jumpBy<?>(3,head2)
+  optional LinkedNode<Int> after2  <- Helper.jumpBy(3,head2)
   optional LinkedNode<Int> before2 <- require(after2).prev()
 
-  \ Testing.checkEquals<?>(require(require(after1).setPrev(before2)).get(),5)
+  \ Testing.checkEquals(require(require(after1).setPrev(before2)).get(),5)
 
-  \ Testing.checkEquals<?>(require(require(after1).prev()).get(),15)
-  \ Testing.checkEquals<?>(present(require(before1).next()),false)
-  \ Testing.checkEquals<?>(present(require(after2).prev()),false)
-  \ Testing.checkEquals<?>(require(require(before2).next()).get(),1)
+  \ Testing.checkEquals(require(require(after1).prev()).get(),15)
+  \ Testing.checkFalse(present(require(before1).next()))
+  \ Testing.checkFalse(present(require(after2).prev()))
+  \ Testing.checkEquals(require(require(before2).next()).get(),1)
 }
 
 unittest setNextSame {
@@ -149,13 +150,13 @@
       .append(4)
       .build()
 
-  optional LinkedNode<Int> after  <- Helper.jumpBy<?>(3,head)
+  optional LinkedNode<Int> after  <- Helper.jumpBy(3,head)
   optional LinkedNode<Int> before <- require(after).prev()
 
   \ require(before).setNext(after)
 
-  \ Testing.checkEquals<?>(require(require(after).prev()).get(),5)
-  \ Testing.checkEquals<?>(require(require(before).next()).get(),1)
+  \ Testing.checkEquals(require(require(after).prev()).get(),5)
+  \ Testing.checkEquals(require(require(before).next()).get(),1)
 }
 
 unittest setPrevSame {
@@ -168,13 +169,13 @@
       .append(4)
       .build()
 
-  optional LinkedNode<Int> after  <- Helper.jumpBy<?>(3,head)
+  optional LinkedNode<Int> after  <- Helper.jumpBy(3,head)
   optional LinkedNode<Int> before <- require(after).prev()
 
   \ require(after).setPrev(before)
 
-  \ Testing.checkEquals<?>(require(require(after).prev()).get(),5)
-  \ Testing.checkEquals<?>(require(require(before).next()).get(),1)
+  \ Testing.checkEquals(require(require(after).prev()).get(),5)
+  \ Testing.checkEquals(require(require(before).next()).get(),1)
 }
 
 unittest setNextSelf {
@@ -187,14 +188,14 @@
       .append(4)
       .build()
 
-  optional LinkedNode<Int> after  <- Helper.jumpBy<?>(3,head)
+  optional LinkedNode<Int> after  <- Helper.jumpBy(3,head)
   optional LinkedNode<Int> before <- require(after).prev()
 
   \ require(before).setNext(before)
 
-  \ Testing.checkEquals<?>(present(require(after).prev()),false)
-  \ Testing.checkEquals<?>(require(require(before).prev()).get(),5)
-  \ Testing.checkEquals<?>(require(require(before).next()).get(),5)
+  \ Testing.checkFalse(present(require(after).prev()))
+  \ Testing.checkEquals(require(require(before).prev()).get(),5)
+  \ Testing.checkEquals(require(require(before).next()).get(),5)
 }
 
 unittest setPrevSelf {
@@ -207,14 +208,14 @@
       .append(4)
       .build()
 
-  optional LinkedNode<Int> after  <- Helper.jumpBy<?>(3,head)
+  optional LinkedNode<Int> after  <- Helper.jumpBy(3,head)
   optional LinkedNode<Int> before <- require(after).prev()
 
   \ require(after).setPrev(after)
 
-  \ Testing.checkEquals<?>(present(require(before).next()),false)
-  \ Testing.checkEquals<?>(require(require(after).prev()).get(),1)
-  \ Testing.checkEquals<?>(require(require(after).next()).get(),1)
+  \ Testing.checkFalse(present(require(before).next()))
+  \ Testing.checkEquals(require(require(after).prev()).get(),1)
+  \ Testing.checkEquals(require(require(after).next()).get(),1)
 }
 
 unittest duplicate {
@@ -236,17 +237,25 @@
       .append(4)
 
   optional LinkedNode<Int> head2 <- require(head).duplicate()
-  \ require(Helper.jumpBy<?>(3,head)).set(7).setNext(empty)
+  \ require(Helper.jumpBy(3,head)).set(7).setNext(empty)
   $Hidden[head]$
 
   traverse (expected.defaultOrder() -> Int value) {
-    \ Testing.checkEquals<?>(require(head2).get(),value)
+    \ Testing.checkEquals(require(head2).get(),value)
   } update {
     head2 <- require(head2).next()
   }
-  \ Testing.checkEquals<?>(present(head2),false)
+  \ Testing.checkFalse(present(head2))
 }
 
+unittest hugeCleanup { $DisableCoverage$
+  scoped {
+    ListBuilder<Int,LinkedNode<Int>> builder <- LinkedNode<Int>.builder()
+  } in traverse (Counter.zeroIndexed(1000000) -> _) {
+    \ builder.append(0)
+  }
+}
+
 unittest forwardTraverseSingle {
   optional ForwardNode<Int> head, _ <- ForwardNode<Int>.builder()
       .append(3)
@@ -266,11 +275,11 @@
       .append(4)
 
   traverse (expected.defaultOrder() -> Int value) {
-    \ Testing.checkEquals<?>(require(head).get(),value)
+    \ Testing.checkEquals(require(head).get(),value)
   } update {
     head <- require(head).next()
   }
-  \ Testing.checkEquals<?>(present(head),false)
+  \ Testing.checkFalse(present(head))
 }
 
 unittest duplicateSingle {
@@ -292,15 +301,23 @@
       .append(4)
 
   optional ForwardNode<Int> head2 <- require(head).duplicate()
-  \ require(Helper.jumpBy<?>(3,head)).set(7).setNext(empty)
+  \ require(Helper.jumpBy(3,head)).set(7).setNext(empty)
   $Hidden[head]$
 
   traverse (expected.defaultOrder() -> Int value) {
-    \ Testing.checkEquals<?>(require(head2).get(),value)
+    \ Testing.checkEquals(require(head2).get(),value)
   } update {
     head2 <- require(head2).next()
   }
-  \ Testing.checkEquals<?>(present(head2),false)
+  \ Testing.checkFalse(present(head2))
+}
+
+unittest hugeCleanupSingle { $DisableCoverage$
+  scoped {
+    ListBuilder<Int,ForwardNode<Int>> builder <- ForwardNode<Int>.builder()
+  } in traverse (Counter.zeroIndexed(1000000) -> _) {
+    \ builder.append(0)
+  }
 }
 
 concrete Helper {
diff --git a/lib/container/test/search-tree.0rt b/lib/container/test/search-tree.0rt
--- a/lib/container/test/search-tree.0rt
+++ b/lib/container/test/search-tree.0rt
@@ -20,16 +20,16 @@
   success
 }
 
-unittest integrationTest { $DisableCoverage$
+unittest integrationTest {
   ValidatedTree<Int,Int> tree <- ValidatedTree<Int,Int>.new()
-  Int count <- 30
+  Int count <- 33
   $ReadOnly[count]$
 
   // Insert values.
   traverse (Counter.zeroIndexed(count) -> Int i) {
     Int new <- ((i + 13) * 3547) % count
     \ tree.set(new,i)
-    \ Testing.checkEquals<?>(tree.size(),i+1)
+    \ Testing.checkEquals(tree.size(),i+1)
   }
 
   // Check and remove values.
@@ -55,7 +55,7 @@
           .writeTo(SimpleOutput.error())
     }
     \ tree.remove(new)
-    \ Testing.checkEquals<?>(tree.size(),count-i-1)
+    \ Testing.checkEquals(tree.size(),count-i-1)
   }
 }
 
@@ -73,12 +73,12 @@
   // Validate the traversal order.
   Int index <- 0
   traverse (tree.defaultOrder() -> KeyValue<Int,Int> entry) {
-    \ Testing.checkEquals<?>(entry.getKey(),index)
-    \ Testing.checkEquals<?>((entry.getValue()*hash)%max,entry.getKey())
+    \ Testing.checkEquals(entry.getKey(),index)
+    \ Testing.checkEquals((entry.getValue()*hash)%max,entry.getKey())
     index <- index+1
   }
 
-  \ Testing.checkEquals<?>(index,max)
+  \ Testing.checkEquals(index,max)
 }
 
 unittest reverseOrder {
@@ -96,11 +96,11 @@
   Int index <- max
   traverse (tree.reverseOrder() -> KeyValue<Int,Int> entry) {
     index <- index-1
-    \ Testing.checkEquals<?>(entry.getKey(),index)
-    \ Testing.checkEquals<?>((entry.getValue()*hash)%max,entry.getKey())
+    \ Testing.checkEquals(entry.getKey(),index)
+    \ Testing.checkEquals((entry.getValue()*hash)%max,entry.getKey())
   }
 
-  \ Testing.checkEquals<?>(index,0)
+  \ Testing.checkEquals(index,0)
 }
 
 unittest getForward {
@@ -118,11 +118,11 @@
   traverse (Counter.zeroIndexed(max) -> Int i) {
     Int index <- i
     traverse (tree.getForward(index) -> KeyValue<Int,Int> entry) {
-      \ Testing.checkEquals<?>(entry.getKey(),index)
-      \ Testing.checkEquals<?>((entry.getValue()*hash)%max,entry.getKey())
+      \ Testing.checkEquals(entry.getKey(),index)
+      \ Testing.checkEquals((entry.getValue()*hash)%max,entry.getKey())
       index <- index+1
     }
-    \ Testing.checkEquals<?>(index,max)
+    \ Testing.checkEquals(index,max)
   }
 }
 
@@ -135,7 +135,7 @@
   } in if (!present(start)) {
     fail("Failed")
   } else {
-    \ Testing.checkEquals<?>(require(start).get().getKey(),5)
+    \ Testing.checkEquals(require(start).get().getKey(),5)
   }
 
   scoped {
@@ -160,11 +160,11 @@
   traverse (Counter.zeroIndexed(max) -> Int i) {
     Int index <- i
     traverse (tree.getReverse(index) -> KeyValue<Int,Int> entry) {
-      \ Testing.checkEquals<?>(entry.getKey(),index)
-      \ Testing.checkEquals<?>((entry.getValue()*hash)%max,entry.getKey())
+      \ Testing.checkEquals(entry.getKey(),index)
+      \ Testing.checkEquals((entry.getValue()*hash)%max,entry.getKey())
       index <- index-1
     }
-    \ Testing.checkEquals<?>(index,-1)
+    \ Testing.checkEquals(index,-1)
   }
 }
 
@@ -177,7 +177,7 @@
   } in if (!present(start)) {
     fail("Failed")
   } else {
-    \ Testing.checkEquals<?>(require(start).get().getKey(),1)
+    \ Testing.checkEquals(require(start).get().getKey(),1)
   }
 
   scoped {
@@ -191,16 +191,23 @@
   SearchTree<Int,Int> tree <- SearchTree<Int,Int>.new().set(1,1).set(2,2).set(3,3)
 
   SearchTree<Int,Int> copy <- tree.duplicate()
-  \ Testing.checkEquals<?>(copy.size(),3)
-  \ Testing.checkOptional<?>(copy.get(1),1)
-  \ Testing.checkOptional<?>(copy.get(2),2)
-  \ Testing.checkOptional<?>(copy.get(3),3)
+  \ Testing.checkEquals(copy.size(),3)
+  \ Testing.checkOptional(copy.get(1),1)
+  \ Testing.checkOptional(copy.get(2),2)
+  \ Testing.checkOptional(copy.get(3),3)
 
   \ copy.remove(2)
   $Hidden[copy]$
 
-  \ Testing.checkEquals<?>(tree.size(),3)
-  \ Testing.checkOptional<?>(tree.get(1),1)
-  \ Testing.checkOptional<?>(tree.get(2),2)
-  \ Testing.checkOptional<?>(tree.get(3),3)
+  \ Testing.checkEquals(tree.size(),3)
+  \ Testing.checkOptional(tree.get(1),1)
+  \ Testing.checkOptional(tree.get(2),2)
+  \ Testing.checkOptional(tree.get(3),3)
+}
+
+unittest duplicateEmpty {
+  SearchTree<Int,Int> tree <- SearchTree<Int,Int>.new()
+
+  SearchTree<Int,Int> copy <- tree.duplicate()
+  \ Testing.checkEquals(copy.size(),0)
 }
diff --git a/lib/container/test/sorting.0rt b/lib/container/test/sorting.0rt
--- a/lib/container/test/sorting.0rt
+++ b/lib/container/test/sorting.0rt
@@ -33,8 +33,8 @@
     \ expected.append(i/3)
   }
 
-  \ Sorting:sort<?>(original)
-  \ Testing.checkEquals<?>(original,expected)
+  \ Sorting:sort(original)
+  \ Testing.checkEquals(original,expected)
 }
 
 unittest sortWith { $DisableCoverage$
@@ -50,15 +50,15 @@
   }
 
   \ Sorting:sortWith<?,Reversed<Int>>(original)
-  \ Testing.checkEquals<?>(original,expected)
+  \ Testing.checkEquals(original,expected)
 }
 
 unittest sortEmpty {
   TestSequence original <- TestSequence.create()
   TestSequence expected <- TestSequence.create()
 
-  \ Sorting:sort<?>(original)
-  \ Testing.checkEquals<?>(original,expected)
+  \ Sorting:sort(original)
+  \ Testing.checkEquals(original,expected)
 }
 
 unittest reverseEvenSize {
@@ -75,8 +75,8 @@
     \ original.append(i)
   }
 
-  \ Sorting:reverse<?>(original)
-  \ Testing.checkEquals<?>(original,expected)
+  \ Sorting:reverse(original)
+  \ Testing.checkEquals(original,expected)
 }
 
 unittest reverseOddSize { $DisableCoverage$
@@ -93,16 +93,16 @@
     \ original.append(i)
   }
 
-  \ Sorting:reverse<?>(original)
-  \ Testing.checkEquals<?>(original,expected)
+  \ Sorting:reverse(original)
+  \ Testing.checkEquals(original,expected)
 }
 
 unittest reverseEmpty {
   TestSequence original <- TestSequence.create()
   TestSequence expected <- TestSequence.create()
 
-  \ Sorting:reverse<?>(original)
-  \ Testing.checkEquals<?>(original,expected)
+  \ Sorting:reverse(original)
+  \ Testing.checkEquals(original,expected)
 }
 
 unittest sortList {
@@ -117,16 +117,14 @@
     \ expected.append(i/3)
   }
 
-  scoped {
-    optional LinkedNode<Int> head, _ <- builder.build()
-  } in optional LinkedNode<Int> actual <- Sorting:sortList<?,?>(head)
+  optional LinkedNode<Int> actual <- Sorting:sortList(builder.build(){0})
 
   traverse (expected.defaultOrder() -> Int n) {
-    \ Testing.checkEquals<?>(require(actual).get(),n)
+    \ Testing.checkEquals(require(actual).get(),n)
   } update {
     actual <- require(actual).next()
   }
-  \ Testing.checkEquals<?>(present(actual),false)
+  \ Testing.checkFalse(present(actual))
 }
 
 unittest sortListSingle { $DisableCoverage$
@@ -141,16 +139,14 @@
     \ expected.append(i/3)
   }
 
-  scoped {
-    optional ForwardNode<Int> head, _ <- builder.build()
-  } in optional ForwardNode<Int> actual <- Sorting:sortList<?,?>(head)
+  optional ForwardNode<Int> actual <- Sorting:sortList(builder.build(){0})
 
   traverse (expected.defaultOrder() -> Int n) {
-    \ Testing.checkEquals<?>(require(actual).get(),n)
+    \ Testing.checkEquals(require(actual).get(),n)
   } update {
     actual <- require(actual).next()
   }
-  \ Testing.checkEquals<?>(present(actual),false)
+  \ Testing.checkFalse(present(actual))
 }
 
 unittest sortListEmpty {
@@ -169,16 +165,14 @@
     \ expected.append(i/3)
   }
 
-  scoped {
-    optional LinkedNode<Int> head, _ <- builder.build()
-  } in optional LinkedNode<Int> actual <- Sorting:sortList<?,?>(head)
+  optional LinkedNode<Int> actual <- Sorting:sortList(builder.build(){0})
 
   traverse (expected.defaultOrder() -> Int n) {
-    \ Testing.checkEquals<?>(require(actual).get(),n)
+    \ Testing.checkEquals(require(actual).get(),n)
   } update {
     actual <- require(actual).next()
   }
-  \ Testing.checkEquals<?>(present(actual),false)
+  \ Testing.checkFalse(present(actual))
 }
 
 unittest sortListWith { $DisableCoverage$
@@ -193,16 +187,14 @@
     \ expected.append(i/3)
   }
 
-  scoped {
-    optional LinkedNode<Int> head, _ <- builder.build()
-  } in optional LinkedNode<Int> actual <- Sorting:sortListWith<?,?,Reversed<Int>>(head)
+  optional LinkedNode<Int> actual <- Sorting:sortListWith<?,?,Reversed<Int>>(builder.build(){0})
 
   traverse (expected.defaultOrder() -> Int n) {
-    \ Testing.checkEquals<?>(require(actual).get(),n)
+    \ Testing.checkEquals(require(actual).get(),n)
   } update {
     actual <- require(actual).next()
   }
-  \ Testing.checkEquals<?>(present(actual),false)
+  \ Testing.checkFalse(present(actual))
 }
 
 unittest reverseList {
@@ -219,16 +211,14 @@
     \ builder.append(i)
   }
 
-  scoped {
-    optional LinkedNode<Int> head, _ <- builder.build()
-  } in optional LinkedNode<Int> reversed <- Sorting:reverseList<?,?>(head)
+  optional LinkedNode<Int> reversed <- Sorting:reverseList(builder.build(){0})
 
   traverse (expected.defaultOrder() -> Int value) {
-    \ Testing.checkEquals<?>(require(reversed).get(),value)
+    \ Testing.checkEquals(require(reversed).get(),value)
   } update {
     reversed <- require(reversed).next()
   }
-  \ Testing.checkEquals<?>(present(reversed),false)
+  \ Testing.checkFalse(present(reversed))
 }
 
 unittest reverseListSingle { $DisableCoverage$
@@ -245,16 +235,14 @@
     \ builder.append(i)
   }
 
-  scoped {
-    optional ForwardNode<Int> head, _ <- builder.build()
-  } in optional ForwardNode<Int> reversed <- Sorting:reverseList<?,?>(head)
+  optional ForwardNode<Int> reversed <- Sorting:reverseList(builder.build(){0})
 
   traverse (expected.defaultOrder() -> Int value) {
-    \ Testing.checkEquals<?>(require(reversed).get(),value)
+    \ Testing.checkEquals(require(reversed).get(),value)
   } update {
     reversed <- require(reversed).next()
   }
-  \ Testing.checkEquals<?>(present(reversed),false)
+  \ Testing.checkFalse(present(reversed))
 }
 
 unittest reverseListEmpty {
@@ -306,6 +294,6 @@
   }
 
   equals (x,y) {
-    return x `ReadAtH:equals<?>` y
+    return x `ReadAtH:equals` y
   }
 }
diff --git a/lib/container/test/tree-set.0rt b/lib/container/test/tree-set.0rt
--- a/lib/container/test/tree-set.0rt
+++ b/lib/container/test/tree-set.0rt
@@ -29,7 +29,7 @@
   // Populate the set in a pseudo-random order.
   traverse (Counter.zeroIndexed(max) -> Int i) {
     \ set.add((i*hash)%max)
-    \ Testing.checkEquals<?>(set.size(),i+1)
+    \ Testing.checkEquals(set.size(),i+1)
   }
 
   // Remove only the odd elements.
@@ -38,12 +38,12 @@
       // Remove it twice to ensure idempotence.
       \ set.remove(i).remove(i)
     }
-    \ Testing.checkEquals<?>(set.size(),max-(i+1)/2)
+    \ Testing.checkEquals(set.size(),max-(i+1)/2)
   }
 
   // Validate set membership.
   traverse (Counter.zeroIndexed(max) -> Int i) {
-    \ Testing.checkEquals<?>(set.member(i),i%2 == 0)
+    \ Testing.checkEquals(set.member(i),i%2 == 0)
   }
 }
 
@@ -61,11 +61,11 @@
   // Validate the traversal order.
   Int index <- 0
   traverse (set.defaultOrder() -> Int entry) {
-    \ Testing.checkEquals<?>(entry,index)
+    \ Testing.checkEquals(entry,index)
     index <- index+1
   }
 
-  \ Testing.checkEquals<?>(index,max)
+  \ Testing.checkEquals(index,max)
 }
 
 unittest reverseOrder {
@@ -83,10 +83,10 @@
   Int index <- max
   traverse (set.reverseOrder() -> Int entry) {
     index <- index-1
-    \ Testing.checkEquals<?>(entry,index)
+    \ Testing.checkEquals(entry,index)
   }
 
-  \ Testing.checkEquals<?>(index,0)
+  \ Testing.checkEquals(index,0)
 }
 
 unittest getForward {
@@ -104,10 +104,10 @@
   traverse (Counter.zeroIndexed(max) -> Int i) {
     Int index <- i
     traverse (set.getForward(index) -> Int entry) {
-      \ Testing.checkEquals<?>(entry,index)
+      \ Testing.checkEquals(entry,index)
       index <- index+1
     }
-    \ Testing.checkEquals<?>(index,max)
+    \ Testing.checkEquals(index,max)
   }
 }
 
@@ -120,7 +120,7 @@
   } in if (!present(start)) {
     fail("Failed")
   } else {
-    \ Testing.checkEquals<?>(require(start).get(),5)
+    \ Testing.checkEquals(require(start).get(),5)
   }
 
   scoped {
@@ -145,10 +145,10 @@
   traverse (Counter.zeroIndexed(max) -> Int i) {
     Int index <- i
     traverse (set.getReverse(index) -> Int entry) {
-      \ Testing.checkEquals<?>(entry,index)
+      \ Testing.checkEquals(entry,index)
       index <- index-1
     }
-    \ Testing.checkEquals<?>(index,-1)
+    \ Testing.checkEquals(index,-1)
   }
 }
 
@@ -161,7 +161,7 @@
   } in if (!present(start)) {
     fail("Failed")
   } else {
-    \ Testing.checkEquals<?>(require(start).get(),1)
+    \ Testing.checkEquals(require(start).get(),1)
   }
 
   scoped {
@@ -175,12 +175,12 @@
   TreeSet<Int> set <- TreeSet<Int>.new().add(1).add(2)
 
   TreeSet<Int> copy <- set.duplicate()
-  \ Testing.checkEquals<?>(copy.size(),2)
-  \ Testing.checkEquals<?>(copy.member(1),true)
-  \ Testing.checkEquals<?>(copy.member(1),true)
+  \ Testing.checkEquals(copy.size(),2)
+  \ Testing.checkTrue(copy.member(1))
+  \ Testing.checkTrue(copy.member(1))
 
   \ copy.remove(2)
-  \ Testing.checkEquals<?>(copy.size(),1)
-  \ Testing.checkEquals<?>(copy.member(1),true)
-  \ Testing.checkEquals<?>(set.member(2),true)
+  \ Testing.checkEquals(copy.size(),1)
+  \ Testing.checkTrue(copy.member(1))
+  \ Testing.checkTrue(set.member(2))
 }
diff --git a/lib/container/test/type-map.0rt b/lib/container/test/type-map.0rt
--- a/lib/container/test/type-map.0rt
+++ b/lib/container/test/type-map.0rt
@@ -27,21 +27,21 @@
   TypeKey<String> keyString <- TypeKey<String>.new()
   TypeKey<Value>  keyValue  <- TypeKey<Value>.new()
 
-  \ Testing.checkOptional<?>(tree.get<?>(keyInt),   empty)
-  \ Testing.checkOptional<?>(tree.get<?>(keyString),empty)
-  \ Testing.checkOptional<?>(tree.get<?>(keyValue), empty)
+  \ Testing.checkOptional(tree.get(keyInt),   empty)
+  \ Testing.checkOptional(tree.get(keyString),empty)
+  \ Testing.checkOptional(tree.get(keyValue), empty)
 
-  \ tree.set<?>(keyInt,1)
-  \ tree.set<?>(keyString,"a")
-  \ tree.set<?>(keyValue,Value.new())
+  \ tree.set(keyInt,1)
+  \ tree.set(keyString,"a")
+  \ tree.set(keyValue,Value.new())
 
-  \ Testing.checkOptional<?>(tree.get<?>(keyInt),   1)
-  \ Testing.checkOptional<?>(tree.get<?>(keyString),"a")
-  \ Testing.checkOptional<?>(tree.get<?>(keyValue), Value.new())
+  \ Testing.checkOptional(tree.get(keyInt),   1)
+  \ Testing.checkOptional(tree.get(keyString),"a")
+  \ Testing.checkOptional(tree.get(keyValue), Value.new())
 
   \ tree.remove(keyString)
 
-  \ Testing.checkOptional<?>(tree.get<?>(keyString),empty)
+  \ Testing.checkOptional(tree.get(keyString),empty)
 }
 
 unittest wrongReturnType {
@@ -51,28 +51,51 @@
 
   // Added as Formatted, which means it cannot be retrieved as String later.
   \ tree.set<Formatted>(keyString,"a")
-  \ Testing.checkOptional<?>(tree.get<String>(keyString),empty)
+  \ Testing.checkOptional(tree.get<String>(keyString),empty)
 
   \ tree.set<String>(keyString,"a")
-  \ Testing.checkOptional<?>(tree.get<String>(keyString),"a")
+  \ Testing.checkOptional(tree.get<String>(keyString),"a")
 }
 
 unittest duplicate {
   TypeKey<Int>    keyInt    <- TypeKey<Int>.new()
   TypeKey<String> keyString <- TypeKey<String>.new()
 
-  TypeMap tree <- TypeMap.new().set<?>(keyInt,1).set<?>(keyString,"a")
+  TypeMap tree <- TypeMap.new().set(keyInt,1).set(keyString,"a")
 
   TypeMap copy <- tree.duplicate()
-  \ Testing.checkOptional<?>(copy.get<?>(keyInt),1)
-  \ Testing.checkOptional<?>(copy.get<?>(keyString),"a")
+  \ Testing.checkOptional(copy.get(keyInt),1)
+  \ Testing.checkOptional(copy.get(keyString),"a")
 
   \ copy.remove(keyString)
-  \ Testing.checkOptional<?>(copy.get<?>(keyInt),1)
-  \ Testing.checkOptional<?>(copy.get<?>(keyString),empty)
-  \ Testing.checkOptional<?>(tree.get<?>(keyString),"a")
+  \ Testing.checkOptional(copy.get(keyInt),1)
+  \ Testing.checkOptional(copy.get(keyString),empty)
+  \ Testing.checkOptional(tree.get(keyString),"a")
 }
 
+unittest getValues {
+  TypeMap tree <- TypeMap.new()
+      .set(TypeKey<String>.new(),"one")
+      .set(TypeKey<Int>.new(),2)
+      .set(TypeKey<String>.new(),"two")
+      .set(TypeKey<Int>.new(),1)
+
+  [Container&SetReader<String>] actual <- tree.getAll(TreeSet<String>.new())
+
+  \ Testing.checkEquals(actual.size(),2)
+  \ Testing.checkTrue(actual.member("one"))
+  \ Testing.checkTrue(actual.member("two"))
+}
+
+unittest keyEquals {
+  TypeKey<Int>    key1 <- TypeKey<Int>.new()
+  TypeKey<Int>    key2 <- TypeKey<Int>.new()
+  TypeKey<String> key3 <- TypeKey<String>.new()
+  \ Testing.checkEquals<TypeKey<Int>>(key1,key1)
+  \ Testing.checkFalse(key1 `TypeKey<any>.equals` key2)
+  \ Testing.checkFalse(key1 `TypeKey<any>.equals` key3)
+}
+
 concrete Value {
   refines Formatted
   defines Equals<Value>
@@ -92,4 +115,14 @@
   equals (_,_) {
     return true
   }
+}
+
+
+testcase "TypeKey formatted" {
+  crash
+  require "TypeKey<Int>\{[0-9]+\}"
+}
+
+unittest test {
+  fail(TypeKey<Int>.new())
 }
diff --git a/lib/container/test/vector.0rt b/lib/container/test/vector.0rt
--- a/lib/container/test/vector.0rt
+++ b/lib/container/test/vector.0rt
@@ -22,12 +22,12 @@
 
 unittest createSize {
   Vector<Int> values <- Vector:createSize<Int>(10)
-  \ Testing.checkEquals<?>(values.size(),10)
+  \ Testing.checkEquals(values.size(),10)
 
   scoped {
     Int i <- 0
   } in while (i < values.size()) {
-    \ Testing.checkEquals<?>(values.readAt(i),Int.default())
+    \ Testing.checkEquals(values.readAt(i),Int.default())
   } update {
     i <- i+1
   }
@@ -35,13 +35,13 @@
 
 unittest copyFrom {
   String original <- "abcde"
-  Vector<Char> values <- Vector:copyFrom<?>(original)
-  \ Testing.checkEquals<?>(values.size(),original.size())
+  Vector<Char> values <- Vector:copyFrom(original)
+  \ Testing.checkEquals(values.size(),original.size())
 
   scoped {
     Int i <- 0
   } in while (i < original.size()) {
-    \ Testing.checkEquals<?>(values.readAt(i),original.readAt(i))
+    \ Testing.checkEquals(values.readAt(i),original.readAt(i))
   } update {
     i <- i+1
   }
@@ -54,16 +54,16 @@
     Int i <- 0
   } in while (i < 10) {
     \ values.append(i)
-    \ Testing.checkEquals<?>(values.size(),i+1)
+    \ Testing.checkEquals(values.size(),i+1)
   } update {
     i <- i+1
   }
-  \ Testing.checkEquals<?>(values.size(),10)
+  \ Testing.checkEquals(values.size(),10)
 
   scoped {
     Int i <- 0
   } in while (i < values.size()) {
-    \ Testing.checkEquals<?>(values.readAt(i),i)
+    \ Testing.checkEquals(values.readAt(i),i)
   } update {
     i <- i+1
   }
@@ -76,21 +76,21 @@
     Int i <- 0
   } in while (i < 10) {
     \ values.push(i)
-    \ Testing.checkEquals<?>(values.size(),i+1)
+    \ Testing.checkEquals(values.size(),i+1)
   } update {
     i <- i+1
   }
-  \ Testing.checkEquals<?>(values.size(),10)
+  \ Testing.checkEquals(values.size(),10)
 
   scoped {
     Int i <- 0
   } in while (i < 10) {
-    \ Testing.checkEquals<?>(values.pop(),10-i-1)
-    \ Testing.checkEquals<?>(values.size(),10-i-1)
+    \ Testing.checkEquals(values.pop(),10-i-1)
+    \ Testing.checkEquals(values.size(),10-i-1)
   } update {
     i <- i+1
   }
-  \ Testing.checkEquals<?>(values.size(),0)
+  \ Testing.checkEquals(values.size(),0)
 }
 
 unittest writeAt {
@@ -107,7 +107,7 @@
   scoped {
     Int i <- 0
   } in while (i < values.size()) {
-    \ Testing.checkEquals<?>(values.readAt(i),2*i)
+    \ Testing.checkEquals(values.readAt(i),2*i)
   } update {
     i <- i+1
   }
@@ -124,25 +124,25 @@
   \ vector.push(1)
 
   traverse (vector.defaultOrder() -> Int i) {
-    \ Testing.checkEquals<?>(i,vector.readAt(index))
+    \ Testing.checkEquals(i,vector.readAt(index))
     index <- index+1
   }
 
-  \ Testing.checkEquals<?>(index,6)
+  \ Testing.checkEquals(index,6)
 }
 
 unittest duplicate {
   Vector<Int> vector <- Vector:create<Int>().push(1).push(2)
 
   Vector<Int> copy <- vector.duplicate()
-  \ Testing.checkEquals<?>(copy.size(),2)
-  \ Testing.checkEquals<?>(copy.readAt(0),1)
-  \ Testing.checkEquals<?>(copy.readAt(1),2)
+  \ Testing.checkEquals(copy.size(),2)
+  \ Testing.checkEquals(copy.readAt(0),1)
+  \ Testing.checkEquals(copy.readAt(1),2)
 
   \ copy.pop()
-  \ Testing.checkEquals<?>(copy.size(),1)
-  \ Testing.checkEquals<?>(copy.readAt(0),1)
-  \ Testing.checkEquals<?>(vector.readAt(1),2)
+  \ Testing.checkEquals(copy.size(),1)
+  \ Testing.checkEquals(copy.readAt(0),1)
+  \ Testing.checkEquals(vector.readAt(1),2)
 }
 
 
@@ -154,13 +154,13 @@
   Vector<Type> values <- Vector:createSize<Type>(10)
   \ values.readAt(3).set(7)
 
-  \ Testing.checkEquals<?>(values.readAt(3),Type.create(7))
+  \ Testing.checkEquals(values.readAt(3),Type.create(7))
 
   scoped {
     Int i <- 0
   } in while (i < values.size()) {
     if (i != 3) {
-      \ Testing.checkEquals<?>(values.readAt(i),Type.create(0))
+      \ Testing.checkEquals(values.readAt(i),Type.create(0))
     }
   } update {
     i <- i+1
diff --git a/lib/container/tree-set.0rp b/lib/container/tree-set.0rp
--- a/lib/container/tree-set.0rp
+++ b/lib/container/tree-set.0rp
@@ -17,6 +17,7 @@
 // Author: Kevin P. Barry [ta0kira@gmail.com]
 
 concrete TreeSet<#k> {
+  refines Append<#k>
   refines Container
   refines DefaultOrder<#k>
   refines Duplicate
diff --git a/lib/container/type-map.0rp b/lib/container/type-map.0rp
--- a/lib/container/type-map.0rp
+++ b/lib/container/type-map.0rp
@@ -44,6 +44,11 @@
   //   example, if the value is actually a String but it was added as Formatted
   //   (a parent of String), then the value cannot be retrieved as a String.
   @value get<#x> (TypeKey<#x>) -> (optional #x)
+
+  // Get all values of type #x.
+  @value getAll<#c,#x>
+    #c requires Append<#x>
+  (#c) -> (#c)
 }
 
 // A typed key for use with TypeMap.
diff --git a/lib/file/src/Extension_RawFileReader.cpp b/lib/file/src/Extension_RawFileReader.cpp
--- a/lib/file/src/Extension_RawFileReader.cpp
+++ b/lib/file/src/Extension_RawFileReader.cpp
@@ -25,7 +25,7 @@
 namespace ZEOLITE_PUBLIC_NAMESPACE {
 #endif  // ZEOLITE_PUBLIC_NAMESPACE
 
-BoxedValue CreateValue_RawFileReader(S<const Type_RawFileReader> parent, const ValueTuple& args);
+BoxedValue CreateValue_RawFileReader(S<const Type_RawFileReader> parent, const ParamsArgs& params_args);
 
 struct ExtCategory_RawFileReader : public Category_RawFileReader {
 };
@@ -33,19 +33,19 @@
 struct ExtType_RawFileReader : public Type_RawFileReader {
   inline ExtType_RawFileReader(Category_RawFileReader& p, Params<0>::Type params) : Type_RawFileReader(p, params) {}
 
-  ReturnTuple Call_open(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_open(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("RawFileReader.open")
-    return ReturnTuple(CreateValue_RawFileReader(PARAM_SELF, args));
+    return ReturnTuple(CreateValue_RawFileReader(PARAM_SELF, params_args));
   }
 };
 
 struct ExtValue_RawFileReader : public Value_RawFileReader {
-  inline ExtValue_RawFileReader(S<const Type_RawFileReader> p, const ValueTuple& args)
+  inline ExtValue_RawFileReader(S<const Type_RawFileReader> p, const ParamsArgs& params_args)
     : Value_RawFileReader(std::move(p)),
-      filename(args.At(0).AsString()),
+      filename(params_args.GetArg(0).AsString()),
       file(new std::ifstream(filename, std::ios::in | std::ios::binary)) {}
 
-  ReturnTuple Call_freeResource(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_freeResource(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("RawFileReader.freeResource")
     std::lock_guard<std::mutex> lock(mutex);
     if (file) {
@@ -54,7 +54,7 @@
     return ReturnTuple();
   }
 
-  ReturnTuple Call_getFileError(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_getFileError(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("RawFileReader.getFileError")
     std::lock_guard<std::mutex> lock(mutex);
     if (!file) {
@@ -69,17 +69,17 @@
     return ReturnTuple(Var_empty);
   }
 
-  ReturnTuple Call_pastEnd(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_pastEnd(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("RawFileReader.pastEnd")
     std::lock_guard<std::mutex> lock(mutex);
     return ReturnTuple(Box_Bool(!file || file->fail() || file->eof()));
   }
 
-  ReturnTuple Call_readBlock(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_readBlock(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("RawFileReader.readBlock")
     TRACE_CREATION
     std::lock_guard<std::mutex> lock(mutex);
-    const PrimInt Var_arg1 = (args.At(0)).AsInt();
+    const PrimInt Var_arg1 = (params_args.GetArg(0)).AsInt();
     if (Var_arg1 < 0) {
       FAIL() << "Read size " << Var_arg1 << " is invalid";
     }
@@ -122,8 +122,8 @@
 
 void RemoveType_RawFileReader(const Params<0>::Type& params) {}
 
-BoxedValue CreateValue_RawFileReader(S<const Type_RawFileReader> parent, const ValueTuple& args) {
-  return BoxedValue::New<ExtValue_RawFileReader>(std::move(parent), args);
+BoxedValue CreateValue_RawFileReader(S<const Type_RawFileReader> parent, const ParamsArgs& params_args) {
+  return BoxedValue::New<ExtValue_RawFileReader>(std::move(parent), params_args);
 }
 
 #ifdef ZEOLITE_PUBLIC_NAMESPACE
diff --git a/lib/file/src/Extension_RawFileWriter.cpp b/lib/file/src/Extension_RawFileWriter.cpp
--- a/lib/file/src/Extension_RawFileWriter.cpp
+++ b/lib/file/src/Extension_RawFileWriter.cpp
@@ -25,7 +25,7 @@
 namespace ZEOLITE_PUBLIC_NAMESPACE {
 #endif  // ZEOLITE_PUBLIC_NAMESPACE
 
-BoxedValue CreateValue_RawFileWriter(S<const Type_RawFileWriter> parent, const ValueTuple& args);
+BoxedValue CreateValue_RawFileWriter(S<const Type_RawFileWriter> parent, const ParamsArgs& params_args);
 
 struct ExtCategory_RawFileWriter : public Category_RawFileWriter {
 };
@@ -33,19 +33,19 @@
 struct ExtType_RawFileWriter : public Type_RawFileWriter {
   inline ExtType_RawFileWriter(Category_RawFileWriter& p, Params<0>::Type params) : Type_RawFileWriter(p, params) {}
 
-  ReturnTuple Call_open(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_open(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("RawFileWriter.open")
-    return ReturnTuple(CreateValue_RawFileWriter(PARAM_SELF, args));
+    return ReturnTuple(CreateValue_RawFileWriter(PARAM_SELF, params_args));
   }
 };
 
 struct ExtValue_RawFileWriter : public Value_RawFileWriter {
-  inline ExtValue_RawFileWriter(S<const Type_RawFileWriter> p, const ValueTuple& args)
+  inline ExtValue_RawFileWriter(S<const Type_RawFileWriter> p, const ParamsArgs& params_args)
     : Value_RawFileWriter(std::move(p)),
-      filename(args.At(0).AsString()),
+      filename(params_args.GetArg(0).AsString()),
       file(new std::ofstream(filename, std::ios::out | std::ios::binary | std::ios::trunc | std::ios::ate)) {}
 
-  ReturnTuple Call_freeResource(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_freeResource(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("RawFileWriter.freeResource")
     std::lock_guard<std::mutex> lock(mutex);
     if (file) {
@@ -54,7 +54,7 @@
     return ReturnTuple();
   }
 
-  ReturnTuple Call_getFileError(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_getFileError(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("RawFileWriter.getFileError")
     std::lock_guard<std::mutex> lock(mutex);
     if (!file) {
@@ -69,11 +69,11 @@
     return ReturnTuple(Var_empty);
   }
 
-  ReturnTuple Call_writeBlock(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_writeBlock(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("RawFileWriter.writeBlock")
     TRACE_CREATION
     std::lock_guard<std::mutex> lock(mutex);
-    const PrimString& Var_arg1 = args.At(0).AsString();
+    const PrimString& Var_arg1 = params_args.GetArg(0).AsString();
     if (!file || file->rdstate() != std::ios::goodbit) {
       FAIL() << "Error writing file \"" << filename << "\"";
     }
@@ -104,8 +104,8 @@
 
 void RemoveType_RawFileWriter(const Params<0>::Type& params) {}
 
-BoxedValue CreateValue_RawFileWriter(S<const Type_RawFileWriter> parent, const ValueTuple& args) {
-  return BoxedValue::New<ExtValue_RawFileWriter>(std::move(parent), args);
+BoxedValue CreateValue_RawFileWriter(S<const Type_RawFileWriter> parent, const ParamsArgs& params_args) {
+  return BoxedValue::New<ExtValue_RawFileWriter>(std::move(parent), params_args);
 }
 
 #ifdef ZEOLITE_PUBLIC_NAMESPACE
diff --git a/lib/file/test/tests.0rt b/lib/file/test/tests.0rt
--- a/lib/file/test/tests.0rt
+++ b/lib/file/test/tests.0rt
@@ -70,7 +70,7 @@
     \ writer.freeResource()
   } in \ writer.writeBlock(data)
 
-  \ Testing.checkEquals<?>(FileTesting.forceReadFile("testfile"),data)
+  \ Testing.checkEquals(FileTesting.forceReadFile("testfile"),data)
 }
 
 unittest readEmpty {
diff --git a/lib/math/src/Extension_Math.cpp b/lib/math/src/Extension_Math.cpp
--- a/lib/math/src/Extension_Math.cpp
+++ b/lib/math/src/Extension_Math.cpp
@@ -33,167 +33,167 @@
 struct ExtType_Math : public Type_Math {
   inline ExtType_Math(Category_Math& p, Params<0>::Type params) : Type_Math(p, params) {}
 
-  ReturnTuple Call_acos(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_acos(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Math.acos")
-    const PrimFloat Var_arg1 = (args.At(0)).AsFloat();
+    const PrimFloat Var_arg1 = (params_args.GetArg(0)).AsFloat();
     return ReturnTuple(Box_Float(std::acos(Var_arg1)));
   }
 
-  ReturnTuple Call_acosh(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_acosh(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Math.acosh")
-    const PrimFloat Var_arg1 = (args.At(0)).AsFloat();
+    const PrimFloat Var_arg1 = (params_args.GetArg(0)).AsFloat();
     return ReturnTuple(Box_Float(std::acosh(Var_arg1)));
   }
 
-  ReturnTuple Call_asin(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_asin(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Math.asin")
-    const PrimFloat Var_arg1 = (args.At(0)).AsFloat();
+    const PrimFloat Var_arg1 = (params_args.GetArg(0)).AsFloat();
     return ReturnTuple(Box_Float(std::asin(Var_arg1)));
   }
 
-  ReturnTuple Call_asinh(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_asinh(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Math.asinh")
-    const PrimFloat Var_arg1 = (args.At(0)).AsFloat();
+    const PrimFloat Var_arg1 = (params_args.GetArg(0)).AsFloat();
     return ReturnTuple(Box_Float(std::asinh(Var_arg1)));
   }
 
-  ReturnTuple Call_atan(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_atan(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Math.atan")
-    const PrimFloat Var_arg1 = (args.At(0)).AsFloat();
+    const PrimFloat Var_arg1 = (params_args.GetArg(0)).AsFloat();
     return ReturnTuple(Box_Float(std::atan(Var_arg1)));
   }
 
-  ReturnTuple Call_atanh(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_atanh(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Math.atanh")
-    const PrimFloat Var_arg1 = (args.At(0)).AsFloat();
+    const PrimFloat Var_arg1 = (params_args.GetArg(0)).AsFloat();
     return ReturnTuple(Box_Float(std::atanh(Var_arg1)));
   }
 
-  ReturnTuple Call_ceil(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_ceil(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Math.ceil")
-    const PrimFloat Var_arg1 = (args.At(0)).AsFloat();
+    const PrimFloat Var_arg1 = (params_args.GetArg(0)).AsFloat();
     return ReturnTuple(Box_Float(std::ceil(Var_arg1)));
   }
 
-  ReturnTuple Call_cos(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_cos(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Math.cos")
-    const PrimFloat Var_arg1 = (args.At(0)).AsFloat();
+    const PrimFloat Var_arg1 = (params_args.GetArg(0)).AsFloat();
     return ReturnTuple(Box_Float(std::cos(Var_arg1)));
   }
 
-  ReturnTuple Call_cosh(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_cosh(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Math.cosh")
-    const PrimFloat Var_arg1 = (args.At(0)).AsFloat();
+    const PrimFloat Var_arg1 = (params_args.GetArg(0)).AsFloat();
     return ReturnTuple(Box_Float(std::cosh(Var_arg1)));
   }
 
-  ReturnTuple Call_exp(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_exp(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Math.exp")
-    const PrimFloat Var_arg1 = (args.At(0)).AsFloat();
+    const PrimFloat Var_arg1 = (params_args.GetArg(0)).AsFloat();
     return ReturnTuple(Box_Float(std::exp(Var_arg1)));
   }
 
-  ReturnTuple Call_fabs(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_fabs(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Math.fabs")
-    const PrimFloat Var_arg1 = (args.At(0)).AsFloat();
+    const PrimFloat Var_arg1 = (params_args.GetArg(0)).AsFloat();
     return ReturnTuple(Box_Float(std::fabs(Var_arg1)));
   }
 
-  ReturnTuple Call_floor(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_floor(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Math.floor")
-    const PrimFloat Var_arg1 = (args.At(0)).AsFloat();
+    const PrimFloat Var_arg1 = (params_args.GetArg(0)).AsFloat();
     return ReturnTuple(Box_Float(std::floor(Var_arg1)));
   }
 
-  ReturnTuple Call_fmod(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_fmod(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Math.fmod")
-    const PrimFloat Var_arg1 = (args.At(0)).AsFloat();
-    const PrimFloat Var_arg2 = (args.At(1)).AsFloat();
+    const PrimFloat Var_arg1 = (params_args.GetArg(0)).AsFloat();
+    const PrimFloat Var_arg2 = (params_args.GetArg(1)).AsFloat();
     return ReturnTuple(Box_Float(std::fmod(Var_arg1,Var_arg2)));
   }
 
-  ReturnTuple Call_isinf(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_isinf(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Math.isinf")
-    const PrimFloat Var_arg1 = (args.At(0)).AsFloat();
+    const PrimFloat Var_arg1 = (params_args.GetArg(0)).AsFloat();
     return ReturnTuple(Box_Bool(std::isinf(Var_arg1)));
   }
 
-  ReturnTuple Call_isnan(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_isnan(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Math.isnan")
-    const PrimFloat Var_arg1 = (args.At(0)).AsFloat();
+    const PrimFloat Var_arg1 = (params_args.GetArg(0)).AsFloat();
     return ReturnTuple(Box_Bool(std::isnan(Var_arg1)));
   }
 
-  ReturnTuple Call_log(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_log(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Math.log")
-    const PrimFloat Var_arg1 = (args.At(0)).AsFloat();
+    const PrimFloat Var_arg1 = (params_args.GetArg(0)).AsFloat();
     return ReturnTuple(Box_Float(std::log(Var_arg1)));
   }
 
-  ReturnTuple Call_log10(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_log10(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Math.log10")
-    const PrimFloat Var_arg1 = (args.At(0)).AsFloat();
+    const PrimFloat Var_arg1 = (params_args.GetArg(0)).AsFloat();
     return ReturnTuple(Box_Float(std::log10(Var_arg1)));
   }
 
-  ReturnTuple Call_log2(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_log2(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Math.log2")
-    const PrimFloat Var_arg1 = (args.At(0)).AsFloat();
+    const PrimFloat Var_arg1 = (params_args.GetArg(0)).AsFloat();
     return ReturnTuple(Box_Float(std::log2(Var_arg1)));
   }
 
-  ReturnTuple Call_pow(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_pow(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Math.pow")
-    const PrimFloat Var_arg1 = (args.At(0)).AsFloat();
-    const PrimFloat Var_arg2 = (args.At(1)).AsFloat();
+    const PrimFloat Var_arg1 = (params_args.GetArg(0)).AsFloat();
+    const PrimFloat Var_arg2 = (params_args.GetArg(1)).AsFloat();
     return ReturnTuple(Box_Float(std::pow(Var_arg1,Var_arg2)));
   }
 
-  ReturnTuple Call_round(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_round(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Math.round")
-    const PrimFloat Var_arg1 = (args.At(0)).AsFloat();
+    const PrimFloat Var_arg1 = (params_args.GetArg(0)).AsFloat();
     return ReturnTuple(Box_Float(std::round(Var_arg1)));
   }
 
-  ReturnTuple Call_sin(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_sin(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Math.sin")
-    const PrimFloat Var_arg1 = (args.At(0)).AsFloat();
+    const PrimFloat Var_arg1 = (params_args.GetArg(0)).AsFloat();
     return ReturnTuple(Box_Float(std::sin(Var_arg1)));
   }
 
-  ReturnTuple Call_sinh(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_sinh(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Math.sinh")
-    const PrimFloat Var_arg1 = (args.At(0)).AsFloat();
+    const PrimFloat Var_arg1 = (params_args.GetArg(0)).AsFloat();
     return ReturnTuple(Box_Float(std::sinh(Var_arg1)));
   }
 
-  ReturnTuple Call_sqrt(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_sqrt(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Math.sqrt")
-    const PrimFloat Var_arg1 = (args.At(0)).AsFloat();
+    const PrimFloat Var_arg1 = (params_args.GetArg(0)).AsFloat();
     return ReturnTuple(Box_Float(std::sqrt(Var_arg1)));
   }
 
-  ReturnTuple Call_tan(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_tan(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Math.tan")
-    const PrimFloat Var_arg1 = (args.At(0)).AsFloat();
+    const PrimFloat Var_arg1 = (params_args.GetArg(0)).AsFloat();
     return ReturnTuple(Box_Float(std::tan(Var_arg1)));
   }
 
-  ReturnTuple Call_tanh(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_tanh(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Math.tanh")
-    const PrimFloat Var_arg1 = (args.At(0)).AsFloat();
+    const PrimFloat Var_arg1 = (params_args.GetArg(0)).AsFloat();
     return ReturnTuple(Box_Float(std::tanh(Var_arg1)));
   }
 
-  ReturnTuple Call_trunc(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_trunc(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Math.trunc")
-    const PrimFloat Var_arg1 = (args.At(0)).AsFloat();
+    const PrimFloat Var_arg1 = (params_args.GetArg(0)).AsFloat();
     return ReturnTuple(Box_Float(std::trunc(Var_arg1)));
   }
 
-  ReturnTuple Call_abs(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_abs(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Math.abs")
-    const PrimInt Var_arg1 = (args.At(0)).AsInt();
+    const PrimInt Var_arg1 = (params_args.GetArg(0)).AsInt();
     return ReturnTuple(Box_Int(std::abs(Var_arg1)));
   }
 };
diff --git a/lib/math/src/Extension_RandomExponential.cpp b/lib/math/src/Extension_RandomExponential.cpp
--- a/lib/math/src/Extension_RandomExponential.cpp
+++ b/lib/math/src/Extension_RandomExponential.cpp
@@ -29,7 +29,7 @@
 namespace ZEOLITE_PUBLIC_NAMESPACE {
 #endif  // ZEOLITE_PUBLIC_NAMESPACE
 
-BoxedValue CreateValue_RandomExponential(S<const Type_RandomExponential> parent, const ValueTuple& args);
+BoxedValue CreateValue_RandomExponential(S<const Type_RandomExponential> parent, const ParamsArgs& params_args);
 
 struct ExtCategory_RandomExponential : public Category_RandomExponential {
 };
@@ -37,31 +37,31 @@
 struct ExtType_RandomExponential : public Type_RandomExponential {
   inline ExtType_RandomExponential(Category_RandomExponential& p, Params<0>::Type params) : Type_RandomExponential(p, params) {}
 
-  ReturnTuple Call_new(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_new(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("RandomExponential.new")
-    const PrimFloat Var_arg1 = (args.At(0)).AsFloat();
+    const PrimFloat Var_arg1 = (params_args.GetArg(0)).AsFloat();
     if (Var_arg1 <= 0) {
       FAIL() << "Invalid lambda " << Var_arg1;
     }
-    return ReturnTuple(CreateValue_RandomExponential(PARAM_SELF, args));
+    return ReturnTuple(CreateValue_RandomExponential(PARAM_SELF, params_args));
   }
 };
 
 struct ExtValue_RandomExponential : public Value_RandomExponential {
-  inline ExtValue_RandomExponential(S<const Type_RandomExponential> p, const ValueTuple& args)
+  inline ExtValue_RandomExponential(S<const Type_RandomExponential> p, const ParamsArgs& params_args)
     : Value_RandomExponential(std::move(p)),
       generator_((int) (long) this),
-      distribution_(args.At(0).AsFloat()) {}
+      distribution_(params_args.GetArg(0).AsFloat()) {}
 
-  ReturnTuple Call_generate(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_generate(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("RandomExponential.generate")
     return ReturnTuple(Box_Float(distribution_(generator_)));
   }
 
-  ReturnTuple Call_setSeed(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_setSeed(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("RandomExponential.setSeed")
     distribution_.reset();
-    generator_.seed(args.At(0).AsInt());
+    generator_.seed(params_args.GetArg(0).AsInt());
     return ReturnTuple(VAR_SELF);
   }
 
@@ -81,8 +81,8 @@
 
 void RemoveType_RandomExponential(const Params<0>::Type& params) {}
 
-BoxedValue CreateValue_RandomExponential(S<const Type_RandomExponential> parent, const ValueTuple& args) {
-  return BoxedValue::New<ExtValue_RandomExponential>(std::move(parent), args);
+BoxedValue CreateValue_RandomExponential(S<const Type_RandomExponential> parent, const ParamsArgs& params_args) {
+  return BoxedValue::New<ExtValue_RandomExponential>(std::move(parent), params_args);
 }
 
 #ifdef ZEOLITE_PUBLIC_NAMESPACE
diff --git a/lib/math/src/Extension_RandomGaussian.cpp b/lib/math/src/Extension_RandomGaussian.cpp
--- a/lib/math/src/Extension_RandomGaussian.cpp
+++ b/lib/math/src/Extension_RandomGaussian.cpp
@@ -29,7 +29,7 @@
 namespace ZEOLITE_PUBLIC_NAMESPACE {
 #endif  // ZEOLITE_PUBLIC_NAMESPACE
 
-BoxedValue CreateValue_RandomGaussian(S<const Type_RandomGaussian> parent, const ValueTuple& args);
+BoxedValue CreateValue_RandomGaussian(S<const Type_RandomGaussian> parent, const ParamsArgs& params_args);
 
 struct ExtCategory_RandomGaussian : public Category_RandomGaussian {
 };
@@ -37,32 +37,32 @@
 struct ExtType_RandomGaussian : public Type_RandomGaussian {
   inline ExtType_RandomGaussian(Category_RandomGaussian& p, Params<0>::Type params) : Type_RandomGaussian(p, params) {}
 
-  ReturnTuple Call_new(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_new(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("RandomGaussian.new")
-    const PrimFloat Var_arg1 = (args.At(0)).AsFloat();
-    const PrimFloat Var_arg2 = (args.At(1)).AsFloat();
+    const PrimFloat Var_arg1 = (params_args.GetArg(0)).AsFloat();
+    const PrimFloat Var_arg2 = (params_args.GetArg(1)).AsFloat();
     if (Var_arg2 <= 0) {
       FAIL() << "Invalid standard deviation " << Var_arg2;
     }
-    return ReturnTuple(CreateValue_RandomGaussian(PARAM_SELF, args));
+    return ReturnTuple(CreateValue_RandomGaussian(PARAM_SELF, params_args));
   }
 };
 
 struct ExtValue_RandomGaussian : public Value_RandomGaussian {
-  inline ExtValue_RandomGaussian(S<const Type_RandomGaussian> p, const ValueTuple& args)
+  inline ExtValue_RandomGaussian(S<const Type_RandomGaussian> p, const ParamsArgs& params_args)
     : Value_RandomGaussian(std::move(p)),
       generator_((int) (long) this),
-      distribution_(args.At(0).AsFloat(), args.At(1).AsFloat()) {}
+      distribution_(params_args.GetArg(0).AsFloat(), params_args.GetArg(1).AsFloat()) {}
 
-  ReturnTuple Call_generate(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_generate(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("RandomGaussian.generate")
     return ReturnTuple(Box_Float(distribution_(generator_)));
   }
 
-  ReturnTuple Call_setSeed(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_setSeed(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("RandomGaussian.setSeed")
     distribution_.reset();
-    generator_.seed(args.At(0).AsInt());
+    generator_.seed(params_args.GetArg(0).AsInt());
     return ReturnTuple(VAR_SELF);
   }
 
@@ -82,8 +82,8 @@
 
 void RemoveType_RandomGaussian(const Params<0>::Type& params) {}
 
-BoxedValue CreateValue_RandomGaussian(S<const Type_RandomGaussian> parent, const ValueTuple& args) {
-  return BoxedValue::New<ExtValue_RandomGaussian>(std::move(parent), args);
+BoxedValue CreateValue_RandomGaussian(S<const Type_RandomGaussian> parent, const ParamsArgs& params_args) {
+  return BoxedValue::New<ExtValue_RandomGaussian>(std::move(parent), params_args);
 }
 
 #ifdef ZEOLITE_PUBLIC_NAMESPACE
diff --git a/lib/math/src/Extension_RandomUniform.cpp b/lib/math/src/Extension_RandomUniform.cpp
--- a/lib/math/src/Extension_RandomUniform.cpp
+++ b/lib/math/src/Extension_RandomUniform.cpp
@@ -28,7 +28,7 @@
 namespace ZEOLITE_PUBLIC_NAMESPACE {
 #endif  // ZEOLITE_PUBLIC_NAMESPACE
 
-BoxedValue CreateValue_RandomUniform(S<const Type_RandomUniform> parent, const ValueTuple& args);
+BoxedValue CreateValue_RandomUniform(S<const Type_RandomUniform> parent, const ParamsArgs& params_args);
 
 struct ExtCategory_RandomUniform : public Category_RandomUniform {
 };
@@ -36,32 +36,32 @@
 struct ExtType_RandomUniform : public Type_RandomUniform {
   inline ExtType_RandomUniform(Category_RandomUniform& p, Params<0>::Type params) : Type_RandomUniform(p, params) {}
 
-  ReturnTuple Call_new(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_new(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("RandomUniform.new")
-    const PrimFloat Var_arg1 = (args.At(0)).AsFloat();
-    const PrimFloat Var_arg2 = (args.At(1)).AsFloat();
+    const PrimFloat Var_arg1 = (params_args.GetArg(0)).AsFloat();
+    const PrimFloat Var_arg2 = (params_args.GetArg(1)).AsFloat();
     if (Var_arg2 <= Var_arg1) {
       FAIL() << "Invalid range (" << Var_arg1 << "," << Var_arg2 << ")";
     }
-    return ReturnTuple(CreateValue_RandomUniform(PARAM_SELF, args));
+    return ReturnTuple(CreateValue_RandomUniform(PARAM_SELF, params_args));
   }
 };
 
 struct ExtValue_RandomUniform : public Value_RandomUniform {
-  inline ExtValue_RandomUniform(S<const Type_RandomUniform> p, const ValueTuple& args)
+  inline ExtValue_RandomUniform(S<const Type_RandomUniform> p, const ParamsArgs& params_args)
     : Value_RandomUniform(std::move(p)),
       generator_((int) (long) this),
-      distribution_(args.At(0).AsFloat(), args.At(1).AsFloat()) {}
+      distribution_(params_args.GetArg(0).AsFloat(), params_args.GetArg(1).AsFloat()) {}
 
-  ReturnTuple Call_generate(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_generate(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("RandomUniform.generate")
     return ReturnTuple(Box_Float(distribution_(generator_)));
   }
 
-  ReturnTuple Call_setSeed(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_setSeed(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("RandomUniform.setSeed")
     distribution_.reset();
-    generator_.seed(args.At(0).AsInt());
+    generator_.seed(params_args.GetArg(0).AsInt());
     return ReturnTuple(VAR_SELF);
   }
 
@@ -81,8 +81,8 @@
 
 void RemoveType_RandomUniform(const Params<0>::Type& params) {}
 
-BoxedValue CreateValue_RandomUniform(S<const Type_RandomUniform> parent, const ValueTuple& args) {
-  return BoxedValue::New<ExtValue_RandomUniform>(std::move(parent), args);
+BoxedValue CreateValue_RandomUniform(S<const Type_RandomUniform> parent, const ParamsArgs& params_args) {
+  return BoxedValue::New<ExtValue_RandomUniform>(std::move(parent), params_args);
 }
 
 #ifdef ZEOLITE_PUBLIC_NAMESPACE
diff --git a/lib/math/src/token.0rx b/lib/math/src/token.0rx
new file mode 100644
--- /dev/null
+++ b/lib/math/src/token.0rx
@@ -0,0 +1,56 @@
+/* -----------------------------------------------------------------------------
+Copyright 2021 Kevin P. Barry
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+----------------------------------------------------------------------------- */
+
+// Author: Kevin P. Barry [ta0kira@gmail.com]
+
+define Token {
+  $ReadOnly[mutex,toToken]$
+
+  @category Mutex mutex <- SpinlockMutex.new()
+  @category SearchTree<String,Token> toToken <- SearchTree<String,Token>.new()
+  @category Int counter <- 0
+
+  @value String string
+  @value Int    value
+
+  from (string) (token) {
+    scoped {
+      \ mutex.lock()
+      optional Token existing <- toToken.get(string)
+    } cleanup {
+      \ mutex.unlock()
+    } in if (present(existing)) {
+      return require(existing)
+    } else {
+      \ toToken.set(string,(token <- Token{ string, (counter <- counter+1) }))
+    }
+  }
+
+  formatted () {
+    return string
+  }
+
+  equals (x,y) {
+    return x.get() == y.get()
+  }
+
+  lessThan (x,y) {
+    return x.get() < y.get()
+  }
+
+  @value get () -> (Int)
+  get () { return value }
+}
diff --git a/lib/math/test/categorical-tree.0rt b/lib/math/test/categorical-tree.0rt
--- a/lib/math/test/categorical-tree.0rt
+++ b/lib/math/test/categorical-tree.0rt
@@ -37,16 +37,16 @@
       .push(5).push(5).push(5).push(5)
       .push(6).push(6)
 
-  \ Testing.checkEquals<?>(tree.getTotal(),expected.size())
-  \ Testing.checkEquals<?>(tree.getWeight(1),3)
-  \ Testing.checkEquals<?>(tree.getWeight(2),7)
-  \ Testing.checkEquals<?>(tree.getWeight(3),1)
-  \ Testing.checkEquals<?>(tree.getWeight(4),3)
-  \ Testing.checkEquals<?>(tree.getWeight(5),4)
-  \ Testing.checkEquals<?>(tree.getWeight(6),2)
+  \ Testing.checkEquals(tree.getTotal(),expected.size())
+  \ Testing.checkEquals(tree.getWeight(1),3)
+  \ Testing.checkEquals(tree.getWeight(2),7)
+  \ Testing.checkEquals(tree.getWeight(3),1)
+  \ Testing.checkEquals(tree.getWeight(4),3)
+  \ Testing.checkEquals(tree.getWeight(5),4)
+  \ Testing.checkEquals(tree.getWeight(6),2)
 
   traverse (Counter.zeroIndexed(tree.getTotal()) -> Int pos) {
-    \ Testing.checkEquals<?>(tree.locate(pos),expected.readAt(pos))
+    \ Testing.checkEquals(tree.locate(pos),expected.readAt(pos))
   }
 }
 
@@ -71,22 +71,22 @@
       .push(6).push(6)
       .push(7).push(7).push(7)
 
-  \ Testing.checkEquals<?>(tree.getTotal(),expected.size())
-  \ Testing.checkEquals<?>(tree.getWeight(1),3)
-  \ Testing.checkEquals<?>(tree.getWeight(2),7)
-  \ Testing.checkEquals<?>(tree.getWeight(3),0)
-  \ Testing.checkEquals<?>(tree.getWeight(4),3)
-  \ Testing.checkEquals<?>(tree.getWeight(5),1)
-  \ Testing.checkEquals<?>(tree.getWeight(6),2)
-  \ Testing.checkEquals<?>(tree.getWeight(7),3)
+  \ Testing.checkEquals(tree.getTotal(),expected.size())
+  \ Testing.checkEquals(tree.getWeight(1),3)
+  \ Testing.checkEquals(tree.getWeight(2),7)
+  \ Testing.checkEquals(tree.getWeight(3),0)
+  \ Testing.checkEquals(tree.getWeight(4),3)
+  \ Testing.checkEquals(tree.getWeight(5),1)
+  \ Testing.checkEquals(tree.getWeight(6),2)
+  \ Testing.checkEquals(tree.getWeight(7),3)
 
   traverse (Counter.zeroIndexed(tree.getTotal()) -> Int pos) {
-    \ Testing.checkEquals<?>(tree.locate(pos),expected.readAt(pos))
+    \ Testing.checkEquals(tree.locate(pos),expected.readAt(pos))
   }
 
   // Use ReadAt aliases for the same functionality.
   traverse (Counter.zeroIndexed(tree.size()) -> Int pos) {
-    \ Testing.checkEquals<?>(tree.readAt(pos),expected.readAt(pos))
+    \ Testing.checkEquals(tree.readAt(pos),expected.readAt(pos))
   }
 }
 
@@ -96,8 +96,8 @@
     .incrWeight(1)
     .incrWeight(1)
 
-  \ Testing.checkEquals<?>(tree.getTotal(),3)
-  \ Testing.checkEquals<?>(tree.getWeight(1),3)
+  \ Testing.checkEquals(tree.getTotal(),3)
+  \ Testing.checkEquals(tree.getWeight(1),3)
 }
 
 unittest decrement {
@@ -107,22 +107,22 @@
     .decrWeight(1)
     .decrWeight(1)
 
-  \ Testing.checkEquals<?>(tree.getTotal(),7)
-  \ Testing.checkEquals<?>(tree.getWeight(1),7)
+  \ Testing.checkEquals(tree.getTotal(),7)
+  \ Testing.checkEquals(tree.getWeight(1),7)
 }
 
 unittest duplicate {
   CategoricalTree<Int> tree <- CategoricalTree<Int>.new().setWeight(1,1).setWeight(2,2)
 
   CategoricalTree<Int> copy <- tree.duplicate()
-  \ Testing.checkEquals<?>(copy.size(),3)
-  \ Testing.checkEquals<?>(copy.getWeight(1),1)
-  \ Testing.checkEquals<?>(copy.getWeight(2),2)
+  \ Testing.checkEquals(copy.size(),3)
+  \ Testing.checkEquals(copy.getWeight(1),1)
+  \ Testing.checkEquals(copy.getWeight(2),2)
 
   \ copy.setWeight(2,0)
-  \ Testing.checkEquals<?>(copy.size(),1)
-  \ Testing.checkEquals<?>(copy.getWeight(1),1)
-  \ Testing.checkEquals<?>(tree.getWeight(2),2)
+  \ Testing.checkEquals(copy.size(),1)
+  \ Testing.checkEquals(copy.getWeight(1),1)
+  \ Testing.checkEquals(tree.getWeight(2),2)
 }
 
 unittest integrationTest { $DisableCoverage$
@@ -159,13 +159,12 @@
     }
     $ReadOnly[key]$
     $Hidden[node]$
-    \ Testing.checkEquals<?>(tree.locate(pos),key)
+    \ Testing.checkEquals(tree.locate(pos),key)
     remaining <- remaining-1
   }
 
-  // NOTE: Order does not refine Formatted => can't use checkEmpty here.
-  \ Testing.checkEquals<?>(present(node),false)
-  \ Testing.checkEquals<?>(remaining,0)
+  \ Testing.checkEmpty(node)
+  \ Testing.checkEquals(remaining,0)
 }
 
 
diff --git a/lib/math/test/math.0rt b/lib/math/test/math.0rt
--- a/lib/math/test/math.0rt
+++ b/lib/math/test/math.0rt
@@ -21,99 +21,99 @@
 }
 
 unittest cos {
-  \ Testing.checkBetween<?>(Math.cos(2.0),-0.42,-0.41)
+  \ Testing.checkBetween(Math.cos(2.0),-0.42,-0.41)
 }
 
 unittest sin {
-  \ Testing.checkBetween<?>(Math.sin(2.0),0.90,0.91)
+  \ Testing.checkBetween(Math.sin(2.0),0.90,0.91)
 }
 
 unittest tan {
-  \ Testing.checkBetween<?>(Math.tan(2.0),-2.19,-2.18)
+  \ Testing.checkBetween(Math.tan(2.0),-2.19,-2.18)
 }
 
 unittest acos {
-  \ Testing.checkBetween<?>(Math.acos(0.5),1.04,1.05)
+  \ Testing.checkBetween(Math.acos(0.5),1.04,1.05)
 }
 
 unittest asin {
-  \ Testing.checkBetween<?>(Math.asin(0.5),0.52,0.53)
+  \ Testing.checkBetween(Math.asin(0.5),0.52,0.53)
 }
 
 unittest atan {
-  \ Testing.checkBetween<?>(Math.atan(0.5),0.46,0.47)
+  \ Testing.checkBetween(Math.atan(0.5),0.46,0.47)
 }
 
 unittest cosh {
-  \ Testing.checkBetween<?>(Math.cosh(2.0),3.76,3.77)
+  \ Testing.checkBetween(Math.cosh(2.0),3.76,3.77)
 }
 
 unittest sinh {
-  \ Testing.checkBetween<?>(Math.sinh(2.0),3.62,3.63)
+  \ Testing.checkBetween(Math.sinh(2.0),3.62,3.63)
 }
 
 unittest tanh {
-  \ Testing.checkBetween<?>(Math.tanh(2.0),0.96,0.97)
+  \ Testing.checkBetween(Math.tanh(2.0),0.96,0.97)
 }
 
 unittest acosh {
-  \ Testing.checkBetween<?>(Math.acosh(2.0),1.31,1.32)
+  \ Testing.checkBetween(Math.acosh(2.0),1.31,1.32)
 }
 
 unittest asinh {
-  \ Testing.checkBetween<?>(Math.asinh(2.0),1.44,1.45)
+  \ Testing.checkBetween(Math.asinh(2.0),1.44,1.45)
 }
 
 unittest atanh {
-  \ Testing.checkBetween<?>(Math.atanh(0.5),0.54,0.55)
+  \ Testing.checkBetween(Math.atanh(0.5),0.54,0.55)
 }
 
 unittest exp {
-  \ Testing.checkBetween<?>(Math.exp(1.0),2.71,2.72)
+  \ Testing.checkBetween(Math.exp(1.0),2.71,2.72)
 }
 
 unittest log {
-  \ Testing.checkBetween<?>(Math.log(9.0),2.19,2.20)
+  \ Testing.checkBetween(Math.log(9.0),2.19,2.20)
 }
 
 unittest log10 {
-  \ Testing.checkBetween<?>(Math.log10(100.0),1.99,2.01)
+  \ Testing.checkBetween(Math.log10(100.0),1.99,2.01)
 }
 
 unittest log2 {
-  \ Testing.checkBetween<?>(Math.log2(8.0),2.99,3.01)
+  \ Testing.checkBetween(Math.log2(8.0),2.99,3.01)
 }
 
 unittest pow {
-  \ Testing.checkBetween<?>(Math.pow(2.0,3.0),7.99,8.01)
+  \ Testing.checkBetween(Math.pow(2.0,3.0),7.99,8.01)
 }
 
 unittest sqrt {
-  \ Testing.checkBetween<?>(Math.sqrt(4.0),1.99,2.01)
+  \ Testing.checkBetween(Math.sqrt(4.0),1.99,2.01)
 }
 
 unittest ceil {
-  \ Testing.checkBetween<?>(Math.ceil(2.2),2.99,3.01)
+  \ Testing.checkBetween(Math.ceil(2.2),2.99,3.01)
 }
 
 unittest floor {
-  \ Testing.checkBetween<?>(Math.floor(2.2),1.99,2.01)
+  \ Testing.checkBetween(Math.floor(2.2),1.99,2.01)
 }
 
 unittest fmod {
-  \ Testing.checkBetween<?>(Math.fmod(7.0,4.0),2.99,3.01)
+  \ Testing.checkBetween(Math.fmod(7.0,4.0),2.99,3.01)
 }
 
 unittest trunc {
-  \ Testing.checkBetween<?>(Math.trunc(2.2),1.99,2.01)
+  \ Testing.checkBetween(Math.trunc(2.2),1.99,2.01)
 }
 
 unittest round {
-  \ Testing.checkBetween<?>(Math.round(2.7),2.99,3.01)
+  \ Testing.checkBetween(Math.round(2.7),2.99,3.01)
 }
 
 unittest fabs {
-  \ Testing.checkBetween<?>(Math.fabs(-10.0),9.99,10.01)
+  \ Testing.checkBetween(Math.fabs(-10.0),9.99,10.01)
 }
 
 unittest isinf {
@@ -129,7 +129,7 @@
 }
 
 unittest abs {
-  \ Testing.checkEquals<?>(Math.abs(-10),10)
-  \ Testing.checkEquals<?>(Math.abs(10),10)
-  \ Testing.checkEquals<?>(Math.abs(0),0)
+  \ Testing.checkEquals(Math.abs(-10),10)
+  \ Testing.checkEquals(Math.abs(10),10)
+  \ Testing.checkEquals(Math.abs(0),0)
 }
diff --git a/lib/math/test/random.0rt b/lib/math/test/random.0rt
--- a/lib/math/test/random.0rt
+++ b/lib/math/test/random.0rt
@@ -83,11 +83,11 @@
   Float sum <- 0.0
   traverse (Counter.zeroIndexed(count) -> _) {
     Float value <- random.generate()
-    \ Testing.checkGreaterThan<?>(value,0.0)
+    \ Testing.checkGreaterThan(value,0.0)
     sum <- sum+value
   }
 
-  \ Testing.checkBetween<?>(sum/count.asFloat(),0.9/lambda,1.1/lambda)
+  \ Testing.checkBetween(sum/count.asFloat(),0.9/lambda,1.1/lambda)
 }
 
 unittest gaussian { $DisableCoverage$
@@ -103,7 +103,7 @@
     sum <- sum+random.generate()
   }
 
-  \ Testing.checkBetween<?>(sum/count.asFloat(),mean-0.1*sd,mean+0.1*sd)
+  \ Testing.checkBetween(sum/count.asFloat(),mean-0.1*sd,mean+0.1*sd)
 }
 
 unittest uniform { $DisableCoverage$
@@ -117,11 +117,11 @@
   Float sum <- 0.0
   traverse (Counter.zeroIndexed(count) -> _) {
     Float value <- random.generate()
-    \ Testing.checkBetween<?>(value,min,max)
+    \ Testing.checkBetween(value,min,max)
     sum <- sum+value
   }
 
-  \ Testing.checkBetween<?>(sum/count.asFloat(),min-0.1*(max-min),max-0.1*(max-min))
+  \ Testing.checkBetween(sum/count.asFloat(),min-0.1*(max-min),max-0.1*(max-min))
 }
 
 
@@ -143,9 +143,9 @@
   random2 <- random2.setSeed(123)
   Float v4 <- random2.generate()
 
-  \ Testing.checkEquals<?>(v1 == v2,false)
-  \ Testing.checkEquals<?>(v1 == v3,false)
-  \ Testing.checkEquals<?>(v3,v4)
+  \ Testing.checkFalse(v1 == v2)
+  \ Testing.checkFalse(v1 == v3)
+  \ Testing.checkEquals(v3,v4)
 }
 
 unittest gaussian {
@@ -163,9 +163,9 @@
   random2 <- random2.setSeed(123)
   Float v4 <- random2.generate()
 
-  \ Testing.checkEquals<?>(v1 == v2,false)
-  \ Testing.checkEquals<?>(v1 == v3,false)
-  \ Testing.checkEquals<?>(v3,v4)
+  \ Testing.checkFalse(v1 == v2)
+  \ Testing.checkFalse(v1 == v3)
+  \ Testing.checkEquals(v3,v4)
 }
 
 unittest uniform {
@@ -183,9 +183,9 @@
   random2 <- random2.setSeed(123)
   Float v4 <- random2.generate()
 
-  \ Testing.checkEquals<?>(v1 == v2,false)
-  \ Testing.checkEquals<?>(v1 == v3,false)
-  \ Testing.checkEquals<?>(v3,v4)
+  \ Testing.checkFalse(v1 == v2)
+  \ Testing.checkFalse(v1 == v3)
+  \ Testing.checkEquals(v3,v4)
 }
 
 
@@ -208,15 +208,15 @@
       .append("c")
 
   Vector<String> output <- Vector:create<String>()
-  \ Randomize:permuteFrom<?>(tree,RandomUniform.new(0.0,1.0),output)
-  \ Sorting:sort<?>(output)
+  \ Randomize:permuteFrom(tree,RandomUniform.new(0.0,1.0),output)
+  \ Sorting:sort(output)
 
-  \ Testing.checkEquals<?>(output.size(),expected.size())
+  \ Testing.checkEquals(output.size(),expected.size())
   traverse (Counter.zeroIndexed(output.size()) -> Int pos) {
-    \ Testing.checkEquals<?>(output.readAt(pos),expected.readAt(pos))
+    \ Testing.checkEquals(output.readAt(pos),expected.readAt(pos))
   }
 
-  \ Testing.checkEquals<?>(tree.getWeight("a"),2)
-  \ Testing.checkEquals<?>(tree.getWeight("b"),3)
-  \ Testing.checkEquals<?>(tree.getWeight("c"),1)
+  \ Testing.checkEquals(tree.getWeight("a"),2)
+  \ Testing.checkEquals(tree.getWeight("b"),3)
+  \ Testing.checkEquals(tree.getWeight("c"),1)
 }
diff --git a/lib/math/test/token.0rt b/lib/math/test/token.0rt
new file mode 100644
--- /dev/null
+++ b/lib/math/test/token.0rt
@@ -0,0 +1,36 @@
+/* -----------------------------------------------------------------------------
+Copyright 2021 Kevin P. Barry
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+----------------------------------------------------------------------------- */
+
+// Author: Kevin P. Barry [ta0kira@gmail.com]
+
+testcase "basic Token tests" {
+  success
+}
+
+unittest equals {
+  \ Testing.checkEquals(Token.from("message"),Token.from("message"))
+  \ Testing.checkFalse(Token.from("message") `Token.equals` Token.from("other"))
+}
+
+unittest lessThan {
+  \ Testing.checkFalse(Token.from("message") `Token.lessThan` Token.from("message"))
+  \ Testing.checkTrue((Token.from("message") `Token.lessThan` Token.from("other")) ||
+                      (Token.from("other") `Token.lessThan` Token.from("message")))
+}
+
+unittest formatted {
+  \ Testing.checkEquals(Token.from("message").formatted(),"message")
+}
diff --git a/lib/math/token.0rp b/lib/math/token.0rp
new file mode 100644
--- /dev/null
+++ b/lib/math/token.0rp
@@ -0,0 +1,35 @@
+/* -----------------------------------------------------------------------------
+Copyright 2021 Kevin P. Barry
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+----------------------------------------------------------------------------- */
+
+// Author: Kevin P. Barry [ta0kira@gmail.com]
+
+// String-based token with fast comparisons.
+concrete Token {
+  immutable
+  defines Equals<Token>
+  // LessThan is not lexicographical.
+  defines LessThan<Token>
+  refines Formatted
+
+  // Get a Token from the provided String.
+  //
+  // Notes:
+  // - This function is not particularly fast. The trade-off is that Equals and
+  //   LessThan do not depend on the original strings' lengths.
+  // - All tokens created with this function will persist until the process
+  //   exits, regardless of if they are no longer in use.
+  @type from (String) -> (Token)
+}
diff --git a/lib/testing/helpers.0rp b/lib/testing/helpers.0rp
--- a/lib/testing/helpers.0rp
+++ b/lib/testing/helpers.0rp
@@ -33,8 +33,8 @@
   // Check the value for empty. Crashes if present.
   //
   // Args:
-  // - optional Formatted: Actual value.
-  @type checkEmpty (optional Formatted) -> ()
+  // - #x: Actual value.
+  @type checkEmpty<#x> (optional #x) -> ()
 
   // Check the value for present. Crashes if empty.
   //
@@ -51,6 +51,18 @@
     #x requires Formatted
     #x defines Equals<#x>
   (optional #x,optional #x) -> ()
+
+  // Check the value for true. Crashes if false.
+  //
+  // Args:
+  // - AsBool: Actual value.
+  @type checkTrue (AsBool) -> ()
+
+  // Check the value for false. Crashes if true.
+  //
+  // Args:
+  // - AsBool: Actual value.
+  @type checkFalse (AsBool) -> ()
 
   // Check that the value is within a range. Crashes if it is not in the range.
   //
diff --git a/lib/testing/src/helpers.0rx b/lib/testing/src/helpers.0rx
--- a/lib/testing/src/helpers.0rx
+++ b/lib/testing/src/helpers.0rx
@@ -1,5 +1,5 @@
 /* -----------------------------------------------------------------------------
-Copyright 2020 Kevin P. Barry
+Copyright 2020-2021 Kevin P. Barry
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -19,7 +19,7 @@
 $TestsOnly$
 
 define Testing {
-  checkEquals (x,y) {
+  checkEquals (x,y) { $NoTrace$
     if (!#x.equals(x,y)) {
       fail(String.builder()
           .append(x)
@@ -29,16 +29,22 @@
     }
   }
 
-  checkEmpty (x) {
+  checkEmpty (x) { $NoTrace$
     if (present(x)) {
-      fail(String.builder()
-          .append(require(x))
-          .append(" is not empty")
-          .build())
+      scoped {
+        optional Formatted formatted <- reduce<#x,Formatted>(require(x))
+      } in if (present(formatted)) {
+        fail(String.builder()
+            .append(require(formatted))
+            .append(" is not empty")
+            .build())
+      } else {
+        fail("(unformatted value) is not empty")
+      }
     }
   }
 
-  checkPresent (x) {
+  checkPresent (x) { $NoTrace$
     if (!present(x)) {
       fail(String.builder()
           .append("expected non-empty value")
@@ -46,9 +52,9 @@
     }
   }
 
-  checkOptional (x,y) {
+  checkOptional (x,y) { $NoTrace$
     if (present(x) && present(y)) {
-      \ checkEquals<?>(require(x),require(y))
+      \ checkEquals(require(x),require(y))
     } elif (!present(x) && present(y)) {
       fail("empty is not equal to " + require(y).formatted())
     } elif (present(x) && !present(y)) {
@@ -56,8 +62,20 @@
     }
   }
 
-  checkBetween (x,l,h) {
-    if (!lessEquals<?>(l,h)) {
+  checkTrue (x) { $NoTrace$
+    if (!x.asBool()) {
+      fail("expected true")
+    }
+  }
+
+  checkFalse (x) { $NoTrace$
+    if (x.asBool()) {
+      fail("expected false")
+    }
+  }
+
+  checkBetween (x,l,h) { $NoTrace$
+    if (!lessEquals(l,h)) {
       fail(String.builder()
           .append("Upper limit ")
           .append(l)
@@ -65,7 +83,7 @@
           .append(h)
           .build())
     }
-    if (!lessEquals<?>(l,x) || !lessEquals<?>(x,h)) {
+    if (!lessEquals(l,x) || !lessEquals(x,h)) {
       fail(String.builder()
           .append(x)
           .append(" is not between ")
@@ -76,8 +94,8 @@
     }
   }
 
-  checkGreaterThan (x,l) {
-    if (lessEquals<?>(x,l)) {
+  checkGreaterThan (x,l) { $NoTrace$
+    if (lessEquals(x,l)) {
       fail(String.builder()
           .append(x)
           .append(" is not greater than ")
@@ -86,8 +104,8 @@
     }
   }
 
-  checkLessThan (x,h) {
-    if (lessEquals<?>(h,x)) {
+  checkLessThan (x,h) { $NoTrace$
+    if (lessEquals(h,x)) {
       fail(String.builder()
           .append(x)
           .append(" is not less than ")
@@ -100,7 +118,7 @@
     #x defines Equals<#x>
     #x defines LessThan<#x>
   (#x,#x) -> (Bool)
-  lessEquals (x,y) {
+  lessEquals (x,y) { $NoTrace$
     // Using !#x.lessThan(y,x) wouldn't account for NaNs.
     return #x.lessThan(x,y) || #x.equals(x,y)
   }
diff --git a/lib/testing/test/tests.0rt b/lib/testing/test/tests.0rt
--- a/lib/testing/test/tests.0rt
+++ b/lib/testing/test/tests.0rt
@@ -1,5 +1,5 @@
 /* -----------------------------------------------------------------------------
-Copyright 2020 Kevin P. Barry
+Copyright 2020-2021 Kevin P. Barry
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -21,7 +21,7 @@
 }
 
 unittest test {
-  \ Testing.checkEquals<?>(13,13)
+  \ Testing.checkEquals(13,13)
 }
 
 
@@ -32,7 +32,7 @@
 }
 
 unittest test {
-  \ Testing.checkEquals<?>(13,15)
+  \ Testing.checkEquals(13,15)
 }
 
 
@@ -40,12 +40,17 @@
   success
 }
 
-unittest test {
+unittest formatted {
   \ Testing.checkEmpty(empty)
 }
 
+unittest notFormatted {
+  optional CharBuffer value <- empty
+  \ Testing.checkEmpty(value)
+}
 
-testcase "checkEmpty fail" {
+
+testcase "checkEmpty fail formatted" {
   crash
   require stderr "13"
   require stderr "empty"
@@ -56,6 +61,18 @@
 }
 
 
+testcase "checkEmpty fail unformatted" {
+  crash
+  require stderr "unformatted"
+  require stderr "empty"
+}
+
+unittest test {
+  CharBuffer value <- CharBuffer.new(10)
+  \ Testing.checkEmpty(value)
+}
+
+
 testcase "checkPresent success" {
   success
 }
@@ -81,7 +98,7 @@
 
 unittest test {
   \ Testing.checkOptional<Int>(empty,empty)
-  \ Testing.checkOptional<?>(13,13)
+  \ Testing.checkOptional(13,13)
 }
 
 
@@ -92,7 +109,7 @@
 }
 
 unittest test {
-  \ Testing.checkOptional<?>(13,empty)
+  \ Testing.checkOptional(13,empty)
 }
 
 
@@ -103,7 +120,7 @@
 }
 
 unittest test {
-  \ Testing.checkOptional<?>(empty,13)
+  \ Testing.checkOptional(empty,13)
 }
 
 
@@ -112,7 +129,7 @@
 }
 
 unittest test {
-  \ Testing.checkBetween<?>(13,13,15)
+  \ Testing.checkBetween(13,13,15)
 }
 
 
@@ -124,7 +141,7 @@
 }
 
 unittest test {
-  \ Testing.checkBetween<?>(13,14,15)
+  \ Testing.checkBetween(13,14,15)
 }
 
 
@@ -136,7 +153,7 @@
 }
 
 unittest test {
-  \ Testing.checkBetween<?>(14,15,13)
+  \ Testing.checkBetween(14,15,13)
 }
 
 
@@ -145,7 +162,7 @@
 }
 
 unittest test {
-  \ Testing.checkGreaterThan<?>(13,11)
+  \ Testing.checkGreaterThan(13,11)
 }
 
 
@@ -156,7 +173,7 @@
 }
 
 unittest test {
-  \ Testing.checkGreaterThan<?>(13,14)
+  \ Testing.checkGreaterThan(13,14)
 }
 
 
@@ -165,7 +182,7 @@
 }
 
 unittest test {
-  \ Testing.checkLessThan<?>(13,14)
+  \ Testing.checkLessThan(13,14)
 }
 
 
@@ -176,5 +193,43 @@
 }
 
 unittest test {
-  \ Testing.checkLessThan<?>(13,11)
+  \ Testing.checkLessThan(13,11)
+}
+
+
+testcase "checkTrue success" {
+  success
+}
+
+unittest test {
+  \ Testing.checkTrue(1)
+}
+
+
+testcase "checkTrue fail" {
+  crash
+  require stderr "true"
+}
+
+unittest test {
+  \ Testing.checkTrue(0)
+}
+
+
+testcase "checkFalse success" {
+  success
+}
+
+unittest test {
+  \ Testing.checkFalse(0)
+}
+
+
+testcase "checkFalse fail" {
+  crash
+  require stderr "false"
+}
+
+unittest test {
+  \ Testing.checkFalse(1)
 }
diff --git a/lib/thread/src/Extension_Enumerated.cpp b/lib/thread/src/Extension_Enumerated.cpp
--- a/lib/thread/src/Extension_Enumerated.cpp
+++ b/lib/thread/src/Extension_Enumerated.cpp
@@ -115,7 +115,7 @@
   inline ExtValue_EnumeratedWait(S<const Type_EnumeratedWait> p, S<Barrier> b, int i)
     : Value_EnumeratedWait(std::move(p)), barrier(b), index(i) {}
 
-  ReturnTuple Call_wait(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_wait(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("EnumeratedWait.wait")
     barrier->Wait(index);
     return ReturnTuple(VAR_SELF);
@@ -165,9 +165,9 @@
 struct ExtType_EnumeratedBarrier : public Type_EnumeratedBarrier {
   inline ExtType_EnumeratedBarrier(Category_EnumeratedBarrier& p, Params<0>::Type params) : Type_EnumeratedBarrier(p, params) {}
 
-  ReturnTuple Call_new(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_new(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("EnumeratedBarrier.new")
-    const PrimInt Var_arg1 = (args.At(0)).AsInt();
+    const PrimInt Var_arg1 = (params_args.GetArg(0)).AsInt();
     if (Var_arg1 < 0) {
       FAIL() << "Invalid barrier thread count " << Var_arg1;
     }
@@ -186,16 +186,16 @@
   inline ExtValue_EnumeratedBarrier(S<const Type_EnumeratedBarrier> p, std::vector<BoxedValue> w)
     : Value_EnumeratedBarrier(std::move(p)), waits(std::move(w)) {}
 
-  ReturnTuple Call_readAt(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_readAt(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("EnumeratedBarrier.readAt")
-    const PrimInt Var_arg1 = (args.At(0)).AsInt();
+    const PrimInt Var_arg1 = (params_args.GetArg(0)).AsInt();
     if (Var_arg1 < 0 || Var_arg1 >= waits.size()) {
       FAIL() << "index " << Var_arg1 << " is out of bounds";
     }
     return ReturnTuple(waits[Var_arg1]);
   }
 
-  ReturnTuple Call_size(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_size(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("EnumeratedBarrier.size")
     return ReturnTuple(Box_Int(waits.size()));
   }
diff --git a/lib/thread/src/Extension_ProcessThread.cpp b/lib/thread/src/Extension_ProcessThread.cpp
--- a/lib/thread/src/Extension_ProcessThread.cpp
+++ b/lib/thread/src/Extension_ProcessThread.cpp
@@ -31,7 +31,7 @@
 namespace ZEOLITE_PUBLIC_NAMESPACE {
 #endif  // ZEOLITE_PUBLIC_NAMESPACE
 
-BoxedValue CreateValue_ProcessThread(S<const Type_ProcessThread> parent, const ValueTuple& args);
+BoxedValue CreateValue_ProcessThread(S<const Type_ProcessThread> parent, const ParamsArgs& params_args);
 
 struct ExtCategory_ProcessThread : public Category_ProcessThread {
 };
@@ -39,17 +39,17 @@
 struct ExtType_ProcessThread : public Type_ProcessThread {
   inline ExtType_ProcessThread(Category_ProcessThread& p, Params<0>::Type params) : Type_ProcessThread(p, params) {}
 
-  ReturnTuple Call_from(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_from(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("ProcessThread.from")
-    return ReturnTuple(CreateValue_ProcessThread(PARAM_SELF, args));
+    return ReturnTuple(CreateValue_ProcessThread(PARAM_SELF, params_args));
   }
 };
 
 struct ExtValue_ProcessThread : public Value_ProcessThread {
-  inline ExtValue_ProcessThread(S<const Type_ProcessThread> p, const ValueTuple& args)
-    : Value_ProcessThread(std::move(p)), routine(args.Only()) {}
+  inline ExtValue_ProcessThread(S<const Type_ProcessThread> p, const ParamsArgs& params_args)
+    : Value_ProcessThread(std::move(p)), routine(params_args.GetArg(0)) {}
 
-  ReturnTuple Call_detach(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_detach(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("ProcessThread.detach")
     S<std::thread> temp = thread;
     thread = nullptr;
@@ -61,12 +61,12 @@
     return ReturnTuple(VAR_SELF);
   }
 
-  ReturnTuple Call_isRunning(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_isRunning(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("ProcessThread.isRunning")
     return ReturnTuple(Box_Bool(isJoinable(thread.get())));
   }
 
-  ReturnTuple Call_join(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_join(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("ProcessThread.join")
     S<std::thread> temp = thread;
     thread = nullptr;
@@ -78,7 +78,7 @@
     return ReturnTuple(VAR_SELF);
   }
 
-  ReturnTuple Call_start(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_start(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("ProcessThread.start")
     if (isJoinable(thread.get())) {
       FAIL() << "thread is already running";
@@ -90,7 +90,7 @@
       thread.reset(new std::thread(
         capture_thread::ThreadCrosser::WrapCall([this,self] {
           TRACE_CREATION
-          TypeValue::Call(routine, Function_Routine_run, ParamTuple(), ArgTuple());
+          TypeValue::Call(routine, Function_Routine_run, PassParamsArgs());
         })));
     }
     return ReturnTuple(VAR_SELF);
@@ -126,8 +126,8 @@
 
 void RemoveType_ProcessThread(const Params<0>::Type& params) {}
 
-BoxedValue CreateValue_ProcessThread(S<const Type_ProcessThread> parent, const ValueTuple& args) {
-  return BoxedValue::New<ExtValue_ProcessThread>(std::move(parent), args);
+BoxedValue CreateValue_ProcessThread(S<const Type_ProcessThread> parent, const ParamsArgs& params_args) {
+  return BoxedValue::New<ExtValue_ProcessThread>(std::move(parent), params_args);
 }
 
 #ifdef ZEOLITE_PUBLIC_NAMESPACE
diff --git a/lib/thread/src/Extension_Realtime.cpp b/lib/thread/src/Extension_Realtime.cpp
--- a/lib/thread/src/Extension_Realtime.cpp
+++ b/lib/thread/src/Extension_Realtime.cpp
@@ -44,18 +44,18 @@
 struct ExtType_Realtime : public Type_Realtime {
   inline ExtType_Realtime(Category_Realtime& p, Params<0>::Type params) : Type_Realtime(p, params) {}
 
-  ReturnTuple Call_sleepSeconds(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_sleepSeconds(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Realtime.sleepSeconds")
-    const PrimFloat Var_arg1 = (args.At(0)).AsFloat();
+    const PrimFloat Var_arg1 = (params_args.GetArg(0)).AsFloat();
     if (Var_arg1 > 0) {
       std::this_thread::sleep_for(std::chrono::duration<double>(Var_arg1));
     }
     return ReturnTuple();
   }
 
-  ReturnTuple Call_sleepSecondsPrecise(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_sleepSecondsPrecise(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Realtime.sleepSecondsPrecise")
-    const PrimFloat Var_arg1 = (args.At(0)).AsFloat();
+    const PrimFloat Var_arg1 = (params_args.GetArg(0)).AsFloat();
 
     const auto spinlock_limit = std::chrono::duration<double>(SLEEP_SPINLOCK_LIMIT);
 
@@ -78,7 +78,7 @@
     return ReturnTuple();
   }
 
-  ReturnTuple Call_monoSeconds(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_monoSeconds(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Realtime.monoSeconds")
     const auto time = std::chrono::steady_clock::now().time_since_epoch();
     const PrimFloat seconds =
diff --git a/lib/thread/src/Extension_ThreadCondition.cpp b/lib/thread/src/Extension_ThreadCondition.cpp
--- a/lib/thread/src/Extension_ThreadCondition.cpp
+++ b/lib/thread/src/Extension_ThreadCondition.cpp
@@ -40,7 +40,7 @@
 struct ExtType_ThreadCondition : public Type_ThreadCondition {
   inline ExtType_ThreadCondition(Category_ThreadCondition& p, Params<0>::Type params) : Type_ThreadCondition(p, params) {}
 
-  ReturnTuple Call_new(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_new(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("ThreadCondition.new")
     return ReturnTuple(CreateValue_ThreadCondition(PARAM_SELF));
   }
@@ -57,7 +57,7 @@
     pthread_mutex_init(&mutex, &mutex_attr);
   }
 
-  ReturnTuple Call_lock(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_lock(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("ThreadCondition.lock")
     int error = 0;
     if ((error = pthread_mutex_lock(&mutex)) != 0) {
@@ -66,7 +66,7 @@
     return ReturnTuple(VAR_SELF);
   }
 
-  ReturnTuple Call_resumeAll(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_resumeAll(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("ThreadCondition.resumeAll")
     int error = 0;
     if ((error = pthread_cond_broadcast(&cond)) != 0) {
@@ -75,7 +75,7 @@
     return ReturnTuple(VAR_SELF);
   }
 
-  ReturnTuple Call_resumeOne(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_resumeOne(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("ThreadCondition.resumeOne")
     int error = 0;
     if ((error = pthread_cond_signal(&cond)) != 0) {
@@ -84,10 +84,10 @@
     return ReturnTuple(VAR_SELF);
   }
 
-  ReturnTuple Call_timedWait(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_timedWait(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("ThreadCondition.timedWait")
     int error = 0;
-    const PrimFloat Var_arg1 = (args.At(0)).AsFloat();
+    const PrimFloat Var_arg1 = (params_args.GetArg(0)).AsFloat();
     if (Var_arg1 < 0) {
       FAIL() << "Bad wait time " << Var_arg1;
     }
@@ -107,7 +107,7 @@
     return ReturnTuple(Box_Bool(true));
   }
 
-  ReturnTuple Call_unlock(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_unlock(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("ThreadCondition.unlock")
     int error = 0;
     if ((error = pthread_mutex_unlock(&mutex)) != 0) {
@@ -116,7 +116,7 @@
     return ReturnTuple(VAR_SELF);
   }
 
-  ReturnTuple Call_wait(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_wait(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("ThreadCondition.wait")
     int error = 0;
     if ((error = pthread_cond_wait(&cond, &mutex)) != 0) {
diff --git a/lib/thread/test/barrier.0rt b/lib/thread/test/barrier.0rt
--- a/lib/thread/test/barrier.0rt
+++ b/lib/thread/test/barrier.0rt
@@ -22,7 +22,7 @@
 
 unittest correctCount {
   ReadAt<BarrierWait> barriers <- EnumeratedBarrier.new(13)
-  \ Testing.checkEquals<?>(barriers.size(),13)
+  \ Testing.checkEquals(barriers.size(),13)
 }
 
 unittest waitForOneThread {
@@ -32,7 +32,7 @@
 
 unittest zeroAllowed {
   ReadAt<BarrierWait> barriers <- EnumeratedBarrier.new(0)
-  \ Testing.checkEquals<?>(barriers.size(),0)
+  \ Testing.checkEquals(barriers.size(),0)
 }
 
 unittest waitForMultipleThreads {
@@ -44,10 +44,10 @@
   Thread thread2 <- ProcessThread.from(WaitAndIncrement.create(2,value,mutex,barriers.readAt(2))).start()
 
   \ barriers.readAt(0).wait()
-  \ Testing.checkEquals<?>(value.get(),2)
+  \ Testing.checkEquals(value.get(),2)
 
   \ barriers.readAt(0).wait()
-  \ Testing.checkEquals<?>(value.get(),4)
+  \ Testing.checkEquals(value.get(),4)
 
   \ thread1.join()
   \ thread2.join()
diff --git a/lib/thread/test/condition.0rt b/lib/thread/test/condition.0rt
--- a/lib/thread/test/condition.0rt
+++ b/lib/thread/test/condition.0rt
@@ -42,7 +42,7 @@
   \ thread1.join()
   \ thread2.join()
 
-  \ Testing.checkEquals<?>(value.get(),4)
+  \ Testing.checkEquals(value.get(),4)
 }
 
 unittest resumeOne {
@@ -69,7 +69,7 @@
   \ thread1.join()
   \ thread2.join()
 
-  \ Testing.checkEquals<?>(value.get(),3)
+  \ Testing.checkEquals(value.get(),3)
 }
 
 unittest forceWaitTimeout {
@@ -82,7 +82,7 @@
   \ cond.resumeOne()
   \ thread.join()
 
-  \ Testing.checkEquals<?>(value.get(),1)
+  \ Testing.checkEquals(value.get(),1)
 }
 
 unittest waitNoTimeout {
diff --git a/lib/thread/test/thread.0rt b/lib/thread/test/thread.0rt
--- a/lib/thread/test/thread.0rt
+++ b/lib/thread/test/thread.0rt
@@ -22,11 +22,11 @@
 
 unittest test {
   CountLoop counter <- CountLoop.create(10000)
-  \ Testing.checkEquals<?>(counter.get(),0)
+  \ Testing.checkEquals(counter.get(),0)
   \ counter.start()
-  \ Testing.checkEquals<?>(counter.get(),10000)
+  \ Testing.checkEquals(counter.get(),10000)
   \ counter.start()
-  \ Testing.checkEquals<?>(counter.get(),20000)
+  \ Testing.checkEquals(counter.get(),20000)
 }
 
 concrete CountLoop {
@@ -89,7 +89,7 @@
   Thread oddThread  <- ProcessThread.from(EvenOrOdd.create(mutex,false,10,value)).start()
   \ evenThread.join()
   \ oddThread.join()
-  \ Testing.checkEquals<?>(value.get(),20)
+  \ Testing.checkEquals(value.get(),20)
 }
 
 concrete EvenOrOdd {
@@ -163,7 +163,7 @@
   }
 
   run () {
-    \ Testing.checkEquals<?>(Argv.global().readAt(1),"arg1")
+    \ Testing.checkEquals(Argv.global().readAt(1),"arg1")
   }
 }
 
diff --git a/lib/thread/test/time.0rt b/lib/thread/test/time.0rt
--- a/lib/thread/test/time.0rt
+++ b/lib/thread/test/time.0rt
@@ -68,12 +68,12 @@
   Float start <- Realtime.monoSeconds()
   \ Realtime.sleepSeconds(0.5)
   Float stop <- Realtime.monoSeconds()
-  \ Testing.checkBetween<?>(stop-start,0.5,0.6)
+  \ Testing.checkBetween(stop-start,0.5,0.6)
 }
 
 unittest testPrecise {
   Float start <- Realtime.monoSeconds()
   \ Realtime.sleepSecondsPrecise(0.5)
   Float stop <- Realtime.monoSeconds()
-  \ Testing.checkBetween<?>(stop-start,0.5,0.51)
+  \ Testing.checkBetween(stop-start,0.5,0.51)
 }
diff --git a/lib/util/helpers.0rp b/lib/util/helpers.0rp
--- a/lib/util/helpers.0rp
+++ b/lib/util/helpers.0rp
@@ -26,7 +26,7 @@
   //
   // Example:
   //
-  //   Bool lt <- x.defaultOrder() `OrderH:lessThan<?>` y.defaultOrder()
+  //   Bool lt <- x.defaultOrder() `OrderH:lessThan` y.defaultOrder()
   @category lessThan<#x>
     #x defines LessThan<#x>
   (optional Order<#x>,optional Order<#x>) -> (Bool)
@@ -48,7 +48,7 @@
   //
   // Example:
   //
-  //   Bool eq <- x.defaultOrder() `OrderH:equals<?>` y.defaultOrder()
+  //   Bool eq <- x.defaultOrder() `OrderH:equals` y.defaultOrder()
   @category equals<#x>
     #x defines Equals<#x>
   (optional Order<#x>,optional Order<#x>) -> (Bool)
@@ -77,7 +77,7 @@
   //
   // Example:
   //
-  //   Bool lt <- x `ReadAtH:lessThan<?>` y
+  //   Bool lt <- x `ReadAtH:lessThan` y
   @category lessThan<#x>
     #x defines LessThan<#x>
   (ReadAt<#x>,ReadAt<#x>) -> (Bool)
@@ -99,7 +99,7 @@
   //
   // Example:
   //
-  //   Bool eq <- x `ReadAtH:equals<?>` y
+  //   Bool eq <- x `ReadAtH:equals` y
   @category equals<#x>
     #x defines Equals<#x>
   (ReadAt<#x>,ReadAt<#x>) -> (Bool)
diff --git a/lib/util/src/Extension_Argv.cpp b/lib/util/src/Extension_Argv.cpp
--- a/lib/util/src/Extension_Argv.cpp
+++ b/lib/util/src/Extension_Argv.cpp
@@ -35,7 +35,7 @@
 struct ExtType_Argv : public Type_Argv {
   inline ExtType_Argv(Category_Argv& p, Params<0>::Type params) : Type_Argv(p, params) {}
 
-  ReturnTuple Call_global(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_global(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Argv.global")
     return ReturnTuple(Var_global);
   }
@@ -45,21 +45,21 @@
   inline ExtValue_Argv(S<const Type_Argv> p, int st, int sz)
     : Value_Argv(std::move(p)), start(st), size(sz) {}
 
-  ReturnTuple Call_readAt(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_readAt(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Argv.readAt")
-    const PrimInt Var_arg1 = (args.At(0)).AsInt();
+    const PrimInt Var_arg1 = (params_args.GetArg(0)).AsInt();
     return ReturnTuple(Box_String(Argv::GetArgAt(start + Var_arg1)));
   }
 
-  ReturnTuple Call_size(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_size(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Argv.size")
     return ReturnTuple(Box_Int(GetSize()));
   }
 
-  ReturnTuple Call_subSequence(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_subSequence(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Argv.subSequence")
-    const PrimInt Var_arg1 = (args.At(0)).AsInt();
-    const PrimInt Var_arg2 = (args.At(1)).AsInt();
+    const PrimInt Var_arg1 = (params_args.GetArg(0)).AsInt();
+    const PrimInt Var_arg2 = (params_args.GetArg(1)).AsInt();
     if (Var_arg1 < 0 || (Var_arg1 > 0 && Var_arg1 >= GetSize())) {
       FAIL() << "index " << Var_arg1 << " is out of bounds";
     }
diff --git a/lib/util/src/Extension_MutexLock.cpp b/lib/util/src/Extension_MutexLock.cpp
--- a/lib/util/src/Extension_MutexLock.cpp
+++ b/lib/util/src/Extension_MutexLock.cpp
@@ -28,7 +28,7 @@
 namespace ZEOLITE_PUBLIC_NAMESPACE {
 #endif  // ZEOLITE_PUBLIC_NAMESPACE
 
-BoxedValue CreateValue_MutexLock(S<const Type_MutexLock> parent, const ValueTuple& args);
+BoxedValue CreateValue_MutexLock(S<const Type_MutexLock> parent, const BoxedValue& mutex);
 
 struct ExtCategory_MutexLock : public Category_MutexLock {
 };
@@ -36,41 +36,41 @@
 struct ExtType_MutexLock : public Type_MutexLock {
   inline ExtType_MutexLock(Category_MutexLock& p, Params<0>::Type params) : Type_MutexLock(p, params) {}
 
-  ReturnTuple Call_lock(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_lock(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("MutexLock.lock")
-    return ReturnTuple(CreateValue_MutexLock(CreateType_MutexLock(Params<0>::Type()), args));
+    return ReturnTuple(CreateValue_MutexLock(CreateType_MutexLock(Params<0>::Type()), params_args.GetArg(0)));
   }
 };
 
 struct ExtValue_MutexLock : public Value_MutexLock {
-  inline ExtValue_MutexLock(S<const Type_MutexLock> p, const ValueTuple& args)
-    : Value_MutexLock(std::move(p)), mutex(args.Only()) {
-    TypeValue::Call(BoxedValue::Require(mutex), Function_Mutex_lock, ParamTuple(), ArgTuple());
+  inline ExtValue_MutexLock(S<const Type_MutexLock> p, const BoxedValue& mutex)
+    : Value_MutexLock(std::move(p)), mutex_(mutex) {
+    TypeValue::Call(BoxedValue::Require(mutex_), Function_Mutex_lock, PassParamsArgs());
   }
 
-  ReturnTuple Call_freeResource(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_freeResource(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("MutexLock.freeResource")
     TRACE_CREATION
-    if (!BoxedValue::Present(mutex)) {
+    if (!BoxedValue::Present(mutex_)) {
       FAIL() << "MutexLock freed multiple times";
     } else {
-      TypeValue::Call(mutex, Function_Mutex_unlock, ParamTuple(), ArgTuple());
-      mutex = Var_empty;
+      TypeValue::Call(mutex_, Function_Mutex_unlock, PassParamsArgs());
+      mutex_ = Var_empty;
     }
     return ReturnTuple();
   }
 
   ~ExtValue_MutexLock() {
-    if (BoxedValue::Present(mutex)) {
+    if (BoxedValue::Present(mutex_)) {
       TRACE_CREATION
-      TypeValue::Call(BoxedValue::Require(mutex), Function_Mutex_unlock, ParamTuple(), ArgTuple());
-      mutex = Var_empty;
+      TypeValue::Call(BoxedValue::Require(mutex_), Function_Mutex_unlock, PassParamsArgs());
+      mutex_ = Var_empty;
       FAIL() << "MutexLock not freed with freeResource()";
     }
   }
 
   // optional Mutex
-  BoxedValue mutex;
+  BoxedValue mutex_;
   CAPTURE_CREATION("MutexLock")
 };
 
@@ -86,8 +86,8 @@
 
 void RemoveType_MutexLock(const Params<0>::Type& params) {}
 
-BoxedValue CreateValue_MutexLock(S<const Type_MutexLock> parent, const ValueTuple& args) {
-  return BoxedValue::New<ExtValue_MutexLock>(std::move(parent), args);
+BoxedValue CreateValue_MutexLock(S<const Type_MutexLock> parent, const BoxedValue& mutex) {
+  return BoxedValue::New<ExtValue_MutexLock>(std::move(parent), mutex);
 }
 
 #ifdef ZEOLITE_PUBLIC_NAMESPACE
diff --git a/lib/util/src/Extension_SimpleInput.cpp b/lib/util/src/Extension_SimpleInput.cpp
--- a/lib/util/src/Extension_SimpleInput.cpp
+++ b/lib/util/src/Extension_SimpleInput.cpp
@@ -38,26 +38,26 @@
 struct ExtType_SimpleInput : public Type_SimpleInput {
   inline ExtType_SimpleInput(Category_SimpleInput& p, Params<0>::Type params) : Type_SimpleInput(p, params) {}
 
-  ReturnTuple Call_stdin(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_stdin(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("SimpleInput.stdin")
     return ReturnTuple(Var_stdin);
   }
 };
 
 struct ExtValue_SimpleInput : public Value_SimpleInput {
-  inline ExtValue_SimpleInput(S<const Type_SimpleInput> p, const ValueTuple& args)
+  inline ExtValue_SimpleInput(S<const Type_SimpleInput> p)
     : Value_SimpleInput(std::move(p)) {}
 
-  ReturnTuple Call_pastEnd(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_pastEnd(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("SimpleInput.pastEnd")
     std::lock_guard<std::mutex> lock(mutex);
     return ReturnTuple(Box_Bool(zero_read));
   }
 
-  ReturnTuple Call_readBlock(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_readBlock(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("SimpleInput.readBlock")
     std::lock_guard<std::mutex> lock(mutex);
-    const int size = args.At(0).AsInt();
+    const int size = params_args.GetArg(0).AsInt();
     if (size < 0) {
       FAIL() << "Read size " << size << " is invalid";
     }
@@ -76,7 +76,7 @@
 };
 
 namespace {
-const BoxedValue Var_stdin = BoxedValue::New<ExtValue_SimpleInput>(CreateType_SimpleInput(Params<0>::Type()), ArgTuple());
+const BoxedValue Var_stdin = BoxedValue::New<ExtValue_SimpleInput>(CreateType_SimpleInput(Params<0>::Type()));
 }  // namespace
 
 Category_SimpleInput& CreateCategory_SimpleInput() {
diff --git a/lib/util/src/Extension_SimpleMutex.cpp b/lib/util/src/Extension_SimpleMutex.cpp
--- a/lib/util/src/Extension_SimpleMutex.cpp
+++ b/lib/util/src/Extension_SimpleMutex.cpp
@@ -35,7 +35,7 @@
 struct ExtType_SimpleMutex : public Type_SimpleMutex {
   inline ExtType_SimpleMutex(Category_SimpleMutex& p, Params<0>::Type params) : Type_SimpleMutex(p, params) {}
 
-  ReturnTuple Call_new(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_new(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("SimpleMutex.new")
     return ReturnTuple(CreateValue_SimpleMutex(CreateType_SimpleMutex(Params<0>::Type())));
   }
@@ -45,13 +45,13 @@
   inline ExtValue_SimpleMutex(S<const Type_SimpleMutex> p)
     : Value_SimpleMutex(std::move(p)) {}
 
-  ReturnTuple Call_lock(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_lock(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("SimpleMutex.lock")
     mutex.lock();
     return ReturnTuple(VAR_SELF);
   }
 
-  ReturnTuple Call_unlock(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_unlock(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("SimpleMutex.unlock")
     mutex.unlock();
     return ReturnTuple(VAR_SELF);
diff --git a/lib/util/src/Extension_SimpleOutput.cpp b/lib/util/src/Extension_SimpleOutput.cpp
--- a/lib/util/src/Extension_SimpleOutput.cpp
+++ b/lib/util/src/Extension_SimpleOutput.cpp
@@ -41,17 +41,17 @@
 struct ExtType_SimpleOutput : public Type_SimpleOutput {
   inline ExtType_SimpleOutput(Category_SimpleOutput& p, Params<0>::Type params) : Type_SimpleOutput(p, params) {}
 
-  ReturnTuple Call_error(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_error(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("SimpleOutput.error")
     return ReturnTuple(Var_error);
   }
 
-  ReturnTuple Call_stderr(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_stderr(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("SimpleOutput.stderr")
     return ReturnTuple(Var_stderr);
   }
 
-  ReturnTuple Call_stdout(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_stdout(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("SimpleOutput.stdout")
     return ReturnTuple(Var_stdout);
   }
@@ -69,19 +69,19 @@
   inline ExtValue_SimpleOutput(S<const Type_SimpleOutput> p, S<Writer> w)
     : Value_SimpleOutput(std::move(p)), writer(w) {}
 
-  ReturnTuple Call_flush(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_flush(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("SimpleOutput.flush")
     std::lock_guard<std::mutex> lock(mutex);
     writer->Flush();
     return ReturnTuple();
   }
 
-  ReturnTuple Call_write(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_write(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("SimpleOutput.write")
-    const BoxedValue& Var_arg1 = (args.At(0));
+    const BoxedValue& Var_arg1 = (params_args.GetArg(0));
     std::lock_guard<std::mutex> lock(mutex);
-    writer->Write(TypeValue::Call(args.At(0), Function_Formatted_formatted,
-                                  ParamTuple(), ArgTuple()).Only().AsString());
+    writer->Write(TypeValue::Call(params_args.GetArg(0), Function_Formatted_formatted,
+                                  PassParamsArgs()).At(0).AsString());
     return ReturnTuple();
   }
 
diff --git a/lib/util/src/Extension_SpinlockMutex.cpp b/lib/util/src/Extension_SpinlockMutex.cpp
--- a/lib/util/src/Extension_SpinlockMutex.cpp
+++ b/lib/util/src/Extension_SpinlockMutex.cpp
@@ -35,7 +35,7 @@
 struct ExtType_SpinlockMutex : public Type_SpinlockMutex {
   inline ExtType_SpinlockMutex(Category_SpinlockMutex& p, Params<0>::Type params) : Type_SpinlockMutex(p, params) {}
 
-  ReturnTuple Call_new(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_new(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("SpinlockMutex.new")
     return ReturnTuple(CreateValue_SpinlockMutex(CreateType_SpinlockMutex(Params<0>::Type())));
   }
@@ -45,13 +45,13 @@
   inline ExtValue_SpinlockMutex(S<const Type_SpinlockMutex> p)
     : Value_SpinlockMutex(std::move(p)) {}
 
-  ReturnTuple Call_lock(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_lock(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("SpinlockMutex.lock")
     while (flag.test_and_set(std::memory_order_acquire));
     return ReturnTuple(VAR_SELF);
   }
 
-  ReturnTuple Call_unlock(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_unlock(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("SpinlockMutex.unlock")
     flag.clear(std::memory_order_release);
     return ReturnTuple(VAR_SELF);
diff --git a/lib/util/src/counters.0rx b/lib/util/src/counters.0rx
--- a/lib/util/src/counters.0rx
+++ b/lib/util/src/counters.0rx
@@ -154,12 +154,12 @@
 }
 
 define Ranges {
-  min (x,y) (z) {
-    z, _ <- minMax<?>(x,y)
+  min (x,y) {
+    return minMax(x,y){0}
   }
 
   max (x,y) (z) {
-    _, z <- minMax<?>(x,y)
+    return minMax(x,y){1}
   }
 
   minMax (x,y) {
diff --git a/lib/util/src/helpers.0rx b/lib/util/src/helpers.0rx
--- a/lib/util/src/helpers.0rx
+++ b/lib/util/src/helpers.0rx
@@ -66,7 +66,7 @@
   }
 
   lessThanWith (x,y) {
-    traverse (Counter.zeroIndexed(Ranges:min<?>(x.size(),y.size())) -> Int i) {
+    traverse (Counter.zeroIndexed(Ranges:min(x.size(),y.size())) -> Int i) {
       $ReadOnly[i]$
       if (x.readAt(i) `#xx.lessThan` y.readAt(i)) {
         return true
diff --git a/lib/util/src/parse.0rx b/lib/util/src/parse.0rx
--- a/lib/util/src/parse.0rx
+++ b/lib/util/src/parse.0rx
@@ -145,7 +145,7 @@
       }
     }
 
-    return ErrorOr:value<?>(total)
+    return ErrorOr:value(total)
   }
 
   int (data) {
@@ -177,7 +177,7 @@
       return ErrorOr:error("missing digits in Int value")
     }
 
-    return ErrorOr:value<?>(sign*total)
+    return ErrorOr:value(sign*total)
   }
 
   hexInt (data) {
@@ -219,7 +219,7 @@
       return ErrorOr:error("missing digits in Int value")
     }
 
-    return ErrorOr:value<?>(sign*total)
+    return ErrorOr:value(sign*total)
   }
 
   octInt (data) {
@@ -251,7 +251,7 @@
       return ErrorOr:error("missing digits in Int value")
     }
 
-    return ErrorOr:value<?>(sign*total)
+    return ErrorOr:value(sign*total)
   }
 
   binInt (data) {
@@ -283,7 +283,7 @@
       return ErrorOr:error("missing digits in Int value")
     }
 
-    return ErrorOr:value<?>(sign*total)
+    return ErrorOr:value(sign*total)
   }
 
   @type autoFail<#x> (Char) -> (ErrorOr<all>)
diff --git a/lib/util/src/testing.0rx b/lib/util/src/testing.0rx
--- a/lib/util/src/testing.0rx
+++ b/lib/util/src/testing.0rx
@@ -37,7 +37,7 @@
           .append(x.getError())
           .build())
     } else {
-      \ Testing.checkEquals<?>(x.getValue(),y)
+      \ Testing.checkEquals(x.getValue(),y)
     }
   }
 
@@ -52,7 +52,7 @@
           .append(x.getError())
           .build())
     } else {
-      \ Testing.checkBetween<?>(x.getValue(),l,h)
+      \ Testing.checkBetween(x.getValue(),l,h)
     }
   }
 }
diff --git a/lib/util/test/counters.0rt b/lib/util/test/counters.0rt
--- a/lib/util/test/counters.0rt
+++ b/lib/util/test/counters.0rt
@@ -24,11 +24,11 @@
   Int index <- 0
 
   traverse (Counter.zeroIndexed(10) -> Int i) {
-    \ Testing.checkEquals<?>(i,index)
+    \ Testing.checkEquals(i,index)
     index <- index+1
   }
 
-  \ Testing.checkEquals<?>(index,10)
+  \ Testing.checkEquals(index,10)
 }
 
 unittest revZeroIndexed {
@@ -36,81 +36,81 @@
 
   traverse (Counter.revZeroIndexed(10) -> Int i) {
     index <- index-1
-    \ Testing.checkEquals<?>(i,index)
+    \ Testing.checkEquals(i,index)
   }
 
-  \ Testing.checkEquals<?>(index,0)
+  \ Testing.checkEquals(index,0)
 }
 
 unittest unlimitedCount {
   Int index <- 0
 
   traverse (Counter.unlimited() -> Int i) {
-    \ Testing.checkEquals<?>(i,index)
+    \ Testing.checkEquals(i,index)
     if ((index <- index+1) >= 10) {
       break
     }
   }
 
-  \ Testing.checkEquals<?>(index,10)
+  \ Testing.checkEquals(index,10)
 }
 
 unittest times {
   Int index <- 0
 
-  traverse ("message" `Repeat:times<?>` 10 -> String m) {
-    \ Testing.checkEquals<?>(m,"message")
+  traverse ("message" `Repeat:times` 10 -> String m) {
+    \ Testing.checkEquals(m,"message")
     index <- index+1
   }
 
-  \ Testing.checkEquals<?>(index,10)
+  \ Testing.checkEquals(index,10)
 }
 
 unittest unlimitedValue {
   Int index <- 0
 
-  traverse (Repeat:unlimited<?>("message") -> String m) {
-    \ Testing.checkEquals<?>(m,"message")
+  traverse (Repeat:unlimited("message") -> String m) {
+    \ Testing.checkEquals(m,"message")
     if ((index <- index+1) >= 10) {
       break
     }
   }
 
-  \ Testing.checkEquals<?>(index,10)
+  \ Testing.checkEquals(index,10)
 }
 
 unittest minMax {
   scoped {
-    Int x, Int y <- Ranges:minMax<?>(10,3)
+    Int x, Int y <- Ranges:minMax(10,3)
   } in {
-    \ Testing.checkEquals<?>(x,3)
-    \ Testing.checkEquals<?>(y,10)
+    \ Testing.checkEquals(x,3)
+    \ Testing.checkEquals(y,10)
   }
 
   scoped {
-    Int x, Int y <- Ranges:minMax<?>(3,10)
+    Int x, Int y <- Ranges:minMax(3,10)
   } in {
-    \ Testing.checkEquals<?>(x,3)
-    \ Testing.checkEquals<?>(y,10)
+    \ Testing.checkEquals(x,3)
+    \ Testing.checkEquals(y,10)
   }
 }
 
 unittest min {
   scoped {
-    Int x <- Ranges:min<?>(10,3)
-  } in \ Testing.checkEquals<?>(x,3)
+    Int x <- Ranges:min(10,3)
+  } in \ Testing.checkEquals(x,3)
 
   scoped {
-    Int x <- Ranges:min<?>(3,10)
-  } in \ Testing.checkEquals<?>(x,3)
+    Int x <- Ranges:min(3,10)
+  } in \ Testing.checkEquals(x,3)
 }
 
 unittest max {
   scoped {
-    Int x <- Ranges:max<?>(10,3)
-  } in \ Testing.checkEquals<?>(x,10)
+    Int x <- Ranges:max(10,3)
+  } in \ Testing.checkEquals(x,10)
 
   scoped {
-    Int x <- Ranges:max<?>(3,10)
-  } in \ Testing.checkEquals<?>(x,10)
+    Int x <- Ranges:max(3,10)
+  } in \ Testing.checkEquals(x,10)
 }
diff --git a/lib/util/test/extra.0rt b/lib/util/test/extra.0rt
--- a/lib/util/test/extra.0rt
+++ b/lib/util/test/extra.0rt
@@ -23,20 +23,20 @@
 
 unittest global {
   Int count <- Argv.global().size()
-  \ Testing.checkEquals<?>(count,5)
-  \ Testing.checkEquals<?>(Argv.global().readAt(0),"testcase")
-  \ Testing.checkEquals<?>(Argv.global().readAt(1),"arg1")
-  \ Testing.checkEquals<?>(Argv.global().readAt(2),"arg2")
-  \ Testing.checkEquals<?>(Argv.global().readAt(3),"arg3")
-  \ Testing.checkEquals<?>(Argv.global().readAt(4),"arg4")
+  \ Testing.checkEquals(count,5)
+  \ Testing.checkEquals(Argv.global().readAt(0),"testcase")
+  \ Testing.checkEquals(Argv.global().readAt(1),"arg1")
+  \ Testing.checkEquals(Argv.global().readAt(2),"arg2")
+  \ Testing.checkEquals(Argv.global().readAt(3),"arg3")
+  \ Testing.checkEquals(Argv.global().readAt(4),"arg4")
 }
 
 unittest subSequence {
   ReadAt<String> argv <- Argv.global().subSequence(2,2)
   Int count <- argv.size()
-  \ Testing.checkEquals<?>(count,2)
-  \ Testing.checkEquals<?>(argv.readAt(0),"arg2")
-  \ Testing.checkEquals<?>(argv.readAt(1),"arg3")
+  \ Testing.checkEquals(count,2)
+  \ Testing.checkEquals(argv.readAt(0),"arg2")
+  \ Testing.checkEquals(argv.readAt(1),"arg3")
 }
 
 
@@ -66,21 +66,21 @@
 
 unittest withValue {
   ErrorOr<Int> value <- ErrorOr:value<Int>(10)
-  \ Testing.checkEquals<?>(value.isError(),false)
-  \ Testing.checkEquals<?>(value.getValue(),10)
+  \ Testing.checkFalse(value.isError())
+  \ Testing.checkEquals(value.getValue(),10)
 }
 
 unittest withError {
   ErrorOr<String> value <- ErrorOr:error("error message")
-  \ Testing.checkEquals<?>(value.isError(),true)
-  \ Testing.checkEquals<?>(value.getError().formatted(),"error message")
+  \ Testing.checkTrue(value.isError())
+  \ Testing.checkEquals(value.getError().formatted(),"error message")
 }
 
 unittest convertError {
   scoped {
     ErrorOr<String> error <- ErrorOr:error("error message")
   } in ErrorOr<Int> value <- error.convertError()
-  \ Testing.checkEquals<?>(value.getError().formatted(),"error message")
+  \ Testing.checkEquals(value.getError().formatted(),"error message")
 }
 
 
diff --git a/lib/util/test/helpers.0rt b/lib/util/test/helpers.0rt
--- a/lib/util/test/helpers.0rt
+++ b/lib/util/test/helpers.0rt
@@ -21,69 +21,69 @@
 }
 
 unittest orderHLessThan {
-  \ Testing.checkEquals<?>("".defaultOrder()    `OrderH:lessThan<?>` "".defaultOrder(),   false)
-  \ Testing.checkEquals<?>("a".defaultOrder()   `OrderH:lessThan<?>` "abc".defaultOrder(),true)
-  \ Testing.checkEquals<?>("b".defaultOrder()   `OrderH:lessThan<?>` "abc".defaultOrder(),false)
-  \ Testing.checkEquals<?>("abc".defaultOrder() `OrderH:lessThan<?>` "abc".defaultOrder(),false)
-  \ Testing.checkEquals<?>("abc".defaultOrder() `OrderH:lessThan<?>` "a".defaultOrder(),  false)
-  \ Testing.checkEquals<?>("abc".defaultOrder() `OrderH:lessThan<?>` "b".defaultOrder(),  true)
+  \ Testing.checkFalse("".defaultOrder()    `OrderH:lessThan` "".defaultOrder())
+  \ Testing.checkTrue("a".defaultOrder()    `OrderH:lessThan` "abc".defaultOrder())
+  \ Testing.checkFalse("b".defaultOrder()   `OrderH:lessThan` "abc".defaultOrder())
+  \ Testing.checkFalse("abc".defaultOrder() `OrderH:lessThan` "abc".defaultOrder())
+  \ Testing.checkFalse("abc".defaultOrder() `OrderH:lessThan` "a".defaultOrder())
+  \ Testing.checkTrue("abc".defaultOrder()  `OrderH:lessThan` "b".defaultOrder())
 }
 
 unittest orderHLessThanWith {
-  \ Testing.checkEquals<?>("".defaultOrder()    `OrderH:lessThanWith<?,Reversed<Char>>` "".defaultOrder(),   false)
-  \ Testing.checkEquals<?>("a".defaultOrder()   `OrderH:lessThanWith<?,Reversed<Char>>` "abc".defaultOrder(),true)
-  \ Testing.checkEquals<?>("b".defaultOrder()   `OrderH:lessThanWith<?,Reversed<Char>>` "abc".defaultOrder(),true)
-  \ Testing.checkEquals<?>("abc".defaultOrder() `OrderH:lessThanWith<?,Reversed<Char>>` "abc".defaultOrder(),false)
-  \ Testing.checkEquals<?>("abc".defaultOrder() `OrderH:lessThanWith<?,Reversed<Char>>` "a".defaultOrder(),  false)
-  \ Testing.checkEquals<?>("abc".defaultOrder() `OrderH:lessThanWith<?,Reversed<Char>>` "b".defaultOrder(),  false)
+  \ Testing.checkFalse("".defaultOrder()    `OrderH:lessThanWith<?,Reversed<Char>>` "".defaultOrder())
+  \ Testing.checkTrue("a".defaultOrder()    `OrderH:lessThanWith<?,Reversed<Char>>` "abc".defaultOrder())
+  \ Testing.checkTrue("b".defaultOrder()    `OrderH:lessThanWith<?,Reversed<Char>>` "abc".defaultOrder())
+  \ Testing.checkFalse("abc".defaultOrder() `OrderH:lessThanWith<?,Reversed<Char>>` "abc".defaultOrder())
+  \ Testing.checkFalse("abc".defaultOrder() `OrderH:lessThanWith<?,Reversed<Char>>` "a".defaultOrder())
+  \ Testing.checkFalse("abc".defaultOrder() `OrderH:lessThanWith<?,Reversed<Char>>` "b".defaultOrder())
 }
 
 unittest orderHEquals {
-  \ Testing.checkEquals<?>("".defaultOrder()    `OrderH:equals<?>` "".defaultOrder(),   true)
-  \ Testing.checkEquals<?>("a".defaultOrder()   `OrderH:equals<?>` "abc".defaultOrder(),false)
-  \ Testing.checkEquals<?>("abc".defaultOrder() `OrderH:equals<?>` "abc".defaultOrder(),true)
-  \ Testing.checkEquals<?>("abc".defaultOrder() `OrderH:equals<?>` "a".defaultOrder(),  false)
+  \ Testing.checkTrue("".defaultOrder()     `OrderH:equals` "".defaultOrder())
+  \ Testing.checkFalse("a".defaultOrder()   `OrderH:equals` "abc".defaultOrder())
+  \ Testing.checkTrue("abc".defaultOrder()  `OrderH:equals` "abc".defaultOrder())
+  \ Testing.checkFalse("abc".defaultOrder() `OrderH:equals` "a".defaultOrder())
 }
 
 unittest orderHEqualsWith {
-  \ Testing.checkEquals<?>("".defaultOrder()    `OrderH:equalsWith<?,IgnoreCase>` "".defaultOrder(),   true)
-  \ Testing.checkEquals<?>("a".defaultOrder()   `OrderH:equalsWith<?,IgnoreCase>` "ABC".defaultOrder(),false)
-  \ Testing.checkEquals<?>("abc".defaultOrder() `OrderH:equalsWith<?,IgnoreCase>` "ABC".defaultOrder(),true)
-  \ Testing.checkEquals<?>("abc".defaultOrder() `OrderH:equalsWith<?,IgnoreCase>` "DEF".defaultOrder(),false)
-  \ Testing.checkEquals<?>("abc".defaultOrder() `OrderH:equalsWith<?,IgnoreCase>` "A".defaultOrder(),  false)
+  \ Testing.checkTrue("".defaultOrder()     `OrderH:equalsWith<?,IgnoreCase>` "".defaultOrder())
+  \ Testing.checkFalse("a".defaultOrder()   `OrderH:equalsWith<?,IgnoreCase>` "ABC".defaultOrder())
+  \ Testing.checkTrue("abc".defaultOrder()  `OrderH:equalsWith<?,IgnoreCase>` "ABC".defaultOrder())
+  \ Testing.checkFalse("abc".defaultOrder() `OrderH:equalsWith<?,IgnoreCase>` "DEF".defaultOrder())
+  \ Testing.checkFalse("abc".defaultOrder() `OrderH:equalsWith<?,IgnoreCase>` "A".defaultOrder())
 }
 
 unittest readAtHLessThan {
-  \ Testing.checkEquals<?>(""    `ReadAtH:lessThan<?>` "",   false)
-  \ Testing.checkEquals<?>("a"   `ReadAtH:lessThan<?>` "abc",true)
-  \ Testing.checkEquals<?>("b"   `ReadAtH:lessThan<?>` "abc",false)
-  \ Testing.checkEquals<?>("abc" `ReadAtH:lessThan<?>` "abc",false)
-  \ Testing.checkEquals<?>("abc" `ReadAtH:lessThan<?>` "a",  false)
-  \ Testing.checkEquals<?>("abc" `ReadAtH:lessThan<?>` "b",  true)
+  \ Testing.checkFalse(""    `ReadAtH:lessThan` "")
+  \ Testing.checkTrue("a"    `ReadAtH:lessThan` "abc")
+  \ Testing.checkFalse("b"   `ReadAtH:lessThan` "abc")
+  \ Testing.checkFalse("abc" `ReadAtH:lessThan` "abc")
+  \ Testing.checkFalse("abc" `ReadAtH:lessThan` "a")
+  \ Testing.checkTrue("abc"  `ReadAtH:lessThan` "b")
 }
 
 unittest readAtHLessThanWith {
-  \ Testing.checkEquals<?>(""    `ReadAtH:lessThanWith<?,Reversed<Char>>` "",   false)
-  \ Testing.checkEquals<?>("a"   `ReadAtH:lessThanWith<?,Reversed<Char>>` "abc",true)
-  \ Testing.checkEquals<?>("b"   `ReadAtH:lessThanWith<?,Reversed<Char>>` "abc",true)
-  \ Testing.checkEquals<?>("abc" `ReadAtH:lessThanWith<?,Reversed<Char>>` "abc",false)
-  \ Testing.checkEquals<?>("abc" `ReadAtH:lessThanWith<?,Reversed<Char>>` "a",  false)
-  \ Testing.checkEquals<?>("abc" `ReadAtH:lessThanWith<?,Reversed<Char>>` "b",  false)
+  \ Testing.checkFalse(""    `ReadAtH:lessThanWith<?,Reversed<Char>>` "")
+  \ Testing.checkTrue("a"    `ReadAtH:lessThanWith<?,Reversed<Char>>` "abc")
+  \ Testing.checkTrue("b"    `ReadAtH:lessThanWith<?,Reversed<Char>>` "abc")
+  \ Testing.checkFalse("abc" `ReadAtH:lessThanWith<?,Reversed<Char>>` "abc")
+  \ Testing.checkFalse("abc" `ReadAtH:lessThanWith<?,Reversed<Char>>` "a")
+  \ Testing.checkFalse("abc" `ReadAtH:lessThanWith<?,Reversed<Char>>` "b")
 }
 
 unittest readAtHEquals {
-  \ Testing.checkEquals<?>(""    `ReadAtH:equals<?>` "",   true)
-  \ Testing.checkEquals<?>("a"   `ReadAtH:equals<?>` "abc",false)
-  \ Testing.checkEquals<?>("abc" `ReadAtH:equals<?>` "abc",true)
-  \ Testing.checkEquals<?>("abc" `ReadAtH:equals<?>` "a",  false)
+  \ Testing.checkTrue(""     `ReadAtH:equals` "")
+  \ Testing.checkFalse("a"   `ReadAtH:equals` "abc")
+  \ Testing.checkTrue("abc"  `ReadAtH:equals` "abc")
+  \ Testing.checkFalse("abc" `ReadAtH:equals` "a")
 }
 
 unittest readAtHEqualsWith {
-  \ Testing.checkEquals<?>(""    `ReadAtH:equalsWith<?,IgnoreCase>` "",   true)
-  \ Testing.checkEquals<?>("a"   `ReadAtH:equalsWith<?,IgnoreCase>` "ABC",false)
-  \ Testing.checkEquals<?>("abc" `ReadAtH:equalsWith<?,IgnoreCase>` "ABC",true)
-  \ Testing.checkEquals<?>("abc" `ReadAtH:equalsWith<?,IgnoreCase>` "DEF",false)
-  \ Testing.checkEquals<?>("abc" `ReadAtH:equalsWith<?,IgnoreCase>` "a",  false)
+  \ Testing.checkTrue(""     `ReadAtH:equalsWith<?,IgnoreCase>` "")
+  \ Testing.checkFalse("a"   `ReadAtH:equalsWith<?,IgnoreCase>` "ABC")
+  \ Testing.checkTrue("abc"  `ReadAtH:equalsWith<?,IgnoreCase>` "ABC")
+  \ Testing.checkFalse("abc" `ReadAtH:equalsWith<?,IgnoreCase>` "DEF")
+  \ Testing.checkFalse("abc" `ReadAtH:equalsWith<?,IgnoreCase>` "a")
 }
 
 concrete IgnoreCase {
@@ -106,34 +106,34 @@
 }
 
 unittest reversed {
-  \ Testing.checkEquals<?>(0 `Reversed<Int>.lessThan` 0,false)
-  \ Testing.checkEquals<?>(0 `Reversed<Int>.lessThan` 1,false)
-  \ Testing.checkEquals<?>(1 `Reversed<Int>.lessThan` 0,true)
+  \ Testing.checkFalse(0 `Reversed<Int>.lessThan` 0)
+  \ Testing.checkFalse(0 `Reversed<Int>.lessThan` 1)
+  \ Testing.checkTrue(1  `Reversed<Int>.lessThan` 0)
 }
 
 unittest bySizeLessThan {
-  \ Testing.checkEquals<?>(""   `BySize.lessThan` "",  false)
-  \ Testing.checkEquals<?>("b"  `BySize.lessThan` "ac",true)
-  \ Testing.checkEquals<?>("ac" `BySize.lessThan` "b", false)
-  \ Testing.checkEquals<?>("a"  `BySize.lessThan` "b", false)
+  \ Testing.checkFalse(""   `BySize.lessThan` "")
+  \ Testing.checkTrue("b"   `BySize.lessThan` "ac")
+  \ Testing.checkFalse("ac" `BySize.lessThan` "b")
+  \ Testing.checkFalse("a"  `BySize.lessThan` "b")
 }
 
 unittest bySizeEquals {
-  \ Testing.checkEquals<?>(""   `BySize.equals` "",  true)
-  \ Testing.checkEquals<?>("b"  `BySize.equals` "ac",false)
-  \ Testing.checkEquals<?>("ac" `BySize.equals` "b", false)
-  \ Testing.checkEquals<?>("a"  `BySize.equals` "b", true)
+  \ Testing.checkTrue(""    `BySize.equals` "")
+  \ Testing.checkFalse("b"  `BySize.equals` "ac")
+  \ Testing.checkFalse("ac" `BySize.equals` "b")
+  \ Testing.checkTrue("a"   `BySize.equals` "b")
 }
 
 unittest alwaysEqualLessThan {
-  \ Testing.checkEquals<?>(""   `AlwaysEqual.lessThan` "",  false)
-  \ Testing.checkEquals<?>("b"  `AlwaysEqual.lessThan` "ac",false)
-  \ Testing.checkEquals<?>("ac" `AlwaysEqual.lessThan` "b", false)
+  \ Testing.checkFalse(""   `AlwaysEqual.lessThan` "")
+  \ Testing.checkFalse("b"  `AlwaysEqual.lessThan` "ac")
+  \ Testing.checkFalse("ac" `AlwaysEqual.lessThan` "b")
 }
 
 unittest alwaysEqualEquals {
-  \ Testing.checkEquals<?>(""   `AlwaysEqual.equals` "",  true)
-  \ Testing.checkEquals<?>("b"  `AlwaysEqual.equals` "ac",true)
-  \ Testing.checkEquals<?>("ac" `AlwaysEqual.equals` "b", true)
-  \ Testing.checkEquals<?>("a"  `AlwaysEqual.equals` "b", true)
+  \ Testing.checkTrue(""   `AlwaysEqual.equals` "")
+  \ Testing.checkTrue("b"  `AlwaysEqual.equals` "ac")
+  \ Testing.checkTrue("ac" `AlwaysEqual.equals` "b")
+  \ Testing.checkTrue("a"  `AlwaysEqual.equals` "b")
 }
diff --git a/lib/util/test/input.0rt b/lib/util/test/input.0rt
--- a/lib/util/test/input.0rt
+++ b/lib/util/test/input.0rt
@@ -30,7 +30,7 @@
       fail("past end")
     }
     String value <- reader.readNextLine()
-    \ Testing.checkEquals<?>(value,line)
+    \ Testing.checkEquals(value,line)
   }
 }
 
@@ -85,7 +85,7 @@
 
 unittest readAll {
   String allContents <- TextReader.readAll(FakeFile.create())
-  \ Testing.checkEquals<?>(allContents,"this is a partial line\nwith some more data\nand\nmore lines\nunfinished")
+  \ Testing.checkEquals(allContents,"this is a partial line\nwith some more data\nand\nmore lines\nunfinished")
 }
 
 
@@ -95,5 +95,5 @@
 }
 
 unittest test {
-  \ Testing.checkEquals<?>(TextReader.readAll(SimpleInput.stdin()),"")
+  \ Testing.checkEquals(TextReader.readAll(SimpleInput.stdin()),"")
 }
diff --git a/lib/util/test/parse.0rt b/lib/util/test/parse.0rt
--- a/lib/util/test/parse.0rt
+++ b/lib/util/test/parse.0rt
@@ -21,59 +21,59 @@
 }
 
 unittest floatNoDecimal {
-  \ UtilTesting.checkSuccessBetween<?>(ParseChars.float("123"),122.9,123.1)
+  \ UtilTesting.checkSuccessBetween(ParseChars.float("123"),122.9,123.1)
 }
 
 unittest floatDecimalLeft {
-  \ UtilTesting.checkSuccessBetween<?>(ParseChars.float(".123"),0.1229,0.1231)
+  \ UtilTesting.checkSuccessBetween(ParseChars.float(".123"),0.1229,0.1231)
 }
 
 unittest floatDecimalRight {
-  \ UtilTesting.checkSuccessBetween<?>(ParseChars.float("123."),122.9,123.1)
+  \ UtilTesting.checkSuccessBetween(ParseChars.float("123."),122.9,123.1)
 }
 
 unittest floatDecimalMiddle {
-  \ UtilTesting.checkSuccessBetween<?>(ParseChars.float("123.456"),123.4559,123.4561)
+  \ UtilTesting.checkSuccessBetween(ParseChars.float("123.456"),123.4559,123.4561)
 }
 
 unittest floatLeadingTrailingZeros {
-  \ UtilTesting.checkSuccessBetween<?>(ParseChars.float("000123.456000"),123.4559,123.4561)
+  \ UtilTesting.checkSuccessBetween(ParseChars.float("000123.456000"),123.4559,123.4561)
 }
 
 unittest floatNegative {
-  \ UtilTesting.checkSuccessBetween<?>(ParseChars.float("-123.456"),-123.4561,-123.4559)
+  \ UtilTesting.checkSuccessBetween(ParseChars.float("-123.456"),-123.4561,-123.4559)
 }
 
 unittest floatPositive {
-  \ UtilTesting.checkSuccessBetween<?>(ParseChars.float("+123.456"),123.4559,123.4561)
+  \ UtilTesting.checkSuccessBetween(ParseChars.float("+123.456"),123.4559,123.4561)
 }
 
 unittest floatNegativeDecimalLeft {
-  \ UtilTesting.checkSuccessBetween<?>(ParseChars.float("-.456"),-0.4561,-0.4559)
+  \ UtilTesting.checkSuccessBetween(ParseChars.float("-.456"),-0.4561,-0.4559)
 }
 
 unittest floatExponentNoDecimal {
-  \ UtilTesting.checkSuccessBetween<?>(ParseChars.float("123E5"),122.9E5,123.1E5)
+  \ UtilTesting.checkSuccessBetween(ParseChars.float("123E5"),122.9E5,123.1E5)
 }
 
 unittest floatNegativeExponent {
-  \ UtilTesting.checkSuccessBetween<?>(ParseChars.float("123E-5"),122.9E-5,123.1E-5)
+  \ UtilTesting.checkSuccessBetween(ParseChars.float("123E-5"),122.9E-5,123.1E-5)
 }
 
 unittest floatPositiveExponent {
-  \ UtilTesting.checkSuccessBetween<?>(ParseChars.float("123E+5"),122.9E5,123.1E5)
+  \ UtilTesting.checkSuccessBetween(ParseChars.float("123E+5"),122.9E5,123.1E5)
 }
 
 unittest floatExponentDecimalRight {
-  \ UtilTesting.checkSuccessBetween<?>(ParseChars.float("123.E5"),122.9E5,123.1E5)
+  \ UtilTesting.checkSuccessBetween(ParseChars.float("123.E5"),122.9E5,123.1E5)
 }
 
 unittest floatExponentDecimalLeft {
-  \ UtilTesting.checkSuccessBetween<?>(ParseChars.float(".123E5"),122.9E2,123.1E2)
+  \ UtilTesting.checkSuccessBetween(ParseChars.float(".123E5"),122.9E2,123.1E2)
 }
 
 unittest floatExponentDecimalMiddle {
-  \ UtilTesting.checkSuccessBetween<?>(ParseChars.float("123.456E5"),123.4559E5,123.4561E5)
+  \ UtilTesting.checkSuccessBetween(ParseChars.float("123.456E5"),123.4559E5,123.4561E5)
 }
 
 unittest floatEmpty {
@@ -109,19 +109,19 @@
 }
 
 unittest int {
-  \ UtilTesting.checkSuccess<?>(ParseChars.int("1234"),1234)
+  \ UtilTesting.checkSuccess(ParseChars.int("1234"),1234)
 }
 
 unittest intNegative {
-  \ UtilTesting.checkSuccess<?>(ParseChars.int("-1234"),-1234)
+  \ UtilTesting.checkSuccess(ParseChars.int("-1234"),-1234)
 }
 
 unittest intPositive {
-  \ UtilTesting.checkSuccess<?>(ParseChars.int("+1234"),1234)
+  \ UtilTesting.checkSuccess(ParseChars.int("+1234"),1234)
 }
 
 unittest intLeadingZeros {
-  \ UtilTesting.checkSuccess<?>(ParseChars.int("001234"),1234)
+  \ UtilTesting.checkSuccess(ParseChars.int("001234"),1234)
 }
 
 unittest intEmpty {
@@ -137,19 +137,19 @@
 }
 
 unittest hexInt {
-  \ UtilTesting.checkSuccess<?>(ParseChars.hexInt("12AbCdEf"),\x12ABCDEF)
+  \ UtilTesting.checkSuccess(ParseChars.hexInt("12AbCdEf"),\x12ABCDEF)
 }
 
 unittest hexIntNegative {
-  \ UtilTesting.checkSuccess<?>(ParseChars.hexInt("-12AbCdEf"),-\x12ABCDEF)
+  \ UtilTesting.checkSuccess(ParseChars.hexInt("-12AbCdEf"),-\x12ABCDEF)
 }
 
 unittest hexIntPositive {
-  \ UtilTesting.checkSuccess<?>(ParseChars.hexInt("+12AbCdEf"),\x12ABCDEF)
+  \ UtilTesting.checkSuccess(ParseChars.hexInt("+12AbCdEf"),\x12ABCDEF)
 }
 
 unittest hexIntLeadingZeros {
-  \ UtilTesting.checkSuccess<?>(ParseChars.hexInt("0012AbCdEf"),\x12ABCDEF)
+  \ UtilTesting.checkSuccess(ParseChars.hexInt("0012AbCdEf"),\x12ABCDEF)
 }
 
 unittest hexIntEmpty {
@@ -165,19 +165,19 @@
 }
 
 unittest octInt {
-  \ UtilTesting.checkSuccess<?>(ParseChars.octInt("127"),\o127)
+  \ UtilTesting.checkSuccess(ParseChars.octInt("127"),\o127)
 }
 
 unittest octIntNegative {
-  \ UtilTesting.checkSuccess<?>(ParseChars.octInt("-127"),-\o127)
+  \ UtilTesting.checkSuccess(ParseChars.octInt("-127"),-\o127)
 }
 
 unittest octIntPositive {
-  \ UtilTesting.checkSuccess<?>(ParseChars.octInt("+127"),\o127)
+  \ UtilTesting.checkSuccess(ParseChars.octInt("+127"),\o127)
 }
 
 unittest octIntLeadingZeros {
-  \ UtilTesting.checkSuccess<?>(ParseChars.octInt("00127"),\o127)
+  \ UtilTesting.checkSuccess(ParseChars.octInt("00127"),\o127)
 }
 
 unittest octIntEmpty {
@@ -193,19 +193,19 @@
 }
 
 unittest binInt {
-  \ UtilTesting.checkSuccess<?>(ParseChars.binInt("100101"),\b100101)
+  \ UtilTesting.checkSuccess(ParseChars.binInt("100101"),\b100101)
 }
 
 unittest binIntNegative {
-  \ UtilTesting.checkSuccess<?>(ParseChars.binInt("-100101"),-\b100101)
+  \ UtilTesting.checkSuccess(ParseChars.binInt("-100101"),-\b100101)
 }
 
 unittest binIntPositive {
-  \ UtilTesting.checkSuccess<?>(ParseChars.binInt("+100101"),\b100101)
+  \ UtilTesting.checkSuccess(ParseChars.binInt("+100101"),\b100101)
 }
 
 unittest binIntLeadingZeros {
-  \ UtilTesting.checkSuccess<?>(ParseChars.binInt("00100101"),\b100101)
+  \ UtilTesting.checkSuccess(ParseChars.binInt("00100101"),\b100101)
 }
 
 unittest binIntEmpty {
diff --git a/lib/util/test/testing.0rt b/lib/util/test/testing.0rt
--- a/lib/util/test/testing.0rt
+++ b/lib/util/test/testing.0rt
@@ -31,7 +31,7 @@
 }
 
 unittest test {
-  \ UtilTesting.checkError(ErrorOr:value<?>("message"))
+  \ UtilTesting.checkError(ErrorOr:value("message"))
 }
 
 
@@ -40,7 +40,7 @@
 }
 
 unittest test {
-  \ UtilTesting.checkSuccess<?>(ErrorOr:value<?>("message"),"message")
+  \ UtilTesting.checkSuccess(ErrorOr:value("message"),"message")
 }
 
 
@@ -51,7 +51,7 @@
 }
 
 unittest test {
-  \ UtilTesting.checkSuccess<?>(ErrorOr:value<?>("foo"),"message")
+  \ UtilTesting.checkSuccess(ErrorOr:value("foo"),"message")
 }
 
 
@@ -62,7 +62,7 @@
 }
 
 unittest test {
-  \ UtilTesting.checkSuccess<?>(ErrorOr:error("failure"),"message")
+  \ UtilTesting.checkSuccess(ErrorOr:error("failure"),"message")
 }
 
 
@@ -71,7 +71,7 @@
 }
 
 unittest test {
-  \ UtilTesting.checkSuccessBetween<?>(ErrorOr:value<?>(1),0,2)
+  \ UtilTesting.checkSuccessBetween(ErrorOr:value(1),0,2)
 }
 
 
@@ -81,7 +81,7 @@
 }
 
 unittest test {
-  \ UtilTesting.checkSuccessBetween<?>(ErrorOr:value<?>(7),0,2)
+  \ UtilTesting.checkSuccessBetween(ErrorOr:value(7),0,2)
 }
 
 
@@ -92,5 +92,5 @@
 }
 
 unittest test {
-  \ UtilTesting.checkSuccessBetween<?>(ErrorOr:error("failure"),0,2)
+  \ UtilTesting.checkSuccessBetween(ErrorOr:error("failure"),0,2)
 }
diff --git a/src/Cli/Compiler.hs b/src/Cli/Compiler.hs
--- a/src/Cli/Compiler.hs
+++ b/src/Cli/Compiler.hs
@@ -74,8 +74,6 @@
 
 data LoadedTests =
   LoadedTests {
-    ltRoot :: FilePath,
-    ltPath :: FilePath,
     ltMetadata :: CompileMetadata,
     ltExprMap :: ExprMap SourceContext,
     ltPublicDeps :: [CompileMetadata],
@@ -88,6 +86,7 @@
   as  <- fmap fixPaths $ mapCompilerM (resolveModule resolver (p </> d)) is
   as2 <- fmap fixPaths $ mapCompilerM (resolveModule resolver (p </> d)) is2
   let ca0 = Map.empty
+  compilerHash <- getCompilerHash backend
   deps1 <- loadPublicDeps compilerHash f ca0 as
   let ca1 = ca0 `Map.union` mapMetadata deps1
   deps2 <- loadPublicDeps compilerHash f ca1 as2
@@ -102,14 +101,15 @@
                  bpDeps <- loadPublicDeps compilerHash f ca2 [base]
                  return $ bpDeps ++ deps1
   time <- errorFromIO getZonedTime
-  path <- errorFromIO $ canonicalizePath $ p </> d
+  root <- errorFromIO $ canonicalizePath p
+  path <- errorFromIO $ canonicalizePath (p </> d)
   extra <- errorFromIO $ sequence $ map (canonicalizePath . (p</>)) ee
   -- NOTE: Making the public namespace deterministic allows freshness checks to
   -- skip checking all inputs/outputs for each dependency.
   let ns0 = StaticNamespace $ publicNamespace  $ show compilerHash ++ path
   let ns1 = StaticNamespace . privateNamespace $ show time ++ show compilerHash ++ path
   let extensions = concat $ map getSourceCategories es
-  cs <- loadModuleGlobals resolver p (ns0,ns1) ps Nothing deps1' deps2
+  (cs,private) <- loadModuleGlobals resolver p (ns0,ns1) ps Nothing deps1' deps2
   let cm = createLanguageModule extensions em cs
   let cs2 = filter (not . hasCodeVisibility FromDependency) cs
   let pc = map (getCategoryName . wvData) $ filter (not . hasCodeVisibility ModuleOnly) cs2
@@ -119,9 +119,10 @@
   fs <- compileLanguageModule cm xa
   mf <- maybeCreateMain cm xa m
   eraseCachedData (p </> d)
-  ps2 <- mapCompilerM (errorFromIO. canonicalizePath . (p </>)) ps
-  xs2 <- mapCompilerM (errorFromIO. canonicalizePath . (p </>)) xs
-  ts2 <- mapCompilerM (errorFromIO. canonicalizePath . (p </>)) ts
+  pps <- fmap (zip ps) $ mapCompilerM (errorFromIO . canonicalizePath . (p</>)) ps
+  let ps2 = map fst $ filter (not . ((`Set.member` private) . snd)) pps
+  let xs2 = xs ++ (map fst $ filter ((`Set.member` private) . snd) pps)
+  let ts2 = ts
   let paths = map (\ns -> getCachedPath (p </> d) ns "") $ nub $ filter (not . null) $ map show $ map coNamespace fs
   paths' <- mapM (errorFromIO . canonicalizePath) paths
   s0 <- errorFromIO $ canonicalizePath $ getCachedPath (p </> d) (show ns0) ""
@@ -145,6 +146,7 @@
   ls <- createLibrary libraryName (getLinkFlags m) (deps1' ++ deps2) allObjects
   let cm2 = CompileMetadata {
       cmVersionHash = compilerHash,
+      cmRoot = root,
       cmPath = path,
       cmExtra = extra,
       cmPublicNamespace = ns0,
@@ -165,9 +167,10 @@
       cmLinkFlags = getLinkFlags m,
       cmObjectFiles = os1' ++ osOther ++ map OtherObjectFile os'
     }
-  bs <- createBinary paths' (cm2:(deps1' ++ deps2)) m mf
+  bs <- createBinary compilerHash paths' (cm2:(deps1' ++ deps2)) m mf
   let cm2' = CompileMetadata {
       cmVersionHash = cmVersionHash cm2,
+      cmRoot = cmRoot cm2,
       cmPath = cmPath cm2,
       cmExtra = cmExtra cm2,
       cmPublicNamespace = cmPublicNamespace cm2,
@@ -191,7 +194,6 @@
   writeMetadata (p </> d) cm2'
   let traces = Set.unions $ map coPossibleTraces $ hxx ++ other
   writePossibleTraces (p </> d) traces where
-    compilerHash = getCompilerHash backend
     ep' = fixPaths $ map (p </>) ep
     writeOutputFile paths ca@(CxxOutput _ f2 ns _ _ _ content) = do
       errorFromIO $ hPutStrLn stderr $ "Writing file " ++ f2
@@ -243,7 +245,7 @@
           fmap Just $ runCxxCommand backend command
       | isSuffixOf ".a" f2 || isSuffixOf ".o" f2 = return (Just f2)
       | otherwise = return Nothing
-    createBinary paths deps (CompileBinary n _ lm o lf) [CxxOutput _ _ _ ns2 req _ content] = do
+    createBinary compilerHash paths deps (CompileBinary n _ lm o lf) [CxxOutput _ _ _ ns2 req _ content] = do
       f0 <- if null o
                 then errorFromIO $ canonicalizePath $ p </> d </> show n
                 else errorFromIO $ canonicalizePath $ p </> d </> o
@@ -267,11 +269,11 @@
         getCommand LinkDynamic mainAbs f0 deps2 paths2 = do
           let objects = getLibrariesForDeps deps2
           return $ CompileToBinary mainAbs objects [] f0 paths2 []
-    createBinary _ _ (CompileBinary n _ _ _ _) [] =
+    createBinary _ _ _ (CompileBinary n _ _ _ _) [] =
       compilerErrorM $ "Main category " ++ show n ++ " not found."
-    createBinary _ _ (CompileBinary n _ _ _ _) _ =
+    createBinary _ _ _ (CompileBinary n _ _ _ _) _ =
       compilerErrorM $ "Multiple matches for main category " ++ show n ++ "."
-    createBinary _ _ _ _ = return []
+    createBinary _ _ _ _ _ = return []
     createLibrary _ _ [] [] = return []
     createLibrary name lf deps os = do
       let flags = lf ++ getLinkFlagsForDeps deps
@@ -289,7 +291,7 @@
 createModuleTemplates resolver p d ds deps1 deps2 = do
   (ps,xs,_) <- findSourceFiles p (d:ds)
   (LanguageModule _ _ _ cs0 ps0 ts0 cs1 ps1 ts1 _ _) <-
-    fmap (createLanguageModule [] Map.empty) $ loadModuleGlobals resolver p (PublicNamespace,PrivateNamespace) ps Nothing deps1 deps2
+    fmap (createLanguageModule [] Map.empty . fst) $ loadModuleGlobals resolver p (PublicNamespace,PrivateNamespace) ps Nothing deps1 deps2
   xs' <- zipWithContents resolver p xs
   ds2 <- mapCompilerM parseInternalSource xs'
   let ds3 = concat $ map (\(_,_,d2) -> d2) ds2
@@ -315,12 +317,12 @@
 runModuleTests :: (PathIOHandler r, CompilerBackend b) =>
   r -> b -> FilePath -> FilePath -> [FilePath] -> LoadedTests ->
   TrackedErrorsIO [((Int,Int),TrackedErrors ())]
-runModuleTests resolver backend cl base tp (LoadedTests p d m em deps1 deps2) = do
+runModuleTests resolver backend cl base tp (LoadedTests m em deps1 deps2) = do
   let paths = base:(cmPublicSubdirs m ++ cmPrivateSubdirs m ++ getIncludePathsForDeps deps1)
   mapCompilerM_ showSkipped $ filter (not . isTestAllowed) $ cmTestFiles m
-  ts' <- zipWithContents resolver p $ map (d </>) $ filter isTestAllowed $ cmTestFiles m
-  path <- errorFromIO $ canonicalizePath (p </> d)
-  cm <- fmap (createLanguageModule [] em) $ loadModuleGlobals resolver path (NoNamespace,NoNamespace) [] (Just m) deps1 []
+  ts' <- zipWithContents resolver (cmRoot m) $ filter isTestAllowed $ cmTestFiles m
+  let path = cmPath m
+  cm <- fmap (createLanguageModule [] em . fst) $ loadModuleGlobals resolver path (NoNamespace,NoNamespace) [] (Just m) deps1 []
   mapCompilerM (runSingleTest backend cl cm paths (m:deps2)) ts' where
     allowTests = Set.fromList tp
     isTestAllowed t
diff --git a/src/Cli/ParseCompileOptions.hs b/src/Cli/ParseCompileOptions.hs
--- a/src/Cli/ParseCompileOptions.hs
+++ b/src/Cli/ParseCompileOptions.hs
@@ -23,11 +23,11 @@
 ) where
 
 import Control.Monad (when)
-import Data.List (isSuffixOf)
 import Text.Regex.TDFA
 
 import Base.CompilerError
 import Cli.CompileOptions
+import Module.ProcessMetadata (isPrivateSource,isPublicSource,isTestSource)
 import Types.TypeCategory (FunctionName(..))
 import Types.TypeInstance (CategoryName(..))
 
@@ -35,39 +35,53 @@
 optionHelpText :: [String]
 optionHelpText = [
     "",
-    "zeolite [options...] --fast [category(.function)] [.0rx source]",
-    "zeolite [options...] -r [modules...]",
-    "zeolite [options...] -R [modules...]",
-    "zeolite [options...] -t [modules...] (--log-traces [filename])",
+    "Compilation Modes:",
     "",
-    "zeolite [options...] -c [module]",
-    "zeolite [options...] -m [category(.function)] (-o [binary]) [module]",
+    "  zeolite [options...] --fast [category(.function)] [.0rx source]",
+    "    Create a binary without needing a config.",
     "",
-    "zeolite [options...] --templates [modules...]",
-    "zeolite --get-path",
-    "zeolite --missed-lines [filename] [modules...]",
-    "zeolite --show-deps [modules...]",
-    "zeolite --show-traces [modules...]",
-    "zeolite --version",
+    "  zeolite [options...] -r [modules...]",
+    "    Recompile using each module's .zeolite-module config.",
     "",
-    "Compilation Modes:",
-    "  --fast: Create a binary without needing a config.",
-    "  -r: Recompile using each module's .zeolite-module config.",
-    "  -R: Recursively recompile using each module's .zeolite-module config.",
-    "  -t: Only execute tests, without other compilation.",
+    "  zeolite [options...] -R [modules...]",
+    "    Recursively recompile using each module's .zeolite-module config.",
     "",
+    "  zeolite [options...] -t (--log-traces [filename]) [modules...] (tests...)",
+    "    Only execute tests, without other compilation.",
+    "",
     "Configuration Modes:",
-    "  -c: Create a new .zeolite-module config for a libary module.",
-    "  -m: Create a new .zeolite-module config for a binary module.",
     "",
+    "  zeolite [options...] -c [module]",
+    "    Create a new .zeolite-module config for a libary module.",
+    "",
+    "  zeolite [options...] -m [category(.function)] (-o [binary]) [module]",
+    "    Create a new .zeolite-module config for a binary module.",
+    "",
     "Special Modes:",
-    "  --templates: Only create C++ templates for undefined categories in .0rp sources.",
-    "  --get-path: Show the data path and immediately exit.",
-    "  --missed-lines: List all lines missed in a log file taken from --log-traces.",
-    "  --show-deps: Show category dependencies for the modules.",
-    "  --show-traces: Show the possible code traces for the modules.",
-    "  --version: Show the compiler version and immediately exit.",
     "",
+    "  zeolite [options...] --templates [modules...]",
+    "    Only create C++ templates for undefined categories in .0rp sources.",
+    "",
+    "  zeolite (-p [path]) --clean [modules...]",
+    "    Remove all cached data for the modules.",
+    "",
+    "  zeolite (-p [path]) --missed-lines [filename] [modules...]",
+    "    List all lines missed in a log file taken from --log-traces.",
+    "",
+    "  zeolite (-p [path]) --show-deps [modules...]",
+    "    Show category dependencies for the modules.",
+    "",
+    "  zeolite (-p [path]) --show-traces [modules...]",
+    "    Show the possible code traces for the modules.",
+    "",
+    "Compiler Info:",
+    "",
+    "  zeolite --get-path",
+    "    Show the data path and immediately exit.",
+    "",
+    "  zeolite --version",
+    "    Show the compiler version and immediately exit.",
+    "",
     "Options:",
     "  -f: Force an operation that zeolite would otherwise reject.",
     "  -i [module]: A single source module to include as a public dependency.",
@@ -80,6 +94,7 @@
     "  category: The name of a concrete category with no params.",
     "  function: The name of a @type function (defaults to \"run\") with no args or params.",
     "  module: Path to a directory containing an existing or to-be-created Zeolite module.",
+    "  test: Name of a .0rt file to limit test execution to.",
     "",
     "Examples:",
     "",
@@ -174,7 +189,7 @@
         (t,fn) <- check $ break (== '.') c
         checkCategoryName n2 t  "--fast"
         checkFunctionName n2 fn "--fast"
-        when (not $ isSuffixOf ".0rx" f2) $ argError n3 f2 $ "Must specify a .0rx source file."
+        when (not $ isPrivateSource f2) $ argError n3 f2 $ "Must specify a .0rx source file."
         let m2 = CompileFast (CategoryName t) (FunctionName fn) f2
         return (os2,CompileOptions (maybeDisableHelp h) is is2 ds es ep p m2 f) where
           check (t,"")     = return (t,defaultMainFunc)
@@ -200,9 +215,9 @@
 
   parseSingle (CompileOptions h is is2 ds es ep p m f) ((n,"-i"):os) = update os where
     update ((n2,d):os2)
-      | isSuffixOf ".0rp" d = argError n2 d "Cannot directly include .0rp source files."
-      | isSuffixOf ".0rx" d = argError n2 d "Cannot directly include .0rx source files."
-      | isSuffixOf ".0rt" d = argError n2 d "Cannot directly include .0rt test files."
+      | isPublicSource  d = argError n2 d "Cannot directly include .0rp source files."
+      | isPrivateSource d = argError n2 d "Cannot directly include .0rx source files."
+      | isTestSource    d = argError n2 d "Cannot directly include .0rt test files."
       | otherwise = do
           checkPathName n2 d "-i"
           return (os2,CompileOptions (maybeDisableHelp h) (is ++ [d]) is2 ds es ep p m f)
@@ -210,9 +225,9 @@
 
   parseSingle (CompileOptions h is is2 ds es ep p m f) ((n,"-I"):os) = update os where
     update ((n2,d):os2)
-      | isSuffixOf ".0rp" d = argError n2 d "Cannot directly include .0rp source files."
-      | isSuffixOf ".0rx" d = argError n2 d "Cannot directly include .0rx source files."
-      | isSuffixOf ".0rt" d = argError n2 d "Cannot directly include .0rt test files."
+      | isPublicSource  d = argError n2 d "Cannot directly include .0rp source files."
+      | isPrivateSource d = argError n2 d "Cannot directly include .0rx source files."
+      | isTestSource    d = argError n2 d "Cannot directly include .0rt test files."
       | otherwise = do
           checkPathName n2 d "-i"
           return (os2,CompileOptions (maybeDisableHelp h) is (is2 ++ [d]) ds es ep p m f)
@@ -229,9 +244,9 @@
   parseSingle _ ((n,o@('-':_)):_) = argError n o "Unknown option."
 
   parseSingle (CompileOptions h is is2 ds es ep p m f) ((n,d):os)
-      | isSuffixOf ".0rp" d = argError n d "Cannot directly include .0rp source files."
-      | isSuffixOf ".0rx" d = argError n d "Cannot directly include .0rx source files."
-      | isSuffixOf ".0rt" d = do
+      | isPublicSource  d = argError n d "Cannot directly include .0rp source files."
+      | isPrivateSource d = argError n d "Cannot directly include .0rx source files."
+      | isTestSource    d = do
         when (not $ isExecuteTests m) $
           argError n d "Test mode (-t) must be enabled before listing any .0rt test files."
         checkPathName n d ""
diff --git a/src/Cli/Programs.hs b/src/Cli/Programs.hs
--- a/src/Cli/Programs.hs
+++ b/src/Cli/Programs.hs
@@ -1,5 +1,5 @@
 {- -----------------------------------------------------------------------------
-Copyright 2020 Kevin P. Barry
+Copyright 2020-2021 Kevin P. Barry
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -32,9 +32,9 @@
 
 
 class CompilerBackend b where
-  runCxxCommand   :: (MonadIO m, ErrorContextM m) => b -> CxxCommand -> m FilePath
-  runTestCommand  :: (MonadIO m, ErrorContextM m) => b -> TestCommand -> m TestCommandResult
-  getCompilerHash :: b -> VersionHash
+  runCxxCommand   :: (MonadIO m, CollectErrorsM m) => b -> CxxCommand -> m FilePath
+  runTestCommand  :: (MonadIO m, CollectErrorsM m) => b -> TestCommand -> m TestCommandResult
+  getCompilerHash :: (MonadIO m, CollectErrorsM m) => b -> m VersionHash
 
 newtype VersionHash = VersionHash String deriving (Eq)
 
diff --git a/src/Cli/RunCompiler.hs b/src/Cli/RunCompiler.hs
--- a/src/Cli/RunCompiler.hs
+++ b/src/Cli/RunCompiler.hs
@@ -56,12 +56,11 @@
     prepareCallLog (Just cl2) = do
       clFull <- errorFromIO $ canonicalizePath (p </> cl2)
       compilerWarningM $ "Logging calls to " ++ clFull
-      errorFromIO $ writeFile clFull (intercalate "," (map show logHeader) ++ "\n")
+      errorFromIO $ writeFile clFull (intercalate "," (map show tracesLogHeader) ++ "\n")
       return clFull
     prepareCallLog _ = return ""
-    logHeader = ["microseconds","pid","function","context"]
-    compilerHash = getCompilerHash backend
     preloadTests (ca,ms) d = do
+      compilerHash <- getCompilerHash backend
       m <- loadModuleMetadata compilerHash f ca (p </> d)
       let ca2 = ca `Map.union` mapMetadata [m]
       rm <- loadRecompile (p </> d)
@@ -71,7 +70,7 @@
       deps2 <- loadPrivateDeps compilerHash f ca4 ([m]++deps1)
       let ca5 = ca4 `Map.union` mapMetadata deps2
       em <- getExprMap (p </> d) rm
-      return (ca5,ms ++ [LoadedTests p d m em (deps1) deps2])
+      return (ca5,ms ++ [LoadedTests m em (deps1) deps2])
     checkTestFilters ts = do
       let possibleTests = concat $ map (cmTestFiles . ltMetadata) ts
       let remaining = filter (not . flip any (map (flip isSuffixOf) possibleTests). flip ($)) tp
@@ -127,8 +126,8 @@
   runRecompileCommon resolver backend f False p ds
 
 runCompiler resolver backend (CompileOptions _ is is2 ds _ _ p CreateTemplates f) = mapM_ compileSingle ds where
-  compilerHash = getCompilerHash backend
   compileSingle d = do
+    compilerHash <- getCompilerHash backend
     d' <- errorFromIO $ canonicalizePath (p </> d)
     (ep,is',is2') <- maybeUseConfig d'
     as  <- fmap fixPaths $ mapCompilerM (resolveModule resolver d') is'
@@ -188,20 +187,17 @@
     teContext :: String
   }
 
+tracesLogHeader :: [String]
+tracesLogHeader = ["microseconds","pid","function","context"]
+
 parseTracesFile :: (FilePath,String) -> TrackedErrorsIO [TraceEntry]
 parseTracesFile (f,s) = runTextParser (between nullParse endOfDoc tracesFile) f s where
   tracesFile =  do
     parseHeader
     many parseSingle
   parseHeader = do
-    _ <- quotedString
-    string_ ","
-    _ <- quotedString
-    string_ ","
-    _ <- quotedString
-    string_ ","
-    _ <- quotedString
-    many (char '\n' <|> char '\n') >> return ()
+    sequence_ $ intercalate [string_ ","] $ map ((:[]) . parseColTitle) tracesLogHeader
+    some (char '\n' <|> char '\r') >> return ()
   parseSingle = do
     ms <- parseDec
     string_ ","
@@ -210,15 +206,17 @@
     func <- quotedString
     string_ ","
     c <- quotedString
-    many (char '\n' <|> char '\n') >> return ()
+    some (char '\n' <|> char '\r') >> return ()
     return $ TraceEntry ms pid func c
+  parseColTitle expected = do
+    title <- quotedString
+    when (expected /= title) $ compilerErrorM $ "Expected column named \"" ++ expected ++ "\" but found \"" ++ title ++ "\""
 
 runRecompileCommon :: (PathIOHandler r, CompilerBackend b) => r -> b ->
   ForceMode -> Bool -> FilePath -> [FilePath] -> TrackedErrorsIO ()
 runRecompileCommon resolver backend f rec p ds = do
   explicit <- fmap Set.fromList $ mapCompilerM (errorFromIO . canonicalizePath . (p </>)) ds
   foldM (recursive resolver explicit) Set.empty (map ((,) p) ds) >> return () where
-    compilerHash = getCompilerHash backend
     recursive r explicit da (p2,d0) = do
       d <- resolveModule r p2 d0
       isSystem <- isSystemModule r p2 d0
@@ -234,6 +232,7 @@
            recompile d
            return da'
     recompile d = do
+      compilerHash <- getCompilerHash backend
       upToDate <- isPathUpToDate compilerHash f d
       if f < ForceAll && upToDate
          then compilerWarningM $ "Path " ++ d ++ " is up to date"
diff --git a/src/Compilation/ScopeContext.hs b/src/Compilation/ScopeContext.hs
--- a/src/Compilation/ScopeContext.hs
+++ b/src/Compilation/ScopeContext.hs
@@ -110,6 +110,7 @@
       mapCompilerM_ (\v -> compilerErrorM $ "Member " ++ show v ++
                                             " does not exist (marked Hidden at " ++
                                             formatFullContext c2 ++ ")") missing
+    checkPragma _ = return ()
     allMembers = Set.fromList $ map dmName ms
     valueMembers = map dmName $ filter ((== ValueScope) . dmScope) ms
     immutableContext t = head $ (map ciContext $ filter isCategoryImmutable (getCategoryPragmas t)) ++ [[]]
diff --git a/src/CompilerCxx/Code.hs b/src/CompilerCxx/Code.hs
--- a/src/CompilerCxx/Code.hs
+++ b/src/CompilerCxx/Code.hs
@@ -189,22 +189,22 @@
 
 useAsArgs :: ExpressionValue -> String
 useAsArgs (OpaqueMulti e)                 = e
-useAsArgs (WrappedSingle e)               = "ArgTuple(" ++ e ++ ")"
-useAsArgs (UnwrappedSingle e)             = "ArgTuple(" ++ e ++ ")"
-useAsArgs (BoxedPrimitive PrimBool e)     = "ArgTuple(Box_Bool(" ++ e ++ "))"
-useAsArgs (BoxedPrimitive PrimString e)   = "ArgTuple(Box_String(" ++ e ++ "))"
-useAsArgs (BoxedPrimitive PrimChar e)     = "ArgTuple(Box_Char(" ++ e ++ "))"
-useAsArgs (BoxedPrimitive PrimInt e)      = "ArgTuple(Box_Int(" ++ e ++ "))"
-useAsArgs (BoxedPrimitive PrimFloat e)    = "ArgTuple(Box_Float(" ++ e ++ "))"
-useAsArgs (UnboxedPrimitive PrimBool e)   = "ArgTuple(Box_Bool(" ++ e ++ "))"
-useAsArgs (UnboxedPrimitive PrimString e) = "ArgTuple(Box_String(" ++ e ++ "))"
-useAsArgs (UnboxedPrimitive PrimChar e)   = "ArgTuple(Box_Char(" ++ e ++ "))"
-useAsArgs (UnboxedPrimitive PrimInt e)    = "ArgTuple(Box_Int(" ++ e ++ "))"
-useAsArgs (UnboxedPrimitive PrimFloat e)  = "ArgTuple(Box_Float(" ++ e ++ "))"
+useAsArgs (WrappedSingle e)               = e
+useAsArgs (UnwrappedSingle e)             = e
+useAsArgs (BoxedPrimitive PrimBool e)     = "Box_Bool(" ++ e ++ ")"
+useAsArgs (BoxedPrimitive PrimString e)   = "Box_String(" ++ e ++ ")"
+useAsArgs (BoxedPrimitive PrimChar e)     = "Box_Char(" ++ e ++ ")"
+useAsArgs (BoxedPrimitive PrimInt e)      = "Box_Int(" ++ e ++ ")"
+useAsArgs (BoxedPrimitive PrimFloat e)    = "Box_Float(" ++ e ++ ")"
+useAsArgs (UnboxedPrimitive PrimBool e)   = "Box_Bool(" ++ e ++ ")"
+useAsArgs (UnboxedPrimitive PrimString e) = "Box_String(" ++ e ++ ")"
+useAsArgs (UnboxedPrimitive PrimChar e)   = "Box_Char(" ++ e ++ ")"
+useAsArgs (UnboxedPrimitive PrimInt e)    = "Box_Int(" ++ e ++ ")"
+useAsArgs (UnboxedPrimitive PrimFloat e)  = "Box_Float(" ++ e ++ ")"
 useAsArgs (LazySingle e)                  = useAsArgs $ getFromLazy e
 
 useAsUnwrapped :: ExpressionValue -> String
-useAsUnwrapped (OpaqueMulti e)                 = "(" ++ e ++ ").Only()"
+useAsUnwrapped (OpaqueMulti e)                 = "(" ++ e ++ ").At(0)"
 useAsUnwrapped (WrappedSingle e)               = e
 useAsUnwrapped (UnwrappedSingle e)             = e
 useAsUnwrapped (BoxedPrimitive PrimBool e)     = "Box_Bool(" ++ e ++ ")"
@@ -220,11 +220,11 @@
 useAsUnwrapped (LazySingle e)                  = useAsUnwrapped $ getFromLazy e
 
 useAsUnboxed :: PrimitiveType -> ExpressionValue -> String
-useAsUnboxed PrimBool   (OpaqueMulti e)     = "(" ++ e ++ ").Only().AsBool()"
-useAsUnboxed PrimString (OpaqueMulti e)     = "(" ++ e ++ ").Only().AsString()"
-useAsUnboxed PrimChar   (OpaqueMulti e)     = "(" ++ e ++ ").Only().AsChar()"
-useAsUnboxed PrimInt    (OpaqueMulti e)     = "(" ++ e ++ ").Only().AsInt()"
-useAsUnboxed PrimFloat  (OpaqueMulti e)     = "(" ++ e ++ ").Only().AsFloat()"
+useAsUnboxed PrimBool   (OpaqueMulti e)     = "(" ++ e ++ ").At(0).AsBool()"
+useAsUnboxed PrimString (OpaqueMulti e)     = "(" ++ e ++ ").At(0).AsString()"
+useAsUnboxed PrimChar   (OpaqueMulti e)     = "(" ++ e ++ ").At(0).AsChar()"
+useAsUnboxed PrimInt    (OpaqueMulti e)     = "(" ++ e ++ ").At(0).AsInt()"
+useAsUnboxed PrimFloat  (OpaqueMulti e)     = "(" ++ e ++ ").At(0).AsFloat()"
 useAsUnboxed PrimBool   (WrappedSingle e)   = "(" ++ e ++ ").AsBool()"
 useAsUnboxed PrimString (WrappedSingle e)   = "(" ++ e ++ ").AsString()"
 useAsUnboxed PrimChar   (WrappedSingle e)   = "(" ++ e ++ ").AsChar()"
@@ -251,7 +251,7 @@
 valueAsWrapped v                               = v
 
 valueAsUnwrapped :: ExpressionValue -> ExpressionValue
-valueAsUnwrapped (OpaqueMulti e)                 = UnwrappedSingle $ "(" ++ e ++ ").Only()"
+valueAsUnwrapped (OpaqueMulti e)                 = UnwrappedSingle $ "(" ++ e ++ ").At(0)"
 valueAsUnwrapped (WrappedSingle e)               = UnwrappedSingle e
 valueAsUnwrapped (UnboxedPrimitive PrimBool e)   = UnwrappedSingle $ "Box_Bool(" ++ e ++ ")"
 valueAsUnwrapped (UnboxedPrimitive PrimString e) = UnwrappedSingle $ "Box_String(" ++ e ++ ")"
diff --git a/src/CompilerCxx/CxxFiles.hs b/src/CompilerCxx/CxxFiles.hs
--- a/src/CompilerCxx/CxxFiles.hs
+++ b/src/CompilerCxx/CxxFiles.hs
@@ -31,6 +31,7 @@
   generateVerboseExtension,
 ) where
 
+import Control.Arrow (second)
 import Data.List (intercalate,sortBy)
 import Data.Hashable (hash)
 import Prelude hiding (pi)
@@ -424,6 +425,7 @@
   defineConcreteValue r params fs t d = concatM [
       return $ onlyCode $ "struct " ++ valueName (getCategoryName t) ++ " : public " ++ valueBase ++ " {",
       fmap indentCompiled $ inlineValueConstructor t d,
+      fmap indentCompiled $ inlineFlatCleanup d,
       return declareValueOverrides,
       fmap indentCompiled $ concatM $ map (declareProcedure t False) fs,
       fmap indentCompiled $ concatM $ map (createMember r params t) members,
@@ -495,23 +497,23 @@
 
   declareCategoryOverrides = onlyCodes [
       "  std::string CategoryName() const final;",
-      "  ReturnTuple Dispatch(const CategoryFunction& label, const ParamTuple& params, const ValueTuple& args) final;"
+      "  ReturnTuple Dispatch(const CategoryFunction& label, const ParamsArgs& params_args) final;"
     ]
   declareTypeOverrides = onlyCodes [
       "  std::string CategoryName() const final;",
       "  void BuildTypeName(std::ostream& output) const final;",
       "  bool TypeArgsForParent(const CategoryId& category, std::vector<S<const TypeInstance>>& args) const final;",
-      "  ReturnTuple Dispatch(const TypeFunction& label, const ParamTuple& params, const ValueTuple& args) const final;",
+      "  ReturnTuple Dispatch(const TypeFunction& label, const ParamsArgs& params_args) const final;",
       "  bool CanConvertFrom(const S<const TypeInstance>& from) const final;"
     ]
   declareValueOverrides = onlyCodes [
       "  std::string CategoryName() const final;",
-      "  ReturnTuple Dispatch(const ValueFunction& label, const ParamTuple& params, const ValueTuple& args) final;"
+      "  ReturnTuple Dispatch(const ValueFunction& label, const ParamsArgs& params_args) final;"
     ]
 
   defineCategoryOverrides t fs = return $ mconcat [
       onlyCode $ "std::string " ++ className ++ "::CategoryName() const { return \"" ++ show (getCategoryName t) ++ "\"; }",
-      onlyCode $ "ReturnTuple " ++ className ++ "::Dispatch(const CategoryFunction& label, const ParamTuple& params, const ValueTuple& args) {",
+      onlyCode $ "ReturnTuple " ++ className ++ "::Dispatch(const CategoryFunction& label, const ParamsArgs& params_args) {",
       createFunctionDispatch t CategoryScope fs,
       onlyCode "}"
     ] where
@@ -524,7 +526,7 @@
       onlyCode $ "bool " ++ className ++ "::TypeArgsForParent(const CategoryId& category, std::vector<S<const TypeInstance>>& args) const {",
       createTypeArgsForParent t,
       onlyCode $ "}",
-      onlyCode $ "ReturnTuple " ++ className ++ "::Dispatch(const TypeFunction& label, const ParamTuple& params, const ValueTuple& args) const {",
+      onlyCode $ "ReturnTuple " ++ className ++ "::Dispatch(const TypeFunction& label, const ParamsArgs& params_args) const {",
       createFunctionDispatch t TypeScope fs,
       onlyCode $ "}",
       onlyCode $ "bool " ++ className ++ "::CanConvertFrom(const S<const TypeInstance>& from) const {",
@@ -535,7 +537,7 @@
       params = map vpParam $ getCategoryParams t
   defineValueOverrides t fs = return $ mconcat [
       onlyCode $ "std::string " ++ className ++ "::CategoryName() const { return parent->CategoryName(); }",
-      onlyCode $ "ReturnTuple " ++ className ++ "::Dispatch(const ValueFunction& label, const ParamTuple& params, const ValueTuple& args) {",
+      onlyCode $ "ReturnTuple " ++ className ++ "::Dispatch(const ValueFunction& label, const ParamsArgs& params_args) {",
       createFunctionDispatch t ValueScope fs,
       onlyCode $ "}"
     ] where
@@ -587,16 +589,31 @@
         prefix ++ "~" ++ typeName (getCategoryName t) ++ "() { " ++ typeRemover (getCategoryName t) ++ "(" ++ params ++ "); }"
       ]
 
+  inlineFlatCleanup d = do
+    let pragmas = filter isFlatCleanup $ dcPragmas d
+    handle pragmas where
+      handle [] = return emptyCode
+      handle [FlatCleanup c v] = do
+        let ms = filter ((== v) . dmName) members
+        case ms of
+             [m] -> return $ onlyCode $ "BoxedValue FlatCleanup() final { return std::move(" ++ variableName (dmName m) ++ "); }"
+             _ -> compilerErrorM $ "FlatCleanup requires a non-weak boxed member" ++ formatFullContextBrace c
+      handle ps = "Only one FlatCleanup is allowed" !!>
+        (mapErrorsM $ map (\p -> "FlatCleanup using " ++ show (fcMember p) ++ formatFullContextBrace (fcContext p)) ps)
+      members = filter ((/= WeakValue) . vtRequired . dmType) $
+                filter (not . isStoredUnboxed . dmType) $
+                filter ((== ValueScope) . dmScope) $ dcMembers d
+
   inlineValueConstructor t d = do
     let argParent = "S<const " ++ typeName (getCategoryName t) ++ "> p"
-    let argsPassed = "const ValueTuple& args"
+    let argsPassed = "const ParamsArgs& params_args"
     let allArgs = intercalate ", " [argParent,argsPassed]
     let initParent = "parent(p)"
     let initArgs = map (\(i,m) -> variableName (dmName m) ++ "(" ++ unwrappedArg i m ++ ")") $ zip ([0..] :: [Int]) members
     let allInit = intercalate ", " $ initParent:initArgs
     return $ onlyCode $ "inline " ++ valueName (getCategoryName t) ++ "(" ++ allArgs ++ ") : " ++ allInit ++ " {}" where
-      unwrappedArg i m = writeStoredVariable (dmType m) (UnwrappedSingle $ "args.At(" ++ show i ++ ")")
-      members = filter ((== ValueScope). dmScope) $ dcMembers d
+      unwrappedArg i m = writeStoredVariable (dmType m) (UnwrappedSingle $ "params_args.GetArg(" ++ show i ++ ")")
+      members = filter ((== ValueScope) . dmScope) $ dcMembers d
 
   abstractValueConstructor t = do
     let argParent = "S<const " ++ typeName (getCategoryName t) ++ "> p"
@@ -614,7 +631,7 @@
     return $ onlyCode $ "inline " ++ typeCustom (getCategoryName t) ++ "(" ++ allArgs ++ ") : " ++ allInit ++ " {}"
   customValueConstructor t = do
     let argParent = "S<const " ++ typeName (getCategoryName t) ++ "> p"
-    let argsPassed = "const ValueTuple& args"
+    let argsPassed = "const ParamsArgs& params_args"
     let allArgs = intercalate ", " [argParent,argsPassed]
     let allInit = valueName (getCategoryName t) ++ "(std::move(p))"
     return $ onlyCode $ "inline " ++ valueCustom (getCategoryName t) ++ "(" ++ allArgs ++ ") : " ++ allInit ++ " {}"
@@ -745,18 +762,18 @@
   name = getCategoryName t
   function
     | null filtered = onlyCode fallback
-    | otherwise = onlyCodes $ [typedef] ++ concat (map table $ byCategory) ++ select
+    | otherwise = onlyCodes $ [typedef] ++ select
   filtered = filter ((== s) . sfScope) fs
   flatten f = f:(concat $ map flatten $ sfMerges f)
   flattened = concat $ map flatten filtered
-  byCategory = Map.toList $ Map.fromListWith (++) $ map (\f -> (sfType f,[f])) flattened
+  byCategory = map (second Set.toList) $ Map.toList $ Map.fromListWith Set.union $ map (\f -> (sfType f,Set.fromList [sfName f])) flattened
   typedef
     | s == CategoryScope = "  using CallType = ReturnTuple(" ++ categoryName name ++
-                           "::*)(const ParamTuple&, const ValueTuple&);"
+                           "::*)(const ParamsArgs&);"
     | s == TypeScope     = "  using CallType = ReturnTuple(" ++ typeName name ++
-                           "::*)(const ParamTuple&, const ValueTuple&) const;"
+                           "::*)(const ParamsArgs&) const;"
     | s == ValueScope    = "  using CallType = ReturnTuple(" ++ valueName name ++
-                           "::*)(const ParamTuple&, const ValueTuple&)" ++ suffix ++ ";"
+                           "::*)(const ParamsArgs&)" ++ suffix ++ ";"
     | otherwise = undefined
   suffix
     | isImmutable t = " const"
@@ -766,11 +783,6 @@
     | s == TypeScope     = typeName name     ++ "::" ++ callName f
     | s == ValueScope    = valueName name    ++ "::" ++ callName f
     | otherwise = undefined
-  table (_,[_]) = []
-  table (n2,fs2) =
-    ["  static const CallType " ++ tableName n2 ++ "[] = {"] ++
-    map (\f -> "    &" ++ funcName f ++ ",") (Set.toList $ Set.fromList $ map sfName fs2) ++
-    ["  };"]
   select = [
       "  switch (label.collection) {"
     ] ++ categoryCases ++ [
@@ -781,21 +793,25 @@
   categoryCases = concat $ map singleCase byCategory
   singleCase (n2,[f]) = [
       "    case " ++ categoryIdName n2 ++ ":",
-      "      return " ++ callName (sfName f) ++ "(" ++ args ++ ");"
+      "      // " ++ show n2 ++ " only has one " ++ show s ++ " function.",
+      "      return " ++ callName f ++ "(" ++ args ++ ");"
     ]
-  singleCase (n2,_) = [
+  singleCase (n2,fs2) = [
       "    case " ++ categoryIdName n2 ++ ":",
+      "      static const CallType " ++ tableName n2 ++ "[] = {"
+    ] ++ map (\f -> "        &" ++ funcName f ++ ",") fs2 ++ [
+      "      };",
       "      return (this->*" ++ tableName n2 ++ "[label.function_num])(" ++ args ++ ");"
     ]
   args
-    | s == CategoryScope = "params, args"
-    | s == TypeScope     = "params, args"
-    | s == ValueScope    = "params, args"
+    | s == CategoryScope = "params_args"
+    | s == TypeScope     = "params_args"
+    | s == ValueScope    = "params_args"
     | otherwise = undefined
   fallback
-    | s == CategoryScope = "  return TypeCategory::Dispatch(label, params, args);"
-    | s == TypeScope     = "  return TypeInstance::Dispatch(label, params, args);"
-    | s == ValueScope    = "  return TypeValue::Dispatch(label, params, args);"
+    | s == CategoryScope = "  return TypeCategory::Dispatch(label, params_args);"
+    | s == TypeScope     = "  return TypeInstance::Dispatch(label, params_args);"
+    | s == ValueScope    = "  return TypeValue::Dispatch(label, params_args);"
     | otherwise = undefined
 
 createCanConvertFrom :: AnyCategory c -> CompiledData [String]
@@ -937,7 +953,7 @@
   onlyCodes [
       "BoxedValue " ++ valueCreator (getCategoryName t) ++
       "(S<const " ++ typeName (getCategoryName t) ++ "> parent, " ++
-      "const ValueTuple& args);"
+      "const ParamsArgs& params_args);"
     ]
 
 defineInternalValue :: AnyCategory c -> CompiledData [String]
@@ -948,8 +964,8 @@
   onlyCodes [
       "BoxedValue " ++ valueCreator (getCategoryName t) ++
       "(S<const " ++ typeName (getCategoryName t) ++ "> parent, " ++
-      "const ValueTuple& args) {",
-      "  return BoxedValue::New<" ++ className ++ ">(std::move(parent), args);",
+      "const ParamsArgs& params_args) {",
+      "  return BoxedValue::New<" ++ className ++ ">(std::move(parent), params_args);",
       "}"
     ]
 
diff --git a/src/CompilerCxx/Procedure.hs b/src/CompilerCxx/Procedure.hs
--- a/src/CompilerCxx/Procedure.hs
+++ b/src/CompilerCxx/Procedure.hs
@@ -73,11 +73,11 @@
     | otherwise = ""
   proto
     | sfScope f == CategoryScope =
-      "ReturnTuple " ++ name ++ "(const ParamTuple& params, const ValueTuple& args)"
+      "ReturnTuple " ++ name ++ "(const ParamsArgs& params_args)"
     | sfScope f == TypeScope =
-      "ReturnTuple " ++ name ++ "(const ParamTuple& params, const ValueTuple& args) const"
+      "ReturnTuple " ++ name ++ "(const ParamsArgs& params_args) const"
     | sfScope f == ValueScope =
-      "ReturnTuple " ++ name ++ "(const ParamTuple& params, const ValueTuple& args)" ++ suffix
+      "ReturnTuple " ++ name ++ "(const ParamsArgs& params_args)" ++ suffix
     | otherwise = undefined
 
 data CxxFunctionType =
@@ -132,11 +132,11 @@
       | otherwise = ""
     proto
       | s == CategoryScope =
-        "ReturnTuple " ++ prefix ++ name ++ "(const ParamTuple& params, const ValueTuple& args)" ++ final ++ " {"
+        "ReturnTuple " ++ prefix ++ name ++ "(const ParamsArgs& params_args)" ++ final ++ " {"
       | s == TypeScope =
-        "ReturnTuple " ++ prefix ++ name ++ "(const ParamTuple& params, const ValueTuple& args) const" ++ final ++ " {"
+        "ReturnTuple " ++ prefix ++ name ++ "(const ParamsArgs& params_args) const" ++ final ++ " {"
       | s == ValueScope =
-        "ReturnTuple " ++ prefix ++ name ++ "(const ParamTuple& params, const ValueTuple& args)" ++ suffix ++ final ++ " {"
+        "ReturnTuple " ++ prefix ++ name ++ "(const ParamsArgs& params_args)" ++ suffix ++ final ++ " {"
       | otherwise = undefined
     setProcedureTrace
       | any isNoTrace pragmas = return []
@@ -151,12 +151,12 @@
       | isUnnamedReturns rs2 = []
       | otherwise            = ["ReturnTuple returns(" ++ show (length $ pValues rs1) ++ ");"]
     nameParams = flip map (zip ([0..] :: [Int]) $ pValues ps1) $
-      (\(i,p2) -> paramType ++ " " ++ paramName (vpParam p2) ++ " = params.At(" ++ show i ++ ");")
+      (\(i,p2) -> paramType ++ " " ++ paramName (vpParam p2) ++ " = params_args.GetParam(" ++ show i ++ ");")
     nameArgs = map nameSingleArg (zip ([0..] :: [Int]) $ zip (pValues as1) (pValues $ avNames as2))
     nameSingleArg (i,(t2,n2))
       | isDiscardedInput n2 = "// Arg " ++ show i ++ " (" ++ show (pvType t2) ++ ") is discarded"
       | otherwise = "const " ++ variableProxyType (pvType t2) ++ " " ++ variableName (ivName n2) ++
-                    " = " ++ writeStoredVariable (pvType t2) (UnwrappedSingle $ "args.At(" ++ show i ++ ")") ++ ";"
+                    " = " ++ writeStoredVariable (pvType t2) (UnwrappedSingle $ "params_args.GetArg(" ++ show i ++ ")") ++ ";"
     nameReturns
       | isUnnamedReturns rs2 = []
       | otherwise = map (\(i,(t2,n2)) -> nameReturn i (pvType t2) n2) (zip ([0..] :: [Int]) $ zip (pValues rs1) (pValues $ nrNames rs2))
@@ -719,6 +719,13 @@
     t' <- requireSingle c ts
     f' <- lookupValueFunction t' f
     compileFunctionCall (Just $ useAsUnwrapped e') f' f
+  transform e (SelectReturn c pos) = do
+    (Positional ts,e') <- e
+    when (not $ isOpaqueMulti e') $
+      compilerErrorM $ "Return selection can only be used with function returns" ++ formatFullContextBrace c
+    when (pos >= length ts) $
+      compilerErrorM $ "Position " ++ show pos ++ " exceeds return count " ++ show (length ts) ++ formatFullContext c
+    return (Positional [ts !! pos],WrappedSingle $ useAsReturns e' ++ ".At(" ++ show pos ++ ")")
   requireSingle _ [t] = return t
   requireSingle c2 ts =
     compilerErrorM $ "Function call requires 1 return but found but found {" ++
@@ -902,7 +909,7 @@
   let typeInstance = getType t' sameType s params
   -- TODO: This is unsafe if used in a type or category constructor.
   return (Positional [ValueType RequiredValue $ singleType $ JustTypeInstance t'],
-          UnwrappedSingle $ valueCreator (tiName t') ++ "(" ++ typeInstance ++ ", " ++ es'' ++ ")")
+          UnwrappedSingle $ valueCreator (tiName t') ++ "(" ++ typeInstance ++ ", PassParamsArgs(" ++ es'' ++ "))")
   where
     getType _  True ValueScope _      = "parent"
     getType _  True TypeScope  _      = "PARAM_SELF"
@@ -913,8 +920,7 @@
     getValues rs = do
       (mapCompilerM_ checkArity $ zip ([0..] :: [Int]) $ map fst rs) <??
         "In return at " ++ formatFullContext c
-      return (map (head . pValues . fst) rs,
-              "ArgTuple(" ++ intercalate ", " (map (useAsUnwrapped . snd) rs) ++ ")")
+      return (map (head . pValues . fst) rs, intercalate ", " (map (useAsUnwrapped . snd) rs))
     checkArity (_,Positional [_]) = return ()
     checkArity (i,Positional ts)  =
       compilerErrorM $ "Initializer position " ++ show i ++ " has " ++ show (length ts) ++ " values but should have 1"
@@ -972,12 +978,15 @@
   fa <- csAllFilters
   es' <- sequence $ map compileExpression $ pValues es
   (ts,es'') <- lift $ getValues es'
+  f' <- lift $ parsedToFunctionType f
+  let psActual = case ps of
+                      (Positional []) -> Positional $ take (length $ pValues $ ftParams f') $ repeat (InferredInstance c)
+                      _ -> ps
   self <- autoSelfType
-  ps' <- lift $ fmap Positional $ mapCompilerM (replaceSelfParam self) $ pValues ps
+  ps' <- lift $ fmap Positional $ mapCompilerM (replaceSelfParam self) $ pValues psActual
   ps2 <- lift $ guessParamsFromArgs r fa f ps' (Positional ts)
-  lift $ mapCompilerM_ backgroundMessage $ zip3 (map vpParam $ pValues $ sfParams f) (pValues ps') (pValues ps2)
-  f' <- lift $ parsedToFunctionType f
   f'' <- lift $ assignFunctionParams r fa Map.empty ps2 f'
+  lift $ mapCompilerM_ backgroundMessage $ zip3 (map vpParam $ pValues $ sfParams f) (pValues ps') (pValues ps2)
   -- Called an extra time so arg count mismatches have reasonable errors.
   lift $ processPairs_ (\_ _ -> return ()) (ftArgs f'') (Positional ts)
   lift $ processPairs_ (checkArg r fa) (ftArgs f'') (Positional $ zip ([0..] :: [Int]) ts)
@@ -998,28 +1007,29 @@
       compilerBackgroundM $ "Parameter " ++ show n ++ " (from " ++ show (sfType f) ++ "." ++
         show (sfName f) ++ ") inferred as " ++ show t ++ " at " ++ formatFullContext c2
     backgroundMessage _ = return ()
+    joinParamsArgs ps2 es2 = "PassParamsArgs(" ++ intercalate ", " (ps2 ++ es2) ++ ")"
     assemble Nothing _ ValueScope ValueScope ps2 es2 =
-      return $ callName (sfName f) ++ "(" ++ ps2 ++ ", " ++ es2 ++ ")"
+      return $ callName (sfName f) ++ "(" ++ joinParamsArgs ps2 es2 ++ ")"
     assemble Nothing _ TypeScope TypeScope ps2 es2 =
-      return $ callName (sfName f) ++ "(" ++ ps2 ++ ", " ++ es2 ++ ")"
+      return $ callName (sfName f) ++ "(" ++ joinParamsArgs ps2 es2 ++ ")"
     assemble Nothing scoped ValueScope TypeScope ps2 es2 =
-      return $ scoped ++ callName (sfName f) ++ "(" ++ ps2 ++ ", " ++ es2 ++ ")"
+      return $ scoped ++ callName (sfName f) ++ "(" ++ joinParamsArgs ps2 es2 ++ ")"
     assemble Nothing scoped _ _ ps2 es2 =
-      return $ scoped ++ callName (sfName f) ++ "(" ++ ps2 ++ ", " ++ es2 ++ ")"
+      return $ scoped ++ callName (sfName f) ++ "(" ++ joinParamsArgs ps2 es2 ++ ")"
     assemble (Just e2) _ _ ValueScope ps2 es2 =
-      return $ valueBase ++ "::Call(" ++ e2 ++ ", " ++ functionName f ++ ", " ++ ps2 ++ ", " ++ es2 ++ ")"
+      return $ valueBase ++ "::Call(" ++ e2 ++ ", " ++ functionName f ++ ", " ++ joinParamsArgs ps2 es2 ++ ")"
     assemble (Just e2) _ _ TypeScope ps2 es2 =
-      return $ typeBase ++ "::Call(" ++ e2 ++ ", " ++ functionName f ++ ", " ++ ps2 ++ ", " ++ es2 ++ ")"
+      return $ typeBase ++ "::Call(" ++ e2 ++ ", " ++ functionName f ++ ", " ++ joinParamsArgs ps2 es2 ++ ")"
     assemble (Just e2) _ _ _ ps2 es2 =
-      return $ e2 ++ ".Call(" ++ functionName f ++ ", " ++ ps2 ++ ", " ++ es2 ++ ")"
+      return $ e2 ++ ".Call(" ++ functionName f ++ ", " ++ joinParamsArgs ps2 es2 ++ ")"
     -- TODO: Lots of duplication with assignments and initialization.
     -- Single expression, but possibly multi-return.
-    getValues [(Positional ts,e2)] = return (ts,useAsArgs e2)
+    getValues [(Positional ts,e2)] = return (ts,[useAsArgs e2])
     -- Multi-expression => must all be singles.
     getValues rs = do
       (mapCompilerM_ checkArity $ zip ([0..] :: [Int]) $ map fst rs) <??
         "In return at " ++ formatFullContext c
-      return (map (head . pValues . fst) rs, "ArgTuple(" ++ intercalate ", " (map (useAsUnwrapped . snd) rs) ++ ")")
+      return (map (head . pValues . fst) rs,map (useAsUnwrapped . snd) rs)
     checkArity (_,Positional [_]) = return ()
     checkArity (i,Positional ts)  =
       compilerErrorM $ "Return position " ++ show i ++ " has " ++ show (length ts) ++ " values but should have 1"
@@ -1152,10 +1162,8 @@
   return $ "T_get(" ++ intercalate ", " ps' ++ ")"
 
 expandParams2 :: (CollectErrorsM m, CompilerContext c m s a) =>
-  Positional GeneralInstance -> CompilerState a m String
-expandParams2 ps = do
-  ps' <- sequence $ map expandGeneralInstance $ pValues ps
-  return $ "ParamTuple(" ++ intercalate "," ps' ++ ")"
+  Positional GeneralInstance -> CompilerState a m [String]
+expandParams2 ps = sequence $ map expandGeneralInstance $ pValues ps
 
 expandCategory :: CompilerContext c m s a =>
   CategoryName -> CompilerState a m String
diff --git a/src/Config/CompilerConfig.hs b/src/Config/CompilerConfig.hs
new file mode 100644
--- /dev/null
+++ b/src/Config/CompilerConfig.hs
@@ -0,0 +1,50 @@
+{- -----------------------------------------------------------------------------
+Copyright 2020-2021 Kevin P. Barry
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+----------------------------------------------------------------------------- -}
+
+-- Author: Kevin P. Barry [ta0kira@gmail.com]
+
+{-# LANGUAGE Safe #-}
+
+module Config.CompilerConfig (
+  Backend(..),
+  LocalConfig(..),
+  Resolver(..),
+) where
+
+
+data LocalConfig =
+  LocalConfig {
+    lcResolver :: Resolver,
+    lcBackend :: Backend
+  }
+  deriving (Eq,Show)
+
+data Backend =
+  UnixBackend {
+    ucCxxBinary :: FilePath,
+    ucCompileFlags :: [String],
+    ucLibraryFlags :: [String],
+    ucBinaryFlags :: [String],
+    ucArBinary :: FilePath
+  }
+  deriving (Eq,Show)
+
+data Resolver =
+  SimpleResolver {
+    srVisibleSystem :: [FilePath],
+    srExtraPaths :: [FilePath]
+  }
+  deriving (Eq,Show)
diff --git a/src/Config/LoadConfig.hs b/src/Config/LoadConfig.hs
--- a/src/Config/LoadConfig.hs
+++ b/src/Config/LoadConfig.hs
@@ -1,5 +1,5 @@
 {- -----------------------------------------------------------------------------
-Copyright 2020 Kevin P. Barry
+Copyright 2020-2021 Kevin P. Barry
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -16,39 +16,45 @@
 
 -- Author: Kevin P. Barry [ta0kira@gmail.com]
 
-{-# LANGUAGE Safe #-}
-
 module Config.LoadConfig (
   localConfigPath,
   loadConfig,
+  saveConfig,
 ) where
 
 import Control.Monad (when)
 import Control.Monad.IO.Class
 import System.Directory
+import System.IO
 
 import Base.CompilerError
 import Config.LocalConfig
+import Config.ParseConfig ()
+import Module.ParseMetadata
 
 import Paths_zeolite_lang (getDataFileName)
 
 
-loadConfig :: (MonadIO m, ErrorContextM m) => m (Resolver,Backend)
+saveConfig :: (MonadIO m, CollectErrorsM m) => (Resolver,Backend) -> m ()
+saveConfig (resolver,backend) = do
+  configFile <- liftIO localConfigPath
+  liftIO $ hPutStrLn stderr $ "Writing local config to " ++ configFile ++ "."
+  serialized <- autoWriteConfig (LocalConfig resolver backend)
+  liftIO $ writeFile configFile serialized
+
+loadConfig :: (MonadIO m, CollectErrorsM m) => m (Resolver,Backend)
 loadConfig = do
   configFile <- liftIO localConfigPath
   isFile <- liftIO $ doesFileExist configFile
   when (not isFile) $ compilerErrorM "Zeolite has not been configured. Please run zeolite-setup."
   configString <- liftIO $ readFile configFile
-  lc <- check $ (reads configString :: [(LocalConfig,String)])
+  lc <- autoReadConfig configFile configString <!! "Zeolite configuration is corrupt. Please rerun zeolite-setup."
   pathsFile   <- liftIO $ globalPathsPath
   pathsExists <- liftIO $ doesFileExist pathsFile
   paths <- if pathsExists
               then liftIO $ readFile pathsFile >>= return . lines
               else return []
-  return (addPaths (lcResolver lc) paths,lcBackend lc) where
-    check [(cm,"")]   = return cm
-    check [(cm,"\n")] = return cm
-    check _ = compilerErrorM "Zeolite configuration is corrupt. Please rerun zeolite-setup."
+  return (addPaths (lcResolver lc) paths,lcBackend lc)
 
 localConfigPath :: IO FilePath
 localConfigPath = getDataFileName localConfigFilename >>= canonicalizePath
diff --git a/src/Config/LocalConfig.hs b/src/Config/LocalConfig.hs
--- a/src/Config/LocalConfig.hs
+++ b/src/Config/LocalConfig.hs
@@ -1,5 +1,5 @@
 {- -----------------------------------------------------------------------------
-Copyright 2020 Kevin P. Barry
+Copyright 2020-2021 Kevin P. Barry
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -16,8 +16,6 @@
 
 -- Author: Kevin P. Barry [ta0kira@gmail.com]
 
-{-# LANGUAGE Safe #-}
-
 module Config.LocalConfig (
   Backend(..),
   LocalConfig(..),
@@ -43,35 +41,14 @@
 
 import Base.CompilerError
 import Cli.Programs
+import Config.CompilerConfig
+import Config.ParseConfig ()
+import Module.ParseMetadata
 import Module.Paths
 
 import Paths_zeolite_lang (getDataFileName,version)
 
 
-data Backend =
-  UnixBackend {
-    ucCxxBinary :: FilePath,
-    ucCompileFlags :: [String],
-    ucLibraryFlags :: [String],
-    ucBinaryFlags :: [String],
-    ucArBinary :: FilePath
-  }
-  deriving (Read,Show)
-
-data Resolver =
-  SimpleResolver {
-    srVisibleSystem :: [FilePath],
-    srExtraPaths :: [FilePath]
-  }
-  deriving (Read,Show)
-
-data LocalConfig =
-  LocalConfig {
-    lcBackend :: Backend,
-    lcResolver :: Resolver
-  }
-  deriving (Read,Show)
-
 rootPath :: IO FilePath
 rootPath = getDataFileName ""
 
@@ -129,8 +106,10 @@
         hDuplicateTo h1 stdout
         hDuplicateTo h2 stderr
         executeFile b True as Nothing
-  getCompilerHash b = VersionHash $ flip showHex "" $ abs $ hash $ minorVersion ++ show b where
-    minorVersion = show $ take 3 $ versionBranch version
+  getCompilerHash b = do
+    let minorVersion = show $ take 3 $ versionBranch version
+    serialized <- autoWriteConfig b
+    return $ VersionHash $ flip showHex "" $ abs $ hash $ minorVersion ++ serialized
 
 executeProcess :: (MonadIO m, ErrorContextM m) => String -> [String] -> m ()
 executeProcess c os = do
diff --git a/src/Config/ParseConfig.hs b/src/Config/ParseConfig.hs
new file mode 100644
--- /dev/null
+++ b/src/Config/ParseConfig.hs
@@ -0,0 +1,86 @@
+{- -----------------------------------------------------------------------------
+Copyright 2020-2021 Kevin P. Barry
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+----------------------------------------------------------------------------- -}
+
+-- Author: Kevin P. Barry [ta0kira@gmail.com]
+
+module Config.ParseConfig (
+) where
+
+import Control.Applicative.Permutations
+
+import Config.CompilerConfig
+import Module.ParseMetadata
+import Parser.Common
+
+
+instance ConfigFormat Backend where
+  readConfig = do
+    sepAfter (string_ "unix_backend")
+    structOpen
+    o <- runPermutation $ UnixBackend
+      <$> parseRequired "cxx_binary:"    parseQuoted
+      <*> parseRequired "compile_flags:" (parseList parseQuoted)
+      <*> parseRequired "library_flags:" (parseList parseQuoted)
+      <*> parseRequired "binary_flags:"  (parseList parseQuoted)
+      <*> parseRequired "ar_binary:"     parseQuoted
+    structClose
+    return o
+  writeConfig (UnixBackend cb cf lf bf ar) = do
+    return $ [
+        "unix_backend {",
+        indent $ "cxx_binary: " ++ show cb,
+        indent "compile_flags: ["
+      ] ++ (indents . indents) (map show cf) ++ [
+        indent "]",
+        indent "library_flags: ["
+      ] ++ (indents . indents) (map show lf) ++ [
+        indent "]",
+        indent "binary_flags: ["
+      ] ++ (indents . indents) (map show bf) ++ [
+        indent "]",
+        indent $ "ar_binary: " ++ show ar,
+        "}"
+      ]
+
+instance ConfigFormat Resolver where
+  readConfig = do
+    sepAfter (string_ "simple_resolver")
+    structOpen
+    o <- runPermutation $ SimpleResolver
+      <$> parseRequired "system_allowed:" (parseList parseQuoted)
+      <*> parseRequired "extra_paths:"    (parseList parseQuoted)
+    structClose
+    return o
+  writeConfig (SimpleResolver ss es) = do
+    return $ [
+        "simple_resolver {",
+        indent "system_allowed: ["
+      ] ++ (indents . indents) (map show ss) ++ [
+        indent "]",
+        indent "extra_paths: ["
+      ] ++ (indents . indents) (map show es) ++ [
+        indent "]",
+        "}"
+      ]
+
+instance ConfigFormat LocalConfig where
+  readConfig = runPermutation $ LocalConfig
+    <$> parseRequired "resolver:" readConfig
+    <*> parseRequired "backend:"  readConfig
+  writeConfig (LocalConfig r b) = do
+    r' <- writeConfig r
+    b' <- writeConfig b
+    return $ ("resolver: " `prependFirst` r') ++ ("backend: " `prependFirst` b')
diff --git a/src/Module/CompileMetadata.hs b/src/Module/CompileMetadata.hs
--- a/src/Module/CompileMetadata.hs
+++ b/src/Module/CompileMetadata.hs
@@ -39,6 +39,7 @@
 data CompileMetadata =
   CompileMetadata {
     cmVersionHash :: VersionHash,
+    cmRoot :: FilePath,
     cmPath :: FilePath,
     cmExtra :: [FilePath],
     cmPublicNamespace :: Namespace,
diff --git a/src/Module/ParseMetadata.hs b/src/Module/ParseMetadata.hs
--- a/src/Module/ParseMetadata.hs
+++ b/src/Module/ParseMetadata.hs
@@ -17,9 +17,18 @@
 -- Author: Kevin P. Barry [ta0kira@gmail.com]
 
 module Module.ParseMetadata (
-  ConfigFormat,
+  ConfigFormat(..),
   autoReadConfig,
   autoWriteConfig,
+  indent,
+  indents,
+  parseList,
+  parseOptional,
+  parseQuoted,
+  parseRequired,
+  prependFirst,
+  structClose,
+  structOpen,
 ) where
 
 import Control.Applicative.Permutations
@@ -124,6 +133,7 @@
 instance ConfigFormat CompileMetadata where
   readConfig = runPermutation $ CompileMetadata
     <$> parseRequired "version_hash:"       parseHash
+    <*> parseRequired "root:"               parseQuoted
     <*> parseRequired "path:"               parseQuoted
     <*> parseOptional "extra_paths:"        [] (parseList parseQuoted)
     <*> parseOptional "public_namespace:"   NoNamespace parseNamespace
@@ -143,7 +153,7 @@
     <*> parseRequired "libraries:"          (parseList parseQuoted)
     <*> parseRequired "link_flags:"         (parseList parseQuoted)
     <*> parseRequired "object_files:"       (parseList readConfig)
-  writeConfig (CompileMetadata h p ee ns1 ns2 is is2 cs1 cs2 ds1 ds2 ps xs ts hxx cxx bs ls lf os) = do
+  writeConfig (CompileMetadata h p d ee ns1 ns2 is is2 cs1 cs2 ds1 ds2 ps xs ts hxx cxx bs ls lf os) = do
     validateHash h
     ns1' <- maybeShowNamespace "public_namespace:"  ns1
     ns2' <- maybeShowNamespace "private_namespace:" ns2
@@ -152,7 +162,8 @@
     os' <- fmap concat $ mapCompilerM writeConfig os
     return $ [
         "version_hash: " ++ show h,
-        "path: " ++ show p
+        "root: " ++ show p,
+        "path: " ++ show d
       ] ++ ns1' ++ ns2' ++ [
         "extra_paths: ["
       ] ++ indents (map show ee) ++ [
diff --git a/src/Module/ProcessMetadata.hs b/src/Module/ProcessMetadata.hs
--- a/src/Module/ProcessMetadata.hs
+++ b/src/Module/ProcessMetadata.hs
@@ -35,6 +35,9 @@
   getSourceFilesForDeps,
   isPathConfigured,
   isPathUpToDate,
+  isPrivateSource,
+  isPublicSource,
+  isTestSource,
   loadModuleGlobals,
   loadModuleMetadata,
   loadPrivateDeps,
@@ -90,6 +93,15 @@
 tracesFilename :: FilePath
 tracesFilename = "traced-lines"
 
+isPublicSource :: FilePath -> Bool
+isPublicSource = isSuffixOf ".0rp"
+
+isPrivateSource :: FilePath -> Bool
+isPrivateSource = isSuffixOf ".0rx"
+
+isTestSource :: FilePath -> Bool
+isTestSource = isSuffixOf ".0rt"
+
 type MetadataMap = Map.Map FilePath CompileMetadata
 
 mapMetadata :: [CompileMetadata] -> MetadataMap
@@ -192,9 +204,9 @@
     when (not isDir) $ compilerErrorM $ "Path \"" ++ absolute ++ "\" does not exist"
     errorFromIO $ getDirectoryContents absolute >>= return . map (p </>)
   select ds = (ps,xs,ts) where
-    ps = nub $ filter (isSuffixOf ".0rp") ds
-    xs = nub $ filter (isSuffixOf ".0rx") ds
-    ts = nub $ filter (isSuffixOf ".0rt") ds
+    ps = nub $ filter isPublicSource  ds
+    xs = nub $ filter isPrivateSource ds
+    ts = nub $ filter isTestSource    ds
 
 getExprMap :: FilePath -> ModuleConfig -> TrackedErrorsIO (ExprMap SourceContext)
 getExprMap p m = do
@@ -207,7 +219,7 @@
 
 getSourceFilesForDeps :: [CompileMetadata] -> [FilePath]
 getSourceFilesForDeps = concat . map extract where
-  extract m = map (cmPath m </>) (cmPublicFiles m)
+  extract m = map (cmRoot m </>) (filter isPublicSource $ cmPublicFiles m ++ cmPrivateFiles m)
 
 getNamespacesForDeps :: [CompileMetadata] -> [Namespace]
 getNamespacesForDeps = filter (not . isNoNamespace) . map cmPublicNamespace
@@ -309,20 +321,21 @@
 checkModuleVersionHash h m = cmVersionHash m == h
 
 checkModuleFreshness :: VersionHash -> MetadataMap -> FilePath -> CompileMetadata -> TrackedErrorsIO ()
-checkModuleFreshness h ca p m@(CompileMetadata _ p2 ep _ _ is is2 _ _ _ _ ps xs ts hxx cxx bs ls _ os) = do
+checkModuleFreshness h ca p m@(CompileMetadata _ p2 d ep _ _ is is2 _ _ _ _ ps xs ts hxx cxx bs ls _ os) = do
   time <- errorFromIO $ getModificationTime $ getCachedPath p "" metadataFilename
-  (ps2,xs2,ts2) <- findSourceFiles "" (p2:ep)
+  (ps2,xs2,ts2) <- findSourceFiles p2 (d:ep)
   let rs = Set.toList $ Set.fromList $ concat $ map getRequires os
+  expectedFiles <- mapCompilerM (errorFromIO . canonicalizePath . (p2</>)) (ps++xs++ts)
+  actualFiles   <- mapCompilerM (errorFromIO . canonicalizePath . (p2</>)) (ps2++xs2++ts2)
+  inputFiles    <- mapCompilerM (errorFromIO . canonicalizePath . (p2</>)) (xs++ts)
   collectAllM_ $ [
       checkHash,
       checkInput time (p </> moduleFilename),
-      mapCompilerM (errorFromIO . canonicalizePath) ps2 >>= checkMissing ps,
-      mapCompilerM (errorFromIO . canonicalizePath) xs2 >>= checkMissing xs,
-      mapCompilerM (errorFromIO . canonicalizePath) ts2 >>= checkMissing ts
+      checkMissing expectedFiles actualFiles
     ] ++
     (map (checkDep time) $ is ++ is2) ++
-    (map (checkInput time) $ ps ++ xs) ++
-    (map (checkInput time . getCachedPath p2 "") $ hxx ++ cxx) ++
+    (map (checkInput time) inputFiles) ++
+    (map (checkInput time . getCachedPath d "") $ hxx ++ cxx) ++
     (map checkOutput bs) ++
     (map checkOutput ls) ++
     (map checkObject os) ++
@@ -341,20 +354,21 @@
       when (not exists) $ compilerErrorM $ "Output file \"" ++ f ++ "\" is missing"
     checkDep time dep = do
       cm <- loadMetadata ca dep
-      mapCompilerM_ (checkInput time . (cmPath cm </>)) (cmPublicFiles cm)
+      files <- mapM (errorFromIO . canonicalizePath . (cmRoot cm </>)) (cmPublicFiles cm)
+      mapCompilerM_ (checkInput time) files
     checkObject (CategoryObjectFile _ _ fs) = mapCompilerM_ checkOutput fs
     checkObject (OtherObjectFile f)         = checkOutput f
     getRequires (CategoryObjectFile _ rs _) = rs
     getRequires _                           = []
-    checkRequire (CategoryIdentifier d c ns) = do
-      cm <- loadMetadata ca d
-      when (cmPath cm /= p2 && ns /= cmPublicNamespace cm) $
+    checkRequire (CategoryIdentifier d2 c ns) = do
+      cm <- loadMetadata ca d2
+      when (cmPath cm /= d && ns /= cmPublicNamespace cm) $
         compilerErrorM $ "Required category " ++ show c ++ " is newer than cached data"
     checkRequire (UnresolvedCategory c) =
       compilerErrorM $ "Required category " ++ show c ++ " is unresolved"
     checkMissing s0 s1 = do
       let missing = Set.toList $ Set.fromList s1 `Set.difference` Set.fromList s0
-      mapCompilerM_ (\f -> compilerErrorM $ "Required path \"" ++ f ++ "\" is not present in cached data") missing
+      mapCompilerM_ (\f -> compilerErrorM $ "Input path \"" ++ f ++ "\" is not present in cached data") missing
     doesFileOrDirExist f2 = do
       existF <- errorFromIO $ doesFileExist f2
       if existF
@@ -427,20 +441,20 @@
 
 loadModuleGlobals :: PathIOHandler r => r -> FilePath -> (Namespace,Namespace) -> [FilePath] ->
   Maybe CompileMetadata -> [CompileMetadata] -> [CompileMetadata] ->
-  TrackedErrorsIO ([WithVisibility (AnyCategory SourceContext)])
+  TrackedErrorsIO ([WithVisibility (AnyCategory SourceContext)],Set.Set FilePath)
 loadModuleGlobals r p (ns0,ns1) fs m deps1 deps2 = do
   let public = Set.fromList $ map cmPath deps1
   let deps2' = filter (\cm -> not $ cmPath cm `Set.member` public) deps2
   cs0 <- fmap concat $ mapCompilerM (processDeps False [FromDependency])            deps1
   cs1 <- fmap concat $ mapCompilerM (processDeps False [FromDependency,ModuleOnly]) deps2'
-  cs2 <- loadAllPublic (ns0,ns1) fs
+  (cs2,xa) <- loadAllPublic (ns0,ns1) fs
   cs3 <- case m of
               Just m2 -> processDeps True [FromDependency] m2
               _       -> return []
-  return (cs0++cs1++cs2++cs3) where
+  return (cs0++cs1++cs2++cs3,xa) where
     processDeps same ss dep = do
       let fs2 = getSourceFilesForDeps [dep]
-      cs <- loadAllPublic (cmPublicNamespace dep,cmPrivateNamespace dep) fs2
+      (cs,_) <- loadAllPublic (cmPublicNamespace dep,cmPrivateNamespace dep) fs2
       let cs' = if not same
                    -- Allow ModuleOnly if the dep is the same module being
                    -- compiled. (Tests load the module being tested as a dep.)
@@ -449,14 +463,18 @@
       return $ map (updateCodeVisibility (Set.union (Set.fromList ss))) cs'
     loadAllPublic (ns2,ns3) fs2 = do
       fs2' <- zipWithContents r p fs2
-      fmap concat $ mapCompilerM loadPublic fs2'
+      loaded <- mapCompilerM loadPublic fs2'
+      return (concat $ map fst loaded,Set.fromList $ concat $ map snd loaded)
       where
         loadPublic p3 = do
           (pragmas,cs) <- parsePublicSource p3
+          xs <- if any isModuleOnly pragmas
+                   then errorFromIO $ fmap (:[]) $ canonicalizePath $ p </> fst p3
+                   else return []
           let tags = Set.fromList $
                      (if any isTestsOnly  pragmas then [TestsOnly]  else []) ++
                      (if any isModuleOnly pragmas then [ModuleOnly] else [])
           let cs' = if any isModuleOnly pragmas
                        then map (setCategoryNamespace ns3) cs
                        else map (setCategoryNamespace ns2) cs
-          return $ map (WithVisibility tags) cs'
+          return (map (WithVisibility tags) cs',xs)
diff --git a/src/Parser/Common.hs b/src/Parser/Common.hs
--- a/src/Parser/Common.hs
+++ b/src/Parser/Common.hs
@@ -374,7 +374,7 @@
 lineComment = labeled "line comment" $
   between (string_ "//")
           lineEnd
-          (many $ satisfy (/= '\n'))
+          (many $ satisfy (\c -> c /= '\n' && c /= '\r'))
 
 blockComment :: TextParser String
 blockComment = labeled "block comment" $
diff --git a/src/Parser/DefinedCategory.hs b/src/Parser/DefinedCategory.hs
--- a/src/Parser/DefinedCategory.hs
+++ b/src/Parser/DefinedCategory.hs
@@ -49,7 +49,11 @@
     return $ DefinedCategory [c] n pragmas ds rs ms ps fs
     where
       parseRefinesDefines = fmap merge2 $ sepBy refineOrDefine optionalSpace
-      singlePragma = readOnly <|> hidden
+      singlePragma = readOnly <|> hidden <|> flatCleanup
+      flatCleanup = autoPragma "FlatCleanup" $ Right parseAt where
+        parseAt c = do
+          v <- labeled "variable name" sourceParser
+          return $ FlatCleanup [c] v
       readOnly = autoPragma "ReadOnly" $ Right parseAt where
         parseAt c = do
           vs <- labeled "variable names" $ sepBy sourceParser (sepAfter $ string ",")
diff --git a/src/Parser/Procedure.hs b/src/Parser/Procedure.hs
--- a/src/Parser/Procedure.hs
+++ b/src/Parser/Procedure.hs
@@ -658,7 +658,7 @@
       return $ EmptyLiteral [c]
 
 instance ParseFromSource (ValueOperation SourceContext) where
-  sourceParser = try valueCall <|> try conversion where
+  sourceParser = try valueCall <|> try conversion <|> selectReturn where
     valueCall = labeled "function call" $ do
       c <- getSourceContext
       valueSymbolGet
@@ -673,6 +673,12 @@
       n <- sourceParser
       f <- parseFunctionCall c n
       return $ ConvertedCall [c] t f
+    selectReturn = labeled "return selection" $ do
+      c <- getSourceContext
+      sepAfter_ (string_ "{")
+      pos <- labeled "return position" parseDec
+      sepAfter_ (string_ "}")
+      return $ SelectReturn [c] (fromIntegral pos)
 
 instance ParseFromSource MacroName where
   sourceParser = labeled "macro name" $ do
diff --git a/src/Test/Common.hs b/src/Test/Common.hs
--- a/src/Test/Common.hs
+++ b/src/Test/Common.hs
@@ -23,6 +23,8 @@
   checkParseError,
   checkParseMatch,
   checkTypeFail,
+  checkWriteFail,
+  checkWriteThenRead,
   checkTypeSuccess,
   containsAtLeast,
   containsAtMost,
@@ -53,6 +55,7 @@
 import Base.CompilerError
 import Base.CompilerMessage
 import Base.TrackedErrors
+import Module.ParseMetadata (ConfigFormat,autoReadConfig,autoWriteConfig)
 import Parser.Common
 import Parser.TextParser
 import Parser.TypeInstance ()
@@ -216,3 +219,27 @@
             compilerErrorM $ "Expected pattern " ++ show m ++ " in error output but got\n" ++ text
       | otherwise =
           compilerErrorM $ "Expected write failure but got\n" ++ show (getCompilerSuccess c)
+
+checkWriteThenRead :: (Eq a, Show a, ConfigFormat a) => a -> IO (TrackedErrors ())
+checkWriteThenRead m = return $ do
+  text <- fmap spamComments $ autoWriteConfig m
+  m' <- autoReadConfig "(string)" text <!! "Serialized >>>\n\n" ++ text ++ "\n<<< Serialized\n\n"
+  when (m' /= m) $
+    compilerErrorM $ "Failed to match after write/read\n" ++
+                   "Before:\n" ++ show m ++ "\n" ++
+                   "After:\n" ++ show m' ++ "\n" ++
+                   "Intermediate:\n" ++ text where
+   spamComments = unlines . map (++ " // spam") . lines
+
+checkWriteFail :: ConfigFormat a => String -> a -> IO (TrackedErrors ())
+checkWriteFail p m = return $ do
+  let m' = autoWriteConfig m
+  check m'
+  where
+    check c
+      | isCompilerError c = do
+          let text = show (getCompilerError c)
+          when (not $ text =~ p) $
+            compilerErrorM $ "Expected pattern " ++ show p ++ " in error output but got\n" ++ text
+      | otherwise =
+          compilerErrorM $ "Expected write failure but got\n" ++ getCompilerSuccess c
diff --git a/src/Test/ParseConfig.hs b/src/Test/ParseConfig.hs
new file mode 100644
--- /dev/null
+++ b/src/Test/ParseConfig.hs
@@ -0,0 +1,61 @@
+{- -----------------------------------------------------------------------------
+Copyright 2021 Kevin P. Barry
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+----------------------------------------------------------------------------- -}
+
+-- Author: Kevin P. Barry [ta0kira@gmail.com]
+
+module Test.ParseConfig (tests) where
+
+import Base.TrackedErrors
+import Config.CompilerConfig
+import Config.ParseConfig ()
+import Test.Common
+
+
+compilerConfig :: LocalConfig
+compilerConfig = LocalConfig {
+    lcBackend = UnixBackend {
+      ucCxxBinary = "/usr/bin/clang++",
+      ucCompileFlags = [
+        "-O2",
+        "-std=c++11",
+        "-fPIC"
+      ],
+      ucLibraryFlags = [
+        "-shared",
+        "-fpic"
+      ],
+      ucBinaryFlags = [
+        "-02",
+        "-std=c++11"
+      ],
+      ucArBinary = "/usr/bin/ar"
+    },
+    lcResolver = SimpleResolver {
+      srVisibleSystem = [
+        "lib",
+        "testing"
+      ],
+      srExtraPaths = [
+        "extra1",
+        "extra2"
+      ]
+    }
+  }
+
+tests :: [IO (TrackedErrors ())]
+tests = [
+    checkWriteThenRead compilerConfig
+  ]
diff --git a/src/Test/ParseMetadata.hs b/src/Test/ParseMetadata.hs
--- a/src/Test/ParseMetadata.hs
+++ b/src/Test/ParseMetadata.hs
@@ -19,7 +19,6 @@
 module Test.ParseMetadata (tests) where
 
 import Control.Monad (when)
-import Text.Regex.TDFA
 
 import Base.CompilerError
 import Base.Positional
@@ -38,6 +37,7 @@
 hugeCompileMetadata :: CompileMetadata  -- testfiles/module-cache.txt
 hugeCompileMetadata = CompileMetadata {
     cmVersionHash = VersionHash "0123456789ABCDEFabcdef",
+    cmRoot = "/home/project",
     cmPath = "/home/project/special",
     cmExtra = [
       "extra1",
@@ -177,6 +177,7 @@
 
     checkWriteFail "bad hash" $ CompileMetadata {
       cmVersionHash = VersionHash "bad hash",
+      cmRoot = "/home/project",
       cmPath = "/home/project/special",
       cmExtra = [],
       cmPublicNamespace = NoNamespace,
@@ -200,6 +201,7 @@
 
     checkWriteFail "bad namespace" $ CompileMetadata {
       cmVersionHash = VersionHash "0123456789ABCDEFabcdef",
+      cmRoot = "/home/project",
       cmPath = "/home/project/special",
       cmExtra = [],
       cmPublicNamespace = StaticNamespace "bad namespace",
@@ -223,6 +225,7 @@
 
     checkWriteFail "bad namespace" $ CompileMetadata {
       cmVersionHash = VersionHash "0123456789ABCDEFabcdef",
+      cmRoot = "/home/project",
       cmPath = "/home/project/special",
       cmExtra = [],
       cmPublicNamespace = NoNamespace,
@@ -246,6 +249,7 @@
 
     checkWriteFail "bad category" $ CompileMetadata {
       cmVersionHash = VersionHash "0123456789ABCDEFabcdef",
+      cmRoot = "/home/project",
       cmPath = "/home/project/special",
       cmExtra = [],
       cmPublicNamespace = NoNamespace,
@@ -271,6 +275,7 @@
 
     checkWriteFail "bad category" $ CompileMetadata {
       cmVersionHash = VersionHash "0123456789ABCDEFabcdef",
+      cmRoot = "/home/project",
       cmPath = "/home/project/special",
       cmExtra = [],
       cmPublicNamespace = NoNamespace,
@@ -410,30 +415,6 @@
 
     checkParsesAs ("testfiles" </> "module-cache.txt") (== hugeCompileMetadata)
   ]
-
-checkWriteThenRead :: (Eq a, Show a, ConfigFormat a) => a -> IO (TrackedErrors ())
-checkWriteThenRead m = return $ do
-  text <- fmap spamComments $ autoWriteConfig m
-  m' <- autoReadConfig "(string)" text <!! "Serialized >>>\n\n" ++ text ++ "\n<<< Serialized\n\n"
-  when (m' /= m) $
-    compilerErrorM $ "Failed to match after write/read\n" ++
-                   "Before:\n" ++ show m ++ "\n" ++
-                   "After:\n" ++ show m' ++ "\n" ++
-                   "Intermediate:\n" ++ text where
-   spamComments = unlines . map (++ " // spam") . lines
-
-checkWriteFail :: ConfigFormat a => String -> a -> IO (TrackedErrors ())
-checkWriteFail p m = return $ do
-  let m' = autoWriteConfig m
-  check m'
-  where
-    check c
-      | isCompilerError c = do
-          let text = show (getCompilerError c)
-          when (not $ text =~ p) $
-            compilerErrorM $ "Expected pattern " ++ show p ++ " in error output but got\n" ++ text
-      | otherwise =
-          compilerErrorM $ "Expected write failure but got\n" ++ getCompilerSuccess c
 
 checkParsesAs :: (Show a, ConfigFormat a) => String -> (a -> Bool) -> IO (TrackedErrors ())
 checkParsesAs f m = do
diff --git a/src/Test/Procedure.hs b/src/Test/Procedure.hs
--- a/src/Test/Procedure.hs
+++ b/src/Test/Procedure.hs
@@ -223,6 +223,11 @@
     checkShortParseSuccess "_ <- 1 / 2 - 3",
     checkShortParseSuccess "_ <- x < y || z > w",
 
+    checkShortParseSuccess "\\ call(){0}",
+    checkShortParseSuccess "\\ call(){0}.bar()",
+    checkShortParseFail "\\ call(){x}",
+    checkShortParseFail "\\ call(){-1}",
+
     checkParsesAs "'\"'"
                   (\e -> case e of
                               (Expression _ (UnambiguousLiteral (CharLiteral _ '"')) []) -> True
diff --git a/src/Test/testfiles/module-cache.txt b/src/Test/testfiles/module-cache.txt
--- a/src/Test/testfiles/module-cache.txt
+++ b/src/Test/testfiles/module-cache.txt
@@ -26,6 +26,7 @@
   "/home/project/public-dep1"
   "/home/project/public-dep2"
 ]
+root: "/home/project"
 private_deps: [
   "/home/project/private-dep1"
   "/home/project/private-dep2"
diff --git a/src/Types/Builtin.hs b/src/Types/Builtin.hs
--- a/src/Types/Builtin.hs
+++ b/src/Types/Builtin.hs
@@ -28,6 +28,7 @@
   floatRequiredValue,
   formattedRequiredValue,
   intRequiredValue,
+  isOpaqueMulti,
   orderOptionalValue,
   stringRequiredValue,
 ) where
@@ -83,3 +84,7 @@
   -- Value with lazy initialization. Requires indirection to get/set.
   LazySingle ExpressionValue
   deriving (Show)
+
+isOpaqueMulti :: ExpressionValue -> Bool
+isOpaqueMulti (OpaqueMulti _) = True
+isOpaqueMulti _               = False
diff --git a/src/Types/DefinedCategory.hs b/src/Types/DefinedCategory.hs
--- a/src/Types/DefinedCategory.hs
+++ b/src/Types/DefinedCategory.hs
@@ -25,6 +25,7 @@
   VariableRule(..),
   VariableValue(..),
   isInitialized,
+  isFlatCleanup,
   isMembersHidden,
   isMembersReadOnly,
   mapMembers,
@@ -81,6 +82,10 @@
   MembersHidden {
     mhContext :: [c],
     mhMembers :: [VariableName]
+  } |
+  FlatCleanup {
+    fcContext :: [c],
+    fcMember :: VariableName
   }
   deriving (Show)
 
@@ -91,6 +96,10 @@
 isMembersHidden :: PragmaDefined c -> Bool
 isMembersHidden (MembersHidden _ _) = True
 isMembersHidden _                   = False
+
+isFlatCleanup :: PragmaDefined c -> Bool
+isFlatCleanup (FlatCleanup _ _) = True
+isFlatCleanup _                 = False
 
 data VariableRule c =
   VariableDefault |
diff --git a/src/Types/Procedure.hs b/src/Types/Procedure.hs
--- a/src/Types/Procedure.hs
+++ b/src/Types/Procedure.hs
@@ -331,7 +331,8 @@
 
 data ValueOperation c =
   ConvertedCall [c] TypeInstance (FunctionCall c) |
-  ValueCall [c] (FunctionCall c)
+  ValueCall [c] (FunctionCall c) |
+  SelectReturn [c] Int
   deriving (Show)
 
 newtype MacroName =
diff --git a/tests/builtin-types.0rt b/tests/builtin-types.0rt
--- a/tests/builtin-types.0rt
+++ b/tests/builtin-types.0rt
@@ -302,23 +302,23 @@
 }
 
 unittest boolDefault {
-  \ Testing.checkEquals<?>(Bool.default(),false)
+  \ Testing.checkFalse(Bool.default())
 }
 
 unittest charDefault {
-  \ Testing.checkEquals<?>(Char.default(),'\000')
+  \ Testing.checkEquals(Char.default(),'\000')
 }
 
 unittest floatDefault {
-  \ Testing.checkEquals<?>(Float.default(),0.0)
+  \ Testing.checkEquals(Float.default(),0.0)
 }
 
 unittest intDefault {
-  \ Testing.checkEquals<?>(Int.default(),0)
+  \ Testing.checkEquals(Int.default(),0)
 }
 
 unittest stringDefault {
-  \ Testing.checkEquals<?>(String.default(),"")
+  \ Testing.checkEquals(String.default(),"")
 }
 
 unittest stringLessThan {
@@ -341,9 +341,9 @@
 
 unittest stringBuilder {
   [Append<Formatted>&Build<String>] builder <- String.builder()
-  \ Testing.checkEquals<?>(builder.build(),"")
-  \ Testing.checkEquals<?>(builder.append("xyz").build(),"xyz")
-  \ Testing.checkEquals<?>(builder.append(123).build(),"xyz123")
+  \ Testing.checkEquals(builder.build(),"")
+  \ Testing.checkEquals(builder.append("xyz").build(),"xyz")
+  \ Testing.checkEquals(builder.append(123).build(),"xyz123")
 }
 
 unittest charLessThan {
@@ -488,24 +488,24 @@
       .writeAt(1,'B')
       .writeAt(2,'C')
 
-  \ Testing.checkEquals<?>(buffer.size(),5)
-  \ Testing.checkEquals<?>(buffer.readAt(0),'a')
-  \ Testing.checkEquals<?>(buffer.readAt(1),'B')
-  \ Testing.checkEquals<?>(buffer.readAt(2),'C')
-  \ Testing.checkEquals<?>(buffer.readAt(3),'d')
-  \ Testing.checkEquals<?>(buffer.readAt(4),'e')
+  \ Testing.checkEquals(buffer.size(),5)
+  \ Testing.checkEquals(buffer.readAt(0),'a')
+  \ Testing.checkEquals(buffer.readAt(1),'B')
+  \ Testing.checkEquals(buffer.readAt(2),'C')
+  \ Testing.checkEquals(buffer.readAt(3),'d')
+  \ Testing.checkEquals(buffer.readAt(4),'e')
 }
 
 unittest resizeDown {
   CharBuffer buffer <- CharBuffer.new(5).resize(3).writeAt(2,'a')
-  \ Testing.checkEquals<?>(buffer.size(),3)
-  \ Testing.checkEquals<?>(buffer.readAt(2),'a')
+  \ Testing.checkEquals(buffer.size(),3)
+  \ Testing.checkEquals(buffer.readAt(2),'a')
 }
 
 unittest resizeUp {
   CharBuffer buffer <- CharBuffer.new(5).resize(7).writeAt(6,'a')
-  \ Testing.checkEquals<?>(buffer.size(),7)
-  \ Testing.checkEquals<?>(buffer.readAt(6),'a')
+  \ Testing.checkEquals(buffer.size(),7)
+  \ Testing.checkEquals(buffer.readAt(6),'a')
 }
 
 
@@ -634,11 +634,11 @@
   String value <- "abcdefg"
 
   traverse (value.defaultOrder() -> Char c) {
-    \ Testing.checkEquals<?>(c,value.readAt(index))
+    \ Testing.checkEquals(c,value.readAt(index))
     index <- index+1
   }
 
-  \ Testing.checkEquals<?>(index,7)
+  \ Testing.checkEquals(index,7)
 }
 
 unittest fromCharBuffer {
@@ -648,7 +648,7 @@
       .writeAt(2,'c')
       .writeAt(3,'d')
       .writeAt(4,'e')
-  \ Testing.checkEquals<?>(String.fromCharBuffer(buffer),"abcde")
+  \ Testing.checkEquals(String.fromCharBuffer(buffer),"abcde")
 }
 
 
@@ -720,19 +720,19 @@
 }
 
 unittest accurateMax {
-  \ Testing.checkEquals<?>(Char.maxBound().asInt(),255)
+  \ Testing.checkEquals(Char.maxBound().asInt(),255)
 }
 
 unittest accurateMin {
-  \ Testing.checkEquals<?>(Char.minBound().asInt(),0)
+  \ Testing.checkEquals(Char.minBound().asInt(),0)
 }
 
 unittest asIntIsUnsigned {
-  \ Testing.checkEquals<?>('\xff'.asInt(),255)
+  \ Testing.checkEquals('\xff'.asInt(),255)
 }
 
 unittest asFloatIsUnsigned {
-  \ Testing.checkEquals<?>('\xff'.asFloat(),255.0)
+  \ Testing.checkEquals('\xff'.asFloat(),255.0)
 }
 
 
@@ -741,21 +741,21 @@
 }
 
 unittest accurateMax {
-  \ Testing.checkEquals<?>(Int.maxBound(),9223372036854775807)
+  \ Testing.checkEquals(Int.maxBound(),9223372036854775807)
 }
 
 unittest accurateMin {
-  \ Testing.checkEquals<?>(Int.minBound(),-9223372036854775808)
+  \ Testing.checkEquals(Int.minBound(),-9223372036854775808)
 }
 
 unittest asCharIsPeriodic {
-  \ Testing.checkEquals<?>((65).asChar(),(65+256).asChar())
-  \ Testing.checkEquals<?>((65).asChar(),(65+1024).asChar())
-  \ Testing.checkEquals<?>((65).asChar(),(65-1024).asChar())
+  \ Testing.checkEquals((65).asChar(),(65+256).asChar())
+  \ Testing.checkEquals((65).asChar(),(65+1024).asChar())
+  \ Testing.checkEquals((65).asChar(),(65-1024).asChar())
 }
 
 unittest hexUnsigned {
-  \ Testing.checkEquals<?>(\xffffffffffffffff,-1)
+  \ Testing.checkEquals(\xffffffffffffffff,-1)
 }
 
 
diff --git a/tests/cli-tests.sh b/tests/cli-tests.sh
--- a/tests/cli-tests.sh
+++ b/tests/cli-tests.sh
@@ -78,6 +78,128 @@
 }
 
 
+require_patterns() {
+  local output=$1
+  local error=0
+  while read pattern; do
+    if ! echo "$output" | egrep -q "$pattern"; then
+      show_message "Expected pattern \"$pattern\" in output"
+      error=1
+    fi
+  done
+  if ((error)); then
+    echo "$output" 1>&2
+    return 1
+  fi
+}
+
+
+exclude_patterns() {
+  local output=$1
+  local error=0
+  while read pattern; do
+    if echo "$output" | egrep -q "$pattern"; then
+      show_message "Unexpected pattern \"$pattern\" in output"
+      error=1
+    fi
+  done
+  if ((error)); then
+    echo "$output" 1>&2
+    return 1
+  fi
+}
+
+
+test_freshness() {
+  mkdir -p "$ZEOLITE_PATH/tests/freshness"/{,sub1/,sub2/}
+  touch "$ZEOLITE_PATH/tests/freshness"/{,sub1/,sub2/}{public{1,2}.0rp,private{1,2}.0rp,source{1,2}.0rx,test{1,2}.0rt}
+  echo '$ModuleOnly$' | tee "$ZEOLITE_PATH/tests/freshness"/{,sub1/,sub2/}private{1,2}.0rp > /dev/null
+  echo 'concrete Type {} define Type {}' | tee "$ZEOLITE_PATH/tests/freshness"/{,sub1/,sub2/}private{1,2}.0rx > /dev/null
+  rm -f "$ZEOLITE_PATH/tests/freshness"/{,sub1/,sub2/}{public3.0rp,source3.0rx,test3.0rt}
+
+  # Compile and test normally.
+  do_zeolite -p "$ZEOLITE_PATH" -R tests/freshness/reverse -f
+  do_zeolite -p "$ZEOLITE_PATH" -t tests/freshness/reverse
+
+  # Modify the module's files.
+  touch "$ZEOLITE_PATH/tests/freshness"/{,sub1/,sub2/}{public{1,3}.0rp,source{1,3}.0rx,test{1,3}.0rt}
+  rm -f "$ZEOLITE_PATH/tests/freshness"/{,sub1/,sub2/}{public2.0rp,private2.0rp,source2.0rx,test2.0rt}
+  rm -f "$ZEOLITE_PATH/tests/freshness/.zeolite-cache"/*.so
+
+  local output=$(do_zeolite -p "$ZEOLITE_PATH" -t tests/freshness/reverse || true)
+  require_patterns "$output" <<END
+tests/freshness/reverse.+out of date
+freshness/public1\.0rp.+newer
+freshness/public2\.0rp.+missing
+freshness/sub1/public1\.0rp.+newer
+freshness/sub1/public2\.0rp.+missing
+freshness/sub2/public1\.0rp.+newer
+freshness/sub2/public2\.0rp.+missing
+END
+[[ $? -eq 0 ]] || return 1
+  exclude_patterns "$output" <<END
+private
+0rt
+0rx
+END
+[[ $? -eq 0 ]] || return 1
+
+  local output=$(do_zeolite -p "$ZEOLITE_PATH" -t tests/freshness || true)
+  require_patterns "$output" <<END
+tests/freshness[^/].+out of date
+freshness/public3\.0rp.+not present
+freshness/source1\.0rx.+newer
+freshness/source2\.0rx.+missing
+freshness/source3\.0rx.+not present
+freshness/sub1/public3\.0rp.+not present
+freshness/sub1/source1\.0rx.+newer
+freshness/sub1/source2\.0rx.+missing
+freshness/sub1/source3\.0rx.+not present
+freshness/sub1/test1\.0rt.+newer
+freshness/sub1/test2\.0rt.+missing
+freshness/sub1/test3\.0rt.+not present
+freshness/sub2/public3\.0rp.+not present
+freshness/sub2/source1\.0rx.+newer
+freshness/sub2/source2\.0rx.+missing
+freshness/sub2/source3\.0rx.+not present
+freshness/sub2/test1\.0rt.+newer
+freshness/sub2/test2\.0rt.+missing
+freshness/sub2/test3\.0rt.+not present
+freshness/test1\.0rt.+newer
+freshness/test2\.0rt.+missing
+freshness/test3\.0rt.+not present
+freshness/.zeolite-cache/public_[a-f0-9]+\.so.+missing
+END
+[[ $? -eq 0 ]] || return 1
+  exclude_patterns "$output" <<END
+cpp
+hpp
+\.o\b
+END
+[[ $? -eq 0 ]] || return 1
+
+  do_zeolite -p "$ZEOLITE_PATH" -r tests/freshness
+
+  local output=$(do_zeolite -p "$ZEOLITE_PATH" -t tests/freshness/reverse || true)
+  require_patterns "$output" <<END
+tests/freshness/reverse.+out of date
+freshness/public1\.0rp.+newer
+freshness/public3\.0rp.+newer
+freshness/sub1/public1\.0rp.+newer
+freshness/sub1/public3\.0rp.+newer
+freshness/sub2/public1\.0rp.+newer
+freshness/sub2/public3\.0rp.+newer
+END
+[[ $? -eq 0 ]] || return 1
+  exclude_patterns "$output" <<END
+private
+0rt
+0rx
+END
+[[ $? -eq 0 ]] || return 1
+}
+
+
 test_leak_check() {
   local binary="$ZEOLITE_PATH/tests/leak-check/LeakTest"
   rm -f "$binary"
@@ -260,11 +382,11 @@
 test_show_deps() {
   local output=$(do_zeolite -p "$ZEOLITE_PATH" --show-deps tests)
   local patterns=(
-    'ExprLookup -> Int ".*zeolite/base"'
-    'ExprLookup -> String ".*zeolite/base"'
+    'ExprLookup -> Int "/.+/base"'
+    'ExprLookup -> String "/.+/base"'
   )
   for pattern in "${patterns[@]}"; do
-    if echo "$output" | egrep -q "$pattern"; then
+    if ! echo "$output" | egrep -q "$pattern"; then
       show_message "Expected \"$pattern\" in --show-deps for tests:"
       echo "$output" 1>&2
       return 1
@@ -380,7 +502,7 @@
   do_zeolite -p "$ZEOLITE_PATH" -r tests/traces -f
   local source_files=("$ZEOLITE_PATH/tests/traces/traces.0rx")
   local expected=$(fgrep -n '// TRACED' "${source_files[@]}" | egrep -o '^[0-9]+' | sort -u)
-  local actual=$(do_zeolite --show-traces "$ZEOLITE_PATH/tests/traces" | grep 0rx | sed -r 's/^line ([0-9]+).*/\1/' | sort -u)
+  local actual=$(do_zeolite -p "$ZEOLITE_PATH" --show-traces "tests/traces" | grep 0rx | sed -r 's/^line ([0-9]+).*/\1/' | sort -u)
   if [[ "$actual" != "$expected" ]]; then
     show_message "Mismatch between expected and actual traced lines:"
     echo "Expected:" $expected 1>&2
@@ -431,6 +553,7 @@
   test_example_primes
   test_example_random
   test_fast_static
+  test_freshness
   test_global_include
   test_leak_check
   test_module_only
diff --git a/tests/expr-lookup.0rt b/tests/expr-lookup.0rt
--- a/tests/expr-lookup.0rt
+++ b/tests/expr-lookup.0rt
@@ -71,7 +71,7 @@
 }
 
 unittest test {
-  \ Testing.checkEquals<?>($ExprLookup[INT_EXPR]$,4)
+  \ Testing.checkEquals($ExprLookup[INT_EXPR]$,4)
   // Use it a second time to ensure that reserving the macro name doesn't
   // carry over to the next statement.
   \ $ExprLookup[INT_EXPR]$
@@ -83,7 +83,7 @@
 }
 
 unittest test {
-  \ Testing.checkEquals<?>(ExprLookup.intExpr(),4)
+  \ Testing.checkEquals(ExprLookup.intExpr(),4)
 }
 
 
@@ -99,7 +99,7 @@
   @category String macroLocalVar <- "hello"
 
   run () {
-    \ Testing.checkEquals<?>($ExprLookup[LOCAL_VAR]$,"hello")
+    \ Testing.checkEquals($ExprLookup[LOCAL_VAR]$,"hello")
   }
 }
 
@@ -124,7 +124,7 @@
 }
 
 unittest test {
-  \ Testing.checkEquals<?>(ExprLookup.localVar(),99)
+  \ Testing.checkEquals(ExprLookup.localVar(),99)
 }
 
 
@@ -133,7 +133,7 @@
 }
 
 unittest test {
-  \ Testing.checkEquals<?>($ExprLookup[META_VAR]$,20)
+  \ Testing.checkEquals($ExprLookup[META_VAR]$,20)
 }
 
 
@@ -142,7 +142,7 @@
 }
 
 unittest test {
-  \ Testing.checkEquals<?>($ExprLookup[INT_EXPR]$.formatted(),"4")
+  \ Testing.checkEquals($ExprLookup[INT_EXPR]$.formatted(),"4")
 }
 
 
diff --git a/tests/freshness/.zeolite-module b/tests/freshness/.zeolite-module
new file mode 100644
--- /dev/null
+++ b/tests/freshness/.zeolite-module
@@ -0,0 +1,7 @@
+root: "../.."
+path: "tests/freshness"
+extra_paths: [
+  "tests/freshness/sub1"
+  "tests/../tests/freshness/sub2"
+]
+mode: incremental {}
diff --git a/tests/freshness/README.md b/tests/freshness/README.md
new file mode 100644
--- /dev/null
+++ b/tests/freshness/README.md
@@ -0,0 +1,4 @@
+# Testing CLI Freshness Check
+
+There isn't much you can do with this module. The CLI tests use it to verify
+that the `zeolite` module-freshness checks work properly.
diff --git a/tests/freshness/reverse/.zeolite-module b/tests/freshness/reverse/.zeolite-module
new file mode 100644
--- /dev/null
+++ b/tests/freshness/reverse/.zeolite-module
@@ -0,0 +1,6 @@
+root: "../../.."
+path: "tests/freshness/reverse"
+private_deps: [
+  "../../../tests/freshness"
+]
+mode: incremental {}
diff --git a/tests/function-calls.0rt b/tests/function-calls.0rt
--- a/tests/function-calls.0rt
+++ b/tests/function-calls.0rt
@@ -454,25 +454,25 @@
 }
 
 unittest callDispatch {
-  \ Testing.checkEquals<?>(Type.new().get(),00)
-  \ Testing.checkEquals<?>(Type.new().get01(),01)
-  \ Testing.checkEquals<?>(Type.new().get02(),02)
-  \ Testing.checkEquals<?>(Type.new().get03(),03)
-  \ Testing.checkEquals<?>(Type.new().get04(),04)
-  \ Testing.checkEquals<?>(Type.new().get05(),05)
-  \ Testing.checkEquals<?>(Type.new().get06(),06)
-  \ Testing.checkEquals<?>(Type.new().get07(),07)
-  \ Testing.checkEquals<?>(Type.new().get08(),08)
-  \ Testing.checkEquals<?>(Type.new().get09(),09)
-  \ Testing.checkEquals<?>(Type.new().get10(),10)
-  \ Testing.checkEquals<?>(Type.new().get11(),11)
-  \ Testing.checkEquals<?>(Type.new().get12(),12)
-  \ Testing.checkEquals<?>(Type.new().get13(),13)
-  \ Testing.checkEquals<?>(Type.new().get14(),14)
-  \ Testing.checkEquals<?>(Type.new().get15(),15)
-  \ Testing.checkEquals<?>(Type.new().get16(),16)
-  \ Testing.checkEquals<?>(Type.new().get17(),17)
-  \ Testing.checkEquals<?>(Type.new().get18(),18)
+  \ Testing.checkEquals(Type.new().get(),00)
+  \ Testing.checkEquals(Type.new().get01(),01)
+  \ Testing.checkEquals(Type.new().get02(),02)
+  \ Testing.checkEquals(Type.new().get03(),03)
+  \ Testing.checkEquals(Type.new().get04(),04)
+  \ Testing.checkEquals(Type.new().get05(),05)
+  \ Testing.checkEquals(Type.new().get06(),06)
+  \ Testing.checkEquals(Type.new().get07(),07)
+  \ Testing.checkEquals(Type.new().get08(),08)
+  \ Testing.checkEquals(Type.new().get09(),09)
+  \ Testing.checkEquals(Type.new().get10(),10)
+  \ Testing.checkEquals(Type.new().get11(),11)
+  \ Testing.checkEquals(Type.new().get12(),12)
+  \ Testing.checkEquals(Type.new().get13(),13)
+  \ Testing.checkEquals(Type.new().get14(),14)
+  \ Testing.checkEquals(Type.new().get15(),15)
+  \ Testing.checkEquals(Type.new().get16(),16)
+  \ Testing.checkEquals(Type.new().get17(),17)
+  \ Testing.checkEquals(Type.new().get18(),18)
 }
 
 unittest reduceBuiltin {
@@ -719,7 +719,7 @@
 }
 
 unittest test {
-  \ Testing.checkEquals<?>(Value.create().call(1,"mess",2,"age"),"message")
+  \ Testing.checkEquals(Value.create().call(1,"mess",2,"age"),"message")
 }
 
 concrete Value {
diff --git a/tests/inference.0rt b/tests/inference.0rt
--- a/tests/inference.0rt
+++ b/tests/inference.0rt
@@ -21,16 +21,45 @@
 }
 
 unittest test {
-  Int value <- Test.get<?>(10)
+  Int value1, String value2 <- Test.get<?,?>(10,"message")
+  \ Testing.checkEquals<?>(value1,10)
+  \ Testing.checkEquals<?>(value2,"message")
 }
 
+unittest testImplicit {
+  Int value1, String value2 <- Test.get(10,"message")
+  \ Testing.checkEquals<?>(value1,10)
+  \ Testing.checkEquals<?>(value2,"message")
+}
+
 concrete Test {
-  @type get<#x> (#x) -> (#x)
+  @type get<#x,#y> (#x,#y) -> (#x,#y)
 }
 
 define Test {
-  get (x) {
-    return x
+  get (x,y) {
+    return x, y
+  }
+}
+
+
+testcase "wrong param count" {
+  error
+  require "mismatch"
+  require "get"
+}
+
+unittest test {
+  Int value1, String value2 <- Test.get<?>(10,"message")
+}
+
+concrete Test {
+  @type get<#x,#y> (#x,#y) -> (#x,#y)
+}
+
+define Test {
+  get (x,y) {
+    return x, y
   }
 }
 
diff --git a/tests/infix-functions.0rt b/tests/infix-functions.0rt
--- a/tests/infix-functions.0rt
+++ b/tests/infix-functions.0rt
@@ -172,20 +172,20 @@
 
 define Test {
   check () {
-    \ Testing.checkEquals<?>(1 `one` 2 `two` 4,5)
+    \ Testing.checkEquals(1 `one` 2 `two` 4,5)
   }
 
   @type one (Int,Int) -> (Int)
   one (x,y) {
-    \ Testing.checkEquals<?>(x,1)
-    \ Testing.checkEquals<?>(y,2)
+    \ Testing.checkEquals(x,1)
+    \ Testing.checkEquals(y,2)
     return 3
   }
 
   @type two (Int,Int) -> (Int)
   two (x,y) {
-    \ Testing.checkEquals<?>(x,3)
-    \ Testing.checkEquals<?>(y,4)
+    \ Testing.checkEquals(x,3)
+    \ Testing.checkEquals(y,4)
     return 5
   }
 }
diff --git a/tests/infix-operations.0rt b/tests/infix-operations.0rt
--- a/tests/infix-operations.0rt
+++ b/tests/infix-operations.0rt
@@ -21,55 +21,55 @@
 }
 
 unittest plus {
-  \ Testing.checkEquals<?>(8 + 2,10)
+  \ Testing.checkEquals(8 + 2,10)
 }
 
 unittest minus {
-  \ Testing.checkEquals<?>(8 - 2,6)
+  \ Testing.checkEquals(8 - 2,6)
 }
 
 unittest times {
-  \ Testing.checkEquals<?>(8 * 2,16)
+  \ Testing.checkEquals(8 * 2,16)
 }
 
 unittest divide {
-  \ Testing.checkEquals<?>(8 / 2,4)
+  \ Testing.checkEquals(8 / 2,4)
 }
 
 unittest modulo {
-  \ Testing.checkEquals<?>(8 % 2,0)
+  \ Testing.checkEquals(8 % 2,0)
 }
 
 unittest left {
-  \ Testing.checkEquals<?>(1 << 2,4)
+  \ Testing.checkEquals(1 << 2,4)
 }
 
 unittest right {
-  \ Testing.checkEquals<?>(7 >> 1,3)
+  \ Testing.checkEquals(7 >> 1,3)
 }
 
 unittest xor {
-  \ Testing.checkEquals<?>(7 ^ 2 ,5)
+  \ Testing.checkEquals(7 ^ 2 ,5)
 }
 
 unittest and {
-  \ Testing.checkEquals<?>(7 & ~2,5)
+  \ Testing.checkEquals(7 & ~2,5)
 }
 
 unittest or {
-  \ Testing.checkEquals<?>(5 | 2 ,7)
+  \ Testing.checkEquals(5 | 2 ,7)
 }
 
 unittest arithmetic {
-  \ Testing.checkEquals<?>(\x10 + 1 * 2 - 8 / 2 - 3 % 2,13)
+  \ Testing.checkEquals(\x10 + 1 * 2 - 8 / 2 - 3 % 2,13)
 }
 
 unittest bitwise {
-  \ Testing.checkEquals<?>(1 << 4 | 7 >> 1 & ~2,17)
+  \ Testing.checkEquals(1 << 4 | 7 >> 1 & ~2,17)
 }
 
 unittest sameOperator {
-  \ Testing.checkEquals<?>(2 - 1 - 1,0)
+  \ Testing.checkEquals(2 - 1 - 1,0)
 }
 
 unittest lessThan {
@@ -136,15 +136,15 @@
 unittest rightAssociative {
   Int x <- 2
   Bool y <- true && (x <- 3) == 2 || x == 3
-  \ Testing.checkEquals<?>(x,3)
-  \ Testing.checkEquals<?>(y,true)
+  \ Testing.checkEquals(x,3)
+  \ Testing.checkTrue(y)
 }
 
 unittest shortCircuit {
   Int x <- 2
   Bool y <- true || (x <- 3) == 2 || x == 3
-  \ Testing.checkEquals<?>(x,2)
-  \ Testing.checkEquals<?>(y,true)
+  \ Testing.checkEquals(x,2)
+  \ Testing.checkTrue(y)
 }
 
 
@@ -153,7 +153,7 @@
 }
 
 unittest precedence {
-  \ Testing.checkEquals<?>(16.0 + 1.0 * 2.0 - 8.0 / 2.0 - 3.0 / 3.0,13.0)
+  \ Testing.checkEquals(16.0 + 1.0 * 2.0 - 8.0 / 2.0 - 3.0 / 3.0,13.0)
 }
 
 unittest lessThan {
@@ -190,7 +190,7 @@
 }
 
 unittest add {
-  \ Testing.checkEquals<?>("x" + "y" + "z","xyz")
+  \ Testing.checkEquals("x" + "y" + "z","xyz")
 }
 
 unittest lessThan {
@@ -227,7 +227,7 @@
 }
 
 unittest minus {
-  \ Testing.checkEquals<?>('z' - 'a',25)
+  \ Testing.checkEquals('z' - 'a',25)
 }
 
 unittest lessThan {
diff --git a/tests/leak-check/leak-check.0rx b/tests/leak-check/leak-check.0rx
--- a/tests/leak-check/leak-check.0rx
+++ b/tests/leak-check/leak-check.0rx
@@ -29,7 +29,7 @@
 
   @type runWith (Int,Int) -> ()
   runWith (iterations,size) {
-    Int threads <- Ranges:max<?>(3,$ExprLookup[CPU_CORE_COUNT]$-1)
+    Int threads <- Ranges:max(3,$ExprLookup[CPU_CORE_COUNT]$-1)
     ReadAt<BarrierWait> barriers <- EnumeratedBarrier.new(threads+1)
     traverse (Counter.zeroIndexed(iterations) -> Int i) {
       \ doIteration<LastReference>(i,size,barriers)
diff --git a/tests/local-rules.0rt b/tests/local-rules.0rt
--- a/tests/local-rules.0rt
+++ b/tests/local-rules.0rt
@@ -52,7 +52,7 @@
     $ReadOnly[foo]$
   }
   foo <- 2
-  \ Testing.checkEquals<?>(foo,2)
+  \ Testing.checkEquals(foo,2)
 }
 
 
@@ -82,7 +82,7 @@
     $ReadOnly[foo]$
   }
   foo <- 2
-  \ Testing.checkEquals<?>(foo,2)
+  \ Testing.checkEquals(foo,2)
 }
 
 
@@ -97,9 +97,9 @@
     foo <- 2
     $ReadOnly[foo]$
   }
-  \ Testing.checkEquals<?>(foo,2)
+  \ Testing.checkEquals(foo,2)
   foo <- 3
-  \ Testing.checkEquals<?>(foo,3)
+  \ Testing.checkEquals(foo,3)
 }
 
 
@@ -151,7 +151,7 @@
   cleanup {
     foo <- 2
   } in $ReadOnly[foo]$
-  \ Testing.checkEquals<?>(foo,2)
+  \ Testing.checkEquals(foo,2)
 }
 
 
@@ -181,7 +181,7 @@
     $Hidden[foo]$
   }
   foo <- 2
-  \ Testing.checkEquals<?>(foo,2)
+  \ Testing.checkEquals(foo,2)
 }
 
 
@@ -240,9 +240,9 @@
     foo <- 2
     $Hidden[foo]$
   }
-  \ Testing.checkEquals<?>(foo,2)
+  \ Testing.checkEquals(foo,2)
   foo <- 3
-  \ Testing.checkEquals<?>(foo,3)
+  \ Testing.checkEquals(foo,3)
 }
 
 
@@ -321,7 +321,7 @@
 }
 
 unittest test {
-  \ Testing.checkEquals<?>(Type:get(),2)
+  \ Testing.checkEquals(Type:get(),2)
 }
 
 concrete Type {
@@ -404,7 +404,7 @@
 }
 
 unittest test {
-  \ Testing.checkEquals<?>(Type.create().get(),1)
+  \ Testing.checkEquals(Type.create().get(),1)
 }
 
 concrete Type {
diff --git a/tests/member-init.0rt b/tests/member-init.0rt
--- a/tests/member-init.0rt
+++ b/tests/member-init.0rt
@@ -90,7 +90,7 @@
 }
 
 unittest test {
-  \ Testing.checkEquals<?>(Test:get(),2)
+  \ Testing.checkEquals(Test:get(),2)
 }
 
 concrete Test {
diff --git a/tests/module-only4/common-source.cpp b/tests/module-only4/common-source.cpp
--- a/tests/module-only4/common-source.cpp
+++ b/tests/module-only4/common-source.cpp
@@ -29,7 +29,7 @@
 namespace ZEOLITE_PRIVATE_NAMESPACE {
 #endif  // ZEOLITE_PRIVATE_NAMESPACE
 
-BoxedValue CreateValue_Type1(S<const Type_Type1> parent, const ValueTuple& args);
+BoxedValue CreateValue_Type1(S<const Type_Type1> parent, const ParamsArgs& params_args);
 
 struct ExtCategory_Type1 : public Category_Type1 {
 };
@@ -37,20 +37,20 @@
 struct ExtType_Type1 : public Type_Type1 {
   inline ExtType_Type1(Category_Type1& p, Params<0>::Type params) : Type_Type1(p, params) {}
 
-  ReturnTuple Call_create(const ParamTuple& params, const ValueTuple& args) const {
+  ReturnTuple Call_create(const ParamsArgs& params_args) const {
     TRACE_FUNCTION("Type1.create")
     return ReturnTuple(CreateValue_Type1(PARAM_SELF,
-      TypeInstance::Call(GetType_Type2(Params<0>::Type()), Function_Type2_create, ParamTuple(), ArgTuple())));
+      PassParamsArgs(TypeInstance::Call(GetType_Type2(Params<0>::Type()), Function_Type2_create, PassParamsArgs()))));
   }
 };
 
 struct ExtValue_Type1 : public Value_Type1 {
-  inline ExtValue_Type1(S<const Type_Type1> p, const ValueTuple& args)
-    : Value_Type1(std::move(p)), value(args.At(0)) {}
+  inline ExtValue_Type1(S<const Type_Type1> p, const ParamsArgs& params_args)
+    : Value_Type1(std::move(p)), value(params_args.GetArg(0)) {}
 
-  ReturnTuple Call_get(const ParamTuple& params, const ValueTuple& args) {
+  ReturnTuple Call_get(const ParamsArgs& params_args) {
     TRACE_FUNCTION("Type1.get")
-    return ReturnTuple(TypeValue::Call(value, Function_Type2_get, ParamTuple(), ArgTuple()));
+    return ReturnTuple(TypeValue::Call(value, Function_Type2_get, PassParamsArgs()));
   }
 
   const BoxedValue value;
@@ -68,8 +68,8 @@
 
 void RemoveType_Type1(const Params<0>::Type& params) {}
 
-BoxedValue CreateValue_Type1(S<const Type_Type1> parent, const ValueTuple& args) {
-  return BoxedValue::New<ExtValue_Type1>(std::move(parent), args);
+BoxedValue CreateValue_Type1(S<const Type_Type1> parent, const ParamsArgs& params_args) {
+  return BoxedValue::New<ExtValue_Type1>(std::move(parent), params_args);
 }
 
 #ifdef ZEOLITE_PRIVATE_NAMESPACE
@@ -82,7 +82,7 @@
 namespace ZEOLITE_PUBLIC_NAMESPACE {
 #endif  // ZEOLITE_PUBLIC_NAMESPACE
 
-BoxedValue CreateValue_Type3(S<const Type_Type3> parent, const ValueTuple& args);
+BoxedValue CreateValue_Type3(S<const Type_Type3> parent, const ParamsArgs& params_args);
 
 struct ExtCategory_Type3 : public Category_Type3 {
 };
@@ -90,20 +90,20 @@
 struct ExtType_Type3 : public Type_Type3 {
   inline ExtType_Type3(Category_Type3& p, Params<0>::Type params) : Type_Type3(p, params) {}
 
-  ReturnTuple Call_create(const ParamTuple& params, const ValueTuple& args) const {
+  ReturnTuple Call_create(const ParamsArgs& params_args) const {
     TRACE_FUNCTION("Type3.create")
     return ReturnTuple(CreateValue_Type3(PARAM_SELF,
-      TypeInstance::Call(GetType_Type2(Params<0>::Type()), Function_Type2_create, ParamTuple(), ArgTuple())));
+      PassParamsArgs(TypeInstance::Call(GetType_Type2(Params<0>::Type()), Function_Type2_create, PassParamsArgs()))));
   }
 };
 
 struct ExtValue_Type3 : public Value_Type3 {
-  inline ExtValue_Type3(S<const Type_Type3> p, const ValueTuple& args)
-    : Value_Type3(std::move(p)), value(args.At(0)) {}
+  inline ExtValue_Type3(S<const Type_Type3> p, const ParamsArgs& params_args)
+    : Value_Type3(std::move(p)), value(params_args.GetArg(0)) {}
 
-  ReturnTuple Call_get(const ParamTuple& params, const ValueTuple& args) {
+  ReturnTuple Call_get(const ParamsArgs& params_args) {
     TRACE_FUNCTION("Type3.get")
-    return ReturnTuple(TypeValue::Call(value, Function_Type2_get, ParamTuple(), ArgTuple()));
+    return ReturnTuple(TypeValue::Call(value, Function_Type2_get, PassParamsArgs()));
   }
 
   const BoxedValue value;
@@ -121,8 +121,8 @@
 
 void RemoveType_Type3(const Params<0>::Type& params) {}
 
-BoxedValue CreateValue_Type3(S<const Type_Type3> parent, const ValueTuple& args) {
-  return BoxedValue::New<ExtValue_Type3>(std::move(parent), args);
+BoxedValue CreateValue_Type3(S<const Type_Type3> parent, const ParamsArgs& params_args) {
+  return BoxedValue::New<ExtValue_Type3>(std::move(parent), params_args);
 }
 
 #ifdef ZEOLITE_PUBLIC_NAMESPACE
diff --git a/tests/multiple-returns.0rt b/tests/multiple-returns.0rt
--- a/tests/multiple-returns.0rt
+++ b/tests/multiple-returns.0rt
@@ -1,5 +1,5 @@
 /* -----------------------------------------------------------------------------
-Copyright 2020 Kevin P. Barry
+Copyright 2020-2021 Kevin P. Barry
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -108,4 +108,91 @@
 concrete Test {
   @type get () -> (Int,Int)
   @type call (Int,Int) -> ()
+}
+
+
+testcase "return selection" {
+  success
+}
+
+unittest correctPosition {
+  \ Testing.checkEquals(Test.get(){0},123)
+  \ Testing.checkEquals(Test.get(){1},"message")
+}
+
+unittest chainedCall {
+  \ Testing.checkEquals(Test.get(){0}.formatted(),"123")
+}
+
+concrete Test {
+  @type get () -> (Int,String)
+}
+
+define Test {
+  get () {
+    return 123, "message"
+  }
+}
+
+
+testcase "bad return-selection index" {
+  error
+  require "100"
+}
+
+unittest test {
+  \ Test.get(){100}
+}
+
+concrete Test {
+  @type get () -> (Int,String)
+}
+
+define Test {
+  get () {
+    return 123, "message"
+  }
+}
+
+
+testcase "cannot select return from literal" {
+  error
+  require "selection"
+}
+
+unittest test {
+  \ "message"{0}
+}
+
+
+testcase "cannot select return from boxed variable" {
+  error
+  require "selection"
+}
+
+unittest test {
+  String value <- "message"
+  \ value{0}
+}
+
+
+testcase "cannot select return from unboxed variable" {
+  error
+  require "selection"
+}
+
+unittest test {
+  Int value <- 123
+  \ value{0}
+}
+
+
+testcase "cannot double select" {
+  error
+  require "selection"
+  exclude "123"
+}
+
+unittest test {
+  \ "message".formatted(){0}{123}
 }
diff --git a/tests/named-returns.0rt b/tests/named-returns.0rt
--- a/tests/named-returns.0rt
+++ b/tests/named-returns.0rt
@@ -287,11 +287,11 @@
 }
 
 unittest withScoped {
-  \ Testing.checkEquals<?>(Test.withScoped(),"message")
+  \ Testing.checkEquals(Test.withScoped(),"message")
 }
 
 unittest withCleanup {
-  \ Testing.checkEquals<?>(Test.withCleanup(),"message")
+  \ Testing.checkEquals(Test.withCleanup(),"message")
 }
 
 concrete Test {
diff --git a/tests/reduce.0rt b/tests/reduce.0rt
--- a/tests/reduce.0rt
+++ b/tests/reduce.0rt
@@ -763,21 +763,21 @@
 }
 
 unittest leftNestedInRight {
-  \ Testing.checkEquals<?>(present(reduce<[A&B],[[A&B]|C]>(AB.create())),true)
+  \ Testing.checkTrue(present(reduce<[A&B],[[A&B]|C]>(AB.create())))
 }
 
 unittest rightNestedInLeft {
-  \ Testing.checkEquals<?>(present(reduce<[[A|B]&C],[A|B]>(AC.create())),true)
+  \ Testing.checkTrue(present(reduce<[[A|B]&C],[A|B]>(AC.create())))
 }
 
 unittest unionToUnion {
-  \ Testing.checkEquals<?>(present(reduce<[[A&B]|C],[[A&B]|C]>(AB.create())),true)
+  \ Testing.checkTrue(present(reduce<[[A&B]|C],[[A&B]|C]>(AB.create())))
 }
 
 unittest intersectToIntersect {
-  \ Testing.checkEquals<?>(present(reduce<[[A|B]&C],[[A|B]&C]>(AC.create())),true)
+  \ Testing.checkTrue(present(reduce<[[A|B]&C],[[A|B]&C]>(AC.create())))
 }
 
 unittest unionToIntersect {
-  \ Testing.checkEquals<?>(present(reduce<[A|B],[A&B]>(AB.create())),false)
+  \ Testing.checkFalse(present(reduce<[A|B],[A&B]>(AB.create())))
 }
diff --git a/tests/scoped.0rt b/tests/scoped.0rt
--- a/tests/scoped.0rt
+++ b/tests/scoped.0rt
@@ -396,7 +396,7 @@
     value <- 1
   } in Int value <- 2
 
-  \ Testing.checkEquals<?>(value,1)
+  \ Testing.checkEquals(value,1)
 }
 
 
@@ -610,13 +610,13 @@
         }
       }
     }
-    \ Testing.checkEquals<?>(x,0)
-    \ Testing.checkEquals<?>(y,2)
-    \ Testing.checkEquals<?>(z,3)
+    \ Testing.checkEquals(x,0)
+    \ Testing.checkEquals(y,2)
+    \ Testing.checkEquals(z,3)
   }
-  \ Testing.checkEquals<?>(x,1)
-  \ Testing.checkEquals<?>(y,2)
-  \ Testing.checkEquals<?>(z,3)
+  \ Testing.checkEquals(x,1)
+  \ Testing.checkEquals(y,2)
+  \ Testing.checkEquals(z,3)
 }
 
 
@@ -634,8 +634,8 @@
   } in {
     while ((called <- called+1) < 3) {
       if (called > 1) {
-        \ Testing.checkEquals<?>(y,2)
-        \ Testing.checkEquals<?>(z,3)
+        \ Testing.checkEquals(y,2)
+        \ Testing.checkEquals(z,3)
       }
       cleanup {
         y <- 2
@@ -647,14 +647,14 @@
         }
       }
     }
-    \ Testing.checkEquals<?>(called,3)
-    \ Testing.checkEquals<?>(x,0)
-    \ Testing.checkEquals<?>(y,2)
-    \ Testing.checkEquals<?>(z,3)
+    \ Testing.checkEquals(called,3)
+    \ Testing.checkEquals(x,0)
+    \ Testing.checkEquals(y,2)
+    \ Testing.checkEquals(z,3)
   }
-  \ Testing.checkEquals<?>(x,1)
-  \ Testing.checkEquals<?>(y,2)
-  \ Testing.checkEquals<?>(z,3)
+  \ Testing.checkEquals(x,1)
+  \ Testing.checkEquals(y,2)
+  \ Testing.checkEquals(z,3)
 }
 
 
@@ -667,16 +667,16 @@
   scoped {
     Int x, Int y, Int z <- value.call()
   } in {
-    \ Testing.checkEquals<?>(x,0)
-    \ Testing.checkEquals<?>(y,0)
-    \ Testing.checkEquals<?>(z,0)
+    \ Testing.checkEquals(x,0)
+    \ Testing.checkEquals(y,0)
+    \ Testing.checkEquals(z,0)
   }
   scoped {
     Int x, Int y, Int z <- value.get()
   } in {
-    \ Testing.checkEquals<?>(x,1)
-    \ Testing.checkEquals<?>(y,2)
-    \ Testing.checkEquals<?>(z,3)
+    \ Testing.checkEquals(x,1)
+    \ Testing.checkEquals(y,2)
+    \ Testing.checkEquals(z,3)
   }
 }
 
diff --git a/tests/self-offset/Extension_Destructor.cpp b/tests/self-offset/Extension_Destructor.cpp
--- a/tests/self-offset/Extension_Destructor.cpp
+++ b/tests/self-offset/Extension_Destructor.cpp
@@ -24,21 +24,21 @@
 namespace ZEOLITE_PUBLIC_NAMESPACE {
 #endif  // ZEOLITE_PUBLIC_NAMESPACE
 
-BoxedValue CreateValue_Destructor(S<const Type_Destructor> parent, const ValueTuple& args);
+BoxedValue CreateValue_Destructor(S<const Type_Destructor> parent);
 
 struct ExtCategory_Destructor : public Category_Destructor {
 };
 
 struct ExtType_Destructor : public Type_Destructor {
   inline ExtType_Destructor(Category_Destructor& p, Params<0>::Type params) : Type_Destructor(p, params) {}
-  ReturnTuple Call_new(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_new(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Destructor.new")
-    return ReturnTuple(CreateValue_Destructor(PARAM_SELF, ArgTuple()));
+    return ReturnTuple(CreateValue_Destructor(PARAM_SELF));
   }
 };
 
 struct ExtValue_Destructor : public Value_Destructor {
-  inline ExtValue_Destructor(S<const Type_Destructor> p, const ValueTuple& args)
+  inline ExtValue_Destructor(S<const Type_Destructor> p)
     : Value_Destructor(std::move(p)) {}
 
   ~ExtValue_Destructor() {
@@ -58,8 +58,8 @@
 
 void RemoveType_Destructor(const Params<0>::Type& params) {}
 
-BoxedValue CreateValue_Destructor(S<const Type_Destructor> parent, const ValueTuple& args) {
-  return BoxedValue::New<ExtValue_Destructor>(std::move(parent), args);
+BoxedValue CreateValue_Destructor(S<const Type_Destructor> parent) {
+  return BoxedValue::New<ExtValue_Destructor>(std::move(parent));
 }
 
 #ifdef ZEOLITE_PUBLIC_NAMESPACE
diff --git a/tests/self-offset/Extension_Offset.cpp b/tests/self-offset/Extension_Offset.cpp
--- a/tests/self-offset/Extension_Offset.cpp
+++ b/tests/self-offset/Extension_Offset.cpp
@@ -32,7 +32,7 @@
 
 }  // namespace
 
-BoxedValue CreateValue_Offset(S<const Type_Offset> parent, const ValueTuple& args);
+BoxedValue CreateValue_Offset(S<const Type_Offset> parent);
 
 struct ExtCategory_Offset : public Category_Offset {
 };
@@ -40,18 +40,18 @@
 struct ExtType_Offset : public Type_Offset {
   inline ExtType_Offset(Category_Offset& p, Params<0>::Type params) : Type_Offset(p, params) {}
 
-  ReturnTuple Call_new(const ParamTuple& params, const ValueTuple& args) const final {
+  ReturnTuple Call_new(const ParamsArgs& params_args) const final {
     TRACE_FUNCTION("Offset.new")
-    return ReturnTuple(CreateValue_Offset(PARAM_SELF, ArgTuple()));
+    return ReturnTuple(CreateValue_Offset(PARAM_SELF));
   }
 };
 
 // Using virtual will (ideally) change the offset.
 struct ExtValue_Offset : public Base, virtual public Value_Offset {
-  inline ExtValue_Offset(S<const Type_Offset> p, const ValueTuple& args)
+  inline ExtValue_Offset(S<const Type_Offset> p)
     : Value_Offset(std::move(p)) {}
 
-  ReturnTuple Call_call(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_call(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("Offset.call")
     if ((void*) this == (void*) static_cast<TypeValue*>(this)) {
       message = "same";
@@ -61,7 +61,7 @@
     return ReturnTuple(VAR_SELF);
   }
 
-  ReturnTuple Call_get(const ParamTuple& params, const ValueTuple& args) final {
+  ReturnTuple Call_get(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("Offset.get")
     return ReturnTuple(Box_String(message));
   }
@@ -79,8 +79,8 @@
 
 void RemoveType_Offset(const Params<0>::Type& params) {}
 
-BoxedValue CreateValue_Offset(S<const Type_Offset> parent, const ValueTuple& args) {
-  return BoxedValue::New<ExtValue_Offset>(std::move(parent), args);
+BoxedValue CreateValue_Offset(S<const Type_Offset> parent) {
+  return BoxedValue::New<ExtValue_Offset>(std::move(parent));
 }
 
 #ifdef ZEOLITE_PUBLIC_NAMESPACE
diff --git a/tests/self-offset/offset.0rt b/tests/self-offset/offset.0rt
--- a/tests/self-offset/offset.0rt
+++ b/tests/self-offset/offset.0rt
@@ -21,7 +21,7 @@
 }
 
 unittest test {
-  \ Testing.checkEquals<?>(Offset.new().call().get(),"different")
+  \ Testing.checkEquals(Offset.new().call().get(),"different")
 }
 
 
diff --git a/tests/self-type.0rt b/tests/self-type.0rt
--- a/tests/self-type.0rt
+++ b/tests/self-type.0rt
@@ -221,10 +221,10 @@
 unittest test {
   Type<String>    value1 <- Type<String>.create()
   Type<Formatted> value2 <- Type<Formatted>.create()
-  \ Testing.checkEquals<?>(value1.checkFrom<Type<Formatted>>(),true)
-  \ Testing.checkEquals<?>(value2.checkFrom<Type<String>>(),false)
-  \ Testing.checkEquals<?>(value1.checkTo<?>(value2),false)
-  \ Testing.checkEquals<?>(value2.checkTo<?>(value1),true)
+  \ Testing.checkTrue(value1.checkFrom<Type<Formatted>>())
+  \ Testing.checkFalse(value2.checkFrom<Type<String>>())
+  \ Testing.checkFalse(value1.checkTo(value2))
+  \ Testing.checkTrue(value2.checkTo(value1))
 }
 
 concrete Type<|#x> {
@@ -253,7 +253,7 @@
 }
 
 unittest test {
-  \ Testing.checkEquals<?>(Type<Bool>.get(),"Type<Type<Bool>>")
+  \ Testing.checkEquals(Type<Bool>.get(),"Type<Type<Bool>>")
 }
 
 concrete Type<#x> {
@@ -272,7 +272,7 @@
 }
 
 unittest test {
-  \ Type.create().call<?>("message")
+  \ Type.create().call("message")
 }
 
 concrete Type {
@@ -374,12 +374,12 @@
 
 unittest withConcrete {
   Type value <- Type.create()
-  \ Testing.checkEquals<?>(value.next().prev().prev().next().next().get(),1)
+  \ Testing.checkEquals(value.next().prev().prev().next().next().get(),1)
 }
 
 unittest withIntersect {
   [Cell<Int>&Forward&Reverse] value <- Type.create()
-  \ Testing.checkEquals<?>(value.next().prev().prev().next().next().get(),1)
+  \ Testing.checkEquals(value.next().prev().prev().next().next().get(),1)
 }
 
 @value interface Cell<|#x> {
@@ -431,7 +431,7 @@
 
 unittest test {
   Type value <- Helper.new<Type>()
-  \ Testing.checkEquals<?>((value `Advance.by<?>` 3).get(),3)
+  \ Testing.checkEquals((value `Advance.by` 3).get(),3)
 }
 
 @value interface Forward {
diff --git a/tests/traces/README.md b/tests/traces/README.md
--- a/tests/traces/README.md
+++ b/tests/traces/README.md
@@ -12,6 +12,6 @@
 
 Note that there *will* be warnings about unreachable code.
 
-You can then examine `zeolite --show-traces $ZEOLITE_PATH/tests/traces` to see
-what trace contexts `zeolite` picked up during compilation. Every line that is
-commented with `TRACED` should have an entry in `--show-traces`.
+You can then examine `zeolite -p $ZEOLITE_PATH --show-traces tests/traces` to
+see what trace contexts `zeolite` picked up during compilation. Every line that
+is commented with `TRACED` should have an entry in `--show-traces`.
diff --git a/tests/traverse.0rt b/tests/traverse.0rt
--- a/tests/traverse.0rt
+++ b/tests/traverse.0rt
@@ -25,7 +25,7 @@
   traverse (Counter.zeroIndexed(5) -> Int i) {
     total <- total+i
   }
-  \ Testing.checkEquals<?>(total,10)
+  \ Testing.checkEquals(total,10)
 }
 
 unittest ignoreValue {
@@ -33,7 +33,7 @@
   traverse (Counter.zeroIndexed(5) -> _) {
     total <- total+1
   }
-  \ Testing.checkEquals<?>(total,5)
+  \ Testing.checkEquals(total,5)
 }
 
 unittest existingVariable {
@@ -42,8 +42,8 @@
   traverse (Counter.zeroIndexed(5) -> i) {
     total <- total+i
   }
-  \ Testing.checkEquals<?>(total,10)
-  \ Testing.checkEquals<?>(i,4)
+  \ Testing.checkEquals(total,10)
+  \ Testing.checkEquals(i,4)
 }
 
 unittest breakAtLimit {
@@ -54,7 +54,7 @@
     }
     total <- total+i
   }
-  \ Testing.checkEquals<?>(total,10)
+  \ Testing.checkEquals(total,10)
 }
 
 unittest continueOnOdd {
@@ -65,7 +65,7 @@
     }
     total <- total+i
   }
-  \ Testing.checkEquals<?>(total,20)
+  \ Testing.checkEquals(total,20)
 }
 
 unittest withUpdate {
@@ -77,7 +77,7 @@
   } update {
     total <- total+i
   }
-  \ Testing.checkEquals<?>(total,45)
+  \ Testing.checkEquals(total,45)
 }
 
 unittest overwriteVisibleValue {
@@ -86,7 +86,7 @@
     // i shouldn't be overwritten after the last iteration.
     i <- 0
   }
-  \ Testing.checkEquals<?>(i,0)
+  \ Testing.checkEquals(i,0)
 }
 
 unittest overwriteLocalValue {
@@ -95,7 +95,7 @@
     i <- 0
   } update {
     // i should also be visible here.
-    \ Testing.checkEquals<?>(i,0)
+    \ Testing.checkEquals(i,0)
   }
 }
 
@@ -191,7 +191,7 @@
     traverse (Counter.new() -> Int i) {
       total <- total+i
     }
-    \ Testing.checkEquals<?>(total,10)
+    \ Testing.checkEquals(total,10)
   }
 }
 
@@ -242,7 +242,7 @@
     traverse (Counter.new() -> Int i) {
       total <- total+i
     }
-    \ Testing.checkEquals<?>(total,10)
+    \ Testing.checkEquals(total,10)
   }
 }
 
@@ -280,7 +280,7 @@
 }
 
 unittest test {
-  \ Testing.checkEquals<?>(Test.run<?>("message"),"message")
+  \ Testing.checkEquals(Test.run("message"),"message")
 }
 
 concrete Test {
@@ -291,10 +291,10 @@
   run (x) (i) {
     i <- x
     Int total <- 0
-    traverse (Repeat:times<?>(x,10) -> i) {
+    traverse (Repeat:times(x,10) -> i) {
       total <- total+1
     }
-    \ Testing.checkEquals<?>(total,10)
+    \ Testing.checkEquals(total,10)
   }
 }
 
diff --git a/tests/typename.0rt b/tests/typename.0rt
--- a/tests/typename.0rt
+++ b/tests/typename.0rt
@@ -21,49 +21,49 @@
 }
 
 unittest string {
-  \ Testing.checkEquals<?>(typename<String>().formatted(),"String")
+  \ Testing.checkEquals(typename<String>().formatted(),"String")
 }
 
 unittest int {
-  \ Testing.checkEquals<?>(typename<Int>().formatted(),"Int")
+  \ Testing.checkEquals(typename<Int>().formatted(),"Int")
 }
 
 unittest char {
-  \ Testing.checkEquals<?>(typename<Char>().formatted(),"Char")
+  \ Testing.checkEquals(typename<Char>().formatted(),"Char")
 }
 
 unittest charBuffer {
-  \ Testing.checkEquals<?>(typename<CharBuffer>().formatted(),"CharBuffer")
+  \ Testing.checkEquals(typename<CharBuffer>().formatted(),"CharBuffer")
 }
 
 unittest float {
-  \ Testing.checkEquals<?>(typename<Float>().formatted(),"Float")
+  \ Testing.checkEquals(typename<Float>().formatted(),"Float")
 }
 
 unittest bool {
-  \ Testing.checkEquals<?>(typename<Bool>().formatted(),"Bool")
+  \ Testing.checkEquals(typename<Bool>().formatted(),"Bool")
 }
 
 unittest anyType {
-  \ Testing.checkEquals<?>(typename<any>().formatted(),"any")
+  \ Testing.checkEquals(typename<any>().formatted(),"any")
 }
 
 unittest allType {
-  \ Testing.checkEquals<?>(typename<all>().formatted(),"all")
+  \ Testing.checkEquals(typename<all>().formatted(),"all")
 }
 
 unittest intersectDedup {
-  \ Testing.checkEquals<?>(typename<[AsBool&Char&Formatted&Int]>().formatted(),
+  \ Testing.checkEquals(typename<[AsBool&Char&Formatted&Int]>().formatted(),
                            typename<[Char&Int]>().formatted())
 }
 
 unittest unionDedup {
-  \ Testing.checkEquals<?>(typename<[AsBool|Char|Formatted|Int]>().formatted(),
+  \ Testing.checkEquals(typename<[AsBool|Char|Formatted|Int]>().formatted(),
                            typename<[AsBool|Formatted]>().formatted())
 }
 
 unittest param {
-  \ Testing.checkEquals<?>(Wrapped:getTypename<String,Value<Int>>().formatted(),
+  \ Testing.checkEquals(Wrapped:getTypename<String,Value<Int>>().formatted(),
                            "Type<String,Value<Int>>")
 }
 
@@ -82,11 +82,11 @@
 }
 
 unittest intersectDedupParam {
-  \ Testing.checkEquals<?>(Wrapped:getTypenameIntersect<Int,Int>().formatted(),"Int")
+  \ Testing.checkEquals(Wrapped:getTypenameIntersect<Int,Int>().formatted(),"Int")
 }
 
 unittest unionDedupParam {
-  \ Testing.checkEquals<?>(Wrapped:getTypenameUnion<Int,Int>().formatted(),"Int")
+  \ Testing.checkEquals(Wrapped:getTypenameUnion<Int,Int>().formatted(),"Int")
 }
 
 @value interface Value<#x> {}
diff --git a/zeolite-lang.cabal b/zeolite-lang.cabal
--- a/zeolite-lang.cabal
+++ b/zeolite-lang.cabal
@@ -1,7 +1,7 @@
 cabal-version:       2.2
 
 name:                zeolite-lang
-version:             0.20.0.1
+version:             0.21.0.0
 synopsis:            Zeolite is a statically-typed, general-purpose programming language.
 
 description:
@@ -145,6 +145,9 @@
                      tests/check-defs/*.0rx,
                      tests/fast-static/README.md,
                      tests/fast-static/*.0rx,
+                     tests/freshness/.zeolite-module,
+                     tests/freshness/README.md,
+                     tests/freshness/reverse/.zeolite-module,
                      tests/leak-check/README.md,
                      tests/leak-check/.zeolite-module,
                      tests/leak-check/*.0rx,
@@ -252,8 +255,10 @@
                        CompilerCxx.LanguageModule,
                        CompilerCxx.Naming,
                        CompilerCxx.Procedure,
+                       Config.CompilerConfig,
                        Config.LoadConfig,
                        Config.LocalConfig,
+                       Config.ParseConfig,
                        Module.CompileMetadata,
                        Module.ParseMetadata,
                        Module.Paths,
@@ -271,6 +276,7 @@
                        Test.DefinedCategory,
                        Test.IntegrationTest,
                        Test.MergeTree,
+                       Test.ParseConfig,
                        Test.ParseMetadata,
                        Test.Parser,
                        Test.Pragma,
@@ -344,6 +350,7 @@
   build-depends:       base,
                        directory,
                        filepath,
+                       mtl,
                        zeolite-internal
 
 
