lib/alloc/arena/src/root.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 //! ## Shared temporary lifetimes
  2 //!
  3 //! Many temporary values created during an operation share an end-of-life
  4 //! point. The package `alloc_arena` provides lifetime-scoped memory owners that
  5 //! manage groups of allocations through an underlying allocator supplied by the
  6 //! caller, termed the *backing allocator*. The owner requests buffers from the
  7 //! backing allocator to serve individual slice requests and returns those
  8 //! buffers when the owner terminates at `deinit`. Physical memory reclamation
  9 //! depends on the policy of that chosen backing allocator.
 10 //!
 11 //! Callers interact with an owner through a borrowed `std.mem.Allocator`
 12 //! handle, which pairs a pointer to the owner instance with an allocation
 13 //! virtual table. For this handle to stay valid, both the owner instance and
 14 //! the state backing its child allocator must remain valid at stable memory
 15 //! locations while the handle is in use. The `std.mem.Allocator` value itself
 16 //! is a small interface handle that callers can pass and copy by value without
 17 //! pinning. Copying an owner struct duplicates its scalar fields rather than
 18 //! the underlying buffer ownership, and moving an owner object leaves existing
 19 //! borrowed handles pointing to the old address. In addition, bulk owner
 20 //! reclamation frees raw storage without invoking object destructors. Any
 21 //! resource managing external state, such as an open file descriptor, must be
 22 //! released explicitly before the owner terminates.
 23 //!
 24 //! When a temporary owner terminates, returning data to an outer scope requires
 25 //! a strategy for extending values past that lifetime boundary. One concrete
 26 //! approach is copy-out: duplicating the final result into memory managed by an
 27 //! independent allocator before destroying the temporary owner. In
 28 //! `lib/chic/src/language/engine/render.zig`, for example, `renderAlloc`
 29 //! instantiates a temporary `Arena` to build formatted diagnostic text, copies
 30 //! the finished bytes into the caller's allocator with `dupe`, and releases the
 31 //! temporary arena through a deferred `deinit`.
 32 //!
 33 //! ```zig
 34 //! const std = @import("std");
 35 //! const alloc_arena = @import("alloc_arena");
 36 //!
 37 //! fn renderDiagnostic(output: std.mem.Allocator) ![]u8 {
 38 //!     var arena = alloc_arena.Arena.init(output);
 39 //!     defer arena.deinit();
 40 //!
 41 //!     const temporary = try arena.allocator().dupe(u8, "diagnostic report payload");
 42 //!     return try output.dupe(u8, temporary);
 43 //! }
 44 //!
 45 //! test "owned output survives temporary arena teardown" {
 46 //!     const slice = try renderDiagnostic(std.testing.allocator);
 47 //!     defer std.testing.allocator.free(slice);
 48 //!
 49 //!     try std.testing.expectEqualStrings("diagnostic report payload", slice);
 50 //! }
 51 //! ```
 52 //!
 53 //! ## Reusing an arena
 54 //!
 55 //! The package provides `Arena` for workloads that benefit from reusing
 56 //! capacity. `Arena` tracks the standard library implementation
 57 //! `std.heap.ArenaAllocator` pinned at Zig 0.17.0-dev.1465+8b2d0ce21. In
 58 //! unobserved builds, `Arena` is a direct type alias for
 59 //! `std.heap.ArenaAllocator`. Under `-Dobserve-allocations=true`, it wraps that
 60 //! standard implementation to publish lifecycle and allocation events. Code
 61 //! designed to run in both configurations relies on the shared common surface:
 62 //! `init`, `allocator`, `deinit`, `queryCapacity`, `reset`, and `ResetMode`.
 63 //! Caller code targeting the unobserved configuration specifically may still
 64 //! access standard library features such as internal allocator state or
 65 //! promotion when those facilities are available.
 66 //!
 67 //! Capacity reporting in `Arena` measures buffer capacity rather than active
 68 //! allocations. The function `queryCapacity` computes the sum of payload
 69 //! capacities across all held buffer nodes, excluding internal node metadata
 70 //! headers. This total includes alignment padding between allocations and
 71 //! unallocated space at the end of buffers, but does not track the live byte
 72 //! count requested by callers.
 73 //!
 74 //! Calling `reset` terminates the lifetime of all existing allocations and
 75 //! invalidates every previously issued slice, regardless of whether the
 76 //! function returns `true` or `false`. The returned boolean indicates whether
 77 //! the requested retention or buffer reallocation succeeded. Even when `reset`
 78 //! returns `false`, the arena remains valid and ready to serve new allocations.
 79 //!
 80 //! The behavior of `reset` depends on the selected mode:
 81 //!
 82 //! - `free_all` releases all tracked nodes to the child allocator, resets
 83 //!   internal state to empty, and always returns `true`.
 84 //! - `retain_capacity` requests retention and preheating of current buffer
 85 //!   capacity to service subsequent requests. Retaining capacity does not
 86 //!   guarantee that future allocations will avoid calls to the backing
 87 //!   allocator.
 88 //! - `retain_with_limit` requests an upper bound on retained payload capacity
 89 //!   after the reset rather than establishing a lifetime memory budget. If
 90 //!   resizing or allocating a replacement node fails, `reset` returns `false`
 91 //!   and may leave an existing rewound node whose capacity exceeds the
 92 //!   requested limit.
 93 //!
 94 //! ## Fitting fresh requests in sequential scopes
 95 //!
 96 //! For tasks that do not require intermediate resets, `SequentialScope`
 97 //! provides an independent, sequential implementation. Its interface exposes
 98 //! `init`, `allocator`, `deinit`, and `queryCapacity`. Unlike `Arena`,
 99 //! `SequentialScope` does not provide a reset function or capacity retention
100 //! modes. It enforces no intrinsic fixed limit on total bytes or node counts,
101 //! so overall memory consumption remains bounded by backing allocator capacity.
102 //!
103 //! When serving an allocation request, `SequentialScope` checks the current
104 //! head buffer first. If that buffer contains sufficient room, the scope carves
105 //! the requested slice directly from it. When the head buffer cannot satisfy
106 //! the request, the owner attempts to enlarge that existing node in place
107 //! through the backing allocator's `rawResize`, testing a larger preferred size
108 //! before falling back to the exact required size. The scope never calls
109 //! `rawRemap` on an existing node, ensuring that addresses already issued to
110 //! callers stay in place.
111 //!
112 //! If in-place enlargement fails, the scope requests a fresh node from the
113 //! backing allocator using `rawAlloc`. This initial request covers the exact
114 //! space needed for the node header, the requested slice length, and
115 //! conservative alignment padding. Only after this exact allocation succeeds
116 //! does the scope attempt to expand the node to a larger preferred size through
117 //! `rawRemap`. Because no caller pointers have been issued into the fresh node
118 //! yet, relocation during this remap cannot disrupt caller addresses. If the
119 //! backing allocator refuses the remap, the smaller exact block still serves
120 //! the request, and the scope links the node at the list head before publishing
121 //! the pointer. Because request sizes and alignment requirements vary, new
122 //! nodes need not be successively larger than their predecessors.
123 //!
124 //! SequentialScope satisfies individual allocations out of existing nodes by
125 //! advancing a frontier offset within the current head node buffer. The
126 //! frontier is the byte offset delimiting the portion of the current head
127 //! buffer already used for allocations, including padding and holes left by
128 //! earlier frees, rather than the sum of active allocations. A free operation
129 //! rewinds this frontier only if the released slice ends at the current head
130 //! frontier. Consequently, shrinking a non-tail allocation succeeds without
131 //! reclaiming buffer space, and a non-tail allocation cannot grow in place.
132 //! Alignment gaps created by earlier requests may remain within the node.
133 //!
134 //! When callers invoke `rawRemap` on an issued allocation, the scope delegates
135 //! to `rawResize` and will not move an existing allocation. It does not invoke
136 //! child.rawResize or grow a backing node. The child allocator's rawResize is
137 //! instead used when satisfying a new allocation via allocateFromNode, which is
138 //! a separate operation. Calling `std.mem.Allocator.realloc` on an interior
139 //! slice may therefore fall back to an allocate-copy-free sequence. Finally,
140 //! scope deinit returns entire backing nodes in newest-first order, where
141 //! physical reuse of that memory depends on the child allocator.
142 //!
143 //! The following test verifies exact-first allocation when a backing allocator
144 //! lacks space for opportunistic growth. A fixed buffer allocator provides
145 //! storage for one node header and payload, but rejects the larger preferred
146 //! expansion. The scope accepts the exact allocation, consumes the backing
147 //! buffer, and releases its memory upon deinitialization. The test fixture's
148 //! three-word header reflects the current internal implementation layout rather
149 //! than a stable ABI:
150 //!
151 //! ```zig
152 //! const std = @import("std");
153 //! const alloc_arena = @import("alloc_arena");
154 //!
155 //! test "fresh exact allocation fits fixed child when preferred expansion is refused" {
156 //!     var storage: [96]u8 align(@alignOf(usize)) = undefined;
157 //!     var child = std.heap.FixedBufferAllocator.init(&storage);
158 //!
159 //!     const header_len = 3 * @sizeOf(usize);
160 //!     const payload_len = storage.len - header_len;
161 //!
162 //!     {
163 //!         var scope = alloc_arena.SequentialScope.init(child.allocator());
164 //!         defer scope.deinit();
165 //!
166 //!         const scoped = scope.allocator();
167 //!         const allocation = try scoped.alignedAlloc(u8, .of(usize), payload_len);
168 //!
169 //!         try std.testing.expectEqual(payload_len, allocation.len);
170 //!         try std.testing.expectEqual(payload_len, scope.queryCapacity());
171 //!         try std.testing.expectEqual(storage.len, child.end_index);
172 //!
173 //!         try std.testing.expectError(error.OutOfMemory, scoped.alloc(u8, 1));
174 //!     }
175 //!
176 //!     try std.testing.expectEqual(@as(usize, 0), child.end_index);
177 //! }
178 //! ```
179 //!
180 //! ## Allocation observation and concurrency
181 //!
182 //! When compiled with `-Dobserve-allocations=true`, both `Arena` and
183 //! `SequentialScope` publish allocation events and lifecycle spans to an event
184 //! consumer through `alloc_observe`. An event's `succeeded` flag indicates
185 //! whether the allocator operation succeeded, rather than whether physical
186 //! bytes were released by the host operating system. Calling `Arena.reset`
187 //! emits an invalidate lifecycle span for the previous generation and advances
188 //! the generation counter, even when `reset` returns `false`. If a subsequent
189 //! allocation receives a memory address previously held by an earlier
190 //! allocation, the new slice represents an independent lifetime rather than a
191 //! revival of the earlier pointer. Calling `Arena.deinit` emits an end
192 //! lifecycle span.
193 //!
194 //! The standard `Arena` allocation vtable supports concurrent allocation when
195 //! the underlying child allocator is thread-safe and the arena remains pinned
196 //! at a stable address. When observation is active, observation callbacks must
197 //! also support concurrent delivery. In contrast, `reset` and `deinit` mutate
198 //! internal state and require exclusive access. The `queryCapacity` method only
199 //! reads state rather than mutating it, but callers must still exclude
200 //! concurrent mutation during the query.
201 //!
202 //! `SequentialScope` is not thread-safe and contains no internal locks or
203 //! runtime concurrency guards, even when wrapped for observation. Callers must
204 //! ensure all operations remain sequential. If multiple threads interact with
205 //! the same scope, the caller must provide external synchronization to
206 //! serialize access.
207 //!
208 //! ## Related lifetime models
209 //!
210 //! Lifetime-scoped allocation groups object lifetimes under explicit
211 //! programmatic control. In [Hanson
212 //! (1990)](https://drhanson.s3.amazonaws.com/storage/documents/fastalloc.pdf),
213 //! dynamic storage allocation organizes memory through linked buffers that are
214 //! deallocated together or retained across cycles. By contrast, static region
215 //! inference, as formalized by [Tofte and Talpin
216 //! (1994)](https://www.cs.cmu.edu/afs/cs/academic/class/15745-s06/web/handouts/tofte-popl94.pdf),
217 //! deduces region lifetimes automatically at compile time and inserts
218 //! allocation and deallocation operations without programmer intervention. The
219 //! package `alloc_arena` operates within the manual model: callers determine
220 //! lifetime boundaries at runtime, manage owner lifecycles explicitly, and
221 //! select when to release or retain storage.
222 
223 const arena = @import("arena.zig");
224 const sequential = @import("sequential.zig");
225 
226 /// Exports the primary arena allocator, which groups multiple allocations under
227 /// a single caller-managed lifetime and supports resetting capacity for reuse.
228 /// At compile time, this alias resolves to ObservedArena when allocation
229 /// observation is enabled and to std.heap.ArenaAllocator when disabled.
230 pub const Arena = arena.Arena;
231 /// The root SequentialScope export provides a caller-controlled sequential
232 /// temporary lifetime ended by deinit, and the caller must serialize all use.
233 /// For fresh nodes, the initial allocation covers the current request including
234 /// the node header and conservative alignment room before optional spare
235 /// capacity expansion, meaning exact-first sizing is not payload-only and is
236 /// not the order for growing an existing node.
237 pub const SequentialScope = sequential.SequentialScope;