cheatah
Source

stdlib/memory/lease.hpp

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#pragma once
5/**
6 * @file lease.hpp
7 * @brief `memory::Lease<T, Mode>` — the only handle to an owned object.
8 *
9 * Move-only RAII. A lease does NOT hold a `std::lock`; the `Owner` coordinator grants logical
10 * exclusion (readers share, a writer is alone) and the lease just carries a **release callback** it
11 * fires on destruction to tell the owner "I'm done". Access:
12 * - `r.read()` → `const T&` (a REFERENCE to the owned object — never a copy or raw pointer).
13 * - `w.write(value)` → SETTER: replace the whole object. `write` NEVER returns an object.
14 * - `w.write(index, v)` → set element `index` (Indexed sequences: vector/array/string/ndarray).
15 * - `w.write(key, v)` → set `key` (Mapping containers: map/unordered_map/dict).
16 * `valid()`/`expired()` track a shared `Gate` the owner flips to ask the holder to yield (drain, or an
17 * immediate-write preempt); `on_interrupt(fn)` fires `fn` once per stop, when the holder observes it.
18 */
20#include <atomic>
21#include <concepts>
22#include <cstddef>
23#include <functional>
24#include <memory>
25#include <utility>
27#include "mode.hpp"
28#include "ownable.hpp"
30namespace cheatah::memory {
32namespace detail {
33/// Shared signal between the owner and a lease. The owner flips `valid` false to ask the holder to yield;
34/// the holder sets `acked` when it observes that and calls `wake` so the owner's cv re-checks. `wake`
35/// (owner-provided) synchronizes on the owner's mutex before notifying — the ack is one-shot, so an
36/// unsynchronized notify racing a waiter mid-predicate would be lost and the waiter would sleep forever.
37struct Gate {
38 std::atomic<bool> valid{true};
39 std::atomic<bool> acked{false};
40 std::function<void()> wake; ///< set by the owner to notify its cv_ when the holder first acks.
41};
42} // namespace detail
44/**
45 * @brief The only handle to an owned object — a move-only RAII lease granted by an `Owner`.
46 *
47 * `M` is `read` (shared) or `write`/`write_renewable` (exclusive). Reads go through `read(...)`; a
48 * write lease sets through `write(...)`. The lease holds a direct pointer to the object plus the
49 * release callback it fires on destruction. @tparam T the owned type (`Ownable`). @tparam M the mode.
50 */
51template <Ownable T, Mode M>
52class Lease {
53public:
54 /**
55 * The owner's grant path (called only by `Owner`). @complexity O(1). @alloc none.
56 * @param obj pointer to the owned object.
57 * @param gate the generation gate that carries the yield signal.
58 * @param release callback fired once on destruction to tell the owner this lease is done.
59 * @concurrency called by the owner with its coordinator mutex held — never construct one yourself.
60 * @test Memory.EveryAccessorReturnsARequestNotABareLease
61 * @test Memory.ReadLeasesCoexist
62 */
63 Lease(T* obj, std::shared_ptr<detail::Gate> gate, std::function<void()> release) noexcept
64 : obj_(obj), gate_(std::move(gate)), release_(std::move(release)) {}
66 /// Move-construct, taking over @p o's grant (it is left released). @param o the lease to move from.
67 /// @complexity O(1). @alloc none. @test Memory.EveryAccessorReturnsARequestNotABareLease
68 /// @test Memory.ReadLeasesCoexist
69 Lease(Lease&& o) noexcept { steal(o); }
70 /// Move-assign: release ours, then take over @p o's grant. @param o source. @return `*this`.
71 /// @complexity O(1). @alloc none. @test Memory.EveryAccessorReturnsARequestNotABareLease
72 Lease& operator=(Lease&& o) noexcept { if (this != &o) { drop(); steal(o); } return *this; }
73 Lease(const Lease&) = delete;
74 Lease& operator=(const Lease&) = delete;
75 /// Release the lease (fires the owner's release callback if still held).
76 /// @complexity O(1). @alloc none.
77 /// @concurrency the release callback takes the owner's coordinator mutex and wakes waiting
78 /// requests (a draining writer proceeds once the last reader releases here).
79 /// @test Memory.WriteWaitsForReadersToDrain
80 ~Lease() { drop(); }
82 /// Read the whole object. Available on every lease. @return a `const T&` — never a copy or `T*`.
83 /// @complexity O(1). @alloc none.
84 /// @concurrency race-free while the lease is held: a writer cannot proceed until this lease
85 /// releases (yielding on `!valid()` is cooperative, not forced). No lock is taken here.
86 /// @test Memory.ReadReturnsAReferenceNotACopyOrPointer
87 const T& read() const { return *obj_; }
89 /// Read element @p index of an Indexed sequence: `r.read(i)`. Mirrors `w.write(i, v)`.
90 /// @param index the position to read. @return a const reference to the element.
91 /// @complexity `T::operator[]`. @alloc none.
92 /// @test Memory.LeasesModifyTheCorrectItemsOfComplexObjects
93 const auto& read(std::size_t index) const
94 requires Indexed<T>
95 { return (*obj_)[index]; }
97 /// Read the value at @p key of a Mapping: `r.read(k)`. Mirrors `w.write(k, v)`. Throws if absent
98 /// (reading a missing key never inserts). @tparam K a type convertible to the key type.
99 /// @param key the key to look up. @return a const reference to the mapped value.
100 /// @complexity `T::at`. @alloc none when @p key already has the key type; a key that needs
101 /// converting (`const char*` to a `std::string` key) builds a temporary.
102 /// @test Memory.LeasesModifyTheCorrectItemsOfComplexObjects
103 template <class K>
104 requires (Mapping<T> && std::convertible_to<K, typename T::key_type>)
105 const auto& read(K&& key) const
106 { return (*obj_).at(std::forward<K>(key)); }
108 /// Read the first element (containers with `front()`: vector / deque / list / string …).
109 /// @return a const reference to the first element. @complexity O(1). @alloc none.
110 /// @test Memory.LeasesModifyTheCorrectItemsOfComplexObjects
111 const auto& read_front() const
112 requires HasFront<T>
113 { return (*obj_).front(); }
115 /// Read the last element (containers with `back()`).
116 /// @return a const reference to the last element. @complexity O(1). @alloc none.
117 /// @test Memory.LeasesModifyTheCorrectItemsOfComplexObjects
118 const auto& read_back() const
119 requires HasBack<T>
120 { return (*obj_).back(); }
122 /// Replace the whole object: `w.write(value)`. The primary write form. Write / write_renewable only.
123 /// @param value the new value (moved in). @complexity O(1) plus assigning @p value.
124 /// @alloc whatever `T`'s assignment allocates.
125 /// @concurrency exclusive: no reader or other writer coexists while this lease is valid. A writer that
126 /// observed `!valid()` must wait for it to flip back — writing while suspended races with the preempter.
127 /// @test Memory.WriteWaitsForReadersToDrain
128 /// @test Memory.LeasesModifyTheCorrectItemsOfComplexObjects
129 /// @systest MemoryCheatah.ScalarWriteReadModifyWrite
130 void write(T value)
131 requires is_write_mode<M>
132 { *obj_ = std::move(value); }
134 /// Set element @p index of an Indexed sequence: `w.write(i, v)`. Deduced. Write modes only.
135 /// @tparam V the element value type. @param index the position to set. @param value the new element.
136 /// @complexity `T::operator[]`. @alloc whatever the element assignment allocates.
137 /// @test Memory.LeasesModifyTheCorrectItemsOfComplexObjects
138 /// @systest MemoryCheatah.OwnerOfNdArrayElements
139 template <class V>
140 requires (is_write_mode<M> && Indexed<T>)
141 void write(std::size_t index, V&& value)
142 { (*obj_)[index] = std::forward<V>(value); }
144 /// Set @p key of a Mapping: `w.write(k, v)`. Deduced. Write modes only. @tparam K key type.
145 /// @tparam V value type. @param key the key to set (may insert). @param value the mapped value.
146 /// @complexity `T::operator[]` (may insert). @alloc whatever the insert/assignment allocates.
147 /// @test Memory.LeasesModifyTheCorrectItemsOfComplexObjects
148 template <class K, class V>
149 requires (is_write_mode<M> && Mapping<T> &&
150 std::convertible_to<K, typename T::key_type>)
151 void write(K&& key, V&& value)
152 { (*obj_)[std::forward<K>(key)] = std::forward<V>(value); }
154 /// Still ours? `true` until the owner asks us to yield (a writer waiting; an immediate-write). The
155 /// holder observing `!valid()` is how the owner learns it has paused. @return whether the lease is
156 /// still valid. @complexity O(1) plus the interrupt handler on first observing a stop. @alloc none.
157 /// @concurrency an atomic acquire load; the first observation of a stop acks and wakes the owner
158 /// (that one call briefly takes the owner's mutex) — polling from the holding thread is what lets a
159 /// drain/preempt make progress. The handle is not internally synchronized: poll from that thread.
160 /// @test Memory.ReadLeaseValidUntilAWriterNeedsIn
161 bool valid() const noexcept {
162 const bool v = gate_->valid.load(std::memory_order_acquire);
163 if (!v) {
164 if (!gate_->acked.exchange(true, std::memory_order_acq_rel) && gate_->wake)
165 gate_->wake(); // first ack: wake the owner's cv_ so it re-checks (we ack off its mutex)
166 if (on_interrupt_ && !fired_) { fired_ = true; on_interrupt_(); }
167 } else {
168 fired_ = false; // reset so a later preempt (write resume then re-preempt) can fire again
169 }
170 return v;
171 }
172 /// Asked to yield? The negation of valid(). @return `true` once the owner needs the lease back.
173 /// @complexity O(1). @alloc none.
174 /// @test Memory.ReadLeaseValidUntilAWriterNeedsIn
175 /// @systest MemoryCheatah.ReadLeaseValidState
176 bool expired() const noexcept { return !valid(); }
178 /// Register the "what to do if the owner interrupts me" handler; fires once per stop, in the holder's
179 /// thread, the first time `valid()` observes it — and again after a resume and a second preempt.
180 /// Replaces any previous handler. @complexity O(1).
181 /// @alloc one callback holder (the @p handler `std::function`, moved in — nothing beyond its own state).
182 /// @param handler the callback to run when the owner asks this lease to yield.
183 /// @concurrency never fires asynchronously — only from inside a `valid()` call, on the polling thread.
184 /// @test Memory.InterruptCallbackFiresWhenTheOwnerNeedsTheLeaseBack
185 void on_interrupt(std::function<void()> handler) { on_interrupt_ = std::move(handler); }
187private:
188 void drop() noexcept { if (release_) { auto r = std::move(release_); release_ = nullptr; r(); } }
189 void steal(Lease& o) noexcept {
190 obj_ = o.obj_; gate_ = std::move(o.gate_); release_ = std::move(o.release_);
191 on_interrupt_ = std::move(o.on_interrupt_); fired_ = o.fired_;
192 o.obj_ = nullptr; o.release_ = nullptr;
193 }
195 T* obj_{}; ///< direct pointer to the object (one deref).
196 std::shared_ptr<detail::Gate> gate_; ///< shared yield signal (per generation / active write).
197 std::function<void()> release_; ///< tells the owner "I'm done" (fired once, in dtor).
198 std::function<void()> on_interrupt_; ///< optional push handler when asked to yield.
199 mutable bool fired_ = false;///< has on_interrupt_ fired for the current stop?
200};
202} // namespace cheatah::memory