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 when that first happens.
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
34/// yield; the holder sets `acked` when it observes that (so the owner knows it has paused) and calls
35/// `wake` so the owner's condition variable re-checks. `wake` (owner-provided) synchronizes on the
36/// owner's mutex before notifying — the ack is one-shot, so an unsynchronized notify racing a waiter
37/// mid-predicate would be lost and the waiter would sleep forever.
38struct Gate {
39 std::atomic<bool> valid{true};
40 std::atomic<bool> acked{false};
41 std::function<void()> wake; ///< set by the owner to notify its cv_ when the holder first acks.
42};
43} // namespace detail
45/**
46 * @brief The only handle to an owned object — a move-only RAII lease granted by an `Owner`.
47 *
48 * `M` is `read` (shared) or `write`/`write_renewable` (exclusive). Reads go through `read(...)`; a
49 * write lease sets through `write(...)`. The lease holds a direct pointer to the object plus the
50 * release callback it fires on destruction. @tparam T the owned type (`Ownable`). @tparam M the mode.
51 */
52template <Ownable T, Mode M>
53class Lease {
54public:
55 /**
56 * The owner's grant path (called only by `Owner`). @complexity O(1). @alloc none.
57 * @param obj pointer to the owned object.
58 * @param gate the generation gate that carries the yield signal.
59 * @param release callback fired once on destruction to tell the owner this lease is done.
60 * @concurrency called by the owner with its coordinator mutex held — never construct one yourself.
61 * @test Memory.EveryAccessorReturnsARequestNotABareLease
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 Lease(Lease&& o) noexcept { steal(o); }
69 /// Move-assign: release ours, then take over @p o's grant. @param o source. @return `*this`.
70 /// @complexity O(1). @alloc none. @test Memory.EveryAccessorReturnsARequestNotABareLease
71 Lease& operator=(Lease&& o) noexcept { if (this != &o) { drop(); steal(o); } return *this; }
72 Lease(const Lease&) = delete;
73 Lease& operator=(const Lease&) = delete;
74 /// Release the lease (fires the owner's release callback if still held).
75 /// @complexity O(1). @alloc none.
76 /// @concurrency the release callback takes the owner's coordinator mutex and wakes waiting
77 /// requests (a draining writer proceeds once the last reader releases here).
78 /// @test Memory.WriteWaitsForReadersToDrain
79 ~Lease() { drop(); }
81 /// Read the whole object. Available on every lease. @return a `const T&` — never a copy or `T*`.
82 /// @complexity O(1). @alloc none.
83 /// @concurrency race-free while the lease is held: a writer cannot proceed until this lease
84 /// releases (yielding on `!valid()` is cooperative, not forced). No lock is taken here.
85 /// @test Memory.ReadReturnsAReferenceNotACopyOrPointer
86 const T& read() const { return *obj_; }
88 /// Read element @p index of an Indexed sequence: `r.read(i)`. Mirrors `w.write(i, v)`.
89 /// @param index the position to read. @return a const reference to the element.
90 /// @complexity `T::operator[]`. @alloc none.
91 /// @test Memory.LeasesModifyTheCorrectItemsOfComplexObjects
92 const auto& read(std::size_t index) const
93 requires Indexed<T>
94 { return (*obj_)[index]; }
96 /// Read the value at @p key of a Mapping: `r.read(k)`. Mirrors `w.write(k, v)`. Throws if absent
97 /// (reading a missing key never inserts). @tparam K a type convertible to the key type.
98 /// @param key the key to look up. @return a const reference to the mapped value.
99 /// @complexity `T::at`. @alloc none.
100 /// @test Memory.LeasesModifyTheCorrectItemsOfComplexObjects
101 template <class K>
102 requires (Mapping<T> && std::convertible_to<K, typename T::key_type>)
103 const auto& read(K&& key) const
104 { return (*obj_).at(std::forward<K>(key)); }
106 /// Read the first element (containers with `front()`: vector / deque / list / string …).
107 /// @return a const reference to the first element. @complexity O(1). @alloc none.
108 /// @test Memory.LeasesModifyTheCorrectItemsOfComplexObjects
109 const auto& read_front() const
110 requires HasFront<T>
111 { return (*obj_).front(); }
113 /// Read the last element (containers with `back()`).
114 /// @return a const reference to the last element. @complexity O(1). @alloc none.
115 /// @test Memory.LeasesModifyTheCorrectItemsOfComplexObjects
116 const auto& read_back() const
117 requires HasBack<T>
118 { return (*obj_).back(); }
120 /// Replace the whole object: `w.write(value)`. The primary write form. Write / write_renewable only.
121 /// @param value the new value (moved in). @complexity O(1) plus assigning @p value.
122 /// @alloc whatever `T`'s assignment allocates.
123 /// @concurrency exclusive: no reader or other writer coexists while this lease is valid. A
124 /// writer that observed `!valid()` (an immediate-write preempted it) must wait for `valid()` to
125 /// flip back before writing again — writing while suspended races with the immediate-write.
126 /// @test Memory.WriteWaitsForReadersToDrain
127 /// @test Memory.LeasesModifyTheCorrectItemsOfComplexObjects
128 /// @systest MemoryCheatah.ScalarWriteReadModifyWrite
129 void write(T value)
130 requires is_write_mode<M>
131 { *obj_ = std::move(value); }
133 /// Set element @p index of an Indexed sequence: `w.write(i, v)`. Deduced. Write modes only.
134 /// @tparam V the element value type. @param index the position to set. @param value the new element.
135 /// @complexity `T::operator[]`. @alloc whatever the element assignment allocates.
136 /// @test Memory.LeasesModifyTheCorrectItemsOfComplexObjects
137 /// @systest MemoryCheatah.OwnerOfNdArrayElements
138 template <class V>
139 requires (is_write_mode<M> && Indexed<T>)
140 void write(std::size_t index, V&& value)
141 { (*obj_)[index] = std::forward<V>(value); }
143 /// Set @p key of a Mapping: `w.write(k, v)`. Deduced. Write modes only. @tparam K key type.
144 /// @tparam V value type. @param key the key to set (may insert). @param value the mapped value.
145 /// @complexity `T::operator[]` (may insert). @alloc whatever the insert/assignment allocates.
146 /// @test Memory.LeasesModifyTheCorrectItemsOfComplexObjects
147 template <class K, class V>
148 requires (is_write_mode<M> && Mapping<T> &&
149 std::convertible_to<K, typename T::key_type>)
150 void write(K&& key, V&& value)
151 { (*obj_)[std::forward<K>(key)] = std::forward<V>(value); }
153 /// Still ours? `true` until the owner asks us to yield (a writer waiting; an immediate-write). The
154 /// holder observing `!valid()` is how the owner learns it has paused. @return whether the lease is
155 /// still valid. @complexity O(1). @alloc none.
156 /// @concurrency an atomic acquire load; the first observation of a stop acks and wakes the owner
157 /// (that one call briefly takes the owner's mutex) — polling this from the holding thread is what
158 /// lets a drain/preempt make progress. The lease handle itself is not internally synchronized:
159 /// poll from the thread that holds the lease.
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, in the holder's thread,
179 /// the first time `valid()` observes the stop. Replaces any previous handler. @complexity O(1).
180 /// @alloc one callback holder (the @p handler `std::function`, moved in — nothing beyond its own state).
181 /// @param handler the callback to run when the owner asks this lease to yield.
182 /// @concurrency the handler never fires asynchronously — only from inside a `valid()` call, on
183 /// the thread that polls it.
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