lib/alloc/phase/src/allocator.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 //! ## Package overview
2 //!
3 //! Systems programs often need to separate startup storage preparation from
4 //! subsequent operational work. Preparing a bounded data structure before
5 //! serving repeated tasks is a motivating case: an owner allocates tables and
6 //! buffers during startup, after which subsequent service loops must execute
7 //! without performing unexpected memory allocations. The runtime phase
8 //! allocators form the lifecycle-enforcement component of `alloc_phase`, which
9 //! also exports separate capacity declarations and `OneRegion` input storage.
10 //!
11 //! ## Storage boundaries and scope
12 //!
13 //! A caller places a phase allocator between an owner and a backing memory
14 //! provider to detect allocator vtable calls routed through the supplied handle
15 //! after the startup boundary. The guard checks raw calls routed through its
16 //! own handle and counts violations during forbidden phases. Calls routed
17 //! through separate allocator handles, global allocators, direct
18 //! operating-system interfaces, or the internal memory management of the
19 //! backing allocator sit outside its coverage, because the guard does not
20 //! provide automatic global interception. High-level zero-length or no-op
21 //! operations on `std.mem.Allocator` can return before reaching a vtable
22 //! function, so the wrapper observes only the operations that reach its vtable.
23 //!
24 //! The phase guard enforces no startup byte budget and offers no proof that all
25 //! possible runtime inputs fit within preallocated buffers. Sizing storage and
26 //! managing overload remain the responsibility of data structure owners and
27 //! dedicated capacity declarations.
28 //!
29 //! ## Lifecycle phases and handle ownership
30 //!
31 //! Each phase allocator tracks state through an internal control block
32 //! allocated from the backing allocator. The control block occupies one of
33 //! three explicit lifecycle phases: `initialization`, `steady`, or `teardown`.
34 //!
35 //! A caller initializes the allocator by invoking `init(backing)`, which
36 //! allocates a `Control` block from the caller-supplied backing allocator. This
37 //! call returns `error.OutOfMemory` when the backing allocator cannot satisfy
38 //! the request. The control block retains the backing allocator and sets all
39 //! violation counters to zero. While in `initialization`, the caller invokes
40 //! `initializationAllocator()` to obtain a `std.mem.Allocator` handle.
41 //!
42 //! Lifecycle transitions advance in one direction only:
43 //!
44 //! - `seal()` moves the allocator from `initialization` to `steady`. This
45 //! transition occurs exactly once and cannot be reset.
46 //! - `beginTeardown()` moves the allocator from `steady` to `teardown`.
47 //! - `abortInitialization()` moves the allocator from `initialization` directly
48 //! to `teardown` when storage preparation encounters an error. Calling
49 //! `abortInitialization()` after `seal()` is invalid, and ordinary
50 //! `beginTeardown()` cannot substitute for `abortInitialization()` during a
51 //! startup failure.
52 //!
53 //! Attempting an invalid phase transition, repeating an existing transition,
54 //! requesting an allocator handle in the wrong phase, or invoking `deinit()`
55 //! before reaching `teardown` causes an immediate panic in all build modes.
56 //! Callers inspect the current lifecycle state by calling `phase()` while the
57 //! control block remains live.
58 //!
59 //! When work is complete, invoking `deinit()` requires the phase to be
60 //! `teardown`. Calling `deinit()` destroys only the `Control` block through the
61 //! backing allocator and marks the receiver undefined. Entering teardown does
62 //! not invalidate user allocation contents or free user storage. The phase
63 //! allocator does not manage user allocation lifetimes, perform automatic bulk
64 //! cleanup, or run object destructors: callers must explicitly free all user
65 //! allocations before destroying the control block. The backing allocator
66 //! remains caller-owned, and the caller must ensure it remains valid and stable
67 //! throughout the lifespan of the control block and any active allocations.
68 //!
69 //! The public wrapper struct stores a pointer to the separately allocated
70 //! `Control` block. When the wrapper creates a `std.mem.Allocator` handle, the
71 //! handle's `ptr` field points directly to the `Control` block rather than to
72 //! the wrapper's local address. Moving or copying the wrapper preserves all
73 //! existing handles. Copied wrappers act as aliases sharing the underlying
74 //! phase, counters, and backing allocator. An allocator handle obtained during
75 //! initialization reads the shared phase on every raw operation, so retaining
76 //! an earlier handle does not bypass a subsequent seal or teardown. The caller
77 //! must destroy the shared control block exactly once. Because the control
78 //! block provides no internal reference counting or protection against repeated
79 //! deinitialization, all aliases and issued handles must stop being used before
80 //! `deinit()` runs.
81 //!
82 //! ## Raw operations and violation policies
83 //!
84 //! A phase violation occurs whenever a raw vtable call arrives during a phase
85 //! that forbids the requested operation. Violations are recorded regardless of
86 //! whether the underlying backing allocator would have succeeded.
87 //!
88 //! The phase guard defines operation permissions at the raw vtable level:
89 //!
90 //! | Operation | Initialization Phase | Steady Phase | Teardown Phase |
91 //! | :--- | :--- | :--- | :--- |
92 //! | `alloc` | Permitted | Violation | Violation |
93 //! | `resize` | Permitted | Violation | Violation |
94 //! | `remap` | Permitted | Violation | Violation |
95 //! | `free` | Permitted | Violation | Permitted |
96 //!
97 //! An ordinary allocation failure during initialization, such as the backing
98 //! allocator returning out of memory, is a runtime error rather than a phase
99 //! violation. During teardown, attempting `alloc`, `resize`, or `remap` counts
100 //! as a phase violation. This restriction includes shrinking existing
101 //! allocations: `resize` and `remap` requests remain forbidden during teardown
102 //! even when requesting a smaller buffer.
103 //!
104 //! The package exports two public allocator types with distinct violation
105 //! policies. `SealedPhaseAllocator` halts execution upon detecting phase
106 //! misuse: it increments the violation counter and immediately panics before
107 //! dispatching the call to the backing allocator. It does not return null or
108 //! `error.OutOfMemory` to recover from a violation.
109 //!
110 //! By contrast, `ObservingPhaseAllocator` records unexpected operations while
111 //! allowing execution to proceed. Upon detecting a violation, it increments the
112 //! violation counter, attempts to record the return address in an available
113 //! call site slot, and forwards the operation to the backing allocator,
114 //! returning the backing allocator's result. That forwarded call can still fail
115 //! at the backing allocator if backing memory is exhausted. Both wrapper types
116 //! enforce identical lifecycle transition rules: an invalid transition or
117 //! premature deinitialization panics immediately under
118 //! `ObservingPhaseAllocator` just as it does under `SealedPhaseAllocator`.
119 //!
120 //! ## Concurrency, observation, and integration patterns
121 //!
122 //! Atomic fields govern the internal scalar state of the control block: the
123 //! phase tag, the four violation counters, and the return address slots each
124 //! use atomic values. These primitives protect their own scalar values, but
125 //! they do not make the backing allocator thread-safe or synchronize the
126 //! broader application lifecycle. The underlying backing allocator must be safe
127 //! for concurrent dispatch if calls overlap. Because `seal()` updates the phase
128 //! tag without draining or waiting for in-flight operations, a thread that read
129 //! the `initialization` phase can still dispatch and complete a backing
130 //! allocation after another thread has transitioned the state to `steady`. When
131 //! a strict boundary is required, the caller must coordinate startup completion
132 //! and phase changes externally. All threads and aliases must reach quiescence
133 //! before the control block is destroyed.
134 //!
135 //! The `violations()` function loads four native atomic counters separately
136 //! into `u64` snapshot fields `allocations`, `resizes`, `remaps`, and frees.
137 //! The `total()` method computes their sum using ordinary integer addition.
138 //! Although individual loads are atomic, concurrent updates can produce a
139 //! mixture of times across the fields. Callers can stop writers to obtain
140 //! stable values across the counters.
141 //!
142 //! For callers tracking where violations happen, `ObservingPhaseAllocator`
143 //! records up to 16 distinct nonzero raw return addresses, fixed by
144 //! `violation_site_capacity`. When additional call sites appear after these 16
145 //! slots fill, the allocator omits the new addresses while continuing to
146 //! increment the violation counters. The recorded list provides an
147 //! arrival-dependent sample of raw addresses rather than stack traces, symbol
148 //! names, or per-site counts, and concurrent inspection is not frozen during
149 //! recording. Calling `violationSites()` returns an owned snapshot value. The
150 //! `slice()` method borrows directly from this snapshot value, meaning the
151 //! caller must keep the snapshot alive and stable while reading the slice. For
152 //! `SealedPhaseAllocator`, `violationSites()` returns an empty snapshot because
153 //! sealed control blocks allocate no site storage. The constant
154 //! `control_footprint` reports the byte size and alignment of the underlying
155 //! `Control` block for the target architecture: it does not measure the wrapper
156 //! struct, total allocator footprint, backing overhead, or startup allocation
157 //! totals.
158 //!
159 //! Three consumers illustrate these patterns:
160 //!
161 //! - In `lib/chic/src/language/testing/allocation.zig`, the test initializes
162 //! pattern hydration storage through a guarded handle, seals before
163 //! exercising hydration, checks for unchanged pointers and zero counters, and
164 //! `frees` buffers during teardown before guard deinitialization.
165 //! - In `lib/hypothesis/src/database.zig`, the test initializes a replay cursor
166 //! through `ObservingPhaseAllocator`, seals for iteration, checks for
167 //! unchanged buffer pointers and zero violations, and `frees` the cursor
168 //! through a teardown handle.
169 //! - In `lib/alloc/src/process.zig`, the consumer selects off, observe, or seal
170 //! with the `lib/alloc` build option `-Dallocator-phase` and wraps its chosen
171 //! process backing allocator. This is not automatic process-wide
172 //! interception, since tracking applies only to code routed through the
173 //! wrapped instance. The `alloc_phase` observing type keeps counters and
174 //! sites independently of `alloc_observe` `Sink` and `Session` callbacks as
175 //! well as -Dobserve-allocations.
176 //!
177 //! ## Examples
178 //!
179 //! The following self-contained examples illustrate phase lifecycle management
180 //! and violation auditing for pinned Zig version `0.17.0-dev.1465+8b2d0ce21`.
181 //!
182 //! ### Sealed phase lifecycle and wrapper aliasing
183 //!
184 //! This example demonstrates preparing memory during initialization, aliasing
185 //! the wrapper, and transitioning to steady state through that alias. The
186 //! cleanup block aborts initialization on failure, or transitions to teardown,
187 //! frees user storage through `teardownAllocator()`, and destroys the shared
188 //! control block.
189 //!
190 //! ```zig
191 //! const std = @import("std");
192 //! const alloc_phase = @import("alloc_phase");
193 //!
194 //! test "sealed phase preparation, wrapper aliasing, and cleanup" {
195 //! var guard = try alloc_phase.SealedPhaseAllocator.init(std.testing.allocator);
196 //! var memory: ?[]u8 = null;
197 //! defer {
198 //! switch (guard.phase()) {
199 //! .initialization => guard.abortInitialization(),
200 //! .steady => guard.beginTeardown(),
201 //! .teardown => {},
202 //! }
203 //! if (memory) |slice| {
204 //! guard.teardownAllocator().free(slice);
205 //! }
206 //! guard.deinit();
207 //! }
208 //!
209 //! const init_allocator = guard.initializationAllocator();
210 //! const slice = try init_allocator.alloc(u8, 8);
211 //! memory = slice;
212 //! slice[0] = 42;
213 //!
214 //! var alias = guard;
215 //! alias.seal();
216 //!
217 //! try std.testing.expect(guard.phase() == .steady);
218 //! try std.testing.expectEqual(@as(u8, 42), memory.?[0]);
219 //! try std.testing.expectEqual(@as(u64, 0), guard.violations().total());
220 //! }
221 //! ```
222 //!
223 //! Invoking `deinit()` on either `guard` or `alias` destroys the underlying
224 //! `Control` block and marks that wrapper undefined. The copied alias must not
225 //! be separately deinitialized, and all aliases become unusable once the shared
226 //! control block is destroyed. Entering teardown does not free user allocations
227 //! or invalidate their contents: the user slice becomes invalid only after its
228 //! explicit free through `teardownAllocator().free(slice)`.
229 //!
230 //! ### Observing forbidden allocation attempts
231 //!
232 //! This example demonstrates an audit with `ObservingPhaseAllocator` when the
233 //! backing allocator is configured to reject an allocation. The test seals the
234 //! allocator and calls `rawAlloc` directly, showing that the attempt increments
235 //! the violation count and forwards to the backing allocator without panicking.
236 //!
237 //! ```zig
238 //! const std = @import("std");
239 //! const alloc_phase = @import("alloc_phase");
240 //!
241 //! test "observing phase counts forbidden attempt when backing allocator rejects" {
242 //! var backing = std.testing.FailingAllocator.init(std.testing.allocator, .{});
243 //! var guard = try alloc_phase.ObservingPhaseAllocator.init(backing.allocator());
244 //! defer {
245 //! switch (guard.phase()) {
246 //! .initialization => guard.abortInitialization(),
247 //! .steady => guard.beginTeardown(),
248 //! .teardown => {},
249 //! }
250 //! guard.deinit();
251 //! }
252 //!
253 //! const allocator = guard.initializationAllocator();
254 //! guard.seal();
255 //!
256 //! backing.fail_index = backing.alloc_index;
257 //! const result = allocator.rawAlloc(8, .@"1", @returnAddress());
258 //! try std.testing.expect(result == null);
259 //!
260 //! const violations = guard.violations();
261 //! try std.testing.expectEqual(@as(u64, 1), violations.allocations);
262 //! try std.testing.expectEqual(@as(u64, 1), violations.total());
263 //! }
264 //! ```
265 //!
266 //! Because the backing allocator rejected the call, no user memory was
267 //! allocated, so no slice cleanup is required.
268
269 const std = @import("std");
270 const capacity = @import("capacity");
271
272 const Allocator = std.mem.Allocator;
273 const Alignment = std.mem.Alignment;
274 const Phase = capacity.Phase;
275
276 /// Represents a by-value snapshot of four operation counters: allocations,
277 /// resizes, remaps, and frees. Each field is an unsigned 64-bit integer
278 /// recording the number of times caller code attempted that raw allocator
279 /// operation outside the permitted lifecycle phase. These values count
280 /// attempted operations rather than allocated byte quantities or successful
281 /// allocations. Because the underlying control block maintains these counts in
282 /// native atomic variables that are read individually, a snapshot retrieved
283 /// during concurrent operations does not represent a single coherent point in
284 /// time across all four fields.
285 pub const PhaseViolations = struct {
286 allocations: u64 = 0,
287 resizes: u64 = 0,
288 remaps: u64 = 0,
289 frees: u64 = 0,
290
291 /// Returns the combined count of allocations, resizes, remaps, and frees as
292 /// an unsigned 64-bit integer. The calculation uses standard addition, so
293 /// integer overflow follows Zig build safety rules rather than saturating
294 /// or reporting an error.
295 pub fn total(self: PhaseViolations) u64 {
296 return self.allocations + self.resizes + self.remaps + self.frees;
297 }
298 };
299
300 /// Defines the fixed capacity of 16 distinct nonzero return addresses recorded
301 /// by `ObservingPhaseAllocator`. Duplicate call sites do not consume additional
302 /// slots, and zero addresses are ignored. When new distinct sites arrive after
303 /// all 16 slots are full, the observing guard omits the new addresses while
304 /// continuing to increment its violation counters. `SealedPhaseAllocator` does
305 /// not record return addresses and allocates no site storage.
306 pub const violation_site_capacity = 16;
307
308 /// Represents an owned by-value snapshot of up to 16 distinct nonzero return
309 /// addresses captured during phase violations. The structure holds raw return
310 /// addresses and an active count without resolving symbols, capturing full
311 /// stack traces, or recording per-site occurrence counts. Because concurrent
312 /// threads race to register unrecorded sites into fixed slots, the retained set
313 /// reflects arrival order rather than a complete or ordered execution trace.
314 /// `SealedPhaseAllocator` maintains no site storage and always returns an empty
315 /// snapshot.
316 pub const ViolationSites = struct {
317 addresses: [violation_site_capacity]usize = @splat(0),
318 count: u8 = 0,
319
320 /// Borrows the populated prefix `addresses[0..count]` from this snapshot as
321 /// a slice of addresses. The returned slice directly references the memory
322 /// of this `ViolationSites` instance rather than the guard control block,
323 /// so the caller must keep the snapshot alive and unmoved while using the
324 /// slice.
325 pub fn slice(self: *const ViolationSites) []const usize {
326 return self.addresses[0..self.count];
327 }
328 };
329
330 const ViolationBehavior = enum {
331 panic,
332 record,
333 observe,
334 };
335
336 /// Enforces strict lifecycle boundaries around a backing allocator by panicking
337 /// whenever caller code attempts a raw operation outside the permitted phase.
338 /// During initialization, the guard permits all four raw operations:
339 /// allocation, resizing, remapping, and freeing. Once sealed to the steady
340 /// phase, every raw allocator call is considered a violation: the guard
341 /// increments the corresponding violation counter and explicitly panics before
342 /// dispatching to the backing allocator, never returning `error.OutOfMemory` to
343 /// signal phase errors. During teardown, freeing remains permitted, but
344 /// allocation, remapping, and resizing are treated as violations and panic,
345 /// including requests that attempt to shrink existing allocations.
346 ///
347 /// The guard stores its state in a separately allocated control block, allowing
348 /// wrapper copies and issued `std.mem.Allocator` handles to share the same
349 /// lifecycle phase, counters, and backing allocator. Because `Allocator.ptr`
350 /// references this shared control block directly, moving or copying the wrapper
351 /// preserves issued handles. Lifecycle transitions occur through explicit calls
352 /// to `seal`, `abortInitialization`, and `beginTeardown`, and any invalid or
353 /// duplicate transition panics in all build modes. This guard intercepts only
354 /// raw calls routed through its own handles: it does not observe global
355 /// allocator usage, distinct allocator handles, operating system calls, or
356 /// backing allocator internal operations. It enforces no startup allocation
357 /// byte budget and provides no guarantee that runtime workloads fit
358 /// preallocated memory.
359 pub const SealedPhaseAllocator = PhaseAllocator(.panic);
360 /// Audits phase compliance across an application lifecycle by recording
361 /// unauthorized raw operations in local counters and call site slots while
362 /// forwarding those requests to the backing allocator. When a caller attempts
363 /// an operation outside the permitted phase, the guard increments the
364 /// corresponding violation counter, attempts to record the return address in
365 /// one of 16 call site slots, and forwards the invocation to the backing
366 /// allocator. If the backing allocator fails the request, the violation counter
367 /// remains incremented because the attempt itself violated lifecycle rules.
368 ///
369 /// The guard stores its state in a separately allocated control block, allowing
370 /// wrapper copies and issued `std.mem.Allocator` handles to share the same
371 /// lifecycle phase, counters, and backing allocator. This control block
372 /// includes atomic return address slots, giving it a larger memory footprint
373 /// than `SealedPhaseAllocator`. The guard enforces the same lifecycle rules as
374 /// `SealedPhaseAllocator`, so invalid phase transitions, duplicate transitions,
375 /// handle requests in the wrong phase, and premature deinitialization trigger
376 /// an explicit panic in all build modes. This mechanism operates entirely
377 /// through its own allocator handles and functions independently from
378 /// `alloc_observe` event sessions.
379 pub const ObservingPhaseAllocator = PhaseAllocator(.observe);
380
381 fn PhaseAllocator(comptime behavior: ViolationBehavior) type {
382 return struct {
383 control: *Control,
384
385 const Self = @This();
386 const SiteStorage = if (behavior == .observe)
387 [violation_site_capacity]std.atomic.Value(usize)
388 else
389 void;
390 const Control = struct {
391 backing: Allocator,
392 phase_state: std.atomic.Value(Phase),
393 allocation_violations: std.atomic.Value(usize),
394 resize_violations: std.atomic.Value(usize),
395 remap_violations: std.atomic.Value(usize),
396 free_violations: std.atomic.Value(usize),
397 violation_sites: SiteStorage,
398 };
399
400 /// Provides the compile-time size and alignment of the separately
401 /// allocated control block for this guard type on the target
402 /// architecture. The control block for `ObservingPhaseAllocator`
403 /// contains 16 atomic return address slots, whereas
404 /// `SealedPhaseAllocator` omits site storage. This value measures only
405 /// the internal control structure itself: it does not reflect the small
406 /// wrapper struct, total allocator runtime footprint, backing allocator
407 /// bookkeeping overhead, or user storage capacity.
408 pub const control_footprint: struct { size: usize, alignment: usize } = .{
409 .size = @sizeOf(Control),
410 .alignment = @alignOf(Control),
411 };
412
413 /// Allocates a shared control block from the backing allocator, sets
414 /// the initial phase to initialization, and returns a guard handle. If
415 /// the control block allocation fails, the function returns
416 /// `error.OutOfMemory` without constructing a guard. Initialization
417 /// clears all violation counters, and for `ObservingPhaseAllocator` it
418 /// also clears the 16 return address slots. The caller retains
419 /// ownership of the backing allocator and must ensure it outlives both
420 /// the control block and any active allocations.
421 pub fn init(backing: Allocator) Allocator.Error!Self {
422 const control = try backing.create(Control);
423 control.* = .{
424 .backing = backing,
425 .phase_state = std.atomic.Value(Phase).init(.initialization),
426 .allocation_violations = std.atomic.Value(usize).init(0),
427 .resize_violations = std.atomic.Value(usize).init(0),
428 .remap_violations = std.atomic.Value(usize).init(0),
429 .free_violations = std.atomic.Value(usize).init(0),
430 .violation_sites = if (comptime behavior == .observe) @splat(.init(0)) else {},
431 };
432 return .{ .control = control };
433 }
434
435 /// Destroys the shared control block through the backing allocator
436 /// after verifying that the guard has reached teardown. Calling this
437 /// method outside teardown triggers an explicit panic in all build
438 /// modes. Deinitialization destroys only the control block: it does not
439 /// free user allocations or run object cleanups, which must be
440 /// performed by the caller before deinitialization. Because the control
441 /// block provides neither reference counting nor double-free
442 /// protection, the caller must ensure that all threads, aliases, and
443 /// allocator handles have ceased operations before ending the control
444 /// block exactly once. The method sets this wrapper to undefined and
445 /// leaves any existing wrapper copies dangling.
446 pub fn deinit(self: *Self) void {
447 self.requirePhase(.teardown);
448 const control = self.control;
449 const backing = control.backing;
450 self.* = undefined;
451 backing.destroy(control);
452 }
453
454 /// Returns a `std.mem.Allocator` handle configured for preparation work
455 /// during the initialization phase. The method panics explicitly in all
456 /// build modes if called when the guard is not in initialization. The
457 /// returned handle sets its context pointer directly to the shared
458 /// control block, so copying or moving the wrapper preserves handle
459 /// validity. Because raw vtable operations read the shared phase state
460 /// on each invocation, retaining an initialization handle across a
461 /// phase transition does not grant lasting permission to allocate. The
462 /// caller must keep both the control block and the backing allocator
463 /// live while using the returned handle.
464 pub fn initializationAllocator(self: *Self) Allocator {
465 self.requirePhase(.initialization);
466 return self.allocator();
467 }
468
469 /// Transitions the guard atomically from initialization to the steady
470 /// phase. The operation succeeds exactly once and panics explicitly in
471 /// all build modes if the guard is not in initialization or has already
472 /// been sealed. Sealing does not drain or wait for in-flight backing
473 /// allocations initiated during startup, does not free acquired
474 /// storage, and does not decouple copied wrappers. When application
475 /// logic requires an exact boundary between storage setup and steady
476 /// execution, the caller must coordinate concurrent threads externally
477 /// before invoking this method.
478 pub fn seal(self: *Self) void {
479 const previous = self.control.phase_state.cmpxchgStrong(
480 .initialization,
481 .steady,
482 .acq_rel,
483 .acquire,
484 );
485 if (previous != null) {
486 @panic("sealed phase allocator can only seal initialization");
487 }
488 }
489
490 /// Transitions the guard atomically from initialization directly to
491 /// teardown when preparation fails before sealing. The method panics
492 /// explicitly in all build modes if called outside initialization or
493 /// after the guard has been sealed. Advancing directly to teardown
494 /// enables error recovery, allowing the caller to obtain a teardown
495 /// allocator handle and free storage acquired during partial
496 /// initialization before calling deinitialization. This transition does
497 /// not release user allocations or run object destructors
498 /// automatically, leaving resource cleanup to the caller.
499 pub fn abortInitialization(self: *Self) void {
500 if (self.control.phase_state.cmpxchgStrong(.initialization, .teardown, .acq_rel, .acquire) != null) {
501 @panic("sealed phase allocator can only abort initialization");
502 }
503 }
504
505 /// Transitions a live control block atomically from steady to teardown.
506 /// Calling this method from any phase other than steady, or repeating
507 /// the transition while the control block is alive, triggers an
508 /// explicit panic in all build modes. Failed startup sequences must
509 /// call `abortInitialization` instead. During teardown, raw free calls
510 /// are permitted, whereas all other raw operations, including shrink
511 /// and other resizes, count as phase violations under the chosen guard
512 /// policy. The transition does not drain or synchronize concurrent
513 /// tasks, so callers must finish all steady-state work externally
514 /// before starting teardown.
515 pub fn beginTeardown(self: *Self) void {
516 if (self.control.phase_state.cmpxchgStrong(.steady, .teardown, .acq_rel, .acquire) != null) {
517 @panic("sealed phase allocator teardown is terminal");
518 }
519 }
520
521 /// Returns a `std.mem.Allocator` handle configured for teardown
522 /// operations. The method panics explicitly in all build modes if the
523 /// guard has not reached the teardown phase. The returned handle shares
524 /// the control block pointer with earlier handles: it dispatches raw
525 /// free requests directly to the backing allocator, but treats
526 /// allocation, remapping, and resizing as phase violations even when a
527 /// resize attempts to shrink memory. The underlying release of backing
528 /// storage follows the policies of the backing allocator.
529 pub fn teardownAllocator(self: *Self) Allocator {
530 self.requirePhase(.teardown);
531 return self.allocator();
532 }
533
534 /// Reads the current lifecycle phase from a live control block using an
535 /// atomic acquire load. This operation provides acquire ordering for
536 /// observed state, but it does not wait for in-flight operations to
537 /// drain or confirm that concurrent threads have completed a phase. For
538 /// example, a thread may read the initialization phase and then
539 /// dispatch an allocation after a concurrent transition has already
540 /// sealed the allocator.
541 pub fn phase(self: *const Self) Phase {
542 return self.control.phase_state.load(.acquire);
543 }
544
545 /// Loads the four internal violation counters individually with acquire
546 /// ordering and returns them as a `PhaseViolations` snapshot. Because
547 /// each native atomic counter is read in a separate operation, the
548 /// returned values do not form a coherent instantaneous snapshot across
549 /// all four fields if allocator calls execute concurrently. The
550 /// counters provide neither a reset mechanism nor a saturation policy,
551 /// so values increment continuously and can wrap if operations exceed
552 /// numeric bounds. To obtain consistent counter readings within their
553 /// numeric range, the caller must ensure all mutating threads are
554 /// quiescent before inspecting violations.
555 pub fn violations(self: *const Self) PhaseViolations {
556 return .{
557 .allocations = @intCast(self.control.allocation_violations.load(.acquire)),
558 .resizes = @intCast(self.control.resize_violations.load(.acquire)),
559 .remaps = @intCast(self.control.remap_violations.load(.acquire)),
560 .frees = @intCast(self.control.free_violations.load(.acquire)),
561 };
562 }
563
564 /// Returns an owned by-value `ViolationSites` snapshot containing up to
565 /// 16 distinct nonzero return addresses captured during phase
566 /// violations. When invoked on `ObservingPhaseAllocator`, the method
567 /// scans its internal atomic site slots sequentially. Concurrent writes
568 /// can therefore yield a partial view rather than an atomic snapshot of
569 /// the entire table. Once all 16 slots are occupied, any new distinct
570 /// addresses are omitted from the buffer while violation counters
571 /// continue to advance. Calling the method on `SealedPhaseAllocator`
572 /// always returns an empty snapshot because sealed allocators allocate
573 /// no call site storage.
574 pub fn violationSites(self: *const Self) ViolationSites {
575 var sites = ViolationSites{};
576 if (comptime behavior != .observe) return sites;
577 for (&self.control.violation_sites) |*slot| {
578 const address = slot.load(.acquire);
579 if (address == 0) continue;
580 sites.addresses[sites.count] = address;
581 sites.count += 1;
582 }
583 return sites;
584 }
585
586 fn allocator(self: *Self) Allocator {
587 return .{ .ptr = self.control, .vtable = &vtable };
588 }
589
590 fn requirePhase(self: *const Self, expected: Phase) void {
591 if (self.phase() != expected) {
592 @panic("sealed phase allocator used from the wrong phase");
593 }
594 }
595
596 fn violation(self: *Self, operation: Operation, return_address: usize) void {
597 switch (operation) {
598 .allocation => _ = self.control.allocation_violations.fetchAdd(1, .monotonic),
599 .resize => _ = self.control.resize_violations.fetchAdd(1, .monotonic),
600 .remap => _ = self.control.remap_violations.fetchAdd(1, .monotonic),
601 .free => _ = self.control.free_violations.fetchAdd(1, .monotonic),
602 }
603 self.recordViolationSite(return_address);
604 if (comptime behavior == .panic) {
605 switch (operation) {
606 .allocation => @panic("allocation attempted during sealed phase"),
607 .resize => @panic("resize attempted during sealed phase"),
608 .remap => @panic("remap attempted during sealed phase"),
609 .free => @panic("free attempted during sealed phase"),
610 }
611 }
612 }
613
614 fn recordViolationSite(self: *Self, return_address: usize) void {
615 if (comptime behavior != .observe) return;
616 if (return_address == 0) return;
617 for (&self.control.violation_sites) |*slot| {
618 const existing = slot.load(.acquire);
619 if (existing == return_address) return;
620 if (existing != 0) continue;
621 const raced = slot.cmpxchgStrong(0, return_address, .acq_rel, .acquire) orelse return;
622 if (raced == return_address) return;
623 }
624 }
625
626 fn rawAlloc(
627 context: *anyopaque,
628 len: usize,
629 alignment: Alignment,
630 return_address: usize,
631 ) ?[*]u8 {
632 const control: *Control = @ptrCast(@alignCast(context));
633 const self = Self{ .control = control };
634 if (self.phase() != .initialization) {
635 var mutable = self;
636 mutable.violation(.allocation, return_address);
637 if (comptime behavior != .observe) return null;
638 }
639 return control.backing.rawAlloc(len, alignment, return_address);
640 }
641
642 fn rawResize(
643 context: *anyopaque,
644 memory: []u8,
645 alignment: Alignment,
646 new_len: usize,
647 return_address: usize,
648 ) bool {
649 const control: *Control = @ptrCast(@alignCast(context));
650 var self = Self{ .control = control };
651 if (self.phase() != .initialization) {
652 self.violation(.resize, return_address);
653 if (comptime behavior != .observe) return false;
654 }
655 return control.backing.rawResize(memory, alignment, new_len, return_address);
656 }
657
658 fn rawRemap(
659 context: *anyopaque,
660 memory: []u8,
661 alignment: Alignment,
662 new_len: usize,
663 return_address: usize,
664 ) ?[*]u8 {
665 const control: *Control = @ptrCast(@alignCast(context));
666 var self = Self{ .control = control };
667 if (self.phase() != .initialization) {
668 self.violation(.remap, return_address);
669 if (comptime behavior != .observe) return null;
670 }
671 return control.backing.rawRemap(memory, alignment, new_len, return_address);
672 }
673
674 fn rawFree(
675 context: *anyopaque,
676 memory: []u8,
677 alignment: Alignment,
678 return_address: usize,
679 ) void {
680 const control: *Control = @ptrCast(@alignCast(context));
681 var self = Self{ .control = control };
682 const active_phase = self.phase();
683 if (active_phase != .initialization and active_phase != .teardown) {
684 self.violation(.free, return_address);
685 if (comptime behavior != .observe) return;
686 }
687 control.backing.rawFree(memory, alignment, return_address);
688 }
689
690 const Operation = enum {
691 allocation,
692 resize,
693 remap,
694 free,
695 };
696
697 const vtable: Allocator.VTable = .{
698 .alloc = rawAlloc,
699 .resize = rawResize,
700 .remap = rawRemap,
701 .free = rawFree,
702 };
703 };
704 }
705
706 test "sealed phase allocator permits initialization and terminal teardown" {
707 var phase_allocator = try SealedPhaseAllocator.init(std.testing.allocator);
708 const initialization_allocator = phase_allocator.initializationAllocator();
709 const storage = try initialization_allocator.alloc(u8, 32);
710
711 phase_allocator.seal();
712 try std.testing.expectEqual(Phase.steady, phase_allocator.phase());
713 try std.testing.expectEqual(@as(u64, 0), phase_allocator.violations().total());
714
715 phase_allocator.beginTeardown();
716 phase_allocator.teardownAllocator().free(storage);
717 try std.testing.expectEqual(Phase.teardown, phase_allocator.phase());
718 try std.testing.expectEqual(@as(u64, 0), phase_allocator.violations().total());
719 phase_allocator.deinit();
720 }
721
722 test "sealed phase allocator can terminate failed initialization" {
723 var phase_allocator = try SealedPhaseAllocator.init(std.testing.allocator);
724 phase_allocator.abortInitialization();
725 try std.testing.expectEqual(Phase.teardown, phase_allocator.phase());
726 phase_allocator.deinit();
727 }
728
729 test "sealed phase allocator initialization failure is retryable" {
730 var failing = std.testing.FailingAllocator.init(
731 std.testing.allocator,
732 .{ .fail_index = 0 },
733 );
734 try std.testing.expectError(
735 error.OutOfMemory,
736 SealedPhaseAllocator.init(failing.allocator()),
737 );
738
739 failing.fail_index = std.math.maxInt(usize);
740 var phase_allocator = try SealedPhaseAllocator.init(failing.allocator());
741 phase_allocator.abortInitialization();
742 phase_allocator.deinit();
743 }
744
745 test "sealed phase allocator records and denies every steady allocator operation" {
746 const RecordingPhaseAllocator = PhaseAllocator(.record);
747 var backing = std.testing.FailingAllocator.init(std.testing.allocator, .{});
748 var phase_allocator = try RecordingPhaseAllocator.init(backing.allocator());
749 const allocator = phase_allocator.initializationAllocator();
750 const storage = try allocator.alloc(u8, 32);
751
752 phase_allocator.seal();
753 try std.testing.expect(allocator.rawAlloc(8, .@"1", @returnAddress()) == null);
754 try std.testing.expect(!allocator.rawResize(storage, .@"1", 16, @returnAddress()));
755 try std.testing.expect(allocator.rawRemap(storage, .@"1", 64, @returnAddress()) == null);
756 allocator.rawFree(storage, .@"1", @returnAddress());
757
758 try std.testing.expectEqual(PhaseViolations{
759 .allocations = 1,
760 .resizes = 1,
761 .remaps = 1,
762 .frees = 1,
763 }, phase_allocator.violations());
764 try std.testing.expectEqual(@as(usize, 0), backing.deallocations);
765
766 phase_allocator.beginTeardown();
767 phase_allocator.teardownAllocator().free(storage);
768 try std.testing.expectEqual(@as(usize, 1), backing.deallocations);
769 phase_allocator.deinit();
770 try std.testing.expectEqual(@as(usize, 2), backing.deallocations);
771 }
772
773 test "copied sealed phase allocator handles share one control block" {
774 const RecordingPhaseAllocator = PhaseAllocator(.record);
775 var phase_allocator = try RecordingPhaseAllocator.init(std.testing.allocator);
776 const allocator = phase_allocator.initializationAllocator();
777 const storage = try allocator.alloc(u8, 32);
778
779 var copied = phase_allocator;
780 copied.seal();
781 try std.testing.expectEqual(Phase.steady, phase_allocator.phase());
782 try std.testing.expect(allocator.rawAlloc(8, .@"1", @returnAddress()) == null);
783 try std.testing.expectEqual(@as(u64, 1), copied.violations().allocations);
784
785 phase_allocator.beginTeardown();
786 copied.teardownAllocator().free(storage);
787 phase_allocator.deinit();
788 }
789
790 test "observing phase allocator counts and forwards every steady operation" {
791 var backing = std.testing.FailingAllocator.init(std.testing.allocator, .{});
792 var phase_allocator = try ObservingPhaseAllocator.init(backing.allocator());
793 const allocator = phase_allocator.initializationAllocator();
794 const startup = try allocator.alloc(u8, 32);
795 try std.testing.expectEqual(@as(u64, 0), phase_allocator.violations().total());
796
797 phase_allocator.seal();
798 const backing_allocations = backing.allocations;
799 const steady_raw = allocator.rawAlloc(8, .@"1", @returnAddress()) orelse return error.OutOfMemory;
800 var steady: []u8 = steady_raw[0..8];
801 try std.testing.expectEqual(backing_allocations + 1, backing.allocations);
802 _ = allocator.rawResize(steady, .@"1", 8, @returnAddress());
803 if (allocator.rawRemap(steady, .@"1", 8, @returnAddress())) |moved| steady = moved[0..8];
804 allocator.rawFree(steady, .@"1", @returnAddress());
805 try std.testing.expectEqual(@as(usize, 1), backing.deallocations);
806 try std.testing.expectEqual(PhaseViolations{
807 .allocations = 1,
808 .resizes = 1,
809 .remaps = 1,
810 .frees = 1,
811 }, phase_allocator.violations());
812
813 phase_allocator.beginTeardown();
814 phase_allocator.teardownAllocator().free(startup);
815 try std.testing.expectEqual(@as(u64, 4), phase_allocator.violations().total());
816 try std.testing.expectEqual(@as(usize, 2), backing.deallocations);
817 phase_allocator.deinit();
818 }
819
820 test "observing phase allocator attributes violations to bounded distinct sites" {
821 var phase_allocator = try ObservingPhaseAllocator.init(std.testing.allocator);
822 const allocator = phase_allocator.initializationAllocator();
823 phase_allocator.seal();
824 try std.testing.expectEqual(@as(u8, 0), phase_allocator.violationSites().count);
825
826 var repeat: u32 = 0;
827 while (repeat < 4) : (repeat += 1) {
828 const block = allocator.rawAlloc(8, .@"1", @returnAddress()) orelse return error.OutOfMemory;
829 allocator.rawFree(block[0..8], .@"1", @returnAddress());
830 }
831
832 const sites = phase_allocator.violationSites();
833 try std.testing.expect(sites.count >= 1);
834 try std.testing.expect(sites.count <= 2);
835 for (sites.slice()) |address| {
836 try std.testing.expect(address != 0);
837 }
838 try std.testing.expectEqual(@as(u64, 8), phase_allocator.violations().total());
839
840 phase_allocator.beginTeardown();
841 phase_allocator.deinit();
842 }
843
844 test "sealed phase allocator reports no violation sites" {
845 const RecordingPhaseAllocator = PhaseAllocator(.record);
846 var phase_allocator = try RecordingPhaseAllocator.init(std.testing.allocator);
847 const allocator = phase_allocator.initializationAllocator();
848 phase_allocator.seal();
849 try std.testing.expect(allocator.rawAlloc(8, .@"1", @returnAddress()) == null);
850 try std.testing.expectEqual(@as(u8, 0), phase_allocator.violationSites().count);
851 phase_allocator.beginTeardown();
852 phase_allocator.deinit();
853 }
854
855 test "teardown denies growth operations and permits free" {
856 const RecordingPhaseAllocator = PhaseAllocator(.record);
857 var backing = std.testing.FailingAllocator.init(std.testing.allocator, .{});
858 var phase_allocator = try RecordingPhaseAllocator.init(backing.allocator());
859 const allocator = phase_allocator.initializationAllocator();
860 const storage = try allocator.alloc(u8, 32);
861
862 phase_allocator.seal();
863 phase_allocator.beginTeardown();
864 const teardown_allocator = phase_allocator.teardownAllocator();
865 try std.testing.expect(teardown_allocator.rawAlloc(8, .@"1", @returnAddress()) == null);
866 try std.testing.expect(!teardown_allocator.rawResize(storage, .@"1", 16, @returnAddress()));
867 try std.testing.expect(teardown_allocator.rawRemap(storage, .@"1", 64, @returnAddress()) == null);
868 teardown_allocator.rawFree(storage, .@"1", @returnAddress());
869
870 try std.testing.expectEqual(PhaseViolations{
871 .allocations = 1,
872 .resizes = 1,
873 .remaps = 1,
874 }, phase_allocator.violations());
875 phase_allocator.deinit();
876 }