lib/alloc/phase/src/input/one.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 //! Provides bounded input admission into a single reusable byte buffer.
2 //!
3 //! Callers configure an upper limit on acceptable input size. Because
4 //! distinguishing an exactly full input from an oversized payload without a
5 //! second growing buffer requires lookahead, standard capacity derivation
6 //! reserves one extra byte beyond the configured limit.
7 //!
8 //! The allocation lifecycle proceeds across distinct phases. During
9 //! initialization, the owner derives capacity and allocates its byte slice
10 //! through a caller-provided allocator. Activation transitions the owner into
11 //! steady operation. Steady-state callers borrow byte slices during read
12 //! operations, consume or copy the borrowed data, and release the buffer before
13 //! initiating the next read. Teardown frees the backing storage through the
14 //! allocator used for initialization. External callers must serialize
15 //! operations because the owner maintains mutable internal state without
16 //! internal synchronization.
17
18 const std = @import("std");
19 const alloc_phase = @import("../root.zig");
20
21 /// Error set returned when capacity derivation fails.
22 ///
23 /// Members:
24 ///
25 /// * `CapacityOverflow`: Adding one lookahead byte to the configured byte limit
26 /// overflowed the range of `usize`.
27 pub const DeriveError = error{CapacityOverflow};
28
29 /// Packed boolean structure that enables specific input capabilities on an
30 /// owner at compile time. All four fields default to `false`:
31 ///
32 /// * `reader`: Enables stream acquisition through `read`.
33 ///
34 /// * `file`: Enables filesystem acquisition through `readFile` and
35 /// `readFileWindow`.
36 /// * `preflight`: Enables size admission checks through `preflight`.
37 /// * `map_file_too_big`: Maps `error.FileTooBig` from an underlying `readFile`
38 /// call into a capacity rejection instead of propagating the raw I/O error.
39 ///
40 /// Because an owner must support at least one input mechanism, specification
41 /// validation requires either `reader` or `file` to be enabled. Mapping
42 /// file-size errors requires filesystem support, so `map_file_too_big` can be
43 /// enabled only when `file` is also set to `true`. Individual acquisition
44 /// methods verify their corresponding flags at compile time and raise a compile
45 /// error when invoked without the required capability enabled.
46 pub const Acquisition = packed struct {
47 reader: bool = false,
48 file: bool = false,
49 preflight: bool = false,
50 map_file_too_big: bool = false,
51 };
52
53 /// Defines the policy applied when an input exceeds the configured capacity
54 /// limit.
55 ///
56 /// Tags:
57 ///
58 /// * `terminal`: Permanently seals the owner upon capacity rejection. Any
59 /// subsequent acquisition attempt returns the capacity error immediately
60 /// without performing I/O or incrementing rejection counters.
61 /// * `recoverable`: Preserves owner availability across rejections. Oversized
62 /// inputs increment the rejection counter but leave the underlying storage free
63 /// for subsequent attempts.
64 ///
65 /// Recovery designates the ability to reuse the allocated storage for later
66 /// inputs. It does not provide source-position rollback, record framing, or
67 /// automatic input drainage.
68 pub const Overload = enum {
69 terminal,
70 recoverable,
71 };
72
73 /// Derives an instance of `Capacity` from a given `Limits` structure by adding
74 /// one lookahead byte to the configured input limit.
75 ///
76 /// The input limit is retrieved from the field named by `limit_field` on
77 /// `limits`. Both the limit field on `Limits` and `Capacity` and the
78 /// `storage_bytes` field on `Capacity` must be typed as `usize`. When addition
79 /// overflows `usize`, the function returns `error.CapacityOverflow`. A limit of
80 /// zero derives a storage size of one byte.
81 ///
82 /// The returned `Capacity` structure initializes only the field named by
83 /// `limit_field` and `storage_bytes`. Any additional fields present on
84 /// `Capacity` remain undefined. Callers must keep `limit_field` distinct from
85 /// `storage_bytes` so that writing `storage_bytes` does not overwrite the limit
86 /// field.
87 pub fn deriveCapacity(
88 comptime Limits: type,
89 comptime Capacity: type,
90 comptime limit_field: []const u8,
91 limits: Limits,
92 ) DeriveError!Capacity {
93 comptime requireCapacityTypes(Limits, Capacity, limit_field);
94 const limit_bytes = @field(limits, limit_field);
95 const storage_bytes = try alloc_phase.capacity.add(usize, limit_bytes, 1);
96 std.debug.assert(storage_bytes > limit_bytes);
97 std.debug.assert(storage_bytes != 0);
98 var capacity: Capacity = undefined;
99 @field(capacity, limit_field) = limit_bytes;
100 capacity.storage_bytes = storage_bytes;
101 return capacity;
102 }
103
104 /// Constructs an allocator-backed owner type for a single reusable input buffer
105 /// based on a static specification structure.
106 ///
107 /// The `Spec` type parameter must satisfy several structural requirements:
108 ///
109 /// * `Limits`: Structure defining input limits. It must contain the field
110 /// designated by `limit_field` typed as `usize`.
111 /// * `Capacity`: Structure defining storage requirements. It must expose a
112 /// `derive(limits: Limits) DeriveError!Capacity` function, the field named by
113 /// `limit_field` typed as `usize`, and `storage_bytes` typed as `usize`.
114 /// * `DeriveError`: Error set returned by `Capacity.derive`.
115 /// * `Exhaustion`: Error set returned when storage cannot admit an input.
116 /// * `Status`: Reporting structure returned by `status`. It must be a struct
117 /// containing exactly seven fields for recoverable owners or eight fields for
118 /// terminal owners: `phase: alloc_phase.capacity.Phase`, `in_use: bool`,
119 /// `storage_bytes: usize`, the mapped limit, loaded, and high-water fields
120 /// typed as `usize`, the mapped rejection field typed as `u64`, and
121 /// conditionally `terminal: bool` when `overload` is `terminal`.
122 /// * `acquisition`: An instance of `Acquisition` specifying enabled I/O
123 /// capabilities.
124 /// * `overload`: An instance of `Overload` setting either `.terminal` or
125 /// `.recoverable` behavior.
126 /// * Field mappings: `limit_field`, `loaded_field`, `high_water_field`, and
127 /// `rejected_field` specify field names on `Capacity` or `Status`. Callers must
128 /// supply distinct names for these fields. Compile-time validation verifies
129 /// field presence and total field counts, but this validation does not prove
130 /// that mapped field names are mutually distinct.
131 /// * Error instances: `in_use_error` and `capacity_error` must both have type
132 /// `Spec.Exhaustion`.
133 /// * `claim`: A static `alloc_phase.capacity.Declaration` metadata descriptor.
134 ///
135 /// The owner asserts that `storage_bytes` strictly exceeds the configured byte
136 /// limit. While standard `deriveCapacity` derivation provides an exact `limit +
137 /// 1` layout, custom `Capacity.derive` implementations may describe larger
138 /// storage: derivation computes a `Capacity` value, while `init` allocates.
139 ///
140 /// A successful read operation returns a borrowed `[]const u8` slice pointing
141 /// directly into the owned buffer and marks the owner as in use, including when
142 /// the input is empty. Borrowers must consume or copy these bytes elsewhere
143 /// before calling `release`. Slices must not be accessed after releasing the
144 /// owner.
145 ///
146 /// A struct copy shares allocated storage but duplicates internal bookkeeping.
147 /// Copies are not independent owners: transferring exclusive ownership requires
148 /// callers to stop using the original. Callers must serialize operations and
149 /// ensure cleanup occurs exactly once.
150 ///
151 /// The owner is not thread-safe and performs no internal synchronization.
152 /// Callers accessing an owner across threads must provide external mutual
153 /// exclusion.
154 ///
155 /// Lifecycle checks across initialization, steady operation, and teardown are
156 /// enforced through debug assertions rather than explicit always-executed phase
157 /// panics.
158 pub fn OneRegion(comptime Spec: type) type {
159 comptime requireSpec(Spec);
160 return struct {
161 phase: alloc_phase.capacity.Phase,
162 capacity: Spec.Capacity,
163 bytes: []u8,
164 in_use: bool = false,
165 terminal: bool = false,
166 loaded_bytes: usize = 0,
167 high_water_bytes: usize = 0,
168 rejected_count: u64 = 0,
169
170 const Self = @This();
171
172 /// Result structure returned by `readFileWindow` representing a bounded
173 /// slice of a file read at a positional offset.
174 ///
175 /// Fields:
176 ///
177 /// * `bytes`: Borrowed const byte slice containing admitted data. Its
178 /// length is at most the requested window size.
179 /// * `complete`: Boolean flag set to `true` when `read_bytes <=
180 /// window_bytes`.
181 ///
182 /// The `complete` flag describes only this positional read and does not
183 /// make an immutable snapshot against concurrent modification during or
184 /// after it.
185 pub const FileWindow = struct {
186 bytes: []const u8,
187 complete: bool,
188 };
189
190 /// Type alias for `Spec.Limits`. Defines the input limit constraints
191 /// supplied by the caller.
192 pub const Limits: type = Spec.Limits;
193 /// Type alias for `Spec.Capacity`. Holds derived storage dimensions,
194 /// including the mandatory `storage_bytes` field.
195 pub const Capacity: type = Spec.Capacity;
196 /// Type alias for `Spec.Exhaustion`. Represents the error set returned
197 /// when input operations encounter a busy buffer or exceed configured
198 /// capacity.
199 pub const Exhaustion: type = Spec.Exhaustion;
200 /// Error set representing failures that can occur during
201 /// initialization. Combines allocator allocation errors with
202 /// `Spec.DeriveError`.
203 pub const InitError = std.mem.Allocator.Error || Spec.DeriveError;
204 /// Static capacity declaration describing the memory allocation
205 /// semantics of the generated owner.
206 ///
207 /// The declaration retains the complete `Spec.claim.source` envelope
208 /// while establishing fresh bindings for the generated owner:
209 ///
210 /// * `owner`: Binds the generated owner type `Self`.
211 /// * `source`: Binds the originating `Spec` configuration type.
212 /// * `seal`: Binds the `activate` method selector under the
213 /// `checked_semantic_fact` class with `checker` authority.
214 /// * `teardown`: Binds the `deinit` method selector under the
215 /// `checked_semantic_fact` class with `checker` authority.
216 ///
217 /// The bindings defined on `Spec.claim.bindings` are not reused.
218 /// Assigning premise labels to these new bindings does not prove their
219 /// truth. Local specification checks verify only that `claim` has type
220 /// `alloc_phase.capacity.Declaration`. Separate structural and
221 /// declaration checking verifies lifecycle declaration shape, but that
222 /// check does not prove whole-program allocation freedom.
223 pub const claim: alloc_phase.capacity.Declaration = .{
224 .source = Spec.claim.source,
225 .bindings = .{
226 .owner = Self,
227 .source = Spec,
228 .seal = .{
229 .family = alloc_phase.capacity.selector(Self.activate),
230 .premise = .{
231 .class = .checked_semantic_fact,
232 .authority = .checker,
233 },
234 },
235 .teardown = .{
236 .family = alloc_phase.capacity.selector(Self.deinit),
237 .premise = .{
238 .class = .checked_semantic_fact,
239 .authority = .checker,
240 },
241 },
242 },
243 };
244
245 /// Initializes a new input region by deriving capacity and allocating
246 /// the backing byte slice.
247 ///
248 /// The function derives capacity by evaluating
249 /// `Capacity.derive(limits)`. When derivation succeeds, it allocates
250 /// `capacity.storage_bytes` through the provided allocator. The
251 /// initialized owner enters the `.initialization` phase and retains the
252 /// allocated slice. The allocator handle itself is not stored on the
253 /// instance, so callers must provide the same allocator during
254 /// deinitialization.
255 ///
256 /// Initialization fails with `Spec.DeriveError` if capacity derivation
257 /// fails, or with `error.OutOfMemory` if memory allocation fails.
258 /// Before returning, debug assertions verify that the allocated slice
259 /// length equals `storage_bytes` and strictly exceeds the configured
260 /// byte limit.
261 pub fn init(allocator: std.mem.Allocator, limits: Limits) InitError!Self {
262 const capacity = try Capacity.derive(limits);
263 const bytes = try allocator.alloc(u8, capacity.storage_bytes);
264 const storage = Self{
265 .phase = .initialization,
266 .capacity = capacity,
267 .bytes = bytes,
268 };
269 storage.assertRegion();
270 return storage;
271 }
272
273 /// Transitions the owner from the initialization phase into steady
274 /// operation.
275 ///
276 /// Callers must activate the owner before performing input operations.
277 /// The function verifies through debug assertions that the owner
278 /// resides in the `.initialization` phase and that the backing buffer
279 /// satisfies region invariants. Once activated, `phase` transitions to
280 /// `.steady`, permitting acquisition calls.
281 pub fn activate(self: *Self) void {
282 std.debug.assert(self.phase == .initialization);
283 self.assertRegion();
284 self.phase = .steady;
285 }
286
287 /// Performs a size admission check for an expected input length without
288 /// executing I/O or modifying buffer contents.
289 ///
290 /// This method requires `Spec.acquisition.preflight` to be enabled at
291 /// compile time. Before evaluating the size, it verifies that the owner
292 /// is available in steady operation. If an operation is currently
293 /// active, it returns `Spec.in_use_error`. If the owner was previously
294 /// sealed in a terminal state, it returns `Spec.capacity_error`.
295 ///
296 /// The proposed length `input_bytes` is cast to `usize`. If the value
297 /// overflows `usize` or exceeds the configured byte limit, the method
298 /// invokes internal rejection, incrementing the saturating rejection
299 /// counter and applying the configured overload policy before returning
300 /// `Spec.capacity_error`.
301 ///
302 /// A successful preflight check confirms only that the proposed size
303 /// fits within the configured limit. It does not reserve storage, lock
304 /// the buffer, or establish an atomic guarantee between checking a size
305 /// and executing a later read.
306 pub fn preflight(self: *Self, input_bytes: u64) Exhaustion!void {
307 comptime requireAcquisition(Spec.acquisition.preflight, "preflight");
308 try self.requireAvailable();
309 const bounded = std.math.cast(usize, input_bytes) orelse
310 return self.reject();
311 if (bounded > self.limitBytes()) return self.reject();
312 }
313
314 /// Reads data from a stream reader into the owned buffer and admits the
315 /// resulting slice if its length falls within the configured limit.
316 ///
317 /// This method requires `Spec.acquisition.reader` to be enabled at
318 /// compile time. It requires the owner to be available in steady
319 /// operation, returning `Spec.in_use_error` if an active borrow exists
320 /// or `Spec.capacity_error` if the owner is terminal.
321 ///
322 /// The method invokes `reader.readSliceShort` over the allocated
323 /// storage slice. If reading from the underlying stream returns a
324 /// `ShortError` such as `ReadFailed`, that error propagates directly to
325 /// the caller. A read failure can occur after writing bytes into the
326 /// buffer and consuming bytes from the stream, without establishing a
327 /// borrow or recording a rejection. The method promises neither buffer
328 /// rollback nor stream rewind nor record framing.
329 ///
330 /// When reading completes without stream error, the read byte count is
331 /// evaluated:
332 ///
333 /// * If the count is at most the configured limit, the slice
334 /// `self.bytes[0..input_bytes]` is admitted. The owner records the
335 /// loaded length, updates the high-water mark, sets `in_use` to `true`,
336 /// and returns the borrowed slice.
337 /// * If the count exceeds the configured limit, the method rejects the
338 /// input. It increments the rejection counter, transitions to terminal
339 /// if configured, and returns `Spec.capacity_error`.
340 ///
341 /// When using helper-based capacity derivation through
342 /// `deriveCapacity`, an oversized input fills the `limit + 1` storage
343 /// bytes and triggers a capacity rejection after reading. This consumes
344 /// `limit + 1` bytes from the stream without draining trailing input.
345 /// Because reader implementations may buffer additional data,
346 /// subsequent reads cannot assume framing alignment with a subsequent
347 /// record.
348 pub fn read(
349 self: *Self,
350 reader: *std.Io.Reader,
351 ) (Exhaustion || std.Io.Reader.ShortError)![]const u8 {
352 comptime requireAcquisition(Spec.acquisition.reader, "reader");
353 try self.requireAvailable();
354 const input_bytes = try reader.readSliceShort(self.bytes);
355 return try self.admit(input_bytes);
356 }
357
358 /// Reads a file from a directory into the bounded buffer and admits the
359 /// resulting slice if the read length does not exceed the configured
360 /// limit.
361 ///
362 /// This method requires `Spec.acquisition.file` to be enabled at
363 /// compile time. It checks that the owner is available in steady
364 /// operation before opening the file.
365 ///
366 /// Reading is executed through `dir.readFile` into the allocated buffer
367 /// slice. Under the pinned standard library implementation, an ordinary
368 /// oversized file fills the buffer prefix before admission rejects the
369 /// input. Opening errors propagate directly, while a read error can
370 /// occur after partial buffer writes and no borrow is admitted.
371 ///
372 /// Handling of `error.FileTooBig` depends on compile-time
373 /// configuration:
374 ///
375 /// * When `Spec.acquisition.map_file_too_big` is `true`,
376 /// `error.FileTooBig` is converted into a capacity rejection. This
377 /// increments the rejection counter, transitions to terminal if
378 /// configured, and returns `Spec.capacity_error`.
379 /// * When `Spec.acquisition.map_file_too_big` is `false`,
380 /// `error.FileTooBig` propagates directly to the caller without
381 /// incrementing the rejection counter or modifying terminal state.
382 ///
383 /// All other filesystem I/O errors propagate directly to the caller
384 /// without a capacity rejection or terminal transition under either
385 /// setting. On success, the method returns a borrowed slice of the
386 /// admitted bytes and marks the owner as in use.
387 pub fn readFile(
388 self: *Self,
389 dir: std.Io.Dir,
390 io: std.Io,
391 file_path: []const u8,
392 ) ![]const u8 {
393 comptime requireAcquisition(Spec.acquisition.file, "file");
394 try self.requireAvailable();
395 const file = if (comptime Spec.acquisition.map_file_too_big)
396 dir.readFile(io, file_path, self.bytes) catch |err| switch (err) {
397 error.FileTooBig => return self.reject(),
398 else => return err,
399 }
400 else
401 try dir.readFile(io, file_path, self.bytes);
402 return try self.admit(file.len);
403 }
404
405 /// Reads a bounded positional window from a file at a specified offset
406 /// without treating trailing bytes as a capacity rejection.
407 ///
408 /// This method requires `Spec.acquisition.file` to be enabled at
409 /// compile time. Callers must supply a window size satisfying `0 <
410 /// window_bytes <= limit`, which debug assertions verify. The owner
411 /// must be available in steady operation before opening the target
412 /// file.
413 ///
414 /// The method opens the file and reads up to `window_bytes + 1` bytes
415 /// at the specified offset using positional I/O into `self.bytes[0 ..
416 /// window_bytes + 1]`. Any filesystem I/O error during open or read
417 /// propagates directly to the caller without a capacity rejection or
418 /// borrow, though the buffer may be partially written.
419 ///
420 /// The read byte count determines completion and admission:
421 ///
422 /// * If the number of read bytes is less than or equal to
423 /// `window_bytes`, `complete` is set to `true`.
424 /// * If reading produces `window_bytes + 1` bytes, additional data
425 /// exists beyond the window, setting `complete` to `false`. This
426 /// condition does not trigger a capacity rejection or modify terminal
427 /// status, even on terminal owners.
428 ///
429 /// The method admits `@min(read_bytes, window_bytes)`, which marks the
430 /// owner as in use and returns a borrowed slice. An immediate EOF
431 /// returning zero bytes still establishes an admitted zero-byte borrow.
432 /// The `complete` flag describes only this positional read and is not
433 /// an instantaneous snapshot against concurrent modifications.
434 pub fn readFileWindow(
435 self: *Self,
436 dir: std.Io.Dir,
437 io: std.Io,
438 file_path: []const u8,
439 offset: u64,
440 window_bytes: usize,
441 ) !FileWindow {
442 comptime requireAcquisition(Spec.acquisition.file, "file");
443 try self.requireAvailable();
444 std.debug.assert(window_bytes > 0);
445 std.debug.assert(window_bytes <= self.limitBytes());
446 var file = try dir.openFile(io, file_path, .{});
447 defer file.close(io);
448 const read_bytes = try file.readPositionalAll(
449 io,
450 self.bytes[0 .. window_bytes + 1],
451 offset,
452 );
453 const complete = read_bytes <= window_bytes;
454 const admitted = try self.admit(@min(read_bytes, window_bytes));
455 return .{ .bytes = admitted, .complete = complete };
456 }
457
458 /// Releases an active borrow, allowing the internal buffer to be reused
459 /// by future input operations.
460 ///
461 /// Callers must call this method only when the owner is in the
462 /// `.steady` phase, holds an active borrow (`in_use` is `true`), and is
463 /// not in a terminal state. Debug assertions verify these
464 /// preconditions.
465 ///
466 /// Calling release sets `in_use` to `false` and resets `loaded_bytes`
467 /// to zero. It does not overwrite or sanitize existing byte values in
468 /// the buffer. Historical tracking metrics, including the high-water
469 /// mark and the cumulative rejection count, remain intact across
470 /// release calls.
471 pub fn release(self: *Self) void {
472 std.debug.assert(self.phase == .steady);
473 std.debug.assert(self.in_use);
474 std.debug.assert(!self.terminal);
475 self.in_use = false;
476 self.loaded_bytes = 0;
477 }
478
479 /// Constructs and returns a snapshot of owner metrics and lifecycle
480 /// state as a `Spec.Status` structure.
481 ///
482 /// The method copies configured status fields by value under caller
483 /// serialization. Because the owner does not use atomic operations or
484 /// internal synchronization, this method does not produce a coherent
485 /// snapshot across concurrent unsynchronized access.
486 ///
487 /// The returned snapshot contains the following fields:
488 ///
489 /// * `phase`: Current lifecycle phase (`.initialization`, `.steady`, or
490 /// `.teardown`).
491 /// * `in_use`: Boolean flag indicating whether a borrowed buffer slice
492 /// is currently outstanding.
493 /// * `terminal`: Present on terminal owners to indicate whether
494 /// capacity exhaustion permanently sealed the owner.
495 /// * Configured limit field: Configured maximum byte limit derived from
496 /// capacity.
497 /// * `storage_bytes`: Total declared capacity of the buffer. Because
498 /// this value reflects declared specification capacity, it does not
499 /// represent a live allocation measurement and remains readable after
500 /// teardown.
501 /// * Configured loaded field: Number of admitted bytes currently held
502 /// by an active borrow. This field is zero whenever the owner is not in
503 /// use.
504 /// * Configured high-water field: Largest admitted byte count observed
505 /// across all successful reads since initialization. Oversized inputs
506 /// rejected during admission do not update this value.
507 /// * Configured rejected field: Cumulative saturating count of capacity
508 /// rejections.
509 pub fn status(self: *const Self) Spec.Status {
510 var result: Spec.Status = undefined;
511 result.phase = self.phase;
512 result.in_use = self.in_use;
513 if (comptime @hasField(Spec.Status, "terminal")) {
514 result.terminal = self.terminal;
515 }
516 @field(result, Spec.limit_field) = self.limitBytes();
517 result.storage_bytes = self.capacity.storage_bytes;
518 @field(result, Spec.loaded_field) = self.loaded_bytes;
519 @field(result, Spec.high_water_field) = self.high_water_bytes;
520 @field(result, Spec.rejected_field) = self.rejected_count;
521 return result;
522 }
523
524 /// Deinitializes the owner and frees its allocated byte buffer using
525 /// the provided allocator.
526 ///
527 /// Callers must supply the same allocator used for `init`. The method
528 /// verifies through debug assertions that the owner has not already
529 /// entered teardown and that no borrowed slice is currently in use
530 /// (`in_use` is `false`).
531 ///
532 /// Deinitialization transitions `phase` to `.teardown`, frees the
533 /// allocated memory slice, clears the internal buffer slice to empty,
534 /// and resets `loaded_bytes` to zero. Callers may invoke this method
535 /// while the owner is still in the `.initialization` phase to clean up
536 /// resources after a failed startup sequence. Cumulative metrics and
537 /// capacity declarations remain stored on the instance after
538 /// deinitialization completes.
539 pub fn deinit(self: *Self, allocator: std.mem.Allocator) void {
540 std.debug.assert(self.phase != .teardown);
541 std.debug.assert(!self.in_use);
542 self.assertRegion();
543 self.phase = .teardown;
544 allocator.free(self.bytes);
545 self.bytes = &.{};
546 self.loaded_bytes = 0;
547 }
548
549 /// Verifies that the owner is ready to accept a new input operation.
550 ///
551 /// Debug assertions confirm that the owner resides in the `.steady`
552 /// phase, that buffer invariants hold, and that previous loaded bytes
553 /// did not exceed the limit. If the owner is marked terminal, the
554 /// function returns `Spec.capacity_error` without incrementing the
555 /// rejection counter. If a borrow is currently active, it returns
556 /// `Spec.in_use_error` without incrementing the rejection counter. When
557 /// available, debug assertions verify that `loaded_bytes` is zero.
558 fn requireAvailable(self: *Self) Exhaustion!void {
559 std.debug.assert(self.phase == .steady);
560 self.assertRegion();
561 std.debug.assert(self.loaded_bytes <= self.limitBytes());
562 if (self.terminal) return Spec.capacity_error;
563 if (self.in_use) return Spec.in_use_error;
564 std.debug.assert(self.loaded_bytes == 0);
565 }
566
567 /// Admits a specified number of bytes into the region or rejects the
568 /// payload if it exceeds the configured limit.
569 ///
570 /// Debug assertions verify that the owner is in steady operation
571 /// without an active borrow or terminal state, and that the requested
572 /// byte count does not exceed allocated storage. If `input_bytes`
573 /// exceeds the configured byte limit, the function invokes `reject`.
574 /// Otherwise, it marks the buffer as in use, records `loaded_bytes`,
575 /// updates `high_water_bytes` if the new length exceeds previous
576 /// admissions, and returns a borrowed slice over the admitted bytes.
577 fn admit(self: *Self, input_bytes: usize) Exhaustion![]const u8 {
578 std.debug.assert(self.phase == .steady);
579 std.debug.assert(!self.in_use);
580 std.debug.assert(!self.terminal);
581 std.debug.assert(input_bytes <= self.capacity.storage_bytes);
582 if (input_bytes > self.limitBytes()) return self.reject();
583 self.in_use = true;
584 self.loaded_bytes = input_bytes;
585 self.high_water_bytes = @max(self.high_water_bytes, input_bytes);
586 return self.bytes[0..input_bytes];
587 }
588
589 /// Records an input capacity rejection and updates owner overload
590 /// state.
591 ///
592 /// Debug assertions verify that the owner is in steady operation, is
593 /// not in use, is not already terminal, and holds zero loaded bytes. If
594 /// `Spec.overload` is configured as `.terminal`, the owner enters the
595 /// terminal state. The cumulative rejection counter is incremented
596 /// using saturating addition, and the function returns
597 /// `Spec.capacity_error`.
598 fn reject(self: *Self) Exhaustion {
599 std.debug.assert(self.phase == .steady);
600 std.debug.assert(!self.in_use);
601 std.debug.assert(!self.terminal);
602 std.debug.assert(self.loaded_bytes == 0);
603 self.terminal = Spec.overload == .terminal;
604 self.rejected_count +|= 1;
605 return Spec.capacity_error;
606 }
607
608 /// Helper function returning the configured byte limit stored on
609 /// `self.capacity` using the field name defined by `Spec.limit_field`.
610 fn limitBytes(self: *const Self) usize {
611 return @field(self.capacity, Spec.limit_field);
612 }
613
614 /// Helper function asserting that the backing byte slice length matches
615 /// `storage_bytes` and strictly exceeds the configured byte limit.
616 fn assertRegion(self: *const Self) void {
617 std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
618 std.debug.assert(self.capacity.storage_bytes > self.limitBytes());
619 }
620 };
621 }
622
623 /// Compile-time validation helper that verifies field types for capacity
624 /// derivation.
625 ///
626 /// The function verifies that `Limits` contains the field specified by
627 /// `limit_field` typed as `usize`. It also verifies that `Capacity` contains
628 /// both the field specified by `limit_field` and `storage_bytes`, requiring
629 /// each to be typed as `usize`. If any requirement is not met, compilation
630 /// fails with an explanatory message.
631 fn requireCapacityTypes(
632 comptime Limits: type,
633 comptime Capacity: type,
634 comptime limit_field: []const u8,
635 ) void {
636 if (!@hasField(Limits, limit_field)) @compileError("Limits lacks byte limit");
637 if (@FieldType(Limits, limit_field) != usize) @compileError("limit must be usize");
638 if (!@hasField(Capacity, limit_field)) @compileError("Capacity lacks byte limit");
639 if (@FieldType(Capacity, limit_field) != usize) @compileError("limit must be usize");
640 if (!@hasField(Capacity, "storage_bytes")) @compileError("Capacity lacks storage_bytes");
641 if (@FieldType(Capacity, "storage_bytes") != usize) {
642 @compileError("storage_bytes must be usize");
643 }
644 }
645
646 /// Compile-time validation helper enforcing structural requirements on the
647 /// `Spec` configuration type.
648 ///
649 /// The function validates type signatures and configuration values:
650 ///
651 /// * Delegates limit and capacity layout verification to
652 /// `requireCapacityTypes`.
653 /// * Verifies that `claim` has type `alloc_phase.capacity.Declaration`.
654 /// * Verifies that `in_use_error` and `capacity_error` both belong to
655 /// `Spec.Exhaustion`.
656 /// * Confirms that `Status` is a struct type possessing exactly seven fields
657 /// for recoverable owners or eight fields for terminal owners.
658 /// * Verifies presence and exact typing for `phase`
659 /// (`alloc_phase.capacity.Phase`), `in_use` (`bool`), `storage_bytes`
660 /// (`usize`), the mapped limit field (`usize`), the mapped loaded field
661 /// (`usize`), the mapped high-water field (`usize`), the mapped rejection field
662 /// (`u64`), and `terminal` (`bool`) when terminal overload is enabled.
663 /// * Confirms that `map_file_too_big` is enabled only when `file` acquisition
664 /// is active.
665 /// * Verifies that at least one acquisition capability (`reader` or `file`) is
666 /// enabled.
667 ///
668 /// Validating field count, named presence, and types does not establish a
669 /// distinct, complete semantic mapping. Because validation does not enforce
670 /// that mapped names are distinct, duplicate names can leave unrelated fields
671 /// uninitialized.
672 fn requireSpec(comptime Spec: type) void {
673 requireCapacityTypes(Spec.Limits, Spec.Capacity, Spec.limit_field);
674 if (@TypeOf(Spec.claim) != alloc_phase.capacity.Declaration) {
675 @compileError("invalid claim");
676 }
677 if (@TypeOf(Spec.in_use_error) != Spec.Exhaustion) @compileError("invalid in-use error");
678 if (@TypeOf(Spec.capacity_error) != Spec.Exhaustion) @compileError("invalid capacity error");
679 if (@typeInfo(Spec.Status) != .@"struct") @compileError("Status must be a struct");
680 const field_count = @typeInfo(Spec.Status).@"struct".field_names.len;
681 const expected_fields: usize = if (Spec.overload == .terminal) 8 else 7;
682 if (field_count != expected_fields) @compileError("Status has unexpected fields");
683 if (!@hasField(Spec.Status, "phase")) @compileError("Status lacks phase");
684 if (@FieldType(Spec.Status, "phase") != alloc_phase.capacity.Phase) {
685 @compileError("Status phase has invalid type");
686 }
687 if (!@hasField(Spec.Status, "in_use")) @compileError("Status lacks in_use");
688 if (@FieldType(Spec.Status, "in_use") != bool) @compileError("Status in_use has invalid type");
689 if (!@hasField(Spec.Status, Spec.limit_field)) @compileError("Status lacks byte limit");
690 if (@FieldType(Spec.Status, Spec.limit_field) != usize) {
691 @compileError("Status byte limit has invalid type");
692 }
693 if (!@hasField(Spec.Status, "storage_bytes")) @compileError("Status lacks storage_bytes");
694 if (@FieldType(Spec.Status, "storage_bytes") != usize) {
695 @compileError("Status storage_bytes has invalid type");
696 }
697 if (!@hasField(Spec.Status, Spec.loaded_field)) @compileError("Status lacks loaded bytes");
698 if (@FieldType(Spec.Status, Spec.loaded_field) != usize) {
699 @compileError("Status loaded bytes has invalid type");
700 }
701 if (!@hasField(Spec.Status, Spec.high_water_field)) @compileError("Status lacks high water");
702 if (@FieldType(Spec.Status, Spec.high_water_field) != usize) {
703 @compileError("Status high water has invalid type");
704 }
705 if (!@hasField(Spec.Status, Spec.rejected_field)) @compileError("Status lacks rejection count");
706 if (@FieldType(Spec.Status, Spec.rejected_field) != u64) {
707 @compileError("Status rejection count has invalid type");
708 }
709 if (Spec.overload == .terminal and !@hasField(Spec.Status, "terminal")) {
710 @compileError("terminal Status lacks terminal field");
711 }
712 if (Spec.overload == .terminal and @FieldType(Spec.Status, "terminal") != bool) {
713 @compileError("Status terminal has invalid type");
714 }
715 if (Spec.acquisition.map_file_too_big and !Spec.acquisition.file) {
716 @compileError("FileTooBig mapping requires file acquisition");
717 }
718 if (!Spec.acquisition.reader and !Spec.acquisition.file) {
719 @compileError("one-region input requires reader or file acquisition");
720 }
721 }
722
723 /// Compile-time validation helper that verifies whether a requested acquisition
724 /// capability is enabled on the owner. If the capability flag is `false`,
725 /// compilation fails with a message identifying the unsupported operation.
726 fn requireAcquisition(comptime enabled: bool, comptime name: []const u8) void {
727 if (!enabled) @compileError("one-region input does not support " ++ name);
728 }
729
730 test "one-region capacity derives one lookahead byte" {
731 const Limits = struct { input_bytes: usize };
732 const Capacity = struct { input_bytes: usize, storage_bytes: usize };
733 try std.testing.expectEqual(
734 Capacity{ .input_bytes = 31, .storage_bytes = 32 },
735 try deriveCapacity(Limits, Capacity, "input_bytes", .{ .input_bytes = 31 }),
736 );
737 try std.testing.expectError(
738 error.CapacityOverflow,
739 deriveCapacity(
740 Limits,
741 Capacity,
742 "input_bytes",
743 .{ .input_bytes = std.math.maxInt(usize) },
744 ),
745 );
746 }
747
748 test "one-region terminal owner acquires once and seals overload" {
749 const Limits = struct { input_bytes: usize };
750 const Capacity = testCapacity(Limits);
751 const Status = struct {
752 phase: alloc_phase.capacity.Phase,
753 in_use: bool,
754 terminal: bool,
755 input_bytes: usize,
756 storage_bytes: usize,
757 loaded_input_bytes: usize,
758 high_water_input_bytes: usize,
759 rejected_input_count: u64,
760 };
761 const Spec = testSpec(Limits, Capacity, Status, .terminal);
762 const Storage = OneRegion(Spec);
763 const failure = struct {
764 fn run(allocator: std.mem.Allocator) !void {
765 var storage = try Storage.init(allocator, .{ .input_bytes = 5 });
766 storage.deinit(allocator);
767 }
768 };
769 try std.testing.checkAllAllocationFailures(
770 std.testing.allocator,
771 failure.run,
772 .{},
773 );
774
775 var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
776 var storage = try Storage.init(counting.allocator(), .{ .input_bytes = 5 });
777 defer storage.deinit(counting.allocator());
778 try std.testing.expectEqual(@as(usize, 1), counting.alloc_index);
779 try std.testing.expectEqual(@as(usize, 6), counting.allocated_bytes);
780 storage.activate();
781 const allocation_count = counting.alloc_index;
782 var exact_reader: std.Io.Reader = .fixed("abcde");
783 const exact = try storage.read(&exact_reader);
784 try std.testing.expectEqualStrings("abcde", exact);
785 try std.testing.expectError(error.InputStorageInUse, storage.preflight(0));
786 storage.release();
787 storage.rejected_count = std.math.maxInt(u64);
788 var oversized_reader: std.Io.Reader = .fixed("abcdef");
789 try std.testing.expectError(
790 error.InputCapacityExceeded,
791 storage.read(&oversized_reader),
792 );
793 const status = storage.status();
794 try std.testing.expect(status.terminal);
795 try std.testing.expectEqual(std.math.maxInt(u64), status.rejected_input_count);
796 try std.testing.expectEqual(allocation_count, counting.alloc_index);
797 }
798
799 test "one-region recoverable owner preserves availability after rejection" {
800 const Limits = struct { input_bytes: usize };
801 const Capacity = testCapacity(Limits);
802 const Status = struct {
803 phase: alloc_phase.capacity.Phase,
804 in_use: bool,
805 input_bytes: usize,
806 storage_bytes: usize,
807 loaded_input_bytes: usize,
808 high_water_input_bytes: usize,
809 rejected_input_count: u64,
810 };
811 const Spec = testSpec(Limits, Capacity, Status, .recoverable);
812 const Storage = OneRegion(Spec);
813 var storage = try Storage.init(std.testing.allocator, .{ .input_bytes = 3 });
814 defer storage.deinit(std.testing.allocator);
815 storage.activate();
816 try std.testing.expectError(error.InputCapacityExceeded, storage.preflight(4));
817 try std.testing.expectEqual(@as(u64, 1), storage.status().rejected_input_count);
818 var reader: std.Io.Reader = .fixed("new");
819 const input = try storage.read(&reader);
820 try std.testing.expectEqualStrings("new", input);
821 try std.testing.expectEqual(@intFromPtr(storage.bytes.ptr), @intFromPtr(input.ptr));
822 storage.release();
823 }
824
825 fn testCapacity(comptime Limits: type) type {
826 return struct {
827 input_bytes: usize,
828 storage_bytes: usize,
829
830 pub fn derive(limits: Limits) DeriveError!@This() {
831 return deriveCapacity(Limits, @This(), "input_bytes", limits);
832 }
833 };
834 }
835
836 fn testSpec(
837 comptime LimitsType: type,
838 comptime CapacityType: type,
839 comptime StatusType: type,
840 comptime overload_policy: Overload,
841 ) type {
842 return struct {
843 pub const Limits = LimitsType;
844 pub const Capacity = CapacityType;
845 pub const Status = StatusType;
846 pub const Exhaustion = error{ InputStorageInUse, InputCapacityExceeded };
847 pub const DeriveError = @import("one.zig").DeriveError;
848 pub const acquisition: Acquisition = .{
849 .reader = true,
850 .preflight = true,
851 };
852 pub const overload: Overload = overload_policy;
853 pub const limit_field = "input_bytes";
854 pub const loaded_field = "loaded_input_bytes";
855 pub const high_water_field = "high_water_input_bytes";
856 pub const rejected_field = "rejected_input_count";
857 pub const in_use_error: Exhaustion = error.InputStorageInUse;
858 pub const capacity_error: Exhaustion = error.InputCapacityExceeded;
859 pub const claim: alloc_phase.capacity.Declaration = undefined;
860 };
861 }