Bladeren bron

feat: re-arrange Random and add Tape, ThreadSafeDevice

Sam Jaffe 2 jaren geleden
bovenliggende
commit
8367dc9489

+ 17 - 0
include/random/device.h

@@ -0,0 +1,17 @@
+//
+//  random_impl.h
+//  pokemon
+//
+//  Created by Sam Jaffe on 12/9/17.
+//
+
+#pragma once
+
+namespace engine::random {
+struct Device {
+  virtual ~Device() = default;
+  virtual unsigned int exclusive(unsigned int max) = 0;
+  virtual double exclusive(double min, double max) = 0;
+  virtual double inclusive(double min, double max) = 0;
+};
+}

+ 15 - 0
include/random/forwards.h

@@ -0,0 +1,15 @@
+//
+//  forwards.h
+//  shared_random_generator
+//
+//  Created by Sam Jaffe on 3/19/23.
+//  Copyright © 2023 Sam Jaffe. All rights reserved.
+//
+
+#pragma once
+
+namespace engine::random {
+class Device;
+class Random;
+class Tape;
+}

+ 38 - 0
include/random/random.h

@@ -0,0 +1,38 @@
+#pragma once
+
+#include <cassert>
+#include <memory>
+
+#include <random/forwards.h>
+
+namespace engine::random {
+class Random {
+private:
+  std::shared_ptr<Device> impl_;
+  std::shared_ptr<Tape> tape_{nullptr};
+
+public:
+  Random();
+  template <typename P> Random(P const & p) : impl_(std::make_shared<P>(p)) {}
+  template <typename P> Random(std::shared_ptr<P> const & p) : impl_(p) {}
+  
+  unsigned int exclusive(unsigned int max) const;
+  double exclusive(double min, double max) const;
+  double inclusive(double min, double max) const;
+  
+  std::shared_ptr<Device> device() const { return impl_; }
+  
+  /**
+   * Create a scope in which all calls to the generator functions will record
+   * their results into a tape. Expects that there is no tape currently set.
+   * @param tape The tape to write to
+   * @return A scope object that stops recording once it is descructed
+   */
+  auto record(std::shared_ptr<Tape> tape) {
+    assert(tape && !tape_);
+    tape_ = tape;
+    auto cleanup = [this](void *) { tape_.reset(); };
+    return std::unique_ptr<void, decltype(cleanup)>(nullptr, cleanup);
+  }
+};
+}

+ 50 - 0
include/random/tape.h

@@ -0,0 +1,50 @@
+//
+//  tape.hpp
+//  shared_random_generator
+//
+//  Created by Sam Jaffe on 3/19/23.
+//  Copyright © 2023 Sam Jaffe. All rights reserved.
+//
+
+#pragma once
+
+#include <string>
+#include <tuple>
+#include <variant>
+#include <vector>
+
+#include <random/device.h>
+
+namespace engine::random {
+class Tape : public Device {
+public:
+  // Allow for the serialization and de-serialization of this Tape
+  using serial_type =
+      std::vector<std::tuple<bool, std::string, double, double, double>>;
+
+private:
+  enum class Type : char;
+  template <typename T> struct EntryT;
+  struct Entry;
+
+private:
+  size_t index_{0}; // transient
+  std::vector<Entry> entries_;
+
+public:
+  Tape() = default;
+  Tape(serial_type serial);
+  explicit operator serial_type() const;
+
+  unsigned int exclusive(unsigned int max) override;
+  double exclusive(double min, double max) override;
+  double inclusive(double min, double max) override;
+
+  void exclusive(unsigned int max, unsigned int result);
+  void exclusive(double min, double max, double result);
+  void inclusive(double min, double max, double result);
+
+private:
+  template <typename T> T fetch(Type type, T min, T max);
+};
+}

+ 30 - 0
include/random/thread_safe.h

@@ -0,0 +1,30 @@
+//
+//  thread_safe.h
+//  shared_random_generator
+//
+//  Created by Sam Jaffe on 3/19/23.
+//  Copyright © 2023 Sam Jaffe. All rights reserved.
+//
+
+#pragma once
+
+#include <mutex>
+
+#include <random/device.h>
+
+namespace engine::random {
+
+class ThreadSafeDevice : public Device {
+private:
+  std::mutex mutex_;
+  std::unique_ptr<Device> impl_;
+  
+public:
+  ThreadSafeDevice(std::unique_ptr<Device> impl);
+  
+  unsigned int exclusive(unsigned int max) final;
+  double exclusive(double min, double max) final;
+  double inclusive(double min, double max) final;
+};
+
+}

+ 0 - 25
include/shared_random_generator/random.h

@@ -1,25 +0,0 @@
-#pragma once
-
-#include <memory>
-
-namespace engine {
-  namespace detail {
-    struct random_impl;
-  }
-
-  class random_number_generator {
-  public:
-    random_number_generator();
-    template <typename Provider>
-    random_number_generator(Provider const & p)
-        : p_impl(std::make_shared<Provider>(p)) {}
-    template <typename Provider>
-    random_number_generator(std::shared_ptr<Provider> const & p) : p_impl(p) {}
-    unsigned int exclusive(unsigned int max) const;
-    double exclusive(double min, double max) const;
-    double inclusive(double min, double max) const;
-
-  private:
-    std::shared_ptr<detail::random_impl> p_impl;
-  };
-}

+ 0 - 17
include/shared_random_generator/random_impl.h

@@ -1,17 +0,0 @@
-//
-//  random_impl.h
-//  pokemon
-//
-//  Created by Sam Jaffe on 12/9/17.
-//
-
-#pragma once
-
-namespace engine { namespace detail {
-  struct random_impl {
-    virtual ~random_impl() = default;
-    virtual unsigned int exclusive(unsigned int max) = 0;
-    virtual double exclusive(double min, double max) = 0;
-    virtual double inclusive(double min, double max) = 0;
-  };
-}}

+ 52 - 6
shared_random_generator.xcodeproj/project.pbxproj

@@ -10,8 +10,11 @@
 		CD89E51824E6F3FD008167A8 /* libshared_random_generator.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = CDED6A4221B2F5A700AB91D0 /* libshared_random_generator.dylib */; };
 		CD89E51F24E6F40B008167A8 /* GoogleMock.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD89E50824E6F3EA008167A8 /* GoogleMock.framework */; };
 		CD89E52124E6F424008167A8 /* random_test.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD89E52024E6F424008167A8 /* random_test.cxx */; };
+		CDE943B829C75E170086A8CA /* tape.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CDE943B629C75E170086A8CA /* tape.cxx */; };
+		CDE943B929C75E170086A8CA /* tape.h in Headers */ = {isa = PBXBuildFile; fileRef = CDE943B729C75E170086A8CA /* tape.h */; };
+		CDE943BC29C7643E0086A8CA /* thread_safe.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CDE943BB29C7643E0086A8CA /* thread_safe.cxx */; };
 		CDED6A4F21B2F62C00AB91D0 /* random.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CDED6A4E21B2F62C00AB91D0 /* random.cxx */; };
-		CDED6A5E21B2F76B00AB91D0 /* shared_random_generator in Headers */ = {isa = PBXBuildFile; fileRef = CDED6A5D21B2F76700AB91D0 /* shared_random_generator */; settings = {ATTRIBUTES = (Public, ); }; };
+		CDED6A5E21B2F76B00AB91D0 /* random in Headers */ = {isa = PBXBuildFile; fileRef = CDED6A5D21B2F76700AB91D0 /* random */; settings = {ATTRIBUTES = (Public, ); }; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
@@ -57,9 +60,17 @@
 		CD89E51324E6F3FD008167A8 /* shared_random_generator-test.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "shared_random_generator-test.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
 		CD89E51724E6F3FD008167A8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 		CD89E52024E6F424008167A8 /* random_test.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = random_test.cxx; sourceTree = "<group>"; };
+		CDD2137C29C76E5600A4582C /* xcode_gtest_helper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = xcode_gtest_helper.h; sourceTree = "<group>"; };
+		CDE943B329C75C610086A8CA /* device.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = device.h; sourceTree = "<group>"; };
+		CDE943B429C75C610086A8CA /* random.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = random.h; sourceTree = "<group>"; };
+		CDE943B529C75CD70086A8CA /* forwards.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = forwards.h; sourceTree = "<group>"; };
+		CDE943B629C75E170086A8CA /* tape.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = tape.cxx; sourceTree = "<group>"; };
+		CDE943B729C75E170086A8CA /* tape.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = tape.h; sourceTree = "<group>"; };
+		CDE943BA29C762C00086A8CA /* thread_safe.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = thread_safe.h; sourceTree = "<group>"; };
+		CDE943BB29C7643E0086A8CA /* thread_safe.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = thread_safe.cxx; sourceTree = "<group>"; };
 		CDED6A4221B2F5A700AB91D0 /* libshared_random_generator.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; includeInIndex = 0; path = libshared_random_generator.dylib; sourceTree = BUILT_PRODUCTS_DIR; };
 		CDED6A4E21B2F62C00AB91D0 /* random.cxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = random.cxx; sourceTree = "<group>"; };
-		CDED6A5D21B2F76700AB91D0 /* shared_random_generator */ = {isa = PBXFileReference; lastKnownFileType = folder; name = shared_random_generator; path = include/shared_random_generator; sourceTree = "<group>"; };
+		CDED6A5D21B2F76700AB91D0 /* random */ = {isa = PBXFileReference; lastKnownFileType = folder; name = random; path = include/random; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -108,11 +119,32 @@
 			name = Frameworks;
 			sourceTree = "<group>";
 		};
+		CDE943AD29C75C250086A8CA /* include */ = {
+			isa = PBXGroup;
+			children = (
+				CDE943B229C75C610086A8CA /* random */,
+			);
+			path = include;
+			sourceTree = "<group>";
+		};
+		CDE943B229C75C610086A8CA /* random */ = {
+			isa = PBXGroup;
+			children = (
+				CDE943B529C75CD70086A8CA /* forwards.h */,
+				CDE943B329C75C610086A8CA /* device.h */,
+				CDE943B429C75C610086A8CA /* random.h */,
+				CDE943B729C75E170086A8CA /* tape.h */,
+				CDE943BA29C762C00086A8CA /* thread_safe.h */,
+			);
+			path = random;
+			sourceTree = "<group>";
+		};
 		CDED6A3921B2F5A700AB91D0 = {
 			isa = PBXGroup;
 			children = (
 				CD89E50024E6F3E9008167A8 /* GoogleMock.xcodeproj */,
-				CDED6A5D21B2F76700AB91D0 /* shared_random_generator */,
+				CDED6A5D21B2F76700AB91D0 /* random */,
+				CDE943AD29C75C250086A8CA /* include */,
 				CDED6A4921B2F5DF00AB91D0 /* src */,
 				CDED6A4A21B2F5DF00AB91D0 /* test */,
 				CD89E51424E6F3FD008167A8 /* shared_random_generator-test */,
@@ -134,6 +166,8 @@
 			isa = PBXGroup;
 			children = (
 				CDED6A4E21B2F62C00AB91D0 /* random.cxx */,
+				CDE943B629C75E170086A8CA /* tape.cxx */,
+				CDE943BB29C7643E0086A8CA /* thread_safe.cxx */,
 			);
 			path = src;
 			sourceTree = "<group>";
@@ -141,6 +175,7 @@
 		CDED6A4A21B2F5DF00AB91D0 /* test */ = {
 			isa = PBXGroup;
 			children = (
+				CDD2137C29C76E5600A4582C /* xcode_gtest_helper.h */,
 				CD89E52024E6F424008167A8 /* random_test.cxx */,
 			);
 			path = test;
@@ -153,7 +188,8 @@
 			isa = PBXHeadersBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
-				CDED6A5E21B2F76B00AB91D0 /* shared_random_generator in Headers */,
+				CDE943B929C75E170086A8CA /* tape.h in Headers */,
+				CDED6A5E21B2F76B00AB91D0 /* random in Headers */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -291,7 +327,9 @@
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				CDE943BC29C7643E0086A8CA /* thread_safe.cxx in Sources */,
 				CDED6A4F21B2F62C00AB91D0 /* random.cxx in Sources */,
+				CDE943B829C75E170086A8CA /* tape.cxx in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -346,7 +384,7 @@
 				ALWAYS_SEARCH_USER_PATHS = NO;
 				CLANG_ANALYZER_NONNULL = YES;
 				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
-				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				CLANG_CXX_LANGUAGE_STANDARD = "c++17";
 				CLANG_CXX_LIBRARY = "libc++";
 				CLANG_ENABLE_MODULES = YES;
 				CLANG_ENABLE_OBJC_ARC = YES;
@@ -397,6 +435,10 @@
 				MTL_FAST_MATH = YES;
 				ONLY_ACTIVE_ARCH = YES;
 				SDKROOT = macosx;
+				SYSTEM_HEADER_SEARCH_PATHS = (
+					"$(PROJECT_DIR)/include",
+					"$(TARGET_BUILD_DIR)/usr/local/include",
+				);
 				USER_HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/include";
 			};
 			name = Debug;
@@ -407,7 +449,7 @@
 				ALWAYS_SEARCH_USER_PATHS = NO;
 				CLANG_ANALYZER_NONNULL = YES;
 				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
-				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				CLANG_CXX_LANGUAGE_STANDARD = "c++17";
 				CLANG_CXX_LIBRARY = "libc++";
 				CLANG_ENABLE_MODULES = YES;
 				CLANG_ENABLE_OBJC_ARC = YES;
@@ -451,6 +493,10 @@
 				MTL_ENABLE_DEBUG_INFO = NO;
 				MTL_FAST_MATH = YES;
 				SDKROOT = macosx;
+				SYSTEM_HEADER_SEARCH_PATHS = (
+					"$(PROJECT_DIR)/include",
+					"$(TARGET_BUILD_DIR)/usr/local/include",
+				);
 				USER_HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/include";
 			};
 			name = Release;

+ 78 - 0
shared_random_generator.xcodeproj/xcshareddata/xcschemes/shared_random_generator.xcscheme

@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1340"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "CDED6A4121B2F5A700AB91D0"
+               BuildableName = "libshared_random_generator.dylib"
+               BlueprintName = "shared_random_generator"
+               ReferencedContainer = "container:shared_random_generator.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      codeCoverageEnabled = "YES">
+      <Testables>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "CD89E51224E6F3FD008167A8"
+               BuildableName = "shared_random_generator-test.xctest"
+               BlueprintName = "shared_random_generator-test"
+               ReferencedContainer = "container:shared_random_generator.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "CDED6A4121B2F5A700AB91D0"
+            BuildableName = "libshared_random_generator.dylib"
+            BlueprintName = "shared_random_generator"
+            ReferencedContainer = "container:shared_random_generator.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>

+ 47 - 39
src/random.cxx

@@ -1,49 +1,57 @@
+#include "random/random.h"
+
 #include <cassert>
 #include <cfloat>
 #include <random>
 
-#include "shared_random_generator/random.h"
-#include "shared_random_generator/random_impl.h"
-
-namespace {
-  class default_random_number_generator : public engine::detail::random_impl {
-  public:
-    unsigned int exclusive(unsigned int max) override {
-      assert(max != 0);
-      return uniform(0, max - 1)(rng);
-    }
-
-    double exclusive(double min, double max) override {
-      return uniform_real(min, max)(rng);
-    }
-
-    double inclusive(double min, double max) override {
-      double real_max = std::nextafter(max, std::numeric_limits<double>::max());
-      return std::min(uniform_real(min, real_max)(rng), max);
-    }
-
-  private:
-    using engine = std::mt19937;
-    using uniform = std::uniform_int_distribution<typename engine::result_type>;
-    using uniform_real = std::uniform_real_distribution<>;
-
-    engine rng{std::random_device{}()};
-  };
-}
-
-namespace engine {
-  random_number_generator::random_number_generator()
-      : p_impl(std::make_shared<default_random_number_generator>()) {}
-
-  unsigned int random_number_generator::exclusive(unsigned int max) const {
-    return p_impl->exclusive(max);
+#include "random/device.h"
+#include "random/tape.h"
+
+#define EVALUATE(func, ...)                                                   \
+  [&, this]() {                                                               \
+    auto result = impl_->func(__VA_ARGS__);                                   \
+    if (tape_) { tape_->func(__VA_ARGS__, result); }                          \
+    return result;                                                            \
+  }()
+
+namespace engine::random {
+class DefaultDevice : public Device {
+public:
+  unsigned int exclusive(unsigned int max) override {
+    assert(max != 0);
+    return uniform(0, max - 1)(rng);
   }
 
-  double random_number_generator::exclusive(double min, double max) const {
-    return p_impl->exclusive(min, max);
+  double exclusive(double min, double max) override {
+    return uniform_real(min, max)(rng);
   }
 
-  double random_number_generator::inclusive(double min, double max) const {
-    return p_impl->inclusive(min, max);
+  double inclusive(double min, double max) override {
+    double real_max = std::nextafter(max, std::numeric_limits<double>::max());
+    return std::min(uniform_real(min, real_max)(rng), max);
   }
+
+private:
+  using engine = std::mt19937;
+  using uniform = std::uniform_int_distribution<typename engine::result_type>;
+  using uniform_real = std::uniform_real_distribution<>;
+
+  engine rng{std::random_device{}()};
+};
+
+Random::Random()
+    : impl_(std::make_shared<DefaultDevice>()) {}
+
+unsigned int Random::exclusive(unsigned int max) const {
+  assert(max != 0);
+  return EVALUATE(exclusive, max);
+}
+
+double Random::exclusive(double min, double max) const {
+  return EVALUATE(exclusive, min, max);
+}
+
+double Random::inclusive(double min, double max) const {
+  return EVALUATE(inclusive, min, max);
+}
 }

+ 89 - 0
src/tape.cxx

@@ -0,0 +1,89 @@
+//
+//  tape.cpp
+//  shared_random_generator
+//
+//  Created by Sam Jaffe on 3/19/23.
+//  Copyright © 2023 Sam Jaffe. All rights reserved.
+//
+
+#include "random/tape.h"
+
+#define IS_A(T) tname == typeid(T).name()
+
+namespace engine::random {
+enum class Tape::Type : char { Inclusive, Exclusive };
+
+template <typename T> struct Tape::EntryT {
+  EntryT(T min, T max) : min(min), max(max), result() {}
+  EntryT(T min, T max, T result) : min(min), max(max), result(result) {}
+
+  T min;
+  T max;
+  T result;
+};
+
+struct Tape::Entry {
+  template <typename T>
+  Entry(Type type, EntryT<T> params) : type(type), params(params) {}
+
+  Type type;
+  std::variant<EntryT<double>, EntryT<unsigned int>> params;
+};
+
+Tape::Tape(serial_type serial) {
+  for (auto const & [exclusive, tname, min, max, result] : serial) {
+    Type const type = exclusive ? Type::Exclusive : Type::Inclusive;
+    if (IS_A(unsigned int)) {
+      entries_.emplace_back(type, EntryT<unsigned int>(min, max, result));
+    } else {
+      entries_.emplace_back(type, EntryT<double>(min, max, result));
+    }
+  }
+}
+
+Tape::operator serial_type() const {
+  serial_type rval;
+  auto insert = [&rval](auto type, auto const & entry) {
+    rval.emplace_back(type == Type::Exclusive, typeid(entry.min).name(),
+                      entry.min, entry.max, entry.result);
+  };
+  for (auto const & [type, entry] : entries_) {
+    std::visit(insert, std::variant<Type>(type), entry);
+  }
+  return rval;
+}
+
+unsigned int Tape::exclusive(unsigned int max) {
+  return fetch(Type::Exclusive, 0u, max);
+}
+
+double Tape::exclusive(double min, double max) {
+  return fetch(Type::Exclusive, min, max);
+}
+
+double Tape::inclusive(double min, double max) {
+  return fetch(Type::Inclusive, min, max);
+}
+
+void Tape::exclusive(unsigned int max, unsigned int result) {
+  entries_.emplace_back(Type::Exclusive, EntryT(0u, max, result));
+}
+
+void Tape::exclusive(double min, double max, double result) {
+  entries_.emplace_back(Type::Exclusive, EntryT(min, max, result));
+}
+
+void Tape::inclusive(double min, double max, double result) {
+  entries_.emplace_back(Type::Inclusive, EntryT(min, max, result));
+}
+
+template <typename T> T Tape::fetch(Type type, T min, T max) {
+  auto const * entry = std::get_if<EntryT<T>>(&entries_[index_].params);
+  if (entries_[index_].type == type && entry && entry->min == min &&
+      entry->max == max) {
+    ++index_;
+    return entry->result;
+  }
+  throw std::logic_error("Attempting replay with different actions");
+}
+}

+ 28 - 0
src/thread_safe.cxx

@@ -0,0 +1,28 @@
+//
+//  thread_safe.cpp
+//  shared_random_generator
+//
+//  Created by Sam Jaffe on 3/19/23.
+//  Copyright © 2023 Sam Jaffe. All rights reserved.
+//
+
+#include "random/thread_safe.h"
+
+namespace engine::random {
+ThreadSafeDevice::ThreadSafeDevice(std::unique_ptr<Device> impl) : impl_(std::move(impl)) {}
+
+unsigned int ThreadSafeDevice::exclusive(unsigned int max) {
+  std::lock_guard lock(mutex_);
+  return impl_->exclusive(max);
+}
+
+double ThreadSafeDevice::exclusive(double min, double max) {
+  std::lock_guard lock(mutex_);
+  return impl_->exclusive(min, max);
+}
+
+double ThreadSafeDevice::inclusive(double min, double max) {
+  std::lock_guard lock(mutex_);
+  return impl_->inclusive(min, max);
+}
+}

+ 15 - 14
test/random_test.cxx

@@ -6,36 +6,37 @@
 //  Copyright © 2020 Sam Jaffe. All rights reserved.
 //
 
-#include "shared_random_generator/random_impl.h"
-#include "shared_random_generator/random.h"
+#include "random/random.h"
 
-#include <gmock/gmock.h>
+#include "random/device.h"
 
-struct mock_random_impl : engine::detail::random_impl {
+#include "xcode_gtest_helper.h"
+
+struct MockDevice : engine::random::Device {
   MOCK_METHOD1(exclusive, uint32_t(uint32_t));
   MOCK_METHOD2(exclusive, double(double, double));
   MOCK_METHOD2(inclusive, double(double, double));
 };
 
 TEST(RandomTest, PassesThroughExclusiveIntegerCall) {
-  auto mock = std::make_shared<mock_random_impl>();
-  engine::random_number_generator generator{mock};
+  auto mock = std::make_shared<MockDevice>();
+  engine::random::Random generator{mock};
   
   EXPECT_CALL(*mock, exclusive(10)).Times(1);
   generator.exclusive(10);
 }
 
 TEST(RandomTest, PassesThroughExclusiveDoubleCall) {
-  auto mock = std::make_shared<mock_random_impl>();
-  engine::random_number_generator generator{mock};
+  auto mock = std::make_shared<MockDevice>();
+  engine::random::Random generator{mock};
   
   EXPECT_CALL(*mock, exclusive(1.0, 10.0)).Times(1);
   generator.exclusive(1.0, 10.0);
 }
 
 TEST(RandomTest, PassesThroughInclusiveDoubleCall) {
-  auto mock = std::make_shared<mock_random_impl>();
-  engine::random_number_generator generator{mock};
+  auto mock = std::make_shared<MockDevice>();
+  engine::random::Random generator{mock};
   
   EXPECT_CALL(*mock, inclusive(1.0, 10.0)).Times(1);
   generator.inclusive(1.0, 10.0);
@@ -61,7 +62,7 @@ kahan_summation & kahan_summation::operator+=(double num) {
 }
 
 TEST(DefaultRandomTest, RandomDistributionIntIsUniform) {
-  engine::random_number_generator generator{};
+  engine::random::Random generator{};
   kahan_summation sum{};
   size_t const iters = 2000000;
   for (size_t i = 0; i < iters; ++i) {
@@ -73,7 +74,7 @@ TEST(DefaultRandomTest, RandomDistributionIntIsUniform) {
 }
 
 TEST(DefaultRandomTest, RandomDistributionDblIsUniform) {
-  engine::random_number_generator generator{};
+  engine::random::Random generator{};
   kahan_summation sum{};
   size_t const iters = 2000000;
   for (size_t i = 0; i < iters; ++i) {
@@ -85,12 +86,12 @@ TEST(DefaultRandomTest, RandomDistributionDblIsUniform) {
 }
 
 TEST(DefaultRandomTest, InclusiveRangeMayIncludeValue) {
-  engine::random_number_generator generator{};
+  engine::random::Random generator{};
   EXPECT_THAT(generator.inclusive(1.0, 1.0), testing::DoubleNear(1.0, 1E-6));
 }
 
 TEST(DefaultRandomTest, RandomDistributionDblInclIsUniform) {
-  engine::random_number_generator generator{};
+  engine::random::Random generator{};
   kahan_summation sum{};
   size_t const iters = 2000000;
   for (size_t i = 0; i < iters; ++i) {

+ 38 - 0
test/xcode_gtest_helper.h

@@ -0,0 +1,38 @@
+//
+//  xcode_gtest_helper.h
+//
+//  Created by Sam Jaffe on 11/25/20.
+//  Copyright © 2020 Sam Jaffe. All rights reserved.
+//
+
+#pragma once
+
+#if defined(__APPLE__)
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wquoted-include-in-framework-header"
+#pragma clang diagnostic ignored "-Wcomma"
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#pragma clang diagnostic pop
+
+#if defined(TARGET_OS_OSX)
+// This is a hack to allow XCode to properly display failures when running
+// unit tests.
+#undef EXPECT_THAT
+#define EXPECT_THAT ASSERT_THAT
+#undef EXPECT_THROW
+#define EXPECT_THROW ASSERT_THROW
+#undef EXPECT_ANY_THROW
+#define EXPECT_ANY_THROW ASSERT_ANY_THROW
+#undef EXPECT_NO_THROW
+#define EXPECT_NO_THROW ASSERT_NO_THROW
+#undef EXPECT_TRUE
+#define EXPECT_TRUE ASSERT_TRUE
+#undef EXPECT_FALSE
+#define EXPECT_FALSE ASSERT_FALSE
+
+#endif
+#endif