lib/zen/src/quiz/storage.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const alloc_phase = @import("alloc_phase");
3 const capacity_mod = @import("capacity.zig");
4 const model = @import("model.zig");
5 const plan_mod = @import("plan.zig");
6
7 pub const Regions = struct {
8 plan: plan_mod.Plan,
9 questions: []model.Question,
10 options: []model.Option,
11 joined_text: []u8,
12 };
13
14 pub const Status = struct {
15 phase: alloc_phase.capacity.Phase,
16 in_use: bool,
17 storage_bytes: usize,
18 max_questions: usize,
19 max_options: usize,
20 max_joined_text_bytes: usize,
21 question_count: usize,
22 option_count: usize,
23 joined_text_bytes: usize,
24 high_water_questions: usize,
25 high_water_options: usize,
26 high_water_joined_text_bytes: usize,
27 rejected_source_count: u64,
28 };
29
30 pub const Storage = struct {
31 phase: alloc_phase.capacity.Phase,
32 capacity: capacity_mod.Capacity,
33 bytes: []align(capacity_mod.storage_alignment) u8,
34 questions: []model.Question,
35 options: []model.Option,
36 joined_text: []u8,
37 in_use: bool = false,
38 question_count: usize = 0,
39 option_count: usize = 0,
40 joined_text_bytes: usize = 0,
41 high_water_questions: usize = 0,
42 high_water_options: usize = 0,
43 high_water_joined_text_bytes: usize = 0,
44 rejected_source_count: u64 = 0,
45
46 pub const Limits: type = capacity_mod.Limits;
47 pub const Capacity: type = capacity_mod.Capacity;
48 pub const Exhaustion: type = model.Exhaustion;
49 pub const InitError = std.mem.Allocator.Error || capacity_mod.DeriveError;
50 pub const AcquireError: type = model.Error;
51
52 pub const claim: alloc_phase.capacity.Declaration = .{
53 .source = .{
54 .id = "zen.quiz_storage",
55 .kind = .phase_static,
56 .limit_source = .caller,
57 .storage = .{
58 .covered = &.{
59 .{
60 .id = "quiz_question_descriptors",
61 .lifetime = .steady,
62 .detail = "quiz question descriptors",
63 },
64 .{
65 .id = "quiz_option_descriptors",
66 .lifetime = .steady,
67 .detail = "quiz option descriptors",
68 },
69 .{
70 .id = "normalized_continued_quiz_text",
71 .lifetime = .steady,
72 .detail = "normalized continued quiz text",
73 },
74 },
75 .excluded = &.{
76 "caller-owned quiz source and borrowed single-line text",
77 "Markdown document, output, and inline-rendering owners",
78 "site catalog, theme, and filesystem output owners",
79 },
80 },
81 .capacity = .{
82 .inputs = &.{
83 alloc_phase.capacity.bindInput(Limits, "max_questions", "max_questions"),
84 alloc_phase.capacity.bindInput(Limits, "max_options", "max_options"),
85 alloc_phase.capacity.bindInput(Limits, "max_joined_text_bytes", "max_joined_text_bytes"),
86 },
87 .type_selectors = &.{
88 alloc_phase.capacity.bindType(model.Question, "question"),
89 alloc_phase.capacity.bindType(model.Option, "option"),
90 },
91 .nodes = &.{
92 .{ .input = 0 },
93 .{ .scale = .{ .node = 0, .coefficient = .{ .size_of_concrete_type = 0 } } },
94 .{ .alignment = .{ .node = 1, .alignment = .{ .literal = 16 } } },
95 .{ .input = 1 },
96 .{ .scale = .{ .node = 3, .coefficient = .{ .size_of_concrete_type = 1 } } },
97 .{ .alignment = .{ .node = 4, .alignment = .{ .literal = 16 } } },
98 .{ .input = 2 },
99 .{ .add = .{ .left = 2, .right = 5 } },
100 .{ .add = .{ .left = 7, .right = 6 } },
101 },
102 .assertions = &.{.{
103 .scope = .closure_total,
104 .measure = .retained,
105 .relation = .exact,
106 .expression = 8,
107 }},
108 },
109 .overload = .{
110 .kind = .reject_before_mutation,
111 .detail = "invalid input and max plus one reject before the backing region or active result changes; only rejection telemetry advances",
112 },
113 .risks = .{
114 .transitive = .{
115 .status = .witnessed,
116 .detail = "scanning, planning, descriptor filling, and continuation joining use borrowed input and acquired slices only",
117 },
118 .foreign = .{
119 .status = .excluded,
120 .detail = "quiz parsing crosses no operating-system or foreign callback boundary",
121 },
122 },
123 .obligations = &.{
124 .{ .key = "zen_quiz_capacity", .role = .capacity_model },
125 .{ .key = "zen_quiz_acquisition", .role = .custom },
126 .{ .key = "zen_quiz_oom", .role = .custom },
127 .{ .key = "zen_quiz_boundaries", .role = .overload },
128 .{ .key = "zen_quiz_reuse", .role = .overload },
129 .{ .key = "zen_quiz_sealed", .role = .transitive_risk },
130 .{ .key = "zen_quiz_root", .role = .custom },
131 .{ .key = "zen_quiz_consumer", .role = .foreign_risk },
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 pub fn init(allocator: std.mem.Allocator, limits: Limits) InitError!Storage {
154 const capacity = try Capacity.derive(limits);
155 const bytes = if (capacity.storage_bytes == 0)
156 @as([]align(capacity_mod.storage_alignment) u8, &.{})
157 else
158 try allocator.alignedAlloc(
159 u8,
160 .fromByteUnits(capacity_mod.storage_alignment),
161 capacity.storage_bytes,
162 );
163 return .{
164 .phase = .initialization,
165 .capacity = capacity,
166 .bytes = bytes,
167 .questions = typedSlice(
168 model.Question,
169 bytes,
170 capacity.questions_offset,
171 limits.max_questions,
172 ),
173 .options = typedSlice(
174 model.Option,
175 bytes,
176 capacity.options_offset,
177 limits.max_options,
178 ),
179 .joined_text = bytes[capacity.joined_text_offset..][0..limits.max_joined_text_bytes],
180 };
181 }
182
183 pub fn activate(self: *Storage) void {
184 std.debug.assert(self.phase == .initialization);
185 self.assertStorage();
186 self.phase = .steady;
187 }
188
189 pub fn acquire(self: *Storage, source: []const u8) AcquireError!Regions {
190 std.debug.assert(self.phase == .steady);
191 if (self.in_use) return error.QuizStorageInUse;
192 const plan = plan_mod.Plan.inspect(source, self.capacity.limits) catch |err| {
193 self.rejected_source_count +|= 1;
194 return err;
195 };
196 self.in_use = true;
197 self.question_count = plan.questions;
198 self.option_count = plan.options;
199 self.joined_text_bytes = plan.joined_text_bytes;
200 self.high_water_questions = @max(self.high_water_questions, plan.questions);
201 self.high_water_options = @max(self.high_water_options, plan.options);
202 self.high_water_joined_text_bytes = @max(
203 self.high_water_joined_text_bytes,
204 plan.joined_text_bytes,
205 );
206 self.assertStorage();
207 return .{
208 .plan = plan,
209 .questions = self.questions[0..plan.questions],
210 .options = self.options[0..plan.options],
211 .joined_text = self.joined_text[0..plan.joined_text_bytes],
212 };
213 }
214
215 pub fn reset(self: *Storage) void {
216 std.debug.assert(self.phase == .steady);
217 std.debug.assert(self.in_use);
218 self.in_use = false;
219 self.question_count = 0;
220 self.option_count = 0;
221 self.joined_text_bytes = 0;
222 self.assertStorage();
223 }
224
225 pub fn status(self: *const Storage) Status {
226 return .{
227 .phase = self.phase,
228 .in_use = self.in_use,
229 .storage_bytes = self.capacity.storage_bytes,
230 .max_questions = self.capacity.limits.max_questions,
231 .max_options = self.capacity.limits.max_options,
232 .max_joined_text_bytes = self.capacity.limits.max_joined_text_bytes,
233 .question_count = self.question_count,
234 .option_count = self.option_count,
235 .joined_text_bytes = self.joined_text_bytes,
236 .high_water_questions = self.high_water_questions,
237 .high_water_options = self.high_water_options,
238 .high_water_joined_text_bytes = self.high_water_joined_text_bytes,
239 .rejected_source_count = self.rejected_source_count,
240 };
241 }
242
243 pub fn deinit(self: *Storage, allocator: std.mem.Allocator) void {
244 std.debug.assert(self.phase != .teardown);
245 std.debug.assert(!self.in_use);
246 self.assertStorage();
247 self.phase = .teardown;
248 allocator.free(self.bytes);
249 self.bytes = &.{};
250 self.questions = &.{};
251 self.options = &.{};
252 self.joined_text = &.{};
253 }
254
255 fn assertStorage(self: *const Storage) void {
256 std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
257 std.debug.assert(self.questions.len == self.capacity.limits.max_questions);
258 std.debug.assert(self.options.len == self.capacity.limits.max_options);
259 std.debug.assert(self.joined_text.len == self.capacity.limits.max_joined_text_bytes);
260 std.debug.assert(self.question_count <= self.questions.len);
261 std.debug.assert(self.option_count <= self.options.len);
262 std.debug.assert(self.joined_text_bytes <= self.joined_text.len);
263 std.debug.assert(self.high_water_questions <= self.questions.len);
264 std.debug.assert(self.high_water_options <= self.options.len);
265 std.debug.assert(self.high_water_joined_text_bytes <= self.joined_text.len);
266 if (!self.in_use) {
267 std.debug.assert(self.question_count == 0);
268 std.debug.assert(self.option_count == 0);
269 std.debug.assert(self.joined_text_bytes == 0);
270 }
271 }
272 };
273
274 fn typedSlice(
275 comptime T: type,
276 bytes: []align(capacity_mod.storage_alignment) u8,
277 offset: usize,
278 count: usize,
279 ) []T {
280 const byte_count = count * @sizeOf(T);
281 const region: []align(@alignOf(T)) u8 = @alignCast(bytes[offset..][0..byte_count]);
282 return std.mem.bytesAsSlice(T, region);
283 }
284
285 fn checkInitFailures(allocator: std.mem.Allocator) !void {
286 var storage = try Storage.init(allocator, .{
287 .max_questions = 2,
288 .max_options = 3,
289 .max_joined_text_bytes = 65,
290 });
291 storage.deinit(allocator);
292 }
293
294 test "quiz storage acquires one exact aligned region" {
295 comptime {
296 @stardustClaim(
297 @import("alloc_phase").capacity.witness(Storage, "zen_quiz_acquisition"),
298 null,
299 null,
300 null,
301 null,
302 null,
303 null,
304 );
305 }
306
307 var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
308 const limits = capacity_mod.Limits{
309 .max_questions = 2,
310 .max_options = 3,
311 .max_joined_text_bytes = 65,
312 };
313 const capacity = try capacity_mod.Capacity.derive(limits);
314 var storage = try Storage.init(counting.allocator(), limits);
315 defer storage.deinit(counting.allocator());
316
317 try std.testing.expectEqual(@as(usize, 1), counting.alloc_index);
318 try std.testing.expectEqual(capacity.storage_bytes, counting.allocated_bytes);
319 try std.testing.expectEqual(alloc_phase.capacity.Phase.initialization, storage.status().phase);
320 storage.activate();
321 const source = "? Prompt\n* Correct\n- Other\n! First\ncontinued\n";
322 const regions = try storage.acquire(source);
323 defer storage.reset();
324 const base = @intFromPtr(storage.bytes.ptr);
325 try std.testing.expectEqual(base + capacity.questions_offset, @intFromPtr(regions.questions.ptr));
326 try std.testing.expectEqual(base + capacity.options_offset, @intFromPtr(regions.options.ptr));
327 try std.testing.expectEqual(base + capacity.joined_text_offset, @intFromPtr(regions.joined_text.ptr));
328 }
329
330 test "quiz storage retries after every allocation failure" {
331 comptime {
332 @stardustClaim(
333 @import("alloc_phase").capacity.witness(Storage, "zen_quiz_oom"),
334 null,
335 null,
336 null,
337 null,
338 null,
339 null,
340 );
341 }
342
343 try std.testing.checkAllAllocationFailures(std.testing.allocator, checkInitFailures, .{});
344 }
345
346 comptime {
347 alloc_phase.capacity.requireAllocatorRejectingOwnerShape(Storage);
348 }