lib/choir/src/product/storage.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const alloc_phase = @import("alloc_phase");
3
4 const Allocator = std.mem.Allocator;
5
6 pub const segment_count: usize = 4;
7
8 pub const Segment = struct {
9 count: usize,
10 element_bytes: usize,
11 alignment: std.mem.Alignment,
12 };
13
14 pub fn segment(comptime T: type, count: usize) Segment {
15 return .{
16 .count = count,
17 .element_bytes = @sizeOf(T),
18 .alignment = .fromByteUnits(@alignOf(T)),
19 };
20 }
21
22 pub const Storage = struct {
23 pub const Limits = struct {
24 segments: [segment_count]Segment,
25 };
26
27 pub const Capacity = struct {
28 offsets: [segment_count]usize,
29 byte_lengths: [segment_count]usize,
30 alignments: [segment_count]std.mem.Alignment,
31 total_bytes: usize,
32 allocation_alignment: std.mem.Alignment,
33
34 pub fn derive(limits: Limits) error{CapacityOverflow}!Capacity {
35 var offsets: [segment_count]usize = undefined;
36 var byte_lengths: [segment_count]usize = undefined;
37 var alignments: [segment_count]std.mem.Alignment = undefined;
38 var cursor: usize = 0;
39 var allocation_alignment: usize = 1;
40 for (limits.segments, &offsets, &byte_lengths, &alignments) |segment_value, *offset, *byte_length, *alignment| {
41 byte_length.* = std.math.mul(
42 usize,
43 segment_value.count,
44 segment_value.element_bytes,
45 ) catch return error.CapacityOverflow;
46 offset.* = try placeBytes(
47 byte_length.*,
48 segment_value.alignment.toByteUnits(),
49 &cursor,
50 &allocation_alignment,
51 );
52 alignment.* = segment_value.alignment;
53 }
54 return .{
55 .offsets = offsets,
56 .byte_lengths = byte_lengths,
57 .alignments = alignments,
58 .total_bytes = cursor,
59 .allocation_alignment = .fromByteUnits(allocation_alignment),
60 };
61 }
62 };
63
64 pub const InitError = Allocator.Error || error{CapacityOverflow};
65
66 pub const claim: alloc_phase.capacity.Declaration = .{
67 .source = .{
68 .id = "choir.product_metadata_storage",
69 .kind = .phase_static,
70 .limit_source = .caller,
71 .storage = .{
72 .covered = &.{
73 .{
74 .id = "durable_product_graph_records_and_names",
75 .lifetime = .steady,
76 .detail = "durable product graph records and names",
77 },
78 .{
79 .id = "durable_refresh_plan_records_and_names",
80 .lifetime = .steady,
81 .detail = "durable refresh plan records and names",
82 },
83 .{
84 .id = "durable_refresh_report_projections",
85 .lifetime = .steady,
86 .detail = "durable refresh report projections",
87 },
88 },
89 .excluded = &.{
90 "borrowing product graph builders and closure workspaces",
91 "caller-owned source metadata and allocator implementation state",
92 "retained exact records in separately bounded revision stores",
93 },
94 },
95 .capacity = .{
96 .inputs = &.{},
97 .type_selectors = &.{},
98 .nodes = &.{
99 .{ .constant = 0 },
100 },
101 .assertions = &.{.{
102 .scope = .closure_total,
103 .measure = .retained,
104 .relation = .exact,
105 .expression = 0,
106 }},
107 },
108 .overload = .{
109 .kind = .reject_before_seal,
110 .detail = "count, name-byte, layout, or allocation failure returns before a durable product value is published",
111 },
112 .risks = .{
113 .transitive = .{
114 .status = .witnessed,
115 .detail = "graph queries read retained immutable revision records; teardown releases those handles after the local region",
116 },
117 .foreign = .{
118 .status = .excluded,
119 .detail = "product metadata construction and queries cross no operating-system or foreign callback boundary",
120 },
121 },
122 .obligations = &.{
123 .{ .key = "choir_product_metadata_capacity", .role = .capacity_model },
124 .{ .key = "choir_product_metadata_acquisition", .role = .custom },
125 .{ .key = "choir_product_metadata_boundary", .role = .overload },
126 .{ .key = "choir_product_metadata_oom", .role = .overload },
127 .{ .key = "choir_product_graph_lifetime_transitive_risk", .role = .transitive_risk },
128 .{ .key = "choir_product_graph_lifetime_foreign_risk", .role = .foreign_risk },
129 .{ .key = "choir_product_refresh_lifetime", .role = .transitive_risk },
130 .{ .key = "choir_product_report_composition_lifetime", .role = .transitive_risk },
131 .{ .key = "choir_product_metadata_integration", .role = .custom },
132 },
133 },
134 .bindings = .{
135 .owner = @This(),
136 .seal = .{
137 .family = alloc_phase.capacity.selector(@This().activate),
138 .premise = .{
139 .class = .checked_semantic_fact,
140 .authority = .checker,
141 },
142 },
143 .teardown = .{
144 .family = alloc_phase.capacity.selector(@This().deinit),
145 .premise = .{
146 .class = .checked_semantic_fact,
147 .authority = .checker,
148 },
149 },
150 },
151 };
152
153 phase: alloc_phase.capacity.Phase,
154 capacity: Capacity,
155 bytes: [*]u8,
156
157 pub fn init(allocator: Allocator, limits: Limits) InitError!Storage {
158 const capacity = try Capacity.derive(limits);
159 std.debug.assert(capacity.total_bytes > 0);
160 const bytes = allocator.rawAlloc(
161 capacity.total_bytes,
162 capacity.allocation_alignment,
163 @returnAddress(),
164 ) orelse return error.OutOfMemory;
165 return .{
166 .phase = .initialization,
167 .capacity = capacity,
168 .bytes = bytes,
169 };
170 }
171
172 pub fn region(self: *Storage, comptime T: type, comptime index: usize, count: usize) []T {
173 comptime std.debug.assert(index < segment_count);
174 std.debug.assert(self.phase == .initialization);
175 const byte_length = std.math.mul(usize, count, @sizeOf(T)) catch unreachable;
176 std.debug.assert(byte_length == self.capacity.byte_lengths[index]);
177 std.debug.assert(@alignOf(T) == self.capacity.alignments[index].toByteUnits());
178 const offset = self.capacity.offsets[index];
179 const address = std.math.add(usize, @intFromPtr(self.bytes), offset) catch unreachable;
180 const raw: [*]u8 = @ptrFromInt(address);
181 const items: [*]T = @ptrCast(@alignCast(raw));
182 return items[0..count];
183 }
184
185 pub fn activate(self: *Storage) void {
186 std.debug.assert(self.phase == .initialization);
187 self.phase = .steady;
188 }
189
190 pub fn status(self: *const Storage) alloc_phase.capacity.Phase {
191 return self.phase;
192 }
193
194 pub fn deinit(self: *Storage, allocator: Allocator) void {
195 std.debug.assert(self.phase != .teardown);
196 self.phase = .teardown;
197 allocator.rawFree(
198 self.bytes[0..self.capacity.total_bytes],
199 self.capacity.allocation_alignment,
200 @returnAddress(),
201 );
202 self.* = undefined;
203 }
204 };
205
206 comptime {
207 alloc_phase.capacity.requireAllocatorExactOwnerShape(Storage);
208 }
209
210 fn placeBytes(
211 bytes: usize,
212 alignment: usize,
213 cursor: *usize,
214 allocation_alignment: *usize,
215 ) error{CapacityOverflow}!usize {
216 std.debug.assert(std.math.isPowerOfTwo(alignment));
217 const mask = std.math.sub(usize, alignment, 1) catch unreachable;
218 const padded = std.math.add(usize, cursor.*, mask) catch return error.CapacityOverflow;
219 const offset = padded & ~mask;
220 cursor.* = std.math.add(usize, offset, bytes) catch return error.CapacityOverflow;
221 allocation_alignment.* = @max(allocation_alignment.*, alignment);
222 return offset;
223 }
224
225 test "product metadata capacity matches an independent aligned byte model" {
226 comptime {
227 @stardustClaim(
228 @import("alloc_phase").capacity.witness(Storage, "choir_product_metadata_capacity"),
229 null,
230 null,
231 null,
232 null,
233 null,
234 null,
235 );
236 }
237
238 const limits = Storage.Limits{ .segments = .{
239 segment(u64, 3),
240 segment(u8, 7),
241 segment(u128, 2),
242 segment(u16, 5),
243 } };
244 const capacity = try Storage.Capacity.derive(limits);
245
246 var expected: usize = 0;
247 inline for (.{ u64, u8, u128, u16 }, .{ 3, 7, 2, 5 }) |T, count| {
248 expected = std.mem.alignForward(usize, expected, @alignOf(T));
249 expected = try std.math.add(usize, expected, try std.math.mul(usize, count, @sizeOf(T)));
250 }
251
252 try std.testing.expectEqual(expected, capacity.total_bytes);
253 try std.testing.expectEqual(@as(usize, @alignOf(u128)), capacity.allocation_alignment.toByteUnits());
254 }
255
256 test "product metadata storage acquires one exact aligned region" {
257 comptime {
258 @stardustClaim(
259 @import("alloc_phase").capacity.witness(Storage, "choir_product_metadata_acquisition"),
260 null,
261 null,
262 null,
263 null,
264 null,
265 null,
266 );
267 }
268
269 const limits = Storage.Limits{ .segments = .{
270 segment(u64, 3),
271 segment(u8, 7),
272 segment(u128, 2),
273 segment(u16, 5),
274 } };
275 const capacity = try Storage.Capacity.derive(limits);
276 var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{});
277 var storage = try Storage.init(failing.allocator(), limits);
278 defer storage.deinit(failing.allocator());
279
280 try std.testing.expectEqual(@as(usize, 1), failing.alloc_index);
281 try std.testing.expectEqual(capacity.total_bytes, failing.allocated_bytes);
282 const allocation_alignment = capacity.allocation_alignment.toByteUnits();
283 std.debug.assert(allocation_alignment > 0);
284 try std.testing.expect(@intFromPtr(storage.bytes) % allocation_alignment == 0);
285 try std.testing.expectEqual(@as(usize, 3), storage.region(u64, 0, 3).len);
286 try std.testing.expectEqual(@as(usize, 7), storage.region(u8, 1, 7).len);
287 try std.testing.expect(!alloc_phase.capacity.typeHasAllocatorCapability(Storage));
288 try std.testing.expectEqual(alloc_phase.capacity.Phase.initialization, storage.status());
289 storage.activate();
290 try std.testing.expectEqual(alloc_phase.capacity.Phase.steady, storage.status());
291 }
292
293 test "product metadata storage rejects max plus one arithmetic" {
294 comptime {
295 @stardustClaim(
296 @import("alloc_phase").capacity.witness(Storage, "choir_product_metadata_boundary"),
297 null,
298 null,
299 null,
300 null,
301 null,
302 null,
303 );
304 }
305
306 try std.testing.expectError(error.CapacityOverflow, Storage.Capacity.derive(.{ .segments = .{
307 segment(usize, std.math.maxInt(usize)),
308 segment(u8, 0),
309 segment(u8, 0),
310 segment(u8, 0),
311 } }));
312 try std.testing.expectError(error.CapacityOverflow, Storage.Capacity.derive(.{ .segments = .{
313 segment(u8, std.math.maxInt(usize)),
314 segment(u128, 1),
315 segment(u8, 0),
316 segment(u8, 0),
317 } }));
318 }
319
320 test "product metadata storage retries after allocation failure" {
321 comptime {
322 @stardustClaim(
323 @import("alloc_phase").capacity.witness(Storage, "choir_product_metadata_oom"),
324 null,
325 null,
326 null,
327 null,
328 null,
329 null,
330 );
331 }
332
333 const limits = Storage.Limits{ .segments = .{
334 segment(usize, 1),
335 segment(u8, 1),
336 segment(u8, 0),
337 segment(u8, 0),
338 } };
339 var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 0 });
340 try std.testing.expectError(error.OutOfMemory, Storage.init(failing.allocator(), limits));
341
342 failing.fail_index = std.math.maxInt(usize);
343 var storage = try Storage.init(failing.allocator(), limits);
344 storage.deinit(failing.allocator());
345 }