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 }