Source
stdlib/memory/tests/memory_test.cpp
1
// Copyright (c) 2026 BigBrain LLC. MIT-licensed (see LICENSE).2
// Original work; see ACKNOWLEDGMENTS.md for the open-source ideas we build upon.3
// SPEC (for review) — the intended behaviour of the `memory` module. NOT wired into the build and4
// NOT expected to pass until the C++ backend engine is implemented; every test is a promise the5
// implementation must keep.6
//7
// Types (namespace cheatah::memory; PascalCase handles, lowercase tags/enums — matches File/Conn/8
// Pattern vs read/write):9
// Owner<T> — the sole owner + coordinator (non-copyable, pinned). `T` is the10
// ONLY class template arg. Policy is a CONSTRUCTOR arg; priority11
// is a compile-time arg on the write accessor.12
// Lease<T, read | write> — the only handle; read()=get, write(v)=set, write()=in-place ref (NOT get()).13
// Request<Lease> — what accessors return; `.acquire(on_interrupt)` BLOCKS -> Lease.14
// Access (EVERY accessor returns a Request for a lease, never a bare lease — a read included):15
// o.rread() -> Request<Lease<T, read>>. r = ....acquire(). r.read() -> const T&.16
// r.valid()/r.expired() track the owner's stop request.17
// o.rwrite<priority>() -> Request<Lease<T, write>>. w = ....acquire(). w.write(value) sets; w.write() -> T& for in-place. Exclusive.18
// priority is a compile-time int or the caller's enum value; higher = higher;19
// NEGATIVE (memory::immediate == -1) = immediate-write (preempt + resume).20
// own(value[, policy]) -> Owner<T>. policy is memory::interleave (default) | memory::writes_first.21
// Request::acquire(on_interrupt) — blocks for the lease; the optional callback is wired to the22
// lease's stop token and fires when the owner needs the lease back.23
// Safety: a write touches no byte until every read lease has released (drain-before-write); a reader24
// that re-acquires after a write sees the object's CURRENT location (no dangling).26
#include <algorithm>27
#include <atomic>28
#include <chrono>29
#include <cstdint>30
#include <map>31
#include <memory>32
#include <string>33
#include <thread>34
#include <type_traits>35
#include <utility>36
#include <vector>38
#include <gtest/gtest.h>40
#include "memory.hpp"42
namespace mem = cheatah::memory;43
using namespace std::chrono_literals;45
namespace { struct Point { int x = 0, y = 0; }; }47
// ── the core invariant: no accessor ever returns a bare lease ─────────────────────────────49
TEST(Memory, EveryAccessorReturnsARequestNotABareLease) {50
auto o = mem::own(Point{});51
static_assert(std::is_same_v<decltype(o.rread()),52
mem::Request<mem::Lease<Point, mem::read>>>,53
"rread() hands back a Request for a read lease, not a read lease");54
static_assert(std::is_same_v<decltype(o.rwrite()),55
mem::Request<mem::Lease<Point, mem::write>>>,56
"rwrite() hands back a Request for a write lease, not a write lease");57
// .acquire() is how a Request redeems into the lease.58
static_assert(std::is_same_v<decltype(o.rread().acquire()), mem::Lease<Point, mem::read>>,59
"Request::acquire() yields the lease");60
SUCCEED();61
}63
// read() must hand back a REFERENCE to the owned object — never a copy, never a raw pointer — so a64
// caller touches the object in place (and a big object isn't copied on every read).65
TEST(Memory, ReadReturnsAReferenceNotACopyOrPointer) {66
struct Big { int a[64]; };67
using RB = mem::Lease<Big, mem::read>;68
static_assert(std::is_same_v<decltype(std::declval<const RB&>().read()), const Big&>,69
"read() returns const T& — a reference to the owned object, not a copy or a T*");70
static_assert(std::is_reference_v<decltype(std::declval<const RB&>().read())>,71
"read() returns a reference");72
static_assert(!std::is_pointer_v<std::remove_reference_t<decltype(std::declval<const RB&>().read())>>,73
"read() does not return a raw pointer");74
static_assert(std::is_void_v<decltype(std::declval<mem::Lease<Big, mem::write>&>().write(std::declval<Big>()))>,75
"write(value) is a SETTER — it returns void, never an object/reference");76
SUCCEED();77
}79
// ── ownership basics ─────────────────────────────────────────────────────────────────────81
TEST(Memory, OwnerIsSoleAndNonCopyable) {82
static_assert(!std::is_copy_constructible_v<mem::Owner<Point>>, "owner is the sole owner");83
static_assert(!std::is_copy_assignable_v<mem::Owner<Point>>, "owner is the sole owner");84
SUCCEED();85
}87
TEST(Memory, ObjectDiesWithOwner) {88
static int live = 0;89
// std::movable (own<T> constrains on it): ctors count, assignments are no-ops for the count.90
struct T {91
T(){++live;} T(const T& /*unused*/){++live;} T(T&& /*unused*/) noexcept {++live;}92
// NOLINT below: this probe's operator= is DELIBERATELY a no-op either way — only93
// construction/destruction move the live count, so self-assignment is trivially safe.94
T& operator=(const T& /*unused*/)= default; T& operator=(T&& /*unused*/) noexcept {return *this;} // NOLINT(cert-oop54-cpp)95
~T(){--live;}96
};97
{ auto o = mem::own(T{}); EXPECT_EQ(live, 1); }98
EXPECT_EQ(live, 0);99
}101
// The object is MOVED into the Owner (consumed), never copied; copying an Owner is forbidden.102
TEST(Memory, OwnerConsumesAndMovesTheObjectInNeverCopies) {103
static_assert(!std::is_copy_constructible_v<mem::Owner<int>>, "Owner is non-copyable");104
static_assert(!std::is_move_constructible_v<mem::Owner<int>>, "Owner is pinned (non-movable)");106
struct Tracker {107
int moves = 0, copies = 0;108
std::shared_ptr<int> resource = std::make_shared<int>(7); // a movable resource we can watch109
Tracker() = default;110
Tracker(const Tracker& o) : moves(o.moves), copies(o.copies + 1), resource(o.resource) {}111
Tracker(Tracker&& o) noexcept112
: moves(o.moves + 1), copies(o.copies), resource(std::move(o.resource)) {}113
Tracker& operator=(const Tracker&) = default;114
Tracker& operator=(Tracker&&) noexcept = default;115
~Tracker() = default;116
};118
Tracker src; // an lvalue we hand over119
auto o = mem::own(std::move(src)); // consume it — Owner<Tracker>(Tracker&&)120
auto r = o.rread().acquire();121
EXPECT_EQ(r.read().copies, 0) << "the object must be MOVED into the Owner, never copied";122
EXPECT_GE(r.read().moves, 1) << "the object must be moved in";123
EXPECT_EQ(*r.read().resource, 7); // the Owner holds the resource124
EXPECT_EQ(src.resource, nullptr) << "the source was consumed — its resource moved out"; // NOLINT(bugprone-use-after-move,clang-analyzer-cplusplus.Move): moved-from state is the assertion125
}127
// For complex objects, each write form reaches the RIGHT item: index → the right element, key → the128
// right entry, whole-value → a clean replacement; everything else stays untouched.129
TEST(Memory, LeasesModifyTheCorrectItemsOfComplexObjects) {130
{ // sequence: the indexed setter hits exactly one element; the symmetric read getters read it back.131
auto o = mem::own(std::vector<int>{10, 20, 30, 40});132
{ auto w = o.rwrite().acquire(); w.write(std::size_t{2}, 99); }133
auto r = o.rread().acquire();134
EXPECT_EQ(r.read(std::size_t{0}), 10); // read(index) mirrors write(index, value)135
EXPECT_EQ(r.read(std::size_t{2}), 99); // only index 2 changed136
EXPECT_EQ(r.read_front(), 10); // read_front / read_back convenience137
EXPECT_EQ(r.read_back(), 40);138
EXPECT_EQ(r.read().size(), 4u); // read() still gives the whole object139
}140
{ // mapping: keyed setter updates/inserts; read(key) mirrors it (and throws on a missing key).141
auto o = mem::own(std::map<std::string, int>{{"a", 1}, {"b", 2}});142
{ auto w = o.rwrite().acquire(); w.write(std::string("b"), 22); } // update143
{ auto w = o.rwrite().acquire(); w.write(std::string("c"), 3); } // insert144
auto r = o.rread().acquire();145
EXPECT_EQ(r.read(std::string("a")), 1); // read(key) mirrors write(key, value)146
EXPECT_EQ(r.read(std::string("b")), 22);147
EXPECT_EQ(r.read(std::string("c")), 3);148
EXPECT_EQ(r.read().size(), 3u);149
}150
{ // nested struct: whole-value setter replaces it; the read sees the new fields.151
struct Inner { int x; std::string name; };152
auto o = mem::own(Inner{1, "old"});153
{ auto w = o.rwrite().acquire(); w.write(Inner{42, "new"}); }154
auto r = o.rread().acquire();155
EXPECT_EQ(r.read().x, 42);156
EXPECT_EQ(r.read().name, "new");157
}158
}160
TEST(Memory, PolicyIsAConstructorArgumentNotATemplateParameter) {161
auto fair = mem::own(0); // default policy: memory::interleave162
auto drain = mem::own(0, mem::writes_first); // policy chosen at construction163
// Both are the same TYPE (Owner<int>) — policy is a stored value, not part of the type.164
static_assert(std::is_same_v<decltype(fair), decltype(drain)>,165
"Owner<T> carries only T; policy does not template the class");166
SUCCEED();167
}169
// ── read leases: coexist, and are accessed via read() (never get()) ──────────────────────171
TEST(Memory, ReadLeasesCoexist) {172
auto o = mem::own(Point{3, 4});173
auto r1 = o.rread().acquire(); // request -> acquire -> Lease<Point, read>174
auto r2 = o.rread().acquire(); // a second read lease at the same time — allowed175
EXPECT_TRUE(r1.valid());176
EXPECT_TRUE(r2.valid());177
EXPECT_EQ(r1.read().x, 3); // lease access is read()/write(), never get()178
EXPECT_EQ(r2.read().y, 4);179
}181
TEST(Memory, ReadLeaseValidUntilAWriterNeedsIn) {182
auto o = mem::own(Point{1, 1});183
std::atomic<bool> reader_saw_expired{false}, reader_holding{false};184
std::thread reader([&] {185
auto r = o.rread().acquire();186
reader_holding = true;187
while (r.valid()) std::this_thread::yield(); // read until the owner asks us to stop188
reader_saw_expired = r.expired(); // we left the loop because the lease expired189
}); // reader releases here190
while (!reader_holding) std::this_thread::yield();191
{ auto w = o.rwrite().acquire(); auto p = w.read(); p.x = 9; w.write(p); } // write expires the read lease192
reader.join();193
EXPECT_TRUE(reader_saw_expired); // the reader observed expired()194
EXPECT_EQ(o.rread().acquire().read().x, 9); // the write landed195
}197
TEST(Memory, InterruptCallbackFiresWhenTheOwnerNeedsTheLeaseBack) {198
// The callback passed INTO acquire() is the requester's "what to do if interrupted" — the owner199
// decides when; the requester only suggests the reaction.200
auto o = mem::own(0);201
std::atomic<bool> asked_to_yield{false}, reader_holding{false};202
std::thread reader([&] {203
auto r = o.rread().acquire([&] { asked_to_yield = true; }); // wired to the lease's stop token204
reader_holding = true;205
while (r.valid()) std::this_thread::yield();206
});207
while (!reader_holding) std::this_thread::yield();208
{ auto w = o.rwrite().acquire(); w.write(42); } // requesting the write trips the reader's stop209
reader.join();210
EXPECT_TRUE(asked_to_yield); // the interrupt handler fired211
}213
// ── drain-before-write: the writer waits until every reader has released ──────────────────215
TEST(Memory, WriteWaitsForReadersToDrain) {216
auto o = mem::own<long long>(0);217
std::atomic<bool> reader_released{false};218
std::atomic<bool> write_began_before_release{false};219
std::thread reader([&] {220
auto r = o.rread().acquire();221
while (r.valid()) std::this_thread::sleep_for(1ms); // hold briefly, honoring the stop222
std::this_thread::sleep_for(5ms);223
reader_released = true;224
}); // release here225
std::this_thread::sleep_for(1ms);226
{227
auto w = o.rwrite().acquire(); // must block until the reader released228
if (!reader_released) write_began_before_release = true;229
w.write(1);230
}231
reader.join();232
EXPECT_FALSE(write_began_before_release); // no byte moved while a reader held on233
EXPECT_EQ(o.rread().acquire().read(), 1);234
}236
// ── renewal: a reader re-acquiring after a write sees the NEW value at the CURRENT location ─238
TEST(Memory, ReaderRenewsAndSeesTheNewValueEvenIfMoved) {239
// A std::string can reallocate (move its bytes) when it grows — the renewed read lease must240
// still be valid, pointing at the object's current location.241
auto o = mem::own(std::string("x"));242
std::atomic<bool> go{false};243
std::string seen;244
std::thread reader([&] {245
while (!go) std::this_thread::yield();246
for (;;) {247
auto r = o.rread().acquire(); // re-request (renew); blocks behind a write248
if (r.read().size() > 100) { seen = r.read(); break; }249
}250
});251
std::thread writer([&] {252
while (!go) std::this_thread::yield();253
auto w = o.rwrite().acquire();254
w.write(std::string(500, 'a')); // grows -> may relocate the buffer255
});256
go = true;257
reader.join();258
writer.join();259
EXPECT_EQ(seen, std::string(500, 'a')); // renewed reader saw the new bytes safely260
}262
// ── writer-may-be-a-reader: releasing your read then writing must not deadlock ────────────264
TEST(Memory, AReaderCanBecomeAWriterWithoutSelfDeadlock) {265
auto o = mem::own(Point{0, 0});266
{ auto r = o.rread().acquire(); EXPECT_EQ(r.read().x, 0); } // finish reading (release)267
{ auto w = o.rwrite().acquire(); auto p = w.read(); p.x = 7; w.write(p); } // then write — no wait-on-self268
{ auto r = o.rread().acquire(); EXPECT_EQ(r.read().x, 7); } // and read again (renew)269
SUCCEED();270
}272
// ── scheduling: priority is a compile-time argument on rwrite; higher is served first ────274
namespace { enum class Job : std::uint8_t { normal = 0, high = 10 }; } // arbitrary names; higher = higher priority276
TEST(Memory, HigherPriorityWriteServedFirst) {277
auto o = mem::own(std::string(""));278
std::atomic<bool> blocker_holding{false}, release_blocker{false};279
// Hold a read lease so both writers must queue; enqueue the normal one first, then the high one.280
std::thread blocker([&] {281
auto r = o.rread().acquire();282
blocker_holding = true;283
while (!release_blocker) std::this_thread::yield(); // hold the read lease so both writers QUEUE284
});285
while (!blocker_holding) std::this_thread::yield();286
std::thread lo([&]{ auto w = o.rwrite<Job::normal>().acquire(); w.write(w.read() + "L"); });287
std::this_thread::sleep_for(2ms); // ensure L enqueues first288
std::thread hi([&]{ auto w = o.rwrite<Job::high>().acquire(); w.write(w.read() + "H"); });289
std::this_thread::sleep_for(2ms);290
release_blocker = true; // now the queue drains by priority291
blocker.join(); lo.join(); hi.join();292
EXPECT_EQ(o.rread().acquire().read(), "HL"); // High ran before Low despite arriving later293
}295
// ── negative priority = immediate-write: bypass queue, preempt active writer, it resumes ──297
TEST(Memory, ImmediateConstantIsANegativeNamedForReadability) {298
static_assert(mem::immediate == -1, "memory::immediate is the readable spelling of -1");299
static_assert(mem::immediate < 0, "any negative priority is an immediate-write");300
SUCCEED();301
}303
// THE RULE THIS TEST OBEYS, and it is the whole reason it looks like this: a preempted writer may304
// wait ONLY by polling `w.valid()`. That poll is what ACKS the preempt — `lease.hpp`: *"the first305
// observation of a stop acks and wakes the owner … polling this from the holding thread is what lets306
// a drain/preempt make progress"* — and `grant_immediate()` blocks on307
// `writer_gate_->acked` until it happens. A writer that waits on anything else while holding its308
// lease (a condition variable, a flag only the immediate-write can set) is a CIRCULAR WAIT: the309
// immediate cannot proceed until the writer acks, and the writer will not ack until the immediate310
// proceeds. That is a deadlock, not a flake, and it is how a previous attempt at this test ended.311
//312
// WHAT WAS WRONG BEFORE. The writer slept 1 ms after each of three chunks and the main thread slept313
// 1 ms once, so the writer needed ~3 ms and the preempt was aimed at a 1 ms window. `sleep_for` was314
// doing the job of a synchronisation primitive, and under load the main thread was descheduled past315
// the writer entirely: the writer finished first, set `finished_first = 2`, and the assertion below316
// failed. Roughly one run in three on a loaded machine, and it blocked every push.317
//318
// Both interleavings are LEGAL to the module — `grant_immediate()` short-circuits on `!writer_`319
// with the comment *"a non-looping writer may release during the wait"* — so the module was never320
// the bug. This test is about the preempting interleaving specifically, so it now ARRANGES that321
// interleaving instead of gambling on the scheduler for it.322
TEST(Memory, NegativePriorityImmediateWritePreemptsTheActiveWriterWhichThenResumes) {323
using clock = std::chrono::steady_clock;324
// Every wait below is bounded. An unbounded spin would turn a genuine preempt/resume regression325
// into a hung CI runner, which is a strictly worse failure than the flake being fixed here.326
constexpr auto kPatience = std::chrono::seconds(5);328
auto o = mem::own(std::string(""));329
std::atomic<bool> writer_holding{false};330
std::atomic<bool> preempt_seen{false};331
std::atomic<bool> timed_out{false};332
std::atomic<const char*> stuck_at{nullptr};333
std::atomic<int> chunks_at_stall{-1};334
std::atomic<int> chunks{0}, finished_first{0}; // 1 = immediate finished first, 2 = writer336
// Cooperative spin: polls (so a preempt can make progress) and gives up rather than hanging.337
//338
// IT BACKS OFF, and that is not a nicety. A pure `yield()` loop keeps this thread permanently339
// runnable, and on a loaded box the scheduler will happily keep feeding it while starving the340
// very thread it is waiting for — the writer spinning at full tilt can hold off the main thread341
// that owes it the preempt. That showed up as this test's own 5 s deadline firing under a 12x342
// load. Spin briefly for latency, then sleep so somebody else can run.343
const auto spin_until = [&](const char* what, auto&& done) {344
const auto deadline = clock::now() + kPatience;345
for (int i = 0; !done(); ++i) {346
if (clock::now() > deadline) {347
stuck_at = what; chunks_at_stall = chunks.load(); timed_out = true; return false;348
}349
if (i < 1000) std::this_thread::yield();350
else std::this_thread::sleep_for(std::chrono::microseconds(100));351
}352
return true;353
};355
// A long-running writer: appends "A" three times, yielding to an immediate-write between chunks.356
std::thread writer([&] {357
auto w = o.rwrite().acquire(); // priority 0358
writer_holding = true;359
// ANY observation of `!valid()` is a preempt, wherever it happens — and it must be recorded360
// there, not only at the top of the loop. Checking only at the top was a real bug and cost a361
// 5 s stall: the immediate-write can be absorbed ENTIRELY by the await-regrant spin below,362
// because that spin's `valid()` call is itself the ack. The writer would then resume with363
// `preempt_seen` still false and sit waiting for a second preempt nobody was going to send.364
const auto lease_valid = [&] {365
const bool v = w.valid();366
if (!v) preempt_seen = true;367
return v;368
};369
for (int i = 0; i < 3; ++i) {370
if (!spin_until("writer:await-regrant", lease_valid)) return; // await regrant371
w.write(w.read() + "A"); // safe: w.valid(), write() is the CURRENT location372
++chunks;373
// Refuse to finish until the preemption has actually been OBSERVED. This is what makes374
// `finished_first` a fact rather than a coin flip. Skipped when the immediate-write375
// already came and went before the first chunk — waiting for a second preempt that376
// nobody will send would hang.377
if (i == 0 && !preempt_seen) {378
if (!spin_until("writer:await-preempt", [&] { return !lease_valid(); })) return;379
preempt_seen = true;380
}381
}382
if (finished_first == 0) finished_first = 2;383
});385
ASSERT_TRUE(spin_until("main:await-writer-holding", [&] { return writer_holding.load(); })) << "the writer never acquired";386
{387
auto w = o.rwrite<mem::immediate>().acquire(); // NEGATIVE priority (== -1) -> immediate-write388
w.write(w.read() + "!"); // emergency correction, mid-writer389
if (finished_first == 0) finished_first = 1;390
} // release -> writer's lease becomes valid() again391
writer.join();393
ASSERT_FALSE(timed_out) << "a wait exceeded " << kPatience.count() << "s at ["394
<< (stuck_at.load() ? stuck_at.load() : "?") << "] with chunks="395
<< chunks_at_stall.load() << " preempt_seen=" << preempt_seen.load()396
<< " finished_first=" << finished_first.load()397
<< " — the preempt/resume handshake is not completing";398
EXPECT_EQ(finished_first, 1); // the immediate-write completed before the writer resumed399
EXPECT_EQ(chunks, 3); // the preempted writer resumed and finished all its work400
const auto result = o.rread().acquire().read();401
EXPECT_NE(result.find('!'), std::string::npos); // the emergency write landed402
EXPECT_EQ(std::count(result.begin(), result.end(), 'A'), 3); // the preempted writer's work survived403
}405
// ── compile-time-only write renewal (deliberate friction) ────────────────────────────────407
TEST(Memory, WriteRenewalIsCompileTimeOnly) {408
// A plain write lease is one-shot; a renewable write lease is a DISTINCT, compile-time-selected409
// type. The type system — not a runtime flag — is what lets a writer re-lease.410
static_assert(!std::is_same_v<mem::Lease<int, mem::write>, mem::Lease<int, mem::write_renewable>>,411
"renewable write is its own type, declared at compile time");412
SUCCEED();413
}