diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,5 +1,47 @@
 # Revision history for zeolite-lang
 
+## 0.23.0.0  -- 2022-10-08
+
+### Language
+
+* **[breaking]** Adds the `exit` builtin to terminate the program with an exit
+  code, with the usual semantics, i.e., 0 means success. This is breaking
+  because `exit` is now a reserved name.
+
+* **[breaking]** Adds the `Pointer<#x>` builtin `concrete` category, for use in
+  C++ extensions. (This is breaking because `Pointer` is now reserved.)
+
+### Compiler CLI
+
+* **[new]** Adds general pointer storage to `BoxedValue` to allow C++ extensions
+  to pass data to each other without marshaling.
+
+  ```c++
+  // Create a value that can be returned/passed in Zeolite function calls.
+  BoxedValue boxed = Box_Pointer<Foo>(&value);
+
+  // Access the pointer stored in the value.
+  Foo* unboxed = boxed.AsPointer<Foo>();
+  ```
+
+  This is only type-safe if the Zeolite code uses a different `#x` in
+  `Pointer<#x>` for each different type used with `Box_Pointer`/`AsPointer`.
+  For example, the code above uses `Foo`, so `Pointer<Foo>` should be used
+  everywhere such values are passed around in the Zeolite code. (`#x` is
+  arbitrary, but it's more clear if the name used in Zeolite corresponds to the
+  name of the C++ type.)
+
+* **[new]** Adds % code coverage in output of `zeolite --missed-lines`.
+
+### Libraries
+
+* **[new]** Moves `Realtime` from `lib/thread` to `lib/util` since it doesn't
+  inherently require threads. (This isn't breaking because `lib/util` was
+  already a *public* dependency of `lib/thread`.)
+
+* **[new]** Adds the `Counter.builder()` function to `lib/util` to build `Int`
+  counters with more flexibile limits and increments.
+
 ## 0.22.1.0  -- 2022-04-18
 
 ### Language
diff --git a/base/.zeolite-module b/base/.zeolite-module
--- a/base/.zeolite-module
+++ b/base/.zeolite-module
@@ -25,6 +25,10 @@
     source: "base/src/Extension_String.cpp"
     categories: [String]
   }
+  category_source {
+    source: "base/src/Extension_Pointer.cpp"
+    categories: [Pointer]
+  }
   "base/boxed.hpp"
   "base/category-header.hpp"
   "base/category-source.hpp"
diff --git a/base/boxed.hpp b/base/boxed.hpp
--- a/base/boxed.hpp
+++ b/base/boxed.hpp
@@ -1,5 +1,5 @@
 /* -----------------------------------------------------------------------------
-Copyright 2021 Kevin P. Barry
+Copyright 2021-2022 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.
@@ -38,6 +38,7 @@
     CHAR,
     INT,
     FLOAT,
+    POINTER,
     BOXED,
   };
 
@@ -54,11 +55,12 @@
 
   union {
     unsigned char* as_bytes_;
-    Pointer*  as_pointer_;
-    PrimBool  as_bool_;
-    PrimChar  as_char_;
-    PrimInt   as_int_;
-    PrimFloat as_float_;
+    Pointer*    as_boxed_;
+    PrimBool    as_bool_;
+    PrimChar    as_char_;
+    PrimInt     as_int_;
+    PrimFloat   as_float_;
+    PrimPointer as_pointer_;
   } __attribute__((packed)) value_;
 } __attribute__((packed));
 
@@ -66,13 +68,13 @@
 class BoxedValue {
  public:
   constexpr BoxedValue()
-    : union_{ .type_ = UnionValue::Type::EMPTY, .value_ = { .as_pointer_ = nullptr } } {}
+    : union_{ .type_ = UnionValue::Type::EMPTY, .value_ = { .as_boxed_ = nullptr } } {}
 
   inline BoxedValue(const BoxedValue& other)
     : union_(other.union_) {
     switch (union_.type_) {
       case UnionValue::Type::BOXED:
-        ++union_.value_.as_pointer_->strong_;
+        ++union_.value_.as_boxed_->strong_;
         break;
       default:
         break;
@@ -87,7 +89,7 @@
       union_ = other.union_;
       switch (union_.type_) {
         case UnionValue::Type::BOXED:
-          ++union_.value_.as_pointer_->strong_;
+          ++union_.value_.as_boxed_->strong_;
           break;
         default:
           break;
@@ -99,7 +101,7 @@
   inline BoxedValue(BoxedValue&& other)
     : union_(other.union_) {
     other.union_.type_  = UnionValue::Type::EMPTY;
-    other.union_.value_.as_pointer_ = nullptr;
+    other.union_.value_.as_boxed_ = nullptr;
   }
 
   inline BoxedValue& operator = (BoxedValue&& other) {
@@ -109,7 +111,7 @@
       }
       union_ = other.union_;
       other.union_.type_  = UnionValue::Type::EMPTY;
-      other.union_.value_.as_pointer_ = nullptr;
+      other.union_.value_.as_boxed_ = nullptr;
     }
     return *this;
   }
@@ -126,6 +128,9 @@
   inline BoxedValue(PrimFloat value)
     : union_{ .type_ = UnionValue::Type::FLOAT, .value_ = { .as_float_ = value } } {}
 
+  inline BoxedValue(PrimPointer value)
+    : union_{ .type_ = UnionValue::Type::POINTER, .value_ = { .as_pointer_ = value } } {}
+
   template<class T, class... As>
   static inline BoxedValue New(const As&... args) {
     using Pointer = UnionValue::Pointer;
@@ -188,6 +193,18 @@
     }
   }
 
+  template<class T>
+  inline T* AsPointer() const {
+    switch (union_.type_) {
+      case UnionValue::Type::POINTER:
+        return reinterpret_cast<T*>(union_.value_.as_boxed_);
+      default:
+        FAIL() << union_.CategoryName() << " is not a Pointer value";
+        __builtin_unreachable();
+        break;
+    }
+  }
+
   inline static bool Present(const BoxedValue& target) {
     return target.union_.type_ != UnionValue::Type::EMPTY;
   }
@@ -229,8 +246,8 @@
     value.union_.type_ = UnionValue::Type::BOXED;
     value.union_.value_.as_bytes_ =
       reinterpret_cast<unsigned char*>(const_cast<T*>(pointer))-sizeof(UnionValue::Pointer);
-    if (value.union_.value_.as_pointer_->object_ != pointer ||
-        ++value.union_.value_.as_pointer_->strong_ == 1) {
+    if (value.union_.value_.as_boxed_->object_ != pointer ||
+        ++value.union_.value_.as_boxed_->strong_ == 1) {
       FAIL() << "Bad VAR_SELF pointer " << pointer << " in " << pointer->CategoryName();
     }
     return value;
diff --git a/base/builtin.0rp b/base/builtin.0rp
--- a/base/builtin.0rp
+++ b/base/builtin.0rp
@@ -1,5 +1,5 @@
 /* -----------------------------------------------------------------------------
-Copyright 2019-2021 Kevin P. Barry
+Copyright 2019-2022 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.
@@ -183,6 +183,19 @@
   //   Specifically, resizing down and then back up again could either preserve
   //   or overwrite any of the data.
   @value resize (Int) -> (CharBuffer)
+}
+
+// A raw pointer type for use in C++ extensions.
+//
+// Literals: None.
+//
+// Notes:
+// - #x is mostly irrelevant. It is only intended for differentiating between
+//   values with incompatible types.
+// - This is category is only meant for passing C++ data between extensions when
+//   it would otherwise require marshaling, which is why it has no functions.
+concrete Pointer<#x> {
+  immutable
 }
 
 // Convertible to Bool.
diff --git a/base/category-source.hpp b/base/category-source.hpp
--- a/base/category-source.hpp
+++ b/base/category-source.hpp
@@ -1,5 +1,5 @@
 /* -----------------------------------------------------------------------------
-Copyright 2019-2021 Kevin P. Barry
+Copyright 2019-2022 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,6 +19,7 @@
 #ifndef CATEGORY_SOURCE_HPP_
 #define CATEGORY_SOURCE_HPP_
 
+#include <cstdlib>
 #include <iostream>  // For occasional debugging output in generated code.
 #include <map>
 #include <sstream>
@@ -32,25 +33,47 @@
 
 
 #define BUILTIN_FAIL(e) { \
-  FAIL() << TypeValue::Call((e), Function_Formatted_formatted, \
-                            PassParamsArgs()).At(0).AsString(); \
-  __builtin_unreachable(); \
+    FAIL() << TypeValue::Call((e), Function_Formatted_formatted, \
+                              PassParamsArgs()).At(0).AsString(); \
+    __builtin_unreachable(); \
   }
 
+#define BUILTIN_EXIT(e) { \
+    std::exit(e); \
+    __builtin_unreachable(); \
+  }
+
 #define RAW_FAIL(e) { \
-  FAIL() << e; \
-  __builtin_unreachable(); \
+    FAIL() << e; \
+    __builtin_unreachable(); \
   }
 
 #define VAR_SELF TypeValue::Var_self(this)
 
 #define PARAM_SELF Param_self()
 
-BoxedValue Box_Bool(PrimBool value);
+inline BoxedValue Box_Bool(PrimBool value) {
+  return BoxedValue(value);
+}
+
+inline BoxedValue Box_Char(PrimChar value) {
+  return BoxedValue(value);
+}
+
+inline BoxedValue Box_Float(PrimFloat value) {
+  return BoxedValue(value);
+}
+
+inline BoxedValue Box_Int(PrimInt value) {
+  return BoxedValue(value);
+}
+
+template<class T1, class T2>
+inline BoxedValue Box_Pointer(T2* value) {
+  return BoxedValue(reinterpret_cast<PrimPointer>(static_cast<T1*>(value)));
+}
+
 BoxedValue Box_String(const PrimString& value);
-BoxedValue Box_Char(PrimChar value);
-BoxedValue Box_Int(PrimInt value);
-BoxedValue Box_Float(PrimFloat value);
 
 S<const TypeInstance> Merge_Intersect(const L<S<const TypeInstance>>& params);
 S<const TypeInstance> Merge_Union(const L<S<const TypeInstance>>& params);
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
@@ -1,5 +1,5 @@
 /* -----------------------------------------------------------------------------
-Copyright 2019-2021 Kevin P. Barry
+Copyright 2019-2022 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.
@@ -98,7 +98,3 @@
 }  // namespace ZEOLITE_PUBLIC_NAMESPACE
 using namespace ZEOLITE_PUBLIC_NAMESPACE;
 #endif  // ZEOLITE_PUBLIC_NAMESPACE
-
-BoxedValue Box_Bool(PrimBool value) {
-  return BoxedValue(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
@@ -1,5 +1,5 @@
 /* -----------------------------------------------------------------------------
-Copyright 2019-2021 Kevin P. Barry
+Copyright 2019-2022 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.
@@ -113,7 +113,3 @@
 }  // namespace ZEOLITE_PUBLIC_NAMESPACE
 using namespace ZEOLITE_PUBLIC_NAMESPACE;
 #endif  // ZEOLITE_PUBLIC_NAMESPACE
-
-BoxedValue Box_Char(PrimChar value) {
-  return BoxedValue(value);
-}
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
@@ -1,5 +1,5 @@
 /* -----------------------------------------------------------------------------
-Copyright 2019-2021 Kevin P. Barry
+Copyright 2019-2022 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.
@@ -105,7 +105,3 @@
 }  // namespace ZEOLITE_PUBLIC_NAMESPACE
 using namespace ZEOLITE_PUBLIC_NAMESPACE;
 #endif  // ZEOLITE_PUBLIC_NAMESPACE
-
-BoxedValue Box_Float(PrimFloat value) {
-  return BoxedValue(value);
-}
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
@@ -1,5 +1,5 @@
 /* -----------------------------------------------------------------------------
-Copyright 2019-2021 Kevin P. Barry
+Copyright 2019-2022 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.
@@ -116,7 +116,3 @@
 }  // namespace ZEOLITE_PUBLIC_NAMESPACE
 using namespace ZEOLITE_PUBLIC_NAMESPACE;
 #endif  // ZEOLITE_PUBLIC_NAMESPACE
-
-BoxedValue Box_Int(PrimInt value) {
-  return BoxedValue(value);
-}
diff --git a/base/src/Extension_Pointer.cpp b/base/src/Extension_Pointer.cpp
new file mode 100644
--- /dev/null
+++ b/base/src/Extension_Pointer.cpp
@@ -0,0 +1,62 @@
+/* -----------------------------------------------------------------------------
+Copyright 2022 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 "category-source.hpp"
+#include "Streamlined_Pointer.hpp"
+
+#ifdef ZEOLITE_PUBLIC_NAMESPACE
+namespace ZEOLITE_PUBLIC_NAMESPACE {
+#endif  // ZEOLITE_PUBLIC_NAMESPACE
+
+struct ExtCategory_Pointer : public Category_Pointer {
+};
+
+struct ExtType_Pointer : public Type_Pointer {
+  inline ExtType_Pointer(Category_Pointer& p, Params<1>::Type params) : Type_Pointer(p, params) {}
+};
+
+ReturnTuple DispatchPointer(PrimPointer value, const ValueFunction& label,
+                            const ParamsArgs& params_args) {
+  switch (label.collection) {
+    default:
+      FAIL() << "Pointer does not implement " << label;
+      __builtin_unreachable();
+  }
+}
+
+Category_Pointer& CreateCategory_Pointer() {
+  static auto& category = *new ExtCategory_Pointer();
+  return category;
+}
+
+static auto& Pointer_instance_cache = *new InstanceCache<1, Type_Pointer>([](const Params<1>::Type& params) {
+    return S_get(new ExtType_Pointer(CreateCategory_Pointer(), params));
+  });
+
+S<const Type_Pointer> CreateType_Pointer(const Params<1>::Type& params) {
+  return Pointer_instance_cache.GetOrCreate(params);
+}
+
+void RemoveType_Pointer(const Params<1>::Type& params) {
+  Pointer_instance_cache.Remove(params);
+}
+
+#ifdef ZEOLITE_PUBLIC_NAMESPACE
+}  // namespace ZEOLITE_PUBLIC_NAMESPACE
+using namespace ZEOLITE_PUBLIC_NAMESPACE;
+#endif  // ZEOLITE_PUBLIC_NAMESPACE
diff --git a/base/src/boxed.cpp b/base/src/boxed.cpp
--- a/base/src/boxed.cpp
+++ b/base/src/boxed.cpp
@@ -1,5 +1,5 @@
 /* -----------------------------------------------------------------------------
-Copyright 2021 Kevin P. Barry
+Copyright 2021-2022 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.
@@ -39,6 +39,9 @@
 ReturnTuple DispatchFloat(PrimFloat value, const ValueFunction& label,
                           const ParamsArgs& params_args) __attribute__((weak));
 
+ReturnTuple DispatchPointer(PrimPointer value, const ValueFunction& label,
+                            const ParamsArgs& params_args) __attribute__((weak));
+
 #ifdef ZEOLITE_PUBLIC_NAMESPACE
 }  // namespace ZEOLITE_PUBLIC_NAMESPACE
 using namespace ZEOLITE_PUBLIC_NAMESPACE;
@@ -51,26 +54,26 @@
 
 void Validate(const std::string& name, const UnionValue& the_union) {
   if (the_union.type_ == UnionValue::Type::BOXED) {
-    const auto strong = the_union.value_.as_pointer_->strong_.load();
-    const auto weak   = the_union.value_.as_pointer_->weak_.load();
-    const TypeValue* const object = the_union.value_.as_pointer_->object_;
+    const auto strong = the_union.value_.as_boxed_->strong_.load();
+    const auto weak   = the_union.value_.as_boxed_->weak_.load();
+    const TypeValue* const object = the_union.value_.as_boxed_->object_;
     const std::string type = object ? the_union.CategoryName() : "deleted value";
 
     if (strong == 0 && object) {
       FAIL() << "Leaked " << type << " " << name << " at "
-             << ((void*) the_union.value_.as_pointer_) << " (S: "
+             << ((void*) the_union.value_.as_boxed_) << " (S: "
              << std::hex << strong << " W: " << weak << ")";
     }
 
     if (strong != 0 && !object) {
       FAIL() << "Prematurely deleted value " << name << " at "
-             << ((void*) the_union.value_.as_pointer_) << " (S: "
+             << ((void*) the_union.value_.as_boxed_) << " (S: "
              << std::hex << strong << " W: " << weak << ")";
     }
 
     if (strong < 0 || weak < 0 || (weak == 0 && strong > 0)) {
       FAIL() << "Invalid counts for " << type << " " << name << " at "
-             << ((void*) the_union.value_.as_pointer_) << " (S: "
+             << ((void*) the_union.value_.as_boxed_) << " (S: "
              << std::hex << strong << " W: " << weak << ")";
     }
   }
@@ -80,16 +83,17 @@
 
 std::string UnionValue::CategoryName() const {
   switch (type_) {
-    case UnionValue::Type::EMPTY: return "empty";
-    case UnionValue::Type::BOOL:  return "Bool";
-    case UnionValue::Type::CHAR:  return "Char";
-    case UnionValue::Type::INT:   return "Int";
-    case UnionValue::Type::FLOAT: return "Float";
+    case UnionValue::Type::EMPTY:   return "empty";
+    case UnionValue::Type::BOOL:    return "Bool";
+    case UnionValue::Type::CHAR:    return "Char";
+    case UnionValue::Type::INT:     return "Int";
+    case UnionValue::Type::FLOAT:   return "Float";
+    case UnionValue::Type::POINTER: return "Pointer";
     case UnionValue::Type::BOXED:
-      if (!value_.as_pointer_ || !value_.as_pointer_->object_) {
+      if (!value_.as_boxed_ || !value_.as_boxed_->object_) {
         FAIL() << "Function called on null pointer";
       }
-      return value_.as_pointer_->object_->CategoryName();
+      return value_.as_boxed_->object_->CategoryName();
   }
 }
 
@@ -97,14 +101,14 @@
   : union_(other.union_) {
   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_ == 1) {
-        --union_.value_.as_pointer_->strong_;
-        union_.value_.as_pointer_->lock_.clear(std::memory_order_release);
+      while (union_.value_.as_boxed_->lock_.test_and_set(std::memory_order_acquire));
+      if (++union_.value_.as_boxed_->strong_ == 1) {
+        --union_.value_.as_boxed_->strong_;
+        union_.value_.as_boxed_->lock_.clear(std::memory_order_release);
         union_.type_ = UnionValue::Type::EMPTY;
-        union_.value_.as_pointer_ = nullptr;
+        union_.value_.as_boxed_ = nullptr;
       } else {
-        union_.value_.as_pointer_->lock_.clear(std::memory_order_release);
+        union_.value_.as_boxed_->lock_.clear(std::memory_order_release);
       }
       break;
     default:
@@ -119,10 +123,10 @@
 const PrimString& BoxedValue::AsString() const {
   switch (union_.type_) {
     case UnionValue::Type::BOXED:
-      if (!union_.value_.as_pointer_ || !union_.value_.as_pointer_->object_) {
+      if (!union_.value_.as_boxed_ || !union_.value_.as_boxed_->object_) {
         FAIL() << "Function called on null pointer";
       }
-      return union_.value_.as_pointer_->object_->AsString();
+      return union_.value_.as_boxed_->object_->AsString();
     default:
       FAIL() << union_.CategoryName() << " is not a String value";
       __builtin_unreachable();
@@ -133,10 +137,10 @@
 PrimCharBuffer& BoxedValue::AsCharBuffer() const {
   switch (union_.type_) {
     case UnionValue::Type::BOXED:
-      if (!union_.value_.as_pointer_ || !union_.value_.as_pointer_->object_) {
+      if (!union_.value_.as_boxed_ || !union_.value_.as_boxed_->object_) {
         FAIL() << "Function called on null pointer";
       }
-      return union_.value_.as_pointer_->object_->AsCharBuffer();
+      return union_.value_.as_boxed_->object_->AsCharBuffer();
     default:
       FAIL() << union_.CategoryName() << " is not a CharBuffer value";
       __builtin_unreachable();
@@ -159,24 +163,26 @@
       return DispatchInt(union_.value_.as_int_, label, params_args);
     case UnionValue::Type::FLOAT:
       return DispatchFloat(union_.value_.as_float_, label, params_args);
+    case UnionValue::Type::POINTER:
+      return DispatchPointer(union_.value_.as_pointer_, label, params_args);
     case UnionValue::Type::BOXED:
-      if (!union_.value_.as_pointer_ || !union_.value_.as_pointer_->object_) {
+      if (!union_.value_.as_boxed_ || !union_.value_.as_boxed_->object_) {
         FAIL() << "Function called on null pointer";
       }
-      return union_.value_.as_pointer_->object_->Dispatch(label, params_args);
+      return union_.value_.as_boxed_->object_->Dispatch(label, params_args);
   }
 }
 
 void BoxedValue::Cleanup() {
   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);
+    while (union_.value_.as_boxed_->lock_.test_and_set(std::memory_order_acquire));
+    if (--union_.value_.as_boxed_->strong_ == 0) {
+      TypeValue* const object = union_.value_.as_boxed_->object_;
+      union_.value_.as_boxed_->object_ = nullptr;
+      union_.value_.as_boxed_->lock_.clear(std::memory_order_release);
       BoxedValue next = object->FlatCleanup();
       object->~TypeValue();
-      if (--union_.value_.as_pointer_->weak_ == 0) {
+      if (--union_.value_.as_boxed_->weak_ == 0) {
         // NOTE: as_bytes_ contains object => ~TypeValue() must happen first.
         free(union_.value_.as_bytes_);
       }
@@ -184,23 +190,23 @@
       // 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);
+      union_.value_.as_boxed_->lock_.clear(std::memory_order_release);
       break;
     }
   }
   union_.type_ = UnionValue::Type::EMPTY;
-  union_.value_.as_pointer_ = nullptr;
+  union_.value_.as_boxed_ = nullptr;
 }
 
 
 WeakValue::WeakValue()
-  : union_{ .type_ = UnionValue::Type::EMPTY, .value_ = { .as_pointer_ = nullptr } } {}
+  : union_{ .type_ = UnionValue::Type::EMPTY, .value_ = { .as_boxed_ = nullptr } } {}
 
 WeakValue::WeakValue(const WeakValue& other)
   : union_(other.union_) {
   switch (union_.type_) {
     case UnionValue::Type::BOXED:
-      ++union_.value_.as_pointer_->weak_;
+      ++union_.value_.as_boxed_->weak_;
       break;
     default:
       break;
@@ -213,7 +219,7 @@
     union_ = other.union_;
     switch (union_.type_) {
       case UnionValue::Type::BOXED:
-        ++union_.value_.as_pointer_->weak_;
+        ++union_.value_.as_boxed_->weak_;
         break;
       default:
         break;
@@ -225,7 +231,7 @@
 WeakValue::WeakValue(WeakValue&& other)
   : union_(other.union_) {
   other.union_.type_  = UnionValue::Type::EMPTY;
-  other.union_.value_.as_pointer_ = nullptr;
+  other.union_.value_.as_boxed_ = nullptr;
 }
 
 WeakValue& WeakValue::operator = (WeakValue&& other) {
@@ -233,7 +239,7 @@
     Cleanup();
     union_ = other.union_;
     other.union_.type_  = UnionValue::Type::EMPTY;
-    other.union_.value_.as_pointer_ = nullptr;
+    other.union_.value_.as_boxed_ = nullptr;
   }
   return *this;
 }
@@ -242,7 +248,7 @@
   : union_(other.union_) {
   switch (union_.type_) {
     case UnionValue::Type::BOXED:
-      ++union_.value_.as_pointer_->weak_;
+      ++union_.value_.as_boxed_->weak_;
       break;
     default:
       break;
@@ -254,7 +260,7 @@
   union_ = other.union_;
   switch (union_.type_) {
     case UnionValue::Type::BOXED:
-      ++union_.value_.as_pointer_->weak_;
+      ++union_.value_.as_boxed_->weak_;
       break;
     default:
       break;
@@ -273,7 +279,7 @@
 void WeakValue::Cleanup() {
   switch (union_.type_) {
     case UnionValue::Type::BOXED:
-      if (--union_.value_.as_pointer_->weak_ == 0) {
+      if (--union_.value_.as_boxed_->weak_ == 0) {
         free(union_.value_.as_bytes_);
       }
       break;
@@ -281,7 +287,7 @@
       break;
   }
   union_.type_  = UnionValue::Type::EMPTY;
-  union_.value_.as_pointer_ = nullptr;
+  union_.value_.as_boxed_ = nullptr;
 }
 
 }  // namespace zeolite_internal
diff --git a/base/src/logging.cpp b/base/src/logging.cpp
--- a/base/src/logging.cpp
+++ b/base/src/logging.cpp
@@ -1,5 +1,5 @@
 /* -----------------------------------------------------------------------------
-Copyright 2019-2021 Kevin P. Barry
+Copyright 2019-2022 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.
@@ -191,6 +191,22 @@
   return argv_;
 }
 
+namespace {
+
+std::unique_ptr<std::fstream> TryOpenOutputLog(const std::string& filename) {
+  if (filename.empty()) return nullptr;
+  std::unique_ptr<std::fstream> file(new std::fstream(
+    filename, std::ios::in | std::ios::out | std::ios::ate | std::ios::app));
+  if (*file) return file;
+  // Maybe the file isn't seekable.
+  file.reset(new std::fstream(filename, std::ios::out));
+  if (*file) return file;
+  FAIL() << "Failed to open call log " << filename << " for writing";
+  __builtin_unreachable();
+}
+
+}  // namespace
+
 unsigned int UniqueId() {
   const auto time = std::chrono::steady_clock::now().time_since_epoch();
   return (1000000009 * std::chrono::duration_cast<std::chrono::microseconds>(time).count());
@@ -206,19 +222,8 @@
 LogCallsToFile::LogCallsToFile(std::string filename)
   : unique_id_(UniqueId()),
     filename_(std::move(filename)),
-    log_file_(filename_.empty()?
-                nullptr :
-                new std::fstream(filename_, std::ios::in |
-                                            std::ios::out |
-                                            std::ios::ate |
-                                            std::ios::app)),
-    cross_and_capture_to_(this) {
-  if (log_file_) {
-    if (!*log_file_) {
-      FAIL() << "Failed to open call log " << filename_ << " for writing";
-    }
-  }
-}
+    log_file_(TryOpenOutputLog(filename_)),
+    cross_and_capture_to_(this) {}
 
 void LogCallsToFile::LogCall(const char* name, const char* at) {
   if (log_file_ && enable_coverage) {
diff --git a/base/types.hpp b/base/types.hpp
--- a/base/types.hpp
+++ b/base/types.hpp
@@ -1,5 +1,5 @@
 /* -----------------------------------------------------------------------------
-Copyright 2019-2021 Kevin P. Barry
+Copyright 2019-2022 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.
@@ -43,6 +43,9 @@
 using PrimChar = char;
 using PrimCharBuffer = std::string;
 using PrimFloat = double;
+
+class OpaqueObject { ~OpaqueObject() = default; };
+using PrimPointer = OpaqueObject*;
 
 template<int S>
 inline PrimString PrimString_FromLiteral(const char(&literal)[S]) {
diff --git a/bin/zeolite.hs b/bin/zeolite.hs
--- a/bin/zeolite.hs
+++ b/bin/zeolite.hs
@@ -1,5 +1,5 @@
 {- -----------------------------------------------------------------------------
-Copyright 2020-2021 Kevin P. Barry
+Copyright 2020-2022 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.
@@ -24,6 +24,7 @@
 import System.Exit
 import GHC.IO.Handle
 import System.IO
+import Text.Printf (printf)
 import qualified Data.Map as Map
 import qualified Data.Set as Set
 
@@ -122,8 +123,17 @@
       h <- getCompilerHash backend
       expected <- fmap Set.unions $ mapCompilerM (getTraces h) ps
       actual <- loadTraces
-      mapM_ (errorFromIO . hPutStrLn stdout) $ Set.toList $ expected `Set.difference` actual
+      let difference = expected `Set.difference` actual
+      mapM_ (errorFromIO . hPutStrLn stdout) $ Set.toList $ difference
+      errorFromIO $ hPutStrLn stdout $ formatCoverage difference expected
     exitSuccess where
+      formatCoverage difference expected =
+        "Coverage: " ++ show actualSize ++ " of " ++ show expectedSize ++
+        " lines (" ++ printf "%.2f" coverage ++ "%)" where
+          diffSize = length $ Set.toList $ difference
+          expectedSize = length $ Set.toList $ expected
+          actualSize = expectedSize - diffSize
+          coverage = 100.0 * (fromIntegral actualSize :: Double) / (fromIntegral expectedSize :: Double)
       loadTraces = do
         l' <- errorFromIO $ canonicalizePath (root </> l)
         errorFromIO $ hPutStrLn stderr $ "Loading trace data from \"" ++ l' ++ "\"."
diff --git a/example/random/.zeolite-module b/example/random/.zeolite-module
--- a/example/random/.zeolite-module
+++ b/example/random/.zeolite-module
@@ -3,7 +3,6 @@
 private_deps: [
   "lib/util"
   "lib/math"
-  "lib/thread"
 ]
 mode: binary {
   category: RandomDemo
diff --git a/lib/thread/.zeolite-module b/lib/thread/.zeolite-module
--- a/lib/thread/.zeolite-module
+++ b/lib/thread/.zeolite-module
@@ -16,10 +16,6 @@
     categories: [ProcessThread]
   }
   category_source {
-    source: "lib/thread/src/Extension_Realtime.cpp"
-    categories: [Realtime]
-  }
-  category_source {
     source: "lib/thread/src/Extension_ThreadCondition.cpp"
     categories: [ThreadCondition]
   }
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
@@ -1,5 +1,5 @@
 /* -----------------------------------------------------------------------------
-Copyright 2021 Kevin P. Barry
+Copyright 2021-2022 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.
@@ -31,6 +31,28 @@
 namespace ZEOLITE_PUBLIC_NAMESPACE {
 #endif  // ZEOLITE_PUBLIC_NAMESPACE
 
+namespace {
+
+class InheritTrace : public TraceContext {
+ public:
+  inline explicit InheritTrace(const TraceList& trace)
+    : trace_(trace), capture_to_(this) {}
+
+ private:
+  void SetLocal(const char*) final {}
+
+  void AppendTrace(TraceList& trace) const final{
+    trace.insert(trace.end(), trace_.begin(), trace_.end());
+  }
+
+  const TraceContext* GetNext() const final { return nullptr; }
+
+  const TraceList& trace_;
+  const ScopedCapture capture_to_;
+};
+
+}  // namespace
+
 BoxedValue CreateValue_ProcessThread(S<const Type_ProcessThread> parent, const ParamsArgs& params_args);
 
 struct ExtCategory_ProcessThread : public Category_ProcessThread {
@@ -53,7 +75,7 @@
     TRACE_FUNCTION("ProcessThread.detach")
     S<std::thread> temp = thread;
     thread = nullptr;
-    if (!isJoinable(temp.get())) {
+    if (!IsJoinable(temp.get())) {
       FAIL() << "thread has not been started";
     } else {
       temp->detach();
@@ -63,14 +85,14 @@
 
   ReturnTuple Call_isRunning(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("ProcessThread.isRunning")
-    return ReturnTuple(Box_Bool(isJoinable(thread.get())));
+    return ReturnTuple(Box_Bool(IsJoinable(thread.get())));
   }
 
   ReturnTuple Call_join(const ParamsArgs& params_args) final {
     TRACE_FUNCTION("ProcessThread.join")
     S<std::thread> temp = thread;
     thread = nullptr;
-    if (!isJoinable(temp.get())) {
+    if (!IsJoinable(temp.get())) {
       FAIL() << "thread has not been started";
     } else {
       temp->join();
@@ -79,8 +101,9 @@
   }
 
   ReturnTuple Call_start(const ParamsArgs& params_args) final {
+    const TraceList copied_trace = TraceContext::GetTrace();
     TRACE_FUNCTION("ProcessThread.start")
-    if (isJoinable(thread.get())) {
+    if (IsJoinable(thread.get())) {
       FAIL() << "thread is already running";
     } else {
       // NOTE: Capture VAR_SELF so that the thread retains a reference while
@@ -88,7 +111,13 @@
       // the thread.
       const BoxedValue self = VAR_SELF;
       thread.reset(new std::thread(
-        capture_thread::ThreadCrosser::WrapCall([this,self] {
+        capture_thread::ThreadCrosser::WrapCall([this,self,copied_trace] {
+#ifndef DISABLE_TRACING
+          InheritTrace inherit_trace(copied_trace);
+#endif
+          // NOTE: Don't include this in copied_trace because the latter defines
+          // SetLocal as a no-op.
+          TRACE_FUNCTION("ProcessThread.start")
           TRACE_CREATION
           TypeValue::Call(routine, Function_Routine_run, PassParamsArgs());
         })));
@@ -96,14 +125,14 @@
     return ReturnTuple(VAR_SELF);
   }
 
-  inline static bool isJoinable(std::thread* thread) {
+  inline static bool IsJoinable(std::thread* thread) {
     return thread && thread->joinable();
   }
 
   ~ExtValue_ProcessThread() {
     S<std::thread> temp = thread;
     thread = nullptr;
-    if (isJoinable(temp.get())) {
+    if (IsJoinable(temp.get())) {
       temp->detach();
     }
   }
diff --git a/lib/thread/src/Extension_Realtime.cpp b/lib/thread/src/Extension_Realtime.cpp
deleted file mode 100644
--- a/lib/thread/src/Extension_Realtime.cpp
+++ /dev/null
@@ -1,106 +0,0 @@
-/* -----------------------------------------------------------------------------
-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]
-
-#include <errno.h>
-#include <math.h>
-#include <string.h>
-#include <time.h>
-
-#include <chrono>
-#include <iomanip>
-#include <thread>
-
-#include "category-source.hpp"
-#include "Streamlined_Realtime.hpp"
-#include "Category_Float.hpp"
-#include "Category_Realtime.hpp"
-
-#ifndef SLEEP_SPINLOCK_LIMIT
-#define SLEEP_SPINLOCK_LIMIT 0.001
-#endif
-
-#ifdef ZEOLITE_PUBLIC_NAMESPACE
-namespace ZEOLITE_PUBLIC_NAMESPACE {
-#endif  // ZEOLITE_PUBLIC_NAMESPACE
-
-struct ExtCategory_Realtime : public Category_Realtime {
-};
-
-struct ExtType_Realtime : public Type_Realtime {
-  inline ExtType_Realtime(Category_Realtime& p, Params<0>::Type params) : Type_Realtime(p, params) {}
-
-  ReturnTuple Call_sleepSeconds(const ParamsArgs& params_args) const final {
-    TRACE_FUNCTION("Realtime.sleepSeconds")
-    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 ParamsArgs& params_args) const final {
-    TRACE_FUNCTION("Realtime.sleepSecondsPrecise")
-    const PrimFloat Var_arg1 = (params_args.GetArg(0)).AsFloat();
-
-    const auto spinlock_limit = std::chrono::duration<double>(SLEEP_SPINLOCK_LIMIT);
-
-    const auto target_time =
-      std::chrono::high_resolution_clock::now().time_since_epoch() +
-      std::chrono::duration<double>(Var_arg1);
-
-    while (true) {
-      const auto now = std::chrono::high_resolution_clock::now().time_since_epoch();
-      if (now >= target_time) break;
-      const auto sleep_time = target_time - now;
-      if (sleep_time <= spinlock_limit) {
-        while (std::chrono::high_resolution_clock::now().time_since_epoch() < target_time);
-        break;
-      } else {
-        std::this_thread::sleep_for(sleep_time - spinlock_limit);
-      }
-    }
-
-    return ReturnTuple();
-  }
-
-  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 =
-      (PrimFloat) std::chrono::duration_cast<std::chrono::microseconds>(time).count() /
-      (PrimFloat) 1000000.0;
-    return ReturnTuple(Box_Float(seconds));
-  }
-};
-
-Category_Realtime& CreateCategory_Realtime() {
-  static auto& category = *new ExtCategory_Realtime();
-  return category;
-}
-
-S<const Type_Realtime> CreateType_Realtime(const Params<0>::Type& params) {
-  static const auto cached = S_get(new ExtType_Realtime(CreateCategory_Realtime(), Params<0>::Type()));
-  return cached;
-}
-
-void RemoveType_Realtime(const Params<0>::Type& params) {}
-
-#ifdef ZEOLITE_PUBLIC_NAMESPACE
-}  // namespace ZEOLITE_PUBLIC_NAMESPACE
-using namespace ZEOLITE_PUBLIC_NAMESPACE;
-#endif  // ZEOLITE_PUBLIC_NAMESPACE
diff --git a/lib/thread/src/testing.0rp b/lib/thread/src/testing.0rp
--- a/lib/thread/src/testing.0rp
+++ b/lib/thread/src/testing.0rp
@@ -1,5 +1,5 @@
 /* -----------------------------------------------------------------------------
-Copyright 2021 Kevin P. Barry
+Copyright 2021-2022 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,12 @@
   refines Routine
 
   @type create () -> (NoOpRoutine)
+}
+
+concrete CrashRoutine {
+  refines Routine
+
+  @type create () -> (CrashRoutine)
 }
 
 concrete InfiniteRoutine {
diff --git a/lib/thread/src/testing.0rx b/lib/thread/src/testing.0rx
--- a/lib/thread/src/testing.0rx
+++ b/lib/thread/src/testing.0rx
@@ -1,5 +1,5 @@
 /* -----------------------------------------------------------------------------
-Copyright 2021 Kevin P. Barry
+Copyright 2021-2022 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.
@@ -24,6 +24,16 @@
   }
 
   run () {}
+}
+
+define CrashRoutine {
+  create () {
+    return CrashRoutine{ }
+  }
+
+  run () {
+    fail("CrashRoutine")
+  }
 }
 
 define InfiniteRoutine {
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
@@ -1,5 +1,5 @@
 /* -----------------------------------------------------------------------------
-Copyright 2021 Kevin P. Barry
+Copyright 2021-2022 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.
@@ -241,5 +241,35 @@
   weak Thread thread <- ProcessThread.from(NoOpRoutine.create())
   if (present(strong(thread))) {
     fail("thread is still present")
+  }
+}
+
+
+testcase "creation tracing in Thread" {
+  crash
+  // Thread creation.
+  require "CrashThread\.new"
+  // Inherited trace from start call.
+  require "CrashThread\.execute"
+}
+
+unittest testName {
+  \ CrashThread.new().execute()
+}
+
+concrete CrashThread {
+  @type new () -> (CrashThread)
+  @value execute () -> ()
+}
+
+define CrashThread {
+  @value ProcessThread thread
+
+  new () {
+    return CrashThread{ ProcessThread.from(CrashRoutine.create()) }
+  }
+
+  execute () {
+    \ thread.start().join()
   }
 }
diff --git a/lib/thread/test/time.0rt b/lib/thread/test/time.0rt
deleted file mode 100644
--- a/lib/thread/test/time.0rt
+++ /dev/null
@@ -1,79 +0,0 @@
-/* -----------------------------------------------------------------------------
-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 "zero sleep is allowed" {
-  success
-}
-
-unittest zeroSleep {
-  \ Realtime.sleepSeconds(0.0)
-}
-
-unittest negativeSleep {
-  \ Realtime.sleepSeconds(-0.1)
-}
-
-unittest zeroPrecise {
-  \ Realtime.sleepSecondsPrecise(0.0)
-}
-
-unittest negativePrecise {
-  \ Realtime.sleepSecondsPrecise(-0.1)
-}
-
-
-testcase "sleepSeconds() does not interfere with test timeout" {
-  crash
-  require "signal 14"
-  timeout 1
-}
-
-unittest test {
-  \ Realtime.sleepSeconds(5.0)
-}
-
-
-testcase "sleepSecondsPrecise() does not interfere with test timeout" {
-  crash
-  require "signal 14"
-  timeout 1
-}
-
-unittest test {
-  \ Realtime.sleepSecondsPrecise(5.0)
-}
-
-
-testcase "monoSeconds() diff is somewhat accurate" {
-  success
-  timeout 2
-}
-
-unittest testSleep {
-  Float start <- Realtime.monoSeconds()
-  \ Realtime.sleepSeconds(0.5)
-  Float stop <- Realtime.monoSeconds()
-  \ 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)
-}
diff --git a/lib/thread/time.0rp b/lib/thread/time.0rp
deleted file mode 100644
--- a/lib/thread/time.0rp
+++ /dev/null
@@ -1,35 +0,0 @@
-/* -----------------------------------------------------------------------------
-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]
-
-// Functions for realtime computation.
-concrete Realtime {
-  // Sleep for the given number of seconds.
-  @type sleepSeconds (Float) -> ()
-
-  // Sleep for the given number of seconds, with more precision.
-  @type sleepSecondsPrecise (Float) -> ()
-
-  // Get a timestamp from a monotonic clock.
-  //
-  // Notes:
-  // - The absolute timestamp is fairly meaningless; use timestamp diffs
-  //   instead, e.g., to compute the wall time of an operation.
-  // - In theory, this is microsecond-precise; however, the effective resolution
-  //   could depend more on the kernel's latency.
-  @type monoSeconds () -> (Float)
-}
diff --git a/lib/util/.zeolite-module b/lib/util/.zeolite-module
--- a/lib/util/.zeolite-module
+++ b/lib/util/.zeolite-module
@@ -31,6 +31,10 @@
     categories: [MutexLock]
   }
   category_source {
+    source: "lib/util/src/Extension_Realtime.cpp"
+    categories: [Realtime]
+  }
+  category_source {
     source: "lib/util/src/Extension_SimpleMutex.cpp"
     categories: [SimpleMutex]
   }
diff --git a/lib/util/counters.0rp b/lib/util/counters.0rp
--- a/lib/util/counters.0rp
+++ b/lib/util/counters.0rp
@@ -1,5 +1,5 @@
 /* -----------------------------------------------------------------------------
-Copyright 2021 Kevin P. Barry
+Copyright 2021-2022 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.
@@ -24,6 +24,8 @@
   @type revZeroIndexed (Int) -> (optional Order<Int>)
   // Create an unbounded Int sequence.
   @type unlimited () -> (optional Order<Int>)
+  // Create a counter using a builder.
+  @type builder () -> (CounterBuilder)
 }
 
 // Creates an Order<#x> that repeats a single value, for use with traverse.
@@ -50,4 +52,53 @@
   @category minMax<#x>
     #x defines LessThan<#x>
   (#x,#x) -> (#x,#x)
+}
+
+// Builds a sequence of values.
+//
+// Many of the functions here overlap; therefore, you should never need all of
+// them for a single counter.
+//
+// Example combinations:
+//
+// - start+count: Set the first value and the number of items, using the default
+//   increment between items.
+// - count+reverse: Use the default start and default increment, but reverse the
+//   order that the items are returned in.
+// - start+increment+limit: Count by the specified increment starting with the
+//   specified starting point until the specified limit.
+concrete CounterBuilder {
+  @type new () -> (#self)
+
+  // Finalize the counter.
+  @value done () -> (optional Order<Int>)
+
+  // Set the first value in the counter.
+  @value start (Int) -> (#self)
+
+  // Set the number of items returned.
+  //
+  // Notes:
+  // - This overrides limit.
+  @value count (Int) -> (#self)
+
+  // Set a strict threshold that will not be met or exceeded.
+  //
+  // Notes:
+  // - If increment < 0 then all items will be > limit.
+  // - If increment > 0 then all items will be < limit.
+  // - This overrides count.
+  @value limit (Int) -> (#self)
+
+  // Set the difference between consecutive items.
+  @value increment (Int) -> (#self)
+
+  // Reverse the order that the items are returned in.
+  //
+  // Notes:
+  // - This has no effect if the count is unlimited, since the last item does
+  //   not exist.
+  // - This has the same effect regardless of when it gets called.
+  // - Calling this twice reverts the counter to forward again.
+  @value reverse () -> (#self)
 }
diff --git a/lib/util/src/Extension_Realtime.cpp b/lib/util/src/Extension_Realtime.cpp
new file mode 100644
--- /dev/null
+++ b/lib/util/src/Extension_Realtime.cpp
@@ -0,0 +1,106 @@
+/* -----------------------------------------------------------------------------
+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]
+
+#include <errno.h>
+#include <math.h>
+#include <string.h>
+#include <time.h>
+
+#include <chrono>
+#include <iomanip>
+#include <thread>
+
+#include "category-source.hpp"
+#include "Streamlined_Realtime.hpp"
+#include "Category_Float.hpp"
+#include "Category_Realtime.hpp"
+
+#ifndef SLEEP_SPINLOCK_LIMIT
+#define SLEEP_SPINLOCK_LIMIT 0.001
+#endif
+
+#ifdef ZEOLITE_PUBLIC_NAMESPACE
+namespace ZEOLITE_PUBLIC_NAMESPACE {
+#endif  // ZEOLITE_PUBLIC_NAMESPACE
+
+struct ExtCategory_Realtime : public Category_Realtime {
+};
+
+struct ExtType_Realtime : public Type_Realtime {
+  inline ExtType_Realtime(Category_Realtime& p, Params<0>::Type params) : Type_Realtime(p, params) {}
+
+  ReturnTuple Call_sleepSeconds(const ParamsArgs& params_args) const final {
+    TRACE_FUNCTION("Realtime.sleepSeconds")
+    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 ParamsArgs& params_args) const final {
+    TRACE_FUNCTION("Realtime.sleepSecondsPrecise")
+    const PrimFloat Var_arg1 = (params_args.GetArg(0)).AsFloat();
+
+    const auto spinlock_limit = std::chrono::duration<double>(SLEEP_SPINLOCK_LIMIT);
+
+    const auto target_time =
+      std::chrono::high_resolution_clock::now().time_since_epoch() +
+      std::chrono::duration<double>(Var_arg1);
+
+    while (true) {
+      const auto now = std::chrono::high_resolution_clock::now().time_since_epoch();
+      if (now >= target_time) break;
+      const auto sleep_time = target_time - now;
+      if (sleep_time <= spinlock_limit) {
+        while (std::chrono::high_resolution_clock::now().time_since_epoch() < target_time);
+        break;
+      } else {
+        std::this_thread::sleep_for(sleep_time - spinlock_limit);
+      }
+    }
+
+    return ReturnTuple();
+  }
+
+  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 =
+      (PrimFloat) std::chrono::duration_cast<std::chrono::microseconds>(time).count() /
+      (PrimFloat) 1000000.0;
+    return ReturnTuple(Box_Float(seconds));
+  }
+};
+
+Category_Realtime& CreateCategory_Realtime() {
+  static auto& category = *new ExtCategory_Realtime();
+  return category;
+}
+
+S<const Type_Realtime> CreateType_Realtime(const Params<0>::Type& params) {
+  static const auto cached = S_get(new ExtType_Realtime(CreateCategory_Realtime(), Params<0>::Type()));
+  return cached;
+}
+
+void RemoveType_Realtime(const Params<0>::Type& params) {}
+
+#ifdef ZEOLITE_PUBLIC_NAMESPACE
+}  // namespace ZEOLITE_PUBLIC_NAMESPACE
+using namespace ZEOLITE_PUBLIC_NAMESPACE;
+#endif  // ZEOLITE_PUBLIC_NAMESPACE
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
@@ -1,5 +1,5 @@
 /* -----------------------------------------------------------------------------
-Copyright 2021 Kevin P. Barry
+Copyright 2021-2022 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,99 +18,114 @@
 
 define Counter {
   zeroIndexed (max) {
-    return ForwardCounter.new(max)
+    return builder().count(max).done()
   }
 
   revZeroIndexed (max) {
-    return ReverseCounter.new(max)
+    return builder().count(max).reverse().done()
   }
 
   unlimited () {
-    return UnlimitedCounter.new()
+    return builder().done()
   }
-}
 
-concrete ForwardCounter {
-  @type new (Int) -> (optional Order<Int>)
+  builder () {
+    return CounterBuilder.new()
+  }
 }
 
-define ForwardCounter {
-  $ReadOnly[limit]$
-
-  refines Order<Int>
-
-  @value Int current
-  @value Int limit
+define CounterBuilder {
+  @value Int start
+  @value Int increment
+  @value optional Int limit
+  @value optional Int count
+  @value Bool reverse
 
-  new (max) {
-    if (max > 0) {
-      return #self{ 0, max }
-    } else {
-      return empty
-    }
+  new () {
+    return #self{ 0, 1, empty, empty, false }
   }
 
-  next () {
-    if (current < limit-1) {
-      current <- current+1
-      return self
+  done () {
+    optional Int actualCount <- defer
+    if (`present` limit) {
+      if (increment != 0) {
+        Bool missedStart <- (`require` limit - start) % increment != 0
+        // NOTE: This still works if limit < start and increment < 0.
+        actualCount <- (`require` limit - start) / increment + missedStart.asInt()
+      } else {
+        actualCount <- empty
+      }
     } else {
-      return empty
+      actualCount <- count
     }
+    Int actualStart <- start
+    Int actualIncrement <- increment
+    if (reverse && `present` actualCount) {
+      actualStart <- start + increment*(require(actualCount)-1)
+      actualIncrement <- -increment
+    }
+    return IntCounter.new(actualStart,actualIncrement,actualCount)
   }
 
-  get () {
-    return current
+  start (x) {
+    start <- x
+    return self
   }
-}
 
-concrete ReverseCounter {
-  @type new (Int) -> (optional Order<Int>)
-}
-
-define ReverseCounter {
-  refines Order<Int>
-
-  @value Int current
+  count (n) {
+    limit <- empty
+    count <- n
+    return self
+  }
 
-  new (max) {
-    if (max > 0) {
-      return #self{ max-1 }
-    } else {
-      return empty
-    }
+  limit (x) {
+    count <- empty
+    limit <- x
+    return self
   }
 
-  next () {
-    if (current > 0) {
-      current <- current-1
-      return self
-    } else {
-      return empty
-    }
+  increment (x) {
+    increment <- x
+    return self
   }
 
-  get () {
-    return current
+  reverse () {
+    reverse <- !reverse
+    return self
   }
 }
 
-concrete UnlimitedCounter {
-  @type new () -> (optional Order<Int>)
+concrete IntCounter {
+  @type new (Int,Int,optional Int) -> (optional Order<Int>)
 }
 
-define UnlimitedCounter {
+define IntCounter {
+  $ReadOnly[increment]$
+
   refines Order<Int>
 
   @value Int current
+  @value Int increment
+  @value optional Int remaining
 
-  new () {
-    return #self{ 0 }
+  new (start,increment,count) {
+    if (`present` count && `require` count <= 0) {
+      return empty
+    } else {
+      return #self{ start, increment, count }
+    }
   }
 
   next () {
-    current <- current+1
-    return self
+    if (`present` remaining && `require` remaining - 1 <= 0) {
+      return empty
+    } else {
+      if (`present` remaining) {
+        remaining <- `require` remaining - 1
+      }
+      current <- current+increment
+      return self
+    }
   }
 
   get () {
@@ -119,32 +134,33 @@
 }
 
 define Repeat {
-  $ReadOnlyExcept[current]$
+  $ReadOnlyExcept[counter]$
 
   refines Order<#x>
 
   @value #x value
-  @value Int current
-  @value optional Int limit
+  @value Order<any> counter
 
   times (value,max) {
-    if (max <= 0) {
+    optional Order<any> counter <- Counter.builder().count(max).done()
+    if (! `present` counter) {
       return empty
     } else {
-      return Repeat<#y>{ value, 0, max }
+      return Repeat<#y>{ value, `require` counter }
     }
   }
 
   unlimited (value) {
-    return Repeat<#y>{ value, 0, empty }
+    return Repeat<#y>{ value, `require` Counter.builder().done() }
   }
 
   next () {
-    if (!present(limit) || current+1 < require(limit)) {
-      current <- current+1
-      return self
-    } else {
+    optional Order<any> next <- counter.next()
+    if (! `present` next) {
       return empty
+    } else {
+      counter <- `require` next
+      return self
     }
   }
 
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
@@ -1,5 +1,5 @@
 /* -----------------------------------------------------------------------------
-Copyright 2021 Kevin P. Barry
+Copyright 2021-2022 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.
@@ -38,14 +38,14 @@
 }
 
 unittest revZeroIndexed {
-  Int index <- 10
+  Int index <- 9
 
   traverse (Counter.revZeroIndexed(10) -> Int i) {
-    index <- index-1
     \ Testing.checkEquals(i,index)
+    index <- index-1
   }
 
-  \ Testing.checkEquals(index,0)
+  \ Testing.checkEquals(index,-1)
 }
 
 unittest revZeroIndexedEmpty {
@@ -65,6 +65,40 @@
   }
 
   \ Testing.checkEquals(index,10)
+}
+
+unittest builderReversePositive {
+  // Forward sequence: 3 8 13
+  Int index <- 13
+
+  traverse (Counter.builder()
+                .start(3)
+                .increment(5)
+                .limit(17)
+                .reverse()
+                .done() -> Int i) {
+    \ Testing.checkEquals(i,index)
+    index <- index-5
+  }
+
+  \ Testing.checkEquals(index,-2)
+}
+
+unittest builderReverseNegative {
+  // Forward sequence: 17 12 7
+  Int index <- 7
+
+  traverse (Counter.builder()
+                .start(17)
+                .increment(-5)
+                .limit(3)
+                .reverse()
+                .done() -> Int i) {
+    \ Testing.checkEquals(i,index)
+    index <- index+5
+  }
+
+  \ Testing.checkEquals(index,22)
 }
 
 unittest times {
diff --git a/lib/util/test/time.0rt b/lib/util/test/time.0rt
new file mode 100644
--- /dev/null
+++ b/lib/util/test/time.0rt
@@ -0,0 +1,79 @@
+/* -----------------------------------------------------------------------------
+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 "zero sleep is allowed" {
+  success
+}
+
+unittest zeroSleep {
+  \ Realtime.sleepSeconds(0.0)
+}
+
+unittest negativeSleep {
+  \ Realtime.sleepSeconds(-0.1)
+}
+
+unittest zeroPrecise {
+  \ Realtime.sleepSecondsPrecise(0.0)
+}
+
+unittest negativePrecise {
+  \ Realtime.sleepSecondsPrecise(-0.1)
+}
+
+
+testcase "sleepSeconds() does not interfere with test timeout" {
+  crash
+  require "signal 14"
+  timeout 1
+}
+
+unittest test {
+  \ Realtime.sleepSeconds(5.0)
+}
+
+
+testcase "sleepSecondsPrecise() does not interfere with test timeout" {
+  crash
+  require "signal 14"
+  timeout 1
+}
+
+unittest test {
+  \ Realtime.sleepSecondsPrecise(5.0)
+}
+
+
+testcase "monoSeconds() diff is somewhat accurate" {
+  success
+  timeout 2
+}
+
+unittest testSleep {
+  Float start <- Realtime.monoSeconds()
+  \ Realtime.sleepSeconds(0.5)
+  Float stop <- Realtime.monoSeconds()
+  \ 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)
+}
diff --git a/lib/util/time.0rp b/lib/util/time.0rp
new file mode 100644
--- /dev/null
+++ b/lib/util/time.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]
+
+// Functions for realtime computation.
+concrete Realtime {
+  // Sleep for the given number of seconds.
+  @type sleepSeconds (Float) -> ()
+
+  // Sleep for the given number of seconds, with more precision.
+  @type sleepSecondsPrecise (Float) -> ()
+
+  // Get a timestamp from a monotonic clock.
+  //
+  // Notes:
+  // - The absolute timestamp is fairly meaningless; use timestamp diffs
+  //   instead, e.g., to compute the wall time of an operation.
+  // - In theory, this is microsecond-precise; however, the effective resolution
+  //   could depend more on the kernel's latency.
+  @type monoSeconds () -> (Float)
+}
diff --git a/src/Cli/Compiler.hs b/src/Cli/Compiler.hs
--- a/src/Cli/Compiler.hs
+++ b/src/Cli/Compiler.hs
@@ -1,5 +1,5 @@
 {- -----------------------------------------------------------------------------
-Copyright 2020-2021 Kevin P. Barry
+Copyright 2020-2022 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.
@@ -28,7 +28,7 @@
 import Control.Monad (foldM,when)
 import Data.Either (partitionEithers)
 import Data.List (isSuffixOf,nub,sort)
-import Data.Time.LocalTime (getZonedTime)
+import Data.Time.Clock (getCurrentTime)
 import System.Directory
 import System.FilePath
 import System.IO
@@ -86,6 +86,7 @@
 
 compileModule :: (PathIOHandler r, CompilerBackend b) => r -> b -> ModuleSpec -> TrackedErrorsIO ()
 compileModule resolver backend (ModuleSpec p d ee em is is2 ps xs ts es cs ep m f pn) = do
+  time <- errorFromIO getCurrentTime
   as  <- fmap fixPaths $ mapCompilerM (resolveModule resolver (p </> d)) is
   as2 <- fmap fixPaths $ mapCompilerM (resolveModule resolver (p </> d)) is2
   let ca0 = Map.empty
@@ -103,7 +104,6 @@
                else do
                  bpDeps <- loadPublicDeps compilerHash f ca2 [base]
                  return $ bpDeps ++ deps1
-  time <- errorFromIO getZonedTime
   root <- errorFromIO $ canonicalizePath p
   path <- errorFromIO $ canonicalizePath (p </> d)
   extra <- errorFromIO $ sequence $ map (canonicalizePath . (p</>)) ee
@@ -134,7 +134,7 @@
   let paths2 = base:s0:s1:(getIncludePathsForDeps (deps1' ++ deps2)) ++ ep' ++ paths'
   let hxx   = filter (isSuffixOf ".hpp" . coFilename)       fs
   let other = filter (not . isSuffixOf ".hpp" . coFilename) fs
-  os1 <- mapCompilerM (writeOutputFile paths2)(hxx ++ other) >>= compileGenerated
+  os1 <- mapCompilerM (writeOutputFile paths2 time) (hxx ++ other) >>= compileGenerated
   let files = map (\f2 -> getCachedPath (p </> d) (show $ coNamespace f2) (coFilename f2)) fs ++
               map (\f2 -> p </> getSourceFile f2) es
   files' <- mapCompilerM checkOwnedFile files
@@ -171,7 +171,7 @@
       cmLinkFlags = getLinkFlags m,
       cmObjectFiles = os1' ++ osOther ++ map OtherObjectFile os'
     }
-  bs <- createBinary compilerHash paths' (cm2:(deps1' ++ deps2)) m mf
+  bs <- createBinary compilerHash paths' (cm2:(deps1' ++ deps2)) time m mf
   let cm2' = CompileMetadata {
       cmVersionHash = cmVersionHash cm2,
       cmRoot = cmRoot cm2,
@@ -195,7 +195,7 @@
       cmLinkFlags = cmLinkFlags cm2,
       cmObjectFiles = cmObjectFiles cm2
     }
-  writeMetadata (p </> d) cm2'
+  writeMetadata (p </> d) cm2' time
   let traces = Set.unions $ map coPossibleTraces $ hxx ++ other
   writePossibleTraces (p </> d) traces where
     ep' = fixPaths $ map (p </>) ep
@@ -206,9 +206,9 @@
              " already defined at " ++ formatFullContextBrace (csContext cc2)
            Nothing -> return ()
       return $ Map.insert n cc cm
-    writeOutputFile paths ca@(CxxOutput _ f2 ns _ _ _ content) = do
+    writeOutputFile paths time ca@(CxxOutput _ f2 ns _ _ _ content) = do
       errorFromIO $ hPutStrLn stderr $ "Writing file " ++ f2
-      writeCachedFile (p </> d) (show ns) f2 $ concat $ map (++ "\n") content
+      _ <- writeCachedFile (p </> d) (show ns) f2 (Just time) $ concat $ map (++ "\n") content
       if isSuffixOf ".cpp" f2 || isSuffixOf ".cc" f2
          then do
            let f2' = getCachedPath (p </> d) (show ns) f2
@@ -269,14 +269,14 @@
           return $ Left $ asyncCxxCommand backend command
       | isSuffixOf ".a" f2 || isSuffixOf ".o" f2 = return $ Right [f2]
       | otherwise = return $ Right []
-    createBinary compilerHash paths deps (CompileBinary n _ lm o lf) [CxxOutput _ _ _ ns2 req _ content] = do
+    createBinary compilerHash paths deps time (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
       let main = takeFileName f0 ++ ".cpp"
       errorFromIO $ hPutStrLn stderr $ "Writing file " ++ main
       let mainAbs = getCachedPath (p </> d) "main" main
-      writeCachedFile (p </> d) "main" main $ concat $ map (++ "\n") content
+      _ <- writeCachedFile (p </> d) "main" main (Just time) $ concat $ map (++ "\n") content
       base <- resolveBaseModule resolver
       deps2  <- loadPrivateDeps compilerHash f (mapMetadata deps) deps
       let paths' = fixPaths $ paths ++ base:(getIncludePathsForDeps deps)
@@ -293,11 +293,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
@@ -314,6 +314,7 @@
   Map.Map CategoryName (CategorySpec SourceContext) ->[CompileMetadata] ->
   [CompileMetadata] -> TrackedErrorsIO ()
 createModuleTemplates resolver p d ds cm deps1 deps2 = do
+  time <- errorFromIO getCurrentTime
   (ps,xs,_) <- findSourceFiles p (d:ds)
   (LanguageModule _ _ _ cs0 ps0 ts0 cs1 ps1 ts1 _ _) <-
     fmap (createLanguageModule [] Map.empty . fst) $ loadModuleGlobals resolver p (PublicNamespace,PrivateNamespace) ps Nothing deps1 deps2
@@ -326,12 +327,12 @@
   let ca' = foldr Set.delete ca $ map dcName ds3
   let testingCats = Set.fromList $ map getCategoryName ts1
   ts <- fmap concat $ mapCompilerM (\n -> generate (n `Set.member` testingCats) tm n) $ Set.toList ca'
-  mapCompilerM_ writeTemplate ts where
+  mapCompilerM_ (writeTemplate time) ts where
     generate testing tm n = do
       (_,t) <- getConcreteCategory tm ([],n)
       let spec = Map.findWithDefault (CategorySpec [] [] []) (getCategoryName t) cm
       generateStreamlinedTemplate testing tm t spec
-    writeTemplate (CxxOutput _ n _ _ _ _ content) = do
+    writeTemplate time (CxxOutput _ n _ _ _ _ content) = do
       let n' = p </> d </> n
       exists <- errorFromIO $ doesFileExist n'
       if exists
@@ -339,6 +340,10 @@
          else do
            errorFromIO $ hPutStrLn stderr $ "Writing file " ++ n
            errorFromIO $ writeFile n' $ concat $ map (++ "\n") content
+           -- This is to avoid a race condition when the module is compiled
+           -- immediately after generating templates, since the former
+           -- explicitly sets the metadata timestamp.
+           errorFromIO $ setModificationTime n' time
 
 runModuleTests :: (PathIOHandler r, CompilerBackend b) =>
   r -> b -> FilePath -> FilePath -> [FilePath] -> LoadedTests ->
@@ -359,7 +364,7 @@
 loadPrivateSource :: PathIOHandler r => r -> VersionHash -> FilePath -> FilePath -> TrackedErrorsIO (PrivateSource SourceContext)
 loadPrivateSource resolver h p f = do
   [f'] <- zipWithContents resolver p [f]
-  time <- errorFromIO getZonedTime
+  time <- errorFromIO getCurrentTime
   path <- errorFromIO $ canonicalizePath (p </> f)
   let ns = StaticNamespace $ privateNamespace $ show time ++ show h ++ path
   (pragmas,cs,ds) <- parseInternalSource f'
diff --git a/src/Compilation/CompilerState.hs b/src/Compilation/CompilerState.hs
--- a/src/Compilation/CompilerState.hs
+++ b/src/Compilation/CompilerState.hs
@@ -1,5 +1,5 @@
 {- -----------------------------------------------------------------------------
-Copyright 2019-2021 Kevin P. Barry
+Copyright 2019-2022 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.
@@ -201,7 +201,7 @@
   JumpContinue |
   JumpBreak |
   JumpReturn |
-  JumpFailCall |
+  JumpImmediateExit |
   JumpMax  -- Max value for use as initial state in folds.
   deriving (Eq,Ord,Show)
 
diff --git a/src/CompilerCxx/Code.hs b/src/CompilerCxx/Code.hs
--- a/src/CompilerCxx/Code.hs
+++ b/src/CompilerCxx/Code.hs
@@ -1,5 +1,5 @@
 {- -----------------------------------------------------------------------------
-Copyright 2019-2021 Kevin P. Barry
+Copyright 2019-2022 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.
@@ -125,19 +125,21 @@
 showCreationTrace = "TRACE_CREATION"
 
 hasPrimitiveValue :: CategoryName -> Bool
-hasPrimitiveValue BuiltinBool  = True
-hasPrimitiveValue BuiltinInt   = True
-hasPrimitiveValue BuiltinFloat = True
-hasPrimitiveValue BuiltinChar  = True
-hasPrimitiveValue _            = False
+hasPrimitiveValue BuiltinBool    = True
+hasPrimitiveValue BuiltinInt     = True
+hasPrimitiveValue BuiltinFloat   = True
+hasPrimitiveValue BuiltinChar    = True
+hasPrimitiveValue BuiltinPointer = True
+hasPrimitiveValue _              = False
 
 isStoredUnboxed :: ValueType -> Bool
 isStoredUnboxed t
-  | t == boolRequiredValue  = True
-  | t == intRequiredValue   = True
-  | t == floatRequiredValue = True
-  | t == charRequiredValue  = True
-  | otherwise               = False
+  | t == boolRequiredValue    = True
+  | t == intRequiredValue     = True
+  | t == floatRequiredValue   = True
+  | t == charRequiredValue    = True
+  | isPointerRequiredValue t = True
+  | otherwise                 = False
 
 expressionFromLiteral :: PrimitiveType -> String -> (ExpressionType,ExpressionValue)
 expressionFromLiteral PrimString e =
@@ -150,6 +152,7 @@
   (Positional [floatRequiredValue],UnboxedPrimitive PrimFloat $ "PrimFloat(" ++ e ++ ")")
 expressionFromLiteral PrimBool e =
   (Positional [boolRequiredValue],UnboxedPrimitive PrimBool e)
+expressionFromLiteral PrimPointer _ = undefined
 
 getFromLazy :: ExpressionValue -> ExpressionValue
 getFromLazy (OpaqueMulti e)        = OpaqueMulti $ e ++ ".Get()"
@@ -168,94 +171,105 @@
 useAsWhatever (LazySingle e)         = useAsWhatever $ getFromLazy e
 
 useAsReturns :: ExpressionValue -> String
-useAsReturns (OpaqueMulti e)                 = e
-useAsReturns (WrappedSingle e)               = "ReturnTuple(" ++ e ++ ")"
-useAsReturns (UnwrappedSingle e)             = "ReturnTuple(" ++ e ++ ")"
-useAsReturns (BoxedPrimitive PrimBool e)     = "ReturnTuple(Box_Bool(" ++ e ++ "))"
-useAsReturns (BoxedPrimitive PrimString e)   = "ReturnTuple(Box_String(" ++ e ++ "))"
-useAsReturns (BoxedPrimitive PrimChar e)     = "ReturnTuple(Box_Char(" ++ e ++ "))"
-useAsReturns (BoxedPrimitive PrimInt e)      = "ReturnTuple(Box_Int(" ++ e ++ "))"
-useAsReturns (BoxedPrimitive PrimFloat e)    = "ReturnTuple(Box_Float(" ++ e ++ "))"
-useAsReturns (UnboxedPrimitive PrimBool e)   = "ReturnTuple(Box_Bool(" ++ e ++ "))"
-useAsReturns (UnboxedPrimitive PrimString e) = "ReturnTuple(Box_String(" ++ e ++ "))"
-useAsReturns (UnboxedPrimitive PrimChar e)   = "ReturnTuple(Box_Char(" ++ e ++ "))"
-useAsReturns (UnboxedPrimitive PrimInt e)    = "ReturnTuple(Box_Int(" ++ e ++ "))"
-useAsReturns (UnboxedPrimitive PrimFloat e)  = "ReturnTuple(Box_Float(" ++ e ++ "))"
-useAsReturns (LazySingle e)                  = useAsReturns $ getFromLazy e
+useAsReturns (OpaqueMulti e)                  = e
+useAsReturns (WrappedSingle e)                = "ReturnTuple(" ++ e ++ ")"
+useAsReturns (UnwrappedSingle e)              = "ReturnTuple(" ++ e ++ ")"
+useAsReturns (BoxedPrimitive PrimBool e)      = "ReturnTuple(Box_Bool(" ++ e ++ "))"
+useAsReturns (BoxedPrimitive PrimString e)    = "ReturnTuple(Box_String(" ++ e ++ "))"
+useAsReturns (BoxedPrimitive PrimChar e)      = "ReturnTuple(Box_Char(" ++ e ++ "))"
+useAsReturns (BoxedPrimitive PrimInt e)       = "ReturnTuple(Box_Int(" ++ e ++ "))"
+useAsReturns (BoxedPrimitive PrimFloat e)     = "ReturnTuple(Box_Float(" ++ e ++ "))"
+useAsReturns (BoxedPrimitive PrimPointer e)   = "ReturnTuple(Box_Pointer<void>(" ++ e ++ "))"
+useAsReturns (UnboxedPrimitive PrimBool e)    = "ReturnTuple(Box_Bool(" ++ e ++ "))"
+useAsReturns (UnboxedPrimitive PrimString e)  = "ReturnTuple(Box_String(" ++ e ++ "))"
+useAsReturns (UnboxedPrimitive PrimChar e)    = "ReturnTuple(Box_Char(" ++ e ++ "))"
+useAsReturns (UnboxedPrimitive PrimInt e)     = "ReturnTuple(Box_Int(" ++ e ++ "))"
+useAsReturns (UnboxedPrimitive PrimFloat e)   = "ReturnTuple(Box_Float(" ++ e ++ "))"
+useAsReturns (UnboxedPrimitive PrimPointer e) = "ReturnTuple(Box_Pointer<void>(" ++ e ++ "))"
+useAsReturns (LazySingle e)                   = useAsReturns $ getFromLazy e
 
 useAsArgs :: ExpressionValue -> String
-useAsArgs (OpaqueMulti e)                 = 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
+useAsArgs (OpaqueMulti e)                  = 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 (BoxedPrimitive PrimPointer e)   = "Box_Pointer<void>(" ++ 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 (UnboxedPrimitive PrimPointer e) = "Box_Pointer<void>(" ++ e ++ ")"
+useAsArgs (LazySingle e)                   = useAsArgs $ getFromLazy e
 
 useAsUnwrapped :: ExpressionValue -> String
-useAsUnwrapped (OpaqueMulti e)                 = "(" ++ e ++ ").At(0)"
-useAsUnwrapped (WrappedSingle e)               = e
-useAsUnwrapped (UnwrappedSingle e)             = e
-useAsUnwrapped (BoxedPrimitive PrimBool e)     = "Box_Bool(" ++ e ++ ")"
-useAsUnwrapped (BoxedPrimitive PrimString e)   = "Box_String(" ++ e ++ ")"
-useAsUnwrapped (BoxedPrimitive PrimChar e)     = "Box_Char(" ++ e ++ ")"
-useAsUnwrapped (BoxedPrimitive PrimInt e)      = "Box_Int(" ++ e ++ ")"
-useAsUnwrapped (BoxedPrimitive PrimFloat e)    = "Box_Float(" ++ e ++ ")"
-useAsUnwrapped (UnboxedPrimitive PrimBool e)   = "Box_Bool(" ++ e ++ ")"
-useAsUnwrapped (UnboxedPrimitive PrimString e) = "Box_String(" ++ e ++ ")"
-useAsUnwrapped (UnboxedPrimitive PrimChar e)   = "Box_Char(" ++ e ++ ")"
-useAsUnwrapped (UnboxedPrimitive PrimInt e)    = "Box_Int(" ++ e ++ ")"
-useAsUnwrapped (UnboxedPrimitive PrimFloat e)  = "Box_Float(" ++ e ++ ")"
-useAsUnwrapped (LazySingle e)                  = useAsUnwrapped $ getFromLazy e
+useAsUnwrapped (OpaqueMulti e)                  = "(" ++ e ++ ").At(0)"
+useAsUnwrapped (WrappedSingle e)                = e
+useAsUnwrapped (UnwrappedSingle e)              = e
+useAsUnwrapped (BoxedPrimitive PrimBool e)      = "Box_Bool(" ++ e ++ ")"
+useAsUnwrapped (BoxedPrimitive PrimString e)    = "Box_String(" ++ e ++ ")"
+useAsUnwrapped (BoxedPrimitive PrimChar e)      = "Box_Char(" ++ e ++ ")"
+useAsUnwrapped (BoxedPrimitive PrimInt e)       = "Box_Int(" ++ e ++ ")"
+useAsUnwrapped (BoxedPrimitive PrimFloat e)     = "Box_Float(" ++ e ++ ")"
+useAsUnwrapped (BoxedPrimitive PrimPointer e)   = "Box_Pointer<void>(" ++ e ++ ")"
+useAsUnwrapped (UnboxedPrimitive PrimBool e)    = "Box_Bool(" ++ e ++ ")"
+useAsUnwrapped (UnboxedPrimitive PrimString e)  = "Box_String(" ++ e ++ ")"
+useAsUnwrapped (UnboxedPrimitive PrimChar e)    = "Box_Char(" ++ e ++ ")"
+useAsUnwrapped (UnboxedPrimitive PrimInt e)     = "Box_Int(" ++ e ++ ")"
+useAsUnwrapped (UnboxedPrimitive PrimFloat e)   = "Box_Float(" ++ e ++ ")"
+useAsUnwrapped (UnboxedPrimitive PrimPointer e) = "Box_Pointer<void>(" ++ e ++ ")"
+useAsUnwrapped (LazySingle e)                   = useAsUnwrapped $ getFromLazy e
 
 useAsUnboxed :: PrimitiveType -> ExpressionValue -> String
-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()"
-useAsUnboxed PrimInt    (WrappedSingle e)   = "(" ++ e ++ ").AsInt()"
-useAsUnboxed PrimFloat  (WrappedSingle e)   = "(" ++ e ++ ").AsFloat()"
-useAsUnboxed PrimBool   (UnwrappedSingle e) = "(" ++ e ++ ").AsBool()"
-useAsUnboxed PrimString (UnwrappedSingle e) = "(" ++ e ++ ").AsString()"
-useAsUnboxed PrimChar   (UnwrappedSingle e) = "(" ++ e ++ ").AsChar()"
-useAsUnboxed PrimInt    (UnwrappedSingle e) = "(" ++ e ++ ").AsInt()"
-useAsUnboxed PrimFloat  (UnwrappedSingle e) = "(" ++ e ++ ").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 PrimPointer (OpaqueMulti e)     = "(" ++ e ++ ").At(0).AsPointer<OpaqueObject>()"
+useAsUnboxed PrimBool    (WrappedSingle e)   = "(" ++ e ++ ").AsBool()"
+useAsUnboxed PrimString  (WrappedSingle e)   = "(" ++ e ++ ").AsString()"
+useAsUnboxed PrimChar    (WrappedSingle e)   = "(" ++ e ++ ").AsChar()"
+useAsUnboxed PrimInt     (WrappedSingle e)   = "(" ++ e ++ ").AsInt()"
+useAsUnboxed PrimFloat   (WrappedSingle e)   = "(" ++ e ++ ").AsFloat()"
+useAsUnboxed PrimPointer (WrappedSingle e)   = "(" ++ e ++ ").AsPointer<OpaqueObject>()"
+useAsUnboxed PrimBool    (UnwrappedSingle e) = "(" ++ e ++ ").AsBool()"
+useAsUnboxed PrimString  (UnwrappedSingle e) = "(" ++ e ++ ").AsString()"
+useAsUnboxed PrimChar    (UnwrappedSingle e) = "(" ++ e ++ ").AsChar()"
+useAsUnboxed PrimInt     (UnwrappedSingle e) = "(" ++ e ++ ").AsInt()"
+useAsUnboxed PrimFloat   (UnwrappedSingle e) = "(" ++ e ++ ").AsFloat()"
+useAsUnboxed PrimPointer (UnwrappedSingle e) = "(" ++ e ++ ").AsPointer<OpaqueObject>()"
 useAsUnboxed _ (BoxedPrimitive _ e)         = e
 useAsUnboxed _ (UnboxedPrimitive _ e)       = e
 useAsUnboxed t (LazySingle e)               = useAsUnboxed t $ getFromLazy e
 
 valueAsWrapped :: ExpressionValue -> ExpressionValue
-valueAsWrapped (UnwrappedSingle e)             = WrappedSingle e
-valueAsWrapped (BoxedPrimitive _ e)            = WrappedSingle e
-valueAsWrapped (UnboxedPrimitive PrimBool e)   = WrappedSingle $ "Box_Bool(" ++ e ++ ")"
-valueAsWrapped (UnboxedPrimitive PrimString e) = WrappedSingle $ "Box_String(" ++ e ++ ")"
-valueAsWrapped (UnboxedPrimitive PrimChar e)   = WrappedSingle $ "Box_Char(" ++ e ++ ")"
-valueAsWrapped (UnboxedPrimitive PrimInt e)    = WrappedSingle $ "Box_Int(" ++ e ++ ")"
-valueAsWrapped (UnboxedPrimitive PrimFloat e)  = WrappedSingle $ "Box_Float(" ++ e ++ ")"
-valueAsWrapped (LazySingle e)                  = valueAsWrapped $ getFromLazy e
-valueAsWrapped v                               = v
+valueAsWrapped (UnwrappedSingle e)              = WrappedSingle e
+valueAsWrapped (BoxedPrimitive _ e)             = WrappedSingle e
+valueAsWrapped (UnboxedPrimitive PrimBool e)    = WrappedSingle $ "Box_Bool(" ++ e ++ ")"
+valueAsWrapped (UnboxedPrimitive PrimString e)  = WrappedSingle $ "Box_String(" ++ e ++ ")"
+valueAsWrapped (UnboxedPrimitive PrimChar e)    = WrappedSingle $ "Box_Char(" ++ e ++ ")"
+valueAsWrapped (UnboxedPrimitive PrimInt e)     = WrappedSingle $ "Box_Int(" ++ e ++ ")"
+valueAsWrapped (UnboxedPrimitive PrimFloat e)   = WrappedSingle $ "Box_Float(" ++ e ++ ")"
+valueAsWrapped (UnboxedPrimitive PrimPointer e) = WrappedSingle $ "Box_Pointer<void>(" ++ e ++ ")"
+valueAsWrapped (LazySingle e)                   = valueAsWrapped $ getFromLazy e
+valueAsWrapped v                                = v
 
 valueAsUnwrapped :: ExpressionValue -> ExpressionValue
-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 ++ ")"
-valueAsUnwrapped (UnboxedPrimitive PrimChar e)   = UnwrappedSingle $ "Box_Char(" ++ e ++ ")"
-valueAsUnwrapped (UnboxedPrimitive PrimInt e)    = UnwrappedSingle $ "Box_Int(" ++ e ++ ")"
-valueAsUnwrapped (UnboxedPrimitive PrimFloat e)  = UnwrappedSingle $ "Box_Float(" ++ e ++ ")"
-valueAsUnwrapped (LazySingle e)                  = valueAsUnwrapped $ getFromLazy e
-valueAsUnwrapped v                               = v
+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 ++ ")"
+valueAsUnwrapped (UnboxedPrimitive PrimChar e)    = UnwrappedSingle $ "Box_Char(" ++ e ++ ")"
+valueAsUnwrapped (UnboxedPrimitive PrimInt e)     = UnwrappedSingle $ "Box_Int(" ++ e ++ ")"
+valueAsUnwrapped (UnboxedPrimitive PrimFloat e)   = UnwrappedSingle $ "Box_Float(" ++ e ++ ")"
+valueAsUnwrapped (UnboxedPrimitive PrimPointer e) = UnwrappedSingle $ "Box_Pointer<void>(" ++ e ++ ")"
+valueAsUnwrapped (LazySingle e)                   = valueAsUnwrapped $ getFromLazy e
+valueAsUnwrapped v                                = v
 
 variableStoredType :: ValueType -> String
 variableStoredType t
@@ -263,6 +277,7 @@
   | t == intRequiredValue    = "PrimInt"
   | t == floatRequiredValue  = "PrimFloat"
   | t == charRequiredValue   = "PrimChar"
+  | isPointerRequiredValue t = "PrimPointer"
   | isWeakValue t            = "WeakValue"
   | otherwise                = "BoxedValue"
 
@@ -275,24 +290,27 @@
   | t == intRequiredValue    = "PrimInt"
   | t == floatRequiredValue  = "PrimFloat"
   | t == charRequiredValue   = "PrimChar"
+  | isPointerRequiredValue t = "PrimPointer"
   | isWeakValue t            = "WeakValue&"
   | otherwise                = "BoxedValue&"
 
 readStoredVariable :: Bool -> ValueType -> String -> ExpressionValue
 readStoredVariable True t s = LazySingle $ readStoredVariable False t s
 readStoredVariable False t s
-  | t == boolRequiredValue   = UnboxedPrimitive PrimBool  s
-  | t == intRequiredValue    = UnboxedPrimitive PrimInt   s
-  | t == floatRequiredValue  = UnboxedPrimitive PrimFloat s
-  | t == charRequiredValue   = UnboxedPrimitive PrimChar  s
+  | t == boolRequiredValue   = UnboxedPrimitive PrimBool    s
+  | t == intRequiredValue    = UnboxedPrimitive PrimInt     s
+  | t == floatRequiredValue  = UnboxedPrimitive PrimFloat   s
+  | t == charRequiredValue   = UnboxedPrimitive PrimChar    s
+  | isPointerRequiredValue t = UnboxedPrimitive PrimPointer s
   | otherwise                = UnwrappedSingle s
 
 writeStoredVariable :: ValueType -> ExpressionValue -> String
 writeStoredVariable t e
-  | t == boolRequiredValue   = useAsUnboxed PrimBool  e
-  | t == intRequiredValue    = useAsUnboxed PrimInt   e
-  | t == floatRequiredValue  = useAsUnboxed PrimFloat e
-  | t == charRequiredValue   = useAsUnboxed PrimChar  e
+  | t == boolRequiredValue   = useAsUnboxed PrimBool    e
+  | t == intRequiredValue    = useAsUnboxed PrimInt     e
+  | t == floatRequiredValue  = useAsUnboxed PrimFloat   e
+  | t == charRequiredValue   = useAsUnboxed PrimChar    e
+  | isPointerRequiredValue t = useAsUnboxed PrimPointer e
   | otherwise                = useAsUnwrapped e
 
 functionLabelType :: ScopedFunction c -> String
diff --git a/src/CompilerCxx/Procedure.hs b/src/CompilerCxx/Procedure.hs
--- a/src/CompilerCxx/Procedure.hs
+++ b/src/CompilerCxx/Procedure.hs
@@ -1,5 +1,5 @@
 {- -----------------------------------------------------------------------------
-Copyright 2019-2021 Kevin P. Barry
+Copyright 2019-2022 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.
@@ -269,11 +269,24 @@
   fa <- csAllFilters
   lift $ (checkValueAssignment r fa t0 formattedRequiredValue) <??
     "In fail call at " ++ formatFullContext c
-  csSetJumpType c JumpFailCall
+  csSetJumpType c JumpImmediateExit
   maybeSetTrace c
   csWrite ["BUILTIN_FAIL(" ++ useAsUnwrapped e0 ++ ")"]
+compileStatement (ExitCall c e) = do
+  csAddRequired (Set.fromList [BuiltinInt])
+  e' <- compileExpression e
+  when (length (pValues $ fst e') /= 1) $
+    compilerErrorM $ "Expected single return in argument" ++ formatFullContextBrace c
+  let (Positional [t0],e0) = e'
+  r <- csResolver
+  fa <- csAllFilters
+  lift $ (checkValueAssignment r fa t0 intRequiredValue) <??
+    "In exit call at " ++ formatFullContext c
+  csSetJumpType c JumpImmediateExit
+  maybeSetTrace c
+  csWrite ["BUILTIN_EXIT(" ++ useAsUnboxed PrimInt e0 ++ ")"]
 compileStatement (RawFailCall s) = do
-  csSetJumpType [] JumpFailCall
+  csSetJumpType [] JumpImmediateExit
   csWrite ["RAW_FAIL(" ++ show s ++ ")"]
 compileStatement (IgnoreValues c e) = do
   (_,e') <- compileExpression e
diff --git a/src/Module/ProcessMetadata.hs b/src/Module/ProcessMetadata.hs
--- a/src/Module/ProcessMetadata.hs
+++ b/src/Module/ProcessMetadata.hs
@@ -1,5 +1,5 @@
 {- -----------------------------------------------------------------------------
-Copyright 2020-2021 Kevin P. Barry
+Copyright 2020-2022 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.
@@ -33,7 +33,6 @@
   getObjectFileResolver,
   getRealPathsForDeps,
   getRecompilePath,
-  getSourceFilesForDeps,
   isPathConfigured,
   isPathUpToDate,
   isPrivateSource,
@@ -60,6 +59,7 @@
 import Control.Monad (when)
 import Data.List (isSuffixOf,nub)
 import Data.Maybe (isJust)
+import Data.Time.Clock (UTCTime)
 import System.Directory
 import System.FilePath
 import System.IO
@@ -140,12 +140,12 @@
   m <- errorFromIO $ toTrackedErrors $ loadRecompile (p </> d)
   return $ not $ isCompilerError m
 
-writeMetadata :: FilePath -> CompileMetadata -> TrackedErrorsIO ()
-writeMetadata p m = do
+writeMetadata :: FilePath -> CompileMetadata -> UTCTime -> TrackedErrorsIO ()
+writeMetadata p m t = do
   p' <- errorFromIO $ canonicalizePath p
   errorFromIO $ hPutStrLn stderr $ "Writing metadata for \"" ++ p' ++ "\"."
   m' <- autoWriteConfig m <?? "In data for " ++ p
-  writeCachedFile p' "" metadataFilename m'
+  writeCachedFile p' "" metadataFilename (Just t) m'
 
 writeRecompile :: FilePath -> ModuleConfig -> TrackedErrorsIO ()
 writeRecompile p m = do
@@ -158,7 +158,7 @@
 writePossibleTraces :: FilePath -> Set.Set String -> TrackedErrorsIO ()
 writePossibleTraces p ts = do
   p' <- errorFromIO $ canonicalizePath p
-  writeCachedFile p' "" tracesFilename $ concat $ map (++"\n") $ Set.toList ts
+  writeCachedFile p' "" tracesFilename Nothing $ concat $ map (++"\n") $ Set.toList ts
 
 readPossibleTraces :: FilePath -> TrackedErrorsIO (Set.Set String)
 readPossibleTraces p = do
@@ -190,11 +190,15 @@
   errorFromIO $ createDirectoryIfMissing False d2
   return d2
 
-writeCachedFile :: FilePath -> String -> FilePath -> String -> TrackedErrorsIO ()
-writeCachedFile p ns f c = do
+writeCachedFile :: FilePath -> String -> FilePath -> Maybe UTCTime -> String -> TrackedErrorsIO ()
+writeCachedFile p ns f t c = do
   createCachePath p
   errorFromIO $ createDirectoryIfMissing False $ p </> cachedDataPath </> ns
-  errorFromIO $ writeFile (getCachedPath p ns f) c
+  let filename = getCachedPath p ns f
+  errorFromIO $ writeFile filename c
+  case t of
+       Just t2 -> errorFromIO $ setModificationTime filename t2
+       Nothing -> return ()
 
 getCachedPath :: FilePath -> String -> FilePath -> FilePath
 getCachedPath p ns f = fixPath $ p </> cachedDataPath </> ns </> f
@@ -225,9 +229,9 @@
 getRealPathsForDeps :: [CompileMetadata] -> [FilePath]
 getRealPathsForDeps = map cmPath
 
-getSourceFilesForDeps :: [CompileMetadata] -> [FilePath]
-getSourceFilesForDeps = concat . map extract where
-  extract m = map (cmRoot m </>) (filter isPublicSource $ cmPublicFiles m ++ cmPrivateFiles m)
+getSourceFilesForDeps :: [CompileMetadata] -> [(FilePath,[FilePath])]
+getSourceFilesForDeps = map extract where
+  extract m = (cmRoot m,filter isPublicSource $ cmPublicFiles m ++ cmPrivateFiles m)
 
 getNamespacesForDeps :: [CompileMetadata] -> [Namespace]
 getNamespacesForDeps = filter (not . isNoNamespace) . map cmPublicNamespace
@@ -460,7 +464,7 @@
   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,xa) <- loadAllPublic (ns0,ns1) fs
+  (cs2,xa) <- loadAllPublic (ns0,ns1) [(p,fs)]
   cs3 <- case m of
               Just m2 -> processDeps True [FromDependency] m2
               _       -> return []
@@ -475,7 +479,7 @@
                    else cs
       return $ map (updateCodeVisibility (Set.union (Set.fromList ss))) cs'
     loadAllPublic (ns2,ns3) fs2 = do
-      fs2' <- zipWithContents r p fs2
+      fs2' <- fmap concat $ mapCompilerM (uncurry $ zipWithContents r) fs2
       loaded <- mapCompilerM loadPublic fs2'
       return (concat $ map fst loaded,Set.fromList $ concat $ map snd loaded)
       where
diff --git a/src/Parser/Common.hs b/src/Parser/Common.hs
--- a/src/Parser/Common.hs
+++ b/src/Parser/Common.hs
@@ -1,5 +1,5 @@
 {- -----------------------------------------------------------------------------
-Copyright 2019-2021 Kevin P. Barry
+Copyright 2019-2022 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.
@@ -46,6 +46,7 @@
   kwElif,
   kwElse,
   kwEmpty,
+  kwExit,
   kwFail,
   kwFalse,
   kwIf,
@@ -217,6 +218,9 @@
 kwEmpty :: TextParser ()
 kwEmpty = keyword "empty"
 
+kwExit :: TextParser ()
+kwExit = keyword "exit"
+
 kwFail :: TextParser ()
 kwFail = keyword "fail"
 
@@ -329,6 +333,7 @@
     kwElif,
     kwElse,
     kwEmpty,
+    kwExit,
     kwFail,
     kwFalse,
     kwIf,
diff --git a/src/Parser/Procedure.hs b/src/Parser/Procedure.hs
--- a/src/Parser/Procedure.hs
+++ b/src/Parser/Procedure.hs
@@ -1,5 +1,5 @@
 {- -----------------------------------------------------------------------------
-Copyright 2019-2021 Kevin P. Barry
+Copyright 2019-2022 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.
@@ -131,6 +131,7 @@
                  parseBreak <|>
                  parseContinue <|>
                  parseFailCall <|>
+                 parseExitCall <|>
                  parseVoid <|>
                  parseAssign <|>
                  parsePragma <|>
@@ -161,6 +162,11 @@
       kwFail
       e <- between (sepAfter $ string_ "(") (sepAfter $ string_ ")") sourceParser
       return $ FailCall [c] e
+    parseExitCall = do
+      c <- getSourceContext
+      kwExit
+      e <- between (sepAfter $ string_ "(") (sepAfter $ string_ ")") sourceParser
+      return $ ExitCall [c] e
     parseIgnore = do
       c <- getSourceContext
       statementStart
diff --git a/src/Parser/TypeInstance.hs b/src/Parser/TypeInstance.hs
--- a/src/Parser/TypeInstance.hs
+++ b/src/Parser/TypeInstance.hs
@@ -1,5 +1,5 @@
 {- -----------------------------------------------------------------------------
-Copyright 2019-2020 Kevin P. Barry
+Copyright 2019-2020,2022 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.
@@ -81,6 +81,7 @@
         | n == "Int"        = BuiltinInt
         | n == "Float"      = BuiltinFloat
         | n == "String"     = BuiltinString
+        | n == "Pointer"    = BuiltinPointer
         | n == "Formatted"  = BuiltinFormatted
         | otherwise = CategoryName n
 
diff --git a/src/Test/Procedure.hs b/src/Test/Procedure.hs
--- a/src/Test/Procedure.hs
+++ b/src/Test/Procedure.hs
@@ -1,5 +1,5 @@
 {- -----------------------------------------------------------------------------
-Copyright 2019-2021 Kevin P. Barry
+Copyright 2019-2022 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.
@@ -144,6 +144,10 @@
     checkShortParseSuccess "if (((var.func())?T.call())) { }",
     checkShortParseSuccess "fail(\"reason\")",
     checkShortParseFail "\\ fail(\"reason\")",
+    checkShortParseFail "\\ fail()",
+    checkShortParseSuccess "exit(1)",
+    checkShortParseFail "\\ exit(1)",
+    checkShortParseFail "\\ exit()",
     checkShortParseSuccess "failed <- 10",
 
     checkShortParseSuccess "\\var?T<#x>.func().func2().func3()",
diff --git a/src/Types/Builtin.hs b/src/Types/Builtin.hs
--- a/src/Types/Builtin.hs
+++ b/src/Types/Builtin.hs
@@ -1,5 +1,5 @@
 {- -----------------------------------------------------------------------------
-Copyright 2019-2021 Kevin P. Barry
+Copyright 2019-2022 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.
@@ -28,6 +28,7 @@
   floatRequiredValue,
   formattedRequiredValue,
   intRequiredValue,
+  isPointerRequiredValue,
   isOpaqueMulti,
   orderOptionalValue,
   stringRequiredValue,
@@ -36,6 +37,7 @@
 import qualified Data.Map as Map
 
 import Base.GeneralType
+import Base.MergeTree (reduceMergeTree)
 import Base.Positional
 import Types.TypeCategory
 import Types.TypeInstance
@@ -59,6 +61,13 @@
 orderOptionalValue :: GeneralInstance -> ValueType
 orderOptionalValue t = ValueType OptionalValue $ singleType $ JustTypeInstance $ TypeInstance BuiltinOrder (Positional [t])
 
+isPointerRequiredValue :: ValueType -> Bool
+isPointerRequiredValue (ValueType RequiredValue t) = check $ extractSingle t where
+  check (Just (JustTypeInstance (TypeInstance BuiltinPointer (Positional [_])))) = True
+  check _ = False
+  extractSingle = reduceMergeTree (const Nothing) (const Nothing) Just
+isPointerRequiredValue _ = False
+
 emptyType :: ValueType
 emptyType = ValueType OptionalValue minBound
 
@@ -67,7 +76,8 @@
   PrimChar |
   PrimInt |
   PrimFloat |
-  PrimString
+  PrimString |
+  PrimPointer
   deriving (Eq,Show)
 
 data ExpressionValue =
diff --git a/src/Types/Procedure.hs b/src/Types/Procedure.hs
--- a/src/Types/Procedure.hs
+++ b/src/Types/Procedure.hs
@@ -1,5 +1,5 @@
 {- -----------------------------------------------------------------------------
-Copyright 2019-2021 Kevin P. Barry
+Copyright 2019-2022 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.
@@ -176,6 +176,7 @@
   LoopBreak [c] |
   LoopContinue [c] |
   FailCall [c] (Expression c) |
+  ExitCall [c] (Expression c) |
   RawFailCall String |
   IgnoreValues [c] (Expression c) |
   Assignment [c] (Positional (Assignable c)) (Expression c) |
@@ -198,6 +199,7 @@
 getStatementContext (LoopBreak c)           = c
 getStatementContext (LoopContinue c)        = c
 getStatementContext (FailCall c _)          = c
+getStatementContext (ExitCall c _)          = c
 getStatementContext (RawFailCall _)         = []
 getStatementContext (IgnoreValues c _)      = c
 getStatementContext (Assignment c _ _)      = c
diff --git a/src/Types/TypeInstance.hs b/src/Types/TypeInstance.hs
--- a/src/Types/TypeInstance.hs
+++ b/src/Types/TypeInstance.hs
@@ -1,5 +1,5 @@
 {- -----------------------------------------------------------------------------
-Copyright 2019-2021 Kevin P. Barry
+Copyright 2019-2022 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.
@@ -140,6 +140,7 @@
   BuiltinInt |
   BuiltinFloat |
   BuiltinString |
+  BuiltinPointer |
   BuiltinFormatted |
   BuiltinOrder |
   CategoryNone
@@ -152,6 +153,7 @@
   show BuiltinInt          = "Int"
   show BuiltinFloat        = "Float"
   show BuiltinString       = "String"
+  show BuiltinPointer      = "Pointer"
   show BuiltinFormatted    = "Formatted"
   show BuiltinOrder        = "Order"
   show CategoryNone        = "(none)"
diff --git a/tests/cli-tests.sh b/tests/cli-tests.sh
--- a/tests/cli-tests.sh
+++ b/tests/cli-tests.sh
@@ -1,7 +1,7 @@
 #!/usr/bin/env bash
 
 # ------------------------------------------------------------------------------
-# Copyright 2020-2021 Kevin P. Barry
+# Copyright 2020-2022 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.
@@ -29,7 +29,7 @@
 
 ZEOLITE=("$@")
 
-if [[ "$PARALLEL" ]]; then
+if [[ "${PARALLEL-}" ]]; then
   PARALLEL="-j $PARALLEL"
 else
   PARALLEL='-j 1'
@@ -378,6 +378,12 @@
 }
 
 
+test_pointer() {
+  do_zeolite -p "$ZEOLITE_PATH" $PARALLEL -r tests/pointer -f
+  do_zeolite -p "$ZEOLITE_PATH" -t tests/pointer
+}
+
+
 test_self_offset() {
   do_zeolite -p "$ZEOLITE_PATH" $PARALLEL -r tests/self-offset -f
   do_zeolite -p "$ZEOLITE_PATH" -t tests/self-offset
@@ -573,6 +579,7 @@
   test_module_only2
   test_module_only3
   test_module_only4
+  test_pointer
   test_self_offset
   test_show_deps
   test_simulate_refs
diff --git a/tests/failure.0rt b/tests/failure.0rt
deleted file mode 100644
--- a/tests/failure.0rt
+++ /dev/null
@@ -1,69 +0,0 @@
-/* -----------------------------------------------------------------------------
-Copyright 2020 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 "fail builtin" {
-  crash
-  require stderr "Failed"
-  require stderr "failedReturn"
-}
-
-unittest test {
-  Int value <- Test:failedReturn()
-}
-
-concrete Test {
-  @category failedReturn () -> (Int)
-}
-
-define Test {
-  failedReturn () {
-    fail("Failed")
-  }
-}
-
-
-
-testcase "wrong type for fail" {
-  error
-  require "fail"
-  require "Formatted"
-}
-
-unittest test {
-  fail(Value.create())
-}
-
-concrete Value {
-  @type create () -> (Value)
-}
-
-define Value {
-  create () {
-    return Value{}
-  }
-}
-
-
-testcase "require empty" {
-  crash
-  require stderr "require.+empty"
-}
-
-unittest test {
-  \ require(empty)
-}
diff --git a/tests/pointer/.zeolite-module b/tests/pointer/.zeolite-module
new file mode 100644
--- /dev/null
+++ b/tests/pointer/.zeolite-module
@@ -0,0 +1,21 @@
+root: "../.."
+path: "tests/pointer"
+private_deps: [
+  "lib/testing"
+]
+extra_files: [
+  category_source {
+    source: "tests/pointer/Extension_Request.cpp"
+    categories: [Request]
+  }
+  category_source {
+    source: "tests/pointer/Extension_Response.cpp"
+    categories: [Response]
+  }
+  category_source {
+    source: "tests/pointer/Extension_Service.cpp"
+    categories: [Service]
+  }
+  "tests/pointer/call.hpp"
+]
+mode: incremental {}
diff --git a/tests/pointer/Extension_Request.cpp b/tests/pointer/Extension_Request.cpp
new file mode 100644
--- /dev/null
+++ b/tests/pointer/Extension_Request.cpp
@@ -0,0 +1,92 @@
+/* -----------------------------------------------------------------------------
+Copyright 2022 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 "category-source.hpp"
+#include "Streamlined_Request.hpp"
+#include "Category_Message.hpp"
+#include "Category_Pointer.hpp"
+#include "Category_Request.hpp"
+#include "Category_String.hpp"
+
+#include "call.hpp"
+
+#ifdef ZEOLITE_PRIVATE_NAMESPACE
+namespace ZEOLITE_PRIVATE_NAMESPACE {
+#endif  // ZEOLITE_PRIVATE_NAMESPACE
+
+BoxedValue CreateValue_Request(S<const Type_Request> parent, const ParamsArgs& params_args);
+
+struct ExtCategory_Request : public Category_Request {
+};
+
+struct ExtType_Request : public Type_Request {
+  inline ExtType_Request(Category_Request& p, Params<0>::Type params) : Type_Request(p, params) {}
+
+  ReturnTuple Call_create(const ParamsArgs& params_args) const final {
+    TRACE_FUNCTION("Request.create")
+    return ReturnTuple(CreateValue_Request(PARAM_SELF, params_args));
+  }
+
+  ReturnTuple Call_getData(const ParamsArgs& params_args) const final {
+    TRACE_FUNCTION("Request.getData")
+    return ReturnTuple(Box_String(params_args.GetArg(0).AsPointer<Request>()->request_data));
+  }
+};
+
+struct ExtValue_Request : public Value_Request {
+  inline ExtValue_Request(S<const Type_Request> p, const ParamsArgs& params_args)
+    : Value_Request(std::move(p)),
+      request_{params_args.GetArg(0).AsString()} {}
+
+  ReturnTuple Call_asMessage(const ParamsArgs& params_args) final {
+    TRACE_FUNCTION("Request.asMessage")
+    return ReturnTuple(Box_Pointer<Message>(&request_));
+  }
+
+  ReturnTuple Call_asRequest(const ParamsArgs& params_args) final {
+    TRACE_FUNCTION("Request.asRequest")
+    return ReturnTuple(Box_Pointer<Request>(&request_));
+  }
+
+  ReturnTuple Call_formatted(const ParamsArgs& params_args) final {
+    TRACE_FUNCTION("Request.formatted")
+    return ReturnTuple(Box_String(request_.request_data));
+  }
+
+  Request request_;
+};
+
+Category_Request& CreateCategory_Request() {
+  static auto& category = *new ExtCategory_Request();
+  return category;
+}
+
+S<const Type_Request> CreateType_Request(const Params<0>::Type& params) {
+  static const auto cached = S_get(new ExtType_Request(CreateCategory_Request(), Params<0>::Type()));
+  return cached;
+}
+
+void RemoveType_Request(const Params<0>::Type& params) {}
+BoxedValue CreateValue_Request(S<const Type_Request> parent, const ParamsArgs& params_args) {
+  return BoxedValue::New<ExtValue_Request>(std::move(parent), params_args);
+}
+
+#ifdef ZEOLITE_PRIVATE_NAMESPACE
+}  // namespace ZEOLITE_PRIVATE_NAMESPACE
+using namespace ZEOLITE_PRIVATE_NAMESPACE;
+#endif  // ZEOLITE_PRIVATE_NAMESPACE
diff --git a/tests/pointer/Extension_Response.cpp b/tests/pointer/Extension_Response.cpp
new file mode 100644
--- /dev/null
+++ b/tests/pointer/Extension_Response.cpp
@@ -0,0 +1,92 @@
+/* -----------------------------------------------------------------------------
+Copyright 2022 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 "category-source.hpp"
+#include "Streamlined_Response.hpp"
+#include "Category_Message.hpp"
+#include "Category_Pointer.hpp"
+#include "Category_Response.hpp"
+#include "Category_String.hpp"
+
+#include "call.hpp"
+
+#ifdef ZEOLITE_PRIVATE_NAMESPACE
+namespace ZEOLITE_PRIVATE_NAMESPACE {
+#endif  // ZEOLITE_PRIVATE_NAMESPACE
+
+BoxedValue CreateValue_Response(S<const Type_Response> parent, const ParamsArgs& params_args);
+
+struct ExtCategory_Response : public Category_Response {
+};
+
+struct ExtType_Response : public Type_Response {
+  inline ExtType_Response(Category_Response& p, Params<0>::Type params) : Type_Response(p, params) {}
+
+  ReturnTuple Call_create(const ParamsArgs& params_args) const final {
+    TRACE_FUNCTION("Response.create")
+    return ReturnTuple(CreateValue_Response(PARAM_SELF, params_args));
+  }
+
+  ReturnTuple Call_getData(const ParamsArgs& params_args) const final {
+    TRACE_FUNCTION("Response.getData")
+    return ReturnTuple(Box_String(params_args.GetArg(0).AsPointer<Response>()->response_data));
+  }
+};
+
+struct ExtValue_Response : public Value_Response {
+  inline ExtValue_Response(S<const Type_Response> p, const ParamsArgs& params_args)
+    : Value_Response(std::move(p)),
+      response_{params_args.GetArg(0).AsString()} {}
+
+  ReturnTuple Call_asMessage(const ParamsArgs& params_args) final {
+    TRACE_FUNCTION("Response.asMessage")
+    return ReturnTuple(Box_Pointer<Message>(&response_));
+  }
+
+  ReturnTuple Call_asResponse(const ParamsArgs& params_args) final {
+    TRACE_FUNCTION("Response.asResponse")
+    return ReturnTuple(Box_Pointer<Response>(&response_));
+  }
+
+  ReturnTuple Call_formatted(const ParamsArgs& params_args) final {
+    TRACE_FUNCTION("Response.formatted")
+    return ReturnTuple(Box_String(response_.response_data));
+  }
+
+  Response response_;
+};
+
+Category_Response& CreateCategory_Response() {
+  static auto& category = *new ExtCategory_Response();
+  return category;
+}
+
+S<const Type_Response> CreateType_Response(const Params<0>::Type& params) {
+  static const auto cached = S_get(new ExtType_Response(CreateCategory_Response(), Params<0>::Type()));
+  return cached;
+}
+
+void RemoveType_Response(const Params<0>::Type& params) {}
+BoxedValue CreateValue_Response(S<const Type_Response> parent, const ParamsArgs& params_args) {
+  return BoxedValue::New<ExtValue_Response>(std::move(parent), params_args);
+}
+
+#ifdef ZEOLITE_PRIVATE_NAMESPACE
+}  // namespace ZEOLITE_PRIVATE_NAMESPACE
+using namespace ZEOLITE_PRIVATE_NAMESPACE;
+#endif  // ZEOLITE_PRIVATE_NAMESPACE
diff --git a/tests/pointer/Extension_Service.cpp b/tests/pointer/Extension_Service.cpp
new file mode 100644
--- /dev/null
+++ b/tests/pointer/Extension_Service.cpp
@@ -0,0 +1,82 @@
+/* -----------------------------------------------------------------------------
+Copyright 2022 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 "category-source.hpp"
+#include "Streamlined_Service.hpp"
+#include "Category_Request.hpp"
+#include "Category_Response.hpp"
+#include "Category_Service.hpp"
+
+#include "call.hpp"
+
+#ifdef ZEOLITE_PRIVATE_NAMESPACE
+namespace ZEOLITE_PRIVATE_NAMESPACE {
+#endif  // ZEOLITE_PRIVATE_NAMESPACE
+
+BoxedValue CreateValue_Service(S<const Type_Service> parent, const ParamsArgs& params_args);
+
+struct ExtCategory_Service : public Category_Service {
+};
+
+struct ExtType_Service : public Type_Service {
+  inline ExtType_Service(Category_Service& p, Params<0>::Type params) : Type_Service(p, params) {}
+
+  ReturnTuple Call_send(const ParamsArgs& params_args) const final {
+    TRACE_FUNCTION("Service.send")
+    const Request* const request = params_args.GetArg(0).AsPointer<Request>();
+    Response* const response = params_args.GetArg(1).AsPointer<Response>();
+    if (!request) FAIL() << "Invalid Pointer<Request>";
+    if (!response) FAIL() << "Invalid Pointer<Response>";
+    response->response_data = request->request_data + " has been processed as Request";
+    return ReturnTuple();
+  }
+
+  ReturnTuple Call_send2(const ParamsArgs& params_args) const final {
+    TRACE_FUNCTION("Service.send2")
+    const Message* const request = params_args.GetArg(0).AsPointer<Message>();
+    Response* const response = params_args.GetArg(1).AsPointer<Response>();
+    if (!request) FAIL() << "Invalid Pointer<Message>";
+    if (!response) FAIL() << "Invalid Pointer<Response>";
+    response->response_data = request->message_data + " has been processed as Message";
+    return ReturnTuple();
+  }
+};
+
+struct ExtValue_Service : public Value_Service {
+  inline ExtValue_Service(S<const Type_Service> p, const ParamsArgs& params_args) : Value_Service(std::move(p)) {}
+};
+
+Category_Service& CreateCategory_Service() {
+  static auto& category = *new ExtCategory_Service();
+  return category;
+}
+
+S<const Type_Service> CreateType_Service(const Params<0>::Type& params) {
+  static const auto cached = S_get(new ExtType_Service(CreateCategory_Service(), Params<0>::Type()));
+  return cached;
+}
+
+void RemoveType_Service(const Params<0>::Type& params) {}
+BoxedValue CreateValue_Service(S<const Type_Service> parent, const ParamsArgs& params_args) {
+  return BoxedValue::New<ExtValue_Service>(std::move(parent), params_args);
+}
+
+#ifdef ZEOLITE_PRIVATE_NAMESPACE
+}  // namespace ZEOLITE_PRIVATE_NAMESPACE
+using namespace ZEOLITE_PRIVATE_NAMESPACE;
+#endif  // ZEOLITE_PRIVATE_NAMESPACE
diff --git a/tests/pointer/README.md b/tests/pointer/README.md
new file mode 100644
--- /dev/null
+++ b/tests/pointer/README.md
@@ -0,0 +1,11 @@
+# C++ Pointer Wrapping Test
+
+This module tests `Pointer<#x>` in C++ extensions.
+
+To compile and execute:
+
+```shell
+ZEOLITE_PATH=$(zeolite --get-path)
+zeolite -p $ZEOLITE_PATH -r tests/pointer
+zeolite -p $ZEOLITE_PATH -t tests/pointer
+```
diff --git a/tests/pointer/call.0rp b/tests/pointer/call.0rp
new file mode 100644
--- /dev/null
+++ b/tests/pointer/call.0rp
@@ -0,0 +1,50 @@
+/* -----------------------------------------------------------------------------
+Copyright 2022 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]
+
+$ModuleOnly$
+$TestsOnly$
+
+@value interface Message {
+  asMessage () -> (Pointer<Message>)
+}
+
+// NOTE: Inheriting Message doesn't matter for the purposes of Pointer; it's
+// just an arbitrary substitution for type checking.
+
+concrete Request {
+  refines Formatted
+  refines Message
+
+  @type create (String) -> (#self)
+  @type getData (Pointer<Request>) -> (String)
+  @value asRequest () -> (Pointer<Request>)
+}
+
+concrete Response {
+  refines Formatted
+  refines Message
+
+  @type create (String) -> (#self)
+  @type getData (Pointer<Response>) -> (String)
+  @value asResponse () -> (Pointer<Response>)
+}
+
+concrete Service {
+  @type send (Pointer<Request>,Pointer<Response>) -> ()
+  @type send2 (Pointer<Message>,Pointer<Response>) -> ()
+}
diff --git a/tests/pointer/call.0rt b/tests/pointer/call.0rt
new file mode 100644
--- /dev/null
+++ b/tests/pointer/call.0rt
@@ -0,0 +1,100 @@
+/* -----------------------------------------------------------------------------
+Copyright 2022 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 "Pointer passing" {
+  success
+}
+
+concrete CallHelper {
+  @type call (String) -> (String)
+  @type call2 (String) -> (String)
+}
+
+define CallHelper {
+  call (data) {
+    Request request <- Request.create(data)
+    Response response <- Response.create("")
+    // This is just meant to test code generated for passing Pointer as a
+    // function arg or return.
+    return Response.getData(callWithPointers(request.asRequest(),response.asResponse()))
+  }
+
+  call2 (data) {
+    Request request <- Request.create(data)
+    Response response <- Response.create("")
+    \ Service.send2(request.asMessage(),response.asResponse())
+    return response.formatted()
+  }
+
+  @type callWithPointers (Pointer<Request>,Pointer<Response>) -> (Pointer<Response>)
+  callWithPointers (request,response) {
+    \ Service.send(request,response)
+    return response
+  }
+}
+
+unittest test {
+  \ CallHelper.call("DATA") `Testing.checkEquals` "DATA has been processed as Request"
+  \ CallHelper.call2("DATA") `Testing.checkEquals` "REQUEST has been processed as Message"
+}
+
+
+testcase "Pointer storage" {
+  success
+}
+
+concrete Helper {
+  @type new () -> (Helper)
+  @value call () -> ()
+}
+
+define Helper {
+  @category Request request <- Request.create("request")
+  @category Pointer<Request> requestPointer <- request.asRequest()
+  @value Response response
+  @value Pointer<Response> responsePointer
+
+  new () {
+    Response response <- Response.create("response")
+    return #self{ response, response.asResponse() }
+  }
+
+  call () {
+    \ Request.getData(requestPointer) `Testing.checkEquals` "request"
+    \ Response.getData(responsePointer) `Testing.checkEquals` "response"
+
+    Pointer<Request> localRequest <- requestPointer
+    Pointer<Response> localResponse <- responsePointer
+    \ Request.getData(localRequest) `Testing.checkEquals` "request"
+    \ Response.getData(localResponse) `Testing.checkEquals` "response"
+
+    localRequest <- request.asRequest()
+    localResponse <- response.asResponse()
+    \ Request.getData(localRequest) `Testing.checkEquals` "request"
+    \ Response.getData(localResponse) `Testing.checkEquals` "response"
+
+    requestPointer <- localRequest
+    responsePointer <- localResponse
+    \ Request.getData(requestPointer) `Testing.checkEquals` "request"
+    \ Response.getData(responsePointer) `Testing.checkEquals` "response"
+  }
+}
+
+unittest test {
+  \ Helper.new().call()
+}
diff --git a/tests/pointer/call.hpp b/tests/pointer/call.hpp
new file mode 100644
--- /dev/null
+++ b/tests/pointer/call.hpp
@@ -0,0 +1,53 @@
+/* -----------------------------------------------------------------------------
+Copyright 2022 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 CALL_H_
+#define CALL_H_
+
+#include <string>
+
+struct Message {
+  std::string message_data;
+};
+
+struct OtherBase {
+  std::string other_base_data;
+};
+
+// NOTE: The inheritance of Message here doesn't need to match the inheritance
+// in call.0rp because inheritance doesn't matter for Pointer.
+
+// The virtual base class is meant to change the offset.
+struct Request : public OtherBase, virtual public Message {
+  explicit inline Request(std::string data)
+    : Message{ "REQUEST" },
+      request_data(std::move(data)) {}
+
+  std::string request_data;
+};
+
+// The virtual base class is meant to change the offset.
+struct Response : public OtherBase, virtual public Message {
+  explicit inline Response(std::string data)
+    : Message{ "RESPONSE" },
+      response_data(std::move(data)) {}
+
+  std::string response_data;
+};
+
+#endif  // CALL_H_
diff --git a/tests/program-exit.0rt b/tests/program-exit.0rt
new file mode 100644
--- /dev/null
+++ b/tests/program-exit.0rt
@@ -0,0 +1,121 @@
+/* -----------------------------------------------------------------------------
+Copyright 2020,2022 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 "fail builtin" {
+  crash
+  require stderr "Failed"
+  require stderr "failedReturn"
+}
+
+unittest test {
+  Int value <- Test:failedReturn()
+}
+
+concrete Test {
+  @category failedReturn () -> (Int)
+}
+
+define Test {
+  failedReturn () {
+    // Calling within a function also validates that return checks are skipped.
+    fail("Failed")
+  }
+}
+
+
+testcase "wrong type for fail" {
+  error
+  require "fail"
+  require "Formatted"
+}
+
+unittest test {
+  fail(Value.create())
+}
+
+concrete Value {
+  @type create () -> (Value)
+}
+
+define Value {
+  create () {
+    return Value{}
+  }
+}
+
+
+testcase "exit builtin zero" {
+  success
+}
+
+unittest test {
+  Int value <- Test:exitReturn()
+}
+
+concrete Test {
+  @category exitReturn () -> (Int)
+}
+
+define Test {
+  exitReturn () {
+    // Calling within a function also validates that return checks are skipped.
+    exit(0)
+  }
+}
+
+
+testcase "exit builtin non-zero" {
+  crash
+  // Should not output a stack trace.
+  exclude "exitReturn"
+}
+
+unittest test {
+  Int value <- Test:exitReturn()
+}
+
+concrete Test {
+  @category exitReturn () -> (Int)
+}
+
+define Test {
+  exitReturn () {
+    exit(1)
+  }
+}
+
+
+testcase "wrong type for exit" {
+  error
+  require "exit"
+  require "Int"
+}
+
+unittest test {
+  exit("message")
+}
+
+
+testcase "require empty" {
+  crash
+  require stderr "require.+empty"
+}
+
+unittest test {
+  \ require(empty)
+}
diff --git a/tests/simple.0rt b/tests/simple.0rt
--- a/tests/simple.0rt
+++ b/tests/simple.0rt
@@ -1,5 +1,5 @@
 /* -----------------------------------------------------------------------------
-Copyright 2020 Kevin P. Barry
+Copyright 2020,2022 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.
@@ -135,3 +135,13 @@
 unittest test {
   while (true) {}
 }
+
+
+testcase "args escaped properly" {
+  success
+  // If the compiler uses quotes without escaping individual characters then
+  // main will exit with an error without executing the test.
+  args "\" }; exit(1); const char fake[] = { \""
+}
+
+unittest test {}
diff --git a/tests/simulate-refs/.zeolite-module b/tests/simulate-refs/.zeolite-module
--- a/tests/simulate-refs/.zeolite-module
+++ b/tests/simulate-refs/.zeolite-module
@@ -9,7 +9,6 @@
 private_deps: [
   "lib/container"
   "lib/math"
-  "lib/thread"
   "lib/util"
 ]
 mode: binary {
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.22.1.0
+version:             0.23.0.0
 synopsis:            Zeolite is a statically-typed, general-purpose programming language.
 
 description:
@@ -59,7 +59,7 @@
 license-file:        LICENSE
 author:              Kevin P. Barry
 maintainer:          Kevin P. Barry <ta0kira@gmail.com>
-copyright:           (c) Kevin P. Barry 2019-2021
+copyright:           (c) Kevin P. Barry 2019-2022
 category:            Compiler
 build-type:          Simple
 
@@ -174,6 +174,12 @@
                      tests/module-only4/*.0rp,
                      tests/module-only4/*.0rt,
                      tests/module-only4/*.0rx,
+                     tests/pointer/README.md,
+                     tests/pointer/.zeolite-module,
+                     tests/pointer/*.cpp,
+                     tests/pointer/*.hpp,
+                     tests/pointer/*.0rp,
+                     tests/pointer/*.0rt,
                      tests/self-offset/README.md,
                      tests/self-offset/.zeolite-module,
                      tests/self-offset/*.cpp,
