lib/choir/src/core/context/memory.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const alloc_fixed = @import("alloc_fixed");
3
4 const Alignment = std.mem.Alignment;
5 const Allocator = std.mem.Allocator;
6 const FixedBufferAllocator = alloc_fixed.FixedBuffer;
7
8 /// The smallest alignment `Capacity.derive` accepts.
9 ///
10 /// Every segment is placed under the limits' `maximum_alignment`, and the
11 /// storage opens with the `Memory` header, so an alignment under the
12 /// header's own would put the header where its own type cannot be read.
13 /// `derive` refuses one with `error.CapacityOverflow`.
14 ///
15 /// THE FLOOR IS PUBLIC BECAUSE A CALLER STATES THE ALIGNMENT. `Memory` is
16 /// not one of the names `Context` re-exports, so a caller that lets a person
17 /// state this figure has no other way to ask what the smallest legal one is,
18 /// and a number copied into the caller drifts the day this one moves.
19 pub const minimum_alignment: Alignment = .fromByteUnits(@alignOf(Memory));
20
21 pub const Limits = struct {
22 maximum_alignment: Alignment,
23 configuration: Configuration,
24 types: Types,
25 attributes: Attributes,
26 operations: Operations,
27 diagnostics: Diagnostics,
28 transient_bytes: usize,
29
30 pub const Configuration = struct {
31 table_bytes: usize,
32 name_bytes: usize,
33 interface_bytes: usize,
34 transaction_bytes: usize,
35 };
36
37 pub const Types = struct {
38 table_bytes: usize,
39 key_bytes: usize,
40 payload_bytes: usize,
41 };
42
43 pub const Attributes = struct {
44 table_bytes: usize,
45 payload_bytes: usize,
46 };
47
48 pub const Operations = struct {
49 storage_bytes: usize,
50 nested_bytes: usize,
51 };
52
53 pub const Diagnostics = struct {
54 handler_bytes: usize,
55 payload_bytes: usize,
56 };
57
58 /// The bytes `limits` reserves for `selected`.
59 ///
60 /// THE GROUPING AND THE NAMES ARE TWO VIEWS OF ONE SET OF FIGURES. The
61 /// fields above are grouped by the table they size, because that is how
62 /// the context is built. `Segment` names the same figures flat, because
63 /// that is how a caller states one of them, how `Usage` reports it, and
64 /// how an exhaustion names the one that filled. This map is the seam
65 /// between the two views, and it lives here so that a segment added to
66 /// the enum cannot compile until this switch answers for it.
67 pub fn at(self: Limits, selected: Segment) usize {
68 var held = self;
69 return held.field(selected).*;
70 }
71
72 /// Replaces the bytes `self` reserves for `selected`, which is how a
73 /// caller states one capacity and leaves the rest as they were.
74 pub fn set(self: *Limits, selected: Segment, bytes: usize) void {
75 self.field(selected).* = bytes;
76 }
77
78 fn field(self: *Limits, selected: Segment) *usize {
79 return switch (selected) {
80 .configuration_tables => &self.configuration.table_bytes,
81 .configuration_names => &self.configuration.name_bytes,
82 .configuration_interfaces => &self.configuration.interface_bytes,
83 .configuration_transactions => &self.configuration.transaction_bytes,
84 .type_tables => &self.types.table_bytes,
85 .type_keys => &self.types.key_bytes,
86 .type_payloads => &self.types.payload_bytes,
87 .attribute_tables => &self.attributes.table_bytes,
88 .attribute_payloads => &self.attributes.payload_bytes,
89 .operation_storage => &self.operations.storage_bytes,
90 .operation_nested => &self.operations.nested_bytes,
91 .diagnostic_handlers => &self.diagnostics.handler_bytes,
92 .diagnostic_payloads => &self.diagnostics.payload_bytes,
93 .transient => &self.transient_bytes,
94 };
95 }
96
97 pub const testing: Limits = .{
98 .maximum_alignment = .@"64",
99 .configuration = .{
100 .table_bytes = 512 * 1024,
101 .name_bytes = 128 * 1024,
102 .interface_bytes = 512 * 1024,
103 .transaction_bytes = 128 * 1024,
104 },
105 .types = .{
106 .table_bytes = 512 * 1024,
107 .key_bytes = 256 * 1024,
108 .payload_bytes = 512 * 1024,
109 },
110 .attributes = .{
111 .table_bytes = 1024 * 1024,
112 .payload_bytes = 1024 * 1024,
113 },
114 .operations = .{
115 .storage_bytes = 16 * 1024 * 1024,
116 .nested_bytes = 16 * 1024 * 1024,
117 },
118 .diagnostics = .{
119 .handler_bytes = 64 * 1024,
120 .payload_bytes = 256 * 1024,
121 },
122 .transient_bytes = 2 * 1024 * 1024,
123 };
124
125 pub const standard: Limits = .{
126 .maximum_alignment = .@"64",
127 .configuration = .{
128 .table_bytes = 64 * 1024,
129 .name_bytes = 32 * 1024,
130 .interface_bytes = 512 * 1024,
131 .transaction_bytes = 64 * 1024,
132 },
133 .types = .{
134 .table_bytes = 128 * 1024,
135 .key_bytes = 128 * 1024,
136 .payload_bytes = 128 * 1024,
137 },
138 .attributes = .{
139 .table_bytes = 1024 * 1024,
140 .payload_bytes = 256 * 1024,
141 },
142 .operations = .{
143 .storage_bytes = 8 * 1024 * 1024,
144 .nested_bytes = 8 * 1024 * 1024,
145 },
146 .diagnostics = .{
147 .handler_bytes = 16 * 1024,
148 .payload_bytes = 128 * 1024,
149 },
150 .transient_bytes = 256 * 1024,
151 };
152 };
153
154 /// The largest total a context's storage may come to.
155 ///
156 /// A JUDGEMENT WITH AN ARGUMENT, NOT A MEASUREMENT OF ONE ALLOCATOR. An
157 /// allocator may add a header to a request, round it to a page, and count the
158 /// request itself more than once before it answers. `ArenaAllocator` counts it
159 /// twice: its fast path has already advanced the node's end index by the
160 /// request by the time it reaches
161 /// `mem.alignForward(usize, @sizeOf(Node) + aligned_index + n, 2)`, so the sum
162 /// it forms there is the request twice over plus what the arena already holds.
163 /// A quarter of the address space leaves the other three quarters for that
164 /// second copy, for everything the allocator holds, and for its own header. A
165 /// half does not: half aborts on a warm arena where a page below half answers
166 /// null, and a page below half is a measurement of one arena at one warmth
167 /// rather than a figure, since the slack a page buys is spent once the arena
168 /// holds more than a page. Exhausting a quarter would take an allocator
169 /// already holding eight exbibytes, which no host grants.
170 ///
171 /// No host grants a total this large either. On a 64-bit `usize` it is more
172 /// than a hundred billion times the total `Limits.testing` states, which is
173 /// the larger of the two presets this file carries. On a 32-bit `usize` the
174 /// quarter comes to one byte under a gibibyte, which is still more than
175 /// twenty-six times that same total and is a quarter of everything such a
176 /// host can address at all. Refusing at or above the figure denies no caller
177 /// anything on either width. Past the edge an allocator's behaviour
178 /// is its own business and not something this library can read:
179 /// `std.mem.Allocator.rawAlloc` is documented to answer null when it cannot
180 /// allocate, and an allocator that aborts instead does so on state this
181 /// library cannot see, since the same figure a fresh `ArenaAllocator` refuses
182 /// cleanly aborts one that already holds a node.
183 ///
184 /// The figure is pinned from below rather than derived: a test places the
185 /// largest limits this admits into a warm arena and holds that `Context.init`
186 /// answers `error.OutOfMemory` and never aborts. That test is what fails the
187 /// day an allocator's growth changes, which is the only derivation that stays
188 /// true.
189 pub const maximum_storage_bytes: usize = std.math.maxInt(usize) / 4;
190
191 /// Which stated figure `derive` cannot place, what was asked for, and the
192 /// largest that segment could have taken.
193 ///
194 /// `largest` IS READ IN `derive`'s PLACEMENT ORDER. It is the most this
195 /// segment could have taken GIVEN THE SEGMENTS PLACED BEFORE IT, so it is the
196 /// answer to "write this figure instead" and not a property of the segment on
197 /// its own. A caller that lowers an earlier segment raises this one.
198 pub const Refusal = struct {
199 segment: Segment,
200 stated: usize,
201 largest: usize,
202 };
203
204 /// The first segment figure `derive` cannot place, or null when every one
205 /// fits.
206 ///
207 /// ALLOCATION FREE AND SIDE EFFECT FREE, so a caller asks before it builds
208 /// anything and says the sentence in its own words, with the segment, the
209 /// figure stated and the figure that would be admitted.
210 ///
211 /// IT ANSWERS ABOUT THE FIGURES AND NOT THE ALIGNMENT. `derive` also refuses
212 /// an alignment under `minimum_alignment`, which is public for exactly that
213 /// reason and which a caller checks against directly. Given an alignment
214 /// `derive` accepts, `derive` answers `error.CapacityOverflow` exactly when
215 /// this answers non-null: a partial sum that would wrap has already crossed
216 /// half the address space, so `maximum_storage_bytes` subsumes the arithmetic
217 /// every `place` performs and no sum inside `derive` can fail once this has
218 /// passed.
219 pub fn refusal(limits: Limits) ?Refusal {
220 const alignment = limits.maximum_alignment.toByteUnits();
221 if (alignment < @alignOf(Memory)) return null;
222 var cursor: usize = @sizeOf(Memory);
223 for (std.enums.values(Segment)) |selected| {
224 const stated = limits.at(selected);
225 const offset = alignedWithin(cursor, alignment) orelse
226 return .{ .segment = selected, .stated = stated, .largest = 0 };
227 const largest = maximum_storage_bytes - offset;
228 if (stated > largest) return .{ .segment = selected, .stated = stated, .largest = largest };
229 cursor = offset + stated;
230 }
231 return null;
232 }
233
234 /// `cursor` rounded up to `alignment`, or null when that lands at or past
235 /// `maximum_storage_bytes`. `cursor` is at most the ceiling on entry, so the
236 /// rounding itself cannot wrap.
237 fn alignedWithin(cursor: usize, alignment: usize) ?usize {
238 std.debug.assert(cursor <= maximum_storage_bytes);
239 std.debug.assert(alignment != 0);
240 if (alignment > maximum_storage_bytes) return null;
241 const mask = alignment - 1;
242 if (cursor > maximum_storage_bytes - mask) return null;
243 const offset = (cursor + mask) & ~mask;
244 if (offset > maximum_storage_bytes) return null;
245 return offset;
246 }
247
248 pub const Segment = enum {
249 configuration_tables,
250 configuration_names,
251 configuration_interfaces,
252 configuration_transactions,
253 type_tables,
254 type_keys,
255 type_payloads,
256 attribute_tables,
257 attribute_payloads,
258 operation_storage,
259 operation_nested,
260 diagnostic_handlers,
261 diagnostic_payloads,
262 transient,
263 };
264
265 /// Which segment a request found full, and how many bytes it asked for.
266 ///
267 /// The limit and the bytes in use at that moment are the segment's own
268 /// `SegmentUsage`, so a caller that reports an exhaustion reads all four
269 /// figures without this type repeating two of them.
270 pub const Exhausted = struct {
271 segment: Segment,
272 requested_bytes: usize,
273 };
274
275 pub const SegmentUsage = struct {
276 frontier_bytes: usize,
277 reserved_bytes: usize,
278
279 pub fn remainingBytes(self: SegmentUsage) usize {
280 return self.reserved_bytes - self.frontier_bytes;
281 }
282 };
283
284 pub const Usage = struct {
285 configuration_tables: SegmentUsage,
286 configuration_names: SegmentUsage,
287 configuration_interfaces: SegmentUsage,
288 configuration_transactions: SegmentUsage,
289 type_tables: SegmentUsage,
290 type_keys: SegmentUsage,
291 type_payloads: SegmentUsage,
292 attribute_tables: SegmentUsage,
293 attribute_payloads: SegmentUsage,
294 operation_storage: SegmentUsage,
295 operation_nested: SegmentUsage,
296 diagnostic_handlers: SegmentUsage,
297 diagnostic_payloads: SegmentUsage,
298 transient: SegmentUsage,
299
300 pub fn get(self: Usage, selected: Segment) SegmentUsage {
301 return switch (selected) {
302 inline else => |segment_name| @field(self, @tagName(segment_name)),
303 };
304 }
305 };
306
307 const SegmentAllocator = struct {
308 fixed: FixedBufferAllocator,
309 buffer: []u8,
310 exhausted_segment: *?Segment,
311 exhausted_request: *usize,
312 segment_name: Segment,
313
314 fn init(buffer: []u8, memory: *Memory, segment_name: Segment) SegmentAllocator {
315 return .{
316 .fixed = FixedBufferAllocator.init(buffer),
317 .buffer = buffer,
318 .exhausted_segment = &memory.exhausted_segment,
319 .exhausted_request = &memory.exhausted_request,
320 .segment_name = segment_name,
321 };
322 }
323
324 pub fn allocator(self: *SegmentAllocator) Allocator {
325 return .{
326 .ptr = self,
327 .vtable = &.{
328 .alloc = alloc,
329 .resize = resize,
330 .remap = remap,
331 .free = free,
332 },
333 };
334 }
335
336 fn alloc(
337 context: *anyopaque,
338 len: usize,
339 alignment: Alignment,
340 return_address: usize,
341 ) ?[*]u8 {
342 const self: *SegmentAllocator = @ptrCast(@alignCast(context));
343 const result = self.fixed.allocator().rawAlloc(len, alignment, return_address);
344 if (result == null) self.recordExhaustion(len);
345 return result;
346 }
347
348 fn resize(
349 context: *anyopaque,
350 memory: []u8,
351 alignment: Alignment,
352 new_len: usize,
353 return_address: usize,
354 ) bool {
355 const self: *SegmentAllocator = @ptrCast(@alignCast(context));
356 return self.fixed.allocator().rawResize(memory, alignment, new_len, return_address);
357 }
358
359 fn remap(
360 context: *anyopaque,
361 memory: []u8,
362 alignment: Alignment,
363 new_len: usize,
364 return_address: usize,
365 ) ?[*]u8 {
366 const self: *SegmentAllocator = @ptrCast(@alignCast(context));
367 return self.fixed.allocator().rawRemap(memory, alignment, new_len, return_address);
368 }
369
370 fn free(
371 context: *anyopaque,
372 memory: []u8,
373 alignment: Alignment,
374 return_address: usize,
375 ) void {
376 const self: *SegmentAllocator = @ptrCast(@alignCast(context));
377 self.fixed.allocator().rawFree(memory, alignment, return_address);
378 }
379
380 fn recordExhaustion(self: *SegmentAllocator, requested_bytes: usize) void {
381 self.exhausted_segment.* = self.segment_name;
382 self.exhausted_request.* = requested_bytes;
383 }
384 };
385
386 pub const Capacity = struct {
387 storage_bytes: usize,
388 storage_alignment: Alignment,
389 memory: Slice,
390 configuration_tables: Slice,
391 configuration_names: Slice,
392 configuration_interfaces: Slice,
393 configuration_transactions: Slice,
394 type_tables: Slice,
395 type_keys: Slice,
396 type_payloads: Slice,
397 attribute_tables: Slice,
398 attribute_payloads: Slice,
399 operation_storage: Slice,
400 operation_nested: Slice,
401 diagnostic_handlers: Slice,
402 diagnostic_payloads: Slice,
403 transient: Slice,
404
405 pub const Slice = struct {
406 offset: usize,
407 bytes: usize,
408 };
409
410 pub fn asLimits(self: Capacity) Limits {
411 return .{
412 .maximum_alignment = self.storage_alignment,
413 .configuration = .{
414 .table_bytes = self.configuration_tables.bytes,
415 .name_bytes = self.configuration_names.bytes,
416 .interface_bytes = self.configuration_interfaces.bytes,
417 .transaction_bytes = self.configuration_transactions.bytes,
418 },
419 .types = .{
420 .table_bytes = self.type_tables.bytes,
421 .key_bytes = self.type_keys.bytes,
422 .payload_bytes = self.type_payloads.bytes,
423 },
424 .attributes = .{
425 .table_bytes = self.attribute_tables.bytes,
426 .payload_bytes = self.attribute_payloads.bytes,
427 },
428 .operations = .{
429 .storage_bytes = self.operation_storage.bytes,
430 .nested_bytes = self.operation_nested.bytes,
431 },
432 .diagnostics = .{
433 .handler_bytes = self.diagnostic_handlers.bytes,
434 .payload_bytes = self.diagnostic_payloads.bytes,
435 },
436 .transient_bytes = self.transient.bytes,
437 };
438 }
439
440 pub fn derive(limits: Limits) error{CapacityOverflow}!Capacity {
441 const requested_alignment = limits.maximum_alignment.toByteUnits();
442 if (requested_alignment < @alignOf(Memory)) return error.CapacityOverflow;
443 if (refusal(limits) != null) return error.CapacityOverflow;
444 var cursor: usize = 0;
445 const memory = try place(@sizeOf(Memory), @alignOf(Memory), &cursor);
446 const configuration_tables = try place(limits.configuration.table_bytes, requested_alignment, &cursor);
447 const configuration_names = try place(limits.configuration.name_bytes, requested_alignment, &cursor);
448 const configuration_interfaces = try place(limits.configuration.interface_bytes, requested_alignment, &cursor);
449 const configuration_transactions = try place(limits.configuration.transaction_bytes, requested_alignment, &cursor);
450 const type_tables = try place(limits.types.table_bytes, requested_alignment, &cursor);
451 const type_keys = try place(limits.types.key_bytes, requested_alignment, &cursor);
452 const type_payloads = try place(limits.types.payload_bytes, requested_alignment, &cursor);
453 const attribute_tables = try place(limits.attributes.table_bytes, requested_alignment, &cursor);
454 const attribute_payloads = try place(limits.attributes.payload_bytes, requested_alignment, &cursor);
455 const operation_storage = try place(limits.operations.storage_bytes, requested_alignment, &cursor);
456 const operation_nested = try place(limits.operations.nested_bytes, requested_alignment, &cursor);
457 const diagnostic_handlers = try place(limits.diagnostics.handler_bytes, requested_alignment, &cursor);
458 const diagnostic_payloads = try place(limits.diagnostics.payload_bytes, requested_alignment, &cursor);
459 const transient = try place(limits.transient_bytes, requested_alignment, &cursor);
460 return .{
461 .storage_bytes = cursor,
462 .storage_alignment = limits.maximum_alignment,
463 .memory = memory,
464 .configuration_tables = configuration_tables,
465 .configuration_names = configuration_names,
466 .configuration_interfaces = configuration_interfaces,
467 .configuration_transactions = configuration_transactions,
468 .type_tables = type_tables,
469 .type_keys = type_keys,
470 .type_payloads = type_payloads,
471 .attribute_tables = attribute_tables,
472 .attribute_payloads = attribute_payloads,
473 .operation_storage = operation_storage,
474 .operation_nested = operation_nested,
475 .diagnostic_handlers = diagnostic_handlers,
476 .diagnostic_payloads = diagnostic_payloads,
477 .transient = transient,
478 };
479 }
480 };
481
482 pub const Memory = struct {
483 exhausted_segment: ?Segment,
484 /// The size of the request that found `exhausted_segment` full. It is
485 /// read only beside that segment, through `exhaustion`.
486 exhausted_request: usize,
487 configuration_tables: SegmentAllocator,
488 configuration_names: SegmentAllocator,
489 configuration_interfaces: SegmentAllocator,
490 configuration_transactions: SegmentAllocator,
491 type_tables: SegmentAllocator,
492 type_keys: SegmentAllocator,
493 type_payloads: SegmentAllocator,
494 attribute_tables: SegmentAllocator,
495 attribute_payloads: SegmentAllocator,
496 operation_storage: SegmentAllocator,
497 operation_nested: SegmentAllocator,
498 diagnostic_handlers: SegmentAllocator,
499 diagnostic_payloads: SegmentAllocator,
500 transient: SegmentAllocator,
501
502 pub fn init(storage: [*]u8, capacity: Capacity) *Memory {
503 const self: *Memory = @ptrCast(@alignCast(storage + capacity.memory.offset));
504 self.exhausted_segment = null;
505 self.exhausted_request = 0;
506 self.configuration_tables = SegmentAllocator.init(segment(storage, capacity.configuration_tables), self, .configuration_tables);
507 self.configuration_names = SegmentAllocator.init(segment(storage, capacity.configuration_names), self, .configuration_names);
508 self.configuration_interfaces = SegmentAllocator.init(segment(storage, capacity.configuration_interfaces), self, .configuration_interfaces);
509 self.configuration_transactions = SegmentAllocator.init(segment(storage, capacity.configuration_transactions), self, .configuration_transactions);
510 self.type_tables = SegmentAllocator.init(segment(storage, capacity.type_tables), self, .type_tables);
511 self.type_keys = SegmentAllocator.init(segment(storage, capacity.type_keys), self, .type_keys);
512 self.type_payloads = SegmentAllocator.init(segment(storage, capacity.type_payloads), self, .type_payloads);
513 self.attribute_tables = SegmentAllocator.init(segment(storage, capacity.attribute_tables), self, .attribute_tables);
514 self.attribute_payloads = SegmentAllocator.init(segment(storage, capacity.attribute_payloads), self, .attribute_payloads);
515 self.operation_storage = SegmentAllocator.init(segment(storage, capacity.operation_storage), self, .operation_storage);
516 self.operation_nested = SegmentAllocator.init(segment(storage, capacity.operation_nested), self, .operation_nested);
517 self.diagnostic_handlers = SegmentAllocator.init(segment(storage, capacity.diagnostic_handlers), self, .diagnostic_handlers);
518 self.diagnostic_payloads = SegmentAllocator.init(segment(storage, capacity.diagnostic_payloads), self, .diagnostic_payloads);
519 self.transient = SegmentAllocator.init(segment(storage, capacity.transient), self, .transient);
520 return self;
521 }
522
523 pub fn usage(self: *const Memory) Usage {
524 return .{
525 .configuration_tables = segmentUsage(&self.configuration_tables),
526 .configuration_names = segmentUsage(&self.configuration_names),
527 .configuration_interfaces = segmentUsage(&self.configuration_interfaces),
528 .configuration_transactions = segmentUsage(&self.configuration_transactions),
529 .type_tables = segmentUsage(&self.type_tables),
530 .type_keys = segmentUsage(&self.type_keys),
531 .type_payloads = segmentUsage(&self.type_payloads),
532 .attribute_tables = segmentUsage(&self.attribute_tables),
533 .attribute_payloads = segmentUsage(&self.attribute_payloads),
534 .operation_storage = segmentUsage(&self.operation_storage),
535 .operation_nested = segmentUsage(&self.operation_nested),
536 .diagnostic_handlers = segmentUsage(&self.diagnostic_handlers),
537 .diagnostic_payloads = segmentUsage(&self.diagnostic_payloads),
538 .transient = segmentUsage(&self.transient),
539 };
540 }
541
542 pub fn exhaustion(self: *const Memory) ?Exhausted {
543 const segment_name = self.exhausted_segment orelse return null;
544 return .{ .segment = segment_name, .requested_bytes = self.exhausted_request };
545 }
546
547 pub fn hasCapacity(
548 self: *const Memory,
549 selected: Segment,
550 bytes: usize,
551 alignment: Alignment,
552 ) bool {
553 const allocator = switch (selected) {
554 inline else => |segment_name| &@field(self, @tagName(segment_name)),
555 };
556 const offset = std.mem.alignPointerOffset(
557 allocator.buffer.ptr + alloc_fixed.used(&allocator.fixed),
558 alignment.toByteUnits(),
559 ) orelse return false;
560 const aligned = std.math.add(usize, alloc_fixed.used(&allocator.fixed), offset) catch return false;
561 const end = std.math.add(usize, aligned, bytes) catch return false;
562 return end <= allocator.buffer.len;
563 }
564 };
565
566 pub fn cast(memory_pointer: *anyopaque) *Memory {
567 return @ptrCast(@alignCast(memory_pointer));
568 }
569
570 pub fn segmentAllocator(context: anytype, selected: Segment) Allocator {
571 if (comptime @hasField(@TypeOf(context.*), "memory")) {
572 const memory = cast(context.memory);
573 return switch (selected) {
574 inline else => |segment_name| @field(memory, @tagName(segment_name)).allocator(),
575 };
576 }
577 return context.allocator;
578 }
579
580 pub fn configurationTableAllocator(memory_pointer: *anyopaque) Allocator {
581 return cast(memory_pointer).configuration_tables.allocator();
582 }
583
584 pub fn configurationNameAllocator(memory_pointer: *anyopaque) Allocator {
585 return cast(memory_pointer).configuration_names.allocator();
586 }
587
588 pub fn configurationInterfaceAllocator(memory_pointer: *anyopaque) Allocator {
589 return cast(memory_pointer).configuration_interfaces.allocator();
590 }
591
592 pub fn configurationTransactionAllocator(memory_pointer: *anyopaque) Allocator {
593 return cast(memory_pointer).configuration_transactions.allocator();
594 }
595
596 pub fn typeTableAllocator(memory_pointer: *anyopaque) Allocator {
597 return cast(memory_pointer).type_tables.allocator();
598 }
599
600 pub fn typeKeyAllocator(memory_pointer: *anyopaque) Allocator {
601 return cast(memory_pointer).type_keys.allocator();
602 }
603
604 pub fn typePayloadAllocator(memory_pointer: *anyopaque) Allocator {
605 return cast(memory_pointer).type_payloads.allocator();
606 }
607
608 pub fn attributeTableAllocator(memory_pointer: *anyopaque) Allocator {
609 return cast(memory_pointer).attribute_tables.allocator();
610 }
611
612 pub fn attributePayloadAllocator(memory_pointer: *anyopaque) Allocator {
613 return cast(memory_pointer).attribute_payloads.allocator();
614 }
615
616 pub fn operationStorageAllocator(memory_pointer: *anyopaque) Allocator {
617 return cast(memory_pointer).operation_storage.allocator();
618 }
619
620 pub fn operationNestedAllocator(memory_pointer: *anyopaque) Allocator {
621 return cast(memory_pointer).operation_nested.allocator();
622 }
623
624 pub fn diagnosticHandlerAllocator(memory_pointer: *anyopaque) Allocator {
625 return cast(memory_pointer).diagnostic_handlers.allocator();
626 }
627
628 pub fn diagnosticPayloadAllocator(memory_pointer: *anyopaque) Allocator {
629 return cast(memory_pointer).diagnostic_payloads.allocator();
630 }
631
632 pub fn transientAllocator(memory_pointer: *anyopaque) Allocator {
633 return cast(memory_pointer).transient.allocator();
634 }
635
636 fn place(bytes: usize, alignment: usize, cursor: *usize) error{CapacityOverflow}!Capacity.Slice {
637 const mask = std.math.sub(usize, alignment, 1) catch return error.CapacityOverflow;
638 const padded = std.math.add(usize, cursor.*, mask) catch return error.CapacityOverflow;
639 const offset = padded & ~mask;
640 cursor.* = std.math.add(usize, offset, bytes) catch return error.CapacityOverflow;
641 return .{ .offset = offset, .bytes = bytes };
642 }
643
644 fn segment(storage: [*]u8, capacity: Capacity.Slice) []u8 {
645 return (storage + capacity.offset)[0..capacity.bytes];
646 }
647
648 fn segmentUsage(allocator: *const SegmentAllocator) SegmentUsage {
649 return .{
650 .frontier_bytes = alloc_fixed.used(&allocator.fixed),
651 .reserved_bytes = allocator.buffer.len,
652 };
653 }
654
655 test "Context capacity follows an independent aligned segment model" {
656 comptime {
657 @stardustClaim(
658 @import("alloc_phase").capacity.witness(@import("./root.zig").Context, "choir_context_capacity"),
659 null,
660 null,
661 null,
662 null,
663 null,
664 null,
665 );
666 }
667
668 const limits = Limits{
669 .maximum_alignment = .@"64",
670 .configuration = .{ .table_bytes = 1, .name_bytes = 2, .interface_bytes = 3, .transaction_bytes = 4 },
671 .types = .{ .table_bytes = 5, .key_bytes = 6, .payload_bytes = 7 },
672 .attributes = .{ .table_bytes = 8, .payload_bytes = 9 },
673 .operations = .{ .storage_bytes = 10, .nested_bytes = 11 },
674 .diagnostics = .{ .handler_bytes = 12, .payload_bytes = 13 },
675 .transient_bytes = 14,
676 };
677 const capacity = try Capacity.derive(limits);
678 var cursor: usize = 0;
679 cursor = std.mem.alignForward(usize, cursor, @alignOf(Memory)) + @sizeOf(Memory);
680 inline for (.{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 }) |bytes| {
681 cursor = std.mem.alignForward(usize, cursor, 64) + bytes;
682 }
683 try std.testing.expectEqual(cursor, capacity.storage_bytes);
684 try std.testing.expectEqualDeep(limits, capacity.asLimits());
685 try std.testing.expectEqualDeep(capacity, try Capacity.derive(capacity.asLimits()));
686 }
687
688 test "Context capacity rejects overflowing segments" {
689 comptime {
690 @stardustClaim(
691 @import("alloc_phase").capacity.witness(@import("./root.zig").Context, "choir_context_capacity_overflow"),
692 null,
693 null,
694 null,
695 null,
696 null,
697 null,
698 );
699 }
700
701 var limits = Limits.testing;
702 limits.configuration.table_bytes = std.math.maxInt(usize);
703 try std.testing.expectError(error.CapacityOverflow, Capacity.derive(limits));
704 }
705
706 test "Limits reads and writes every segment through its own field" {
707 var limits = Limits.testing;
708 for (std.enums.values(Segment), 0..) |named, index| {
709 limits.set(named, (index + 1) * 4096);
710 }
711 for (std.enums.values(Segment), 0..) |named, index| {
712 try std.testing.expectEqual((index + 1) * 4096, limits.at(named));
713 }
714 try std.testing.expectEqual(limits.operations.storage_bytes, limits.at(.operation_storage));
715 try std.testing.expectEqual(limits.operations.nested_bytes, limits.at(.operation_nested));
716 try std.testing.expectEqual(limits.transient_bytes, limits.at(.transient));
717 }
718
719 test "Limits names one segment for each figure it reserves" {
720 const counted = comptime blk: {
721 var total: usize = 0;
722 for (@typeInfo(Limits).@"struct".field_types) |outer| {
723 if (outer == usize) {
724 total += 1;
725 continue;
726 }
727 if (@typeInfo(outer) != .@"struct") continue;
728 for (@typeInfo(outer).@"struct".field_types) |inner| {
729 if (inner == usize) total += 1;
730 }
731 }
732 break :blk total;
733 };
734 try std.testing.expectEqual(counted, std.enums.values(Segment).len);
735 }
736
737 test "Limits leaves every other segment where it was" {
738 var limits = Limits.testing;
739 const before = limits;
740 limits.set(.type_keys, 1234);
741 for (std.enums.values(Segment)) |named| {
742 if (named == .type_keys) continue;
743 try std.testing.expectEqual(before.at(named), limits.at(named));
744 }
745 try std.testing.expectEqual(@as(usize, 1234), limits.at(.type_keys));
746 }
747
748 /// Limits whose every segment states `bytes`, under the testing alignment, so
749 /// a test can move one figure and leave the rest flat.
750 fn flatLimits(bytes: usize) Limits {
751 var limits = Limits.testing;
752 for (std.enums.values(Segment)) |selected| limits.set(selected, bytes);
753 return limits;
754 }
755
756 test "derive refuses exactly the limits refusal names" {
757 const testing = std.testing;
758 const cases = [_]Limits{
759 Limits.testing,
760 Limits.standard,
761 flatLimits(0),
762 flatLimits(4096),
763 flatLimits(maximum_storage_bytes),
764 flatLimits(maximum_storage_bytes / 14),
765 flatLimits(std.math.maxInt(usize)),
766 };
767 for (cases) |limits| {
768 const named = refusal(limits);
769 if (Capacity.derive(limits)) |_| {
770 try testing.expect(named == null);
771 } else |err| {
772 try testing.expectEqual(error.CapacityOverflow, err);
773 try testing.expect(named != null);
774 }
775 }
776 }
777
778 test "a total that crosses only at the last segment placed is refused at that segment" {
779 const testing = std.testing;
780 var limits = Limits.testing;
781 for (std.enums.values(Segment)) |selected| limits.set(selected, 0);
782
783 const last = std.enums.values(Segment)[std.enums.values(Segment).len - 1];
784 try testing.expectEqual(Segment.transient, last);
785
786 limits.set(last, maximum_storage_bytes);
787 const named = refusal(limits) orelse return error.TestExpectedRefusal;
788 try testing.expectEqual(last, named.segment);
789 try testing.expectEqual(maximum_storage_bytes, named.stated);
790 try testing.expect(named.largest < maximum_storage_bytes);
791 try testing.expectError(error.CapacityOverflow, Capacity.derive(limits));
792
793 limits.set(last, named.largest);
794 try testing.expect(refusal(limits) == null);
795 const capacity = try Capacity.derive(limits);
796 try testing.expect(capacity.storage_bytes <= maximum_storage_bytes);
797 }
798
799 test "refusal names the first segment in derive's own placement order" {
800 const testing = std.testing;
801 var limits = flatLimits(0);
802 limits.set(.type_keys, maximum_storage_bytes);
803 limits.set(.transient, maximum_storage_bytes);
804 const named = refusal(limits) orelse return error.TestExpectedRefusal;
805 try testing.expectEqual(Segment.type_keys, named.segment);
806
807 var later = flatLimits(0);
808 later.set(.transient, maximum_storage_bytes);
809 const only_last = refusal(later) orelse return error.TestExpectedRefusal;
810 try testing.expectEqual(Segment.transient, only_last.segment);
811 }
812
813 test "the largest figure a segment may take falls as the segments before it rise" {
814 const testing = std.testing;
815 var lean = flatLimits(0);
816 lean.set(.transient, maximum_storage_bytes);
817 const roomy = (refusal(lean) orelse return error.TestExpectedRefusal).largest;
818
819 var heavy = flatLimits(1024 * 1024);
820 heavy.set(.transient, maximum_storage_bytes);
821 const cramped = (refusal(heavy) orelse return error.TestExpectedRefusal).largest;
822
823 try testing.expect(cramped < roomy);
824 }