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 }