lib/zen/src/quiz/capacity.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const model = @import("model.zig");
3 const plan_mod = @import("plan.zig");
4
5 pub const storage_alignment: usize = @max(
6 @alignOf(model.Question),
7 @alignOf(model.Option),
8 );
9
10 pub const Limits = plan_mod.Limits;
11 pub const DeriveError = error{CapacityOverflow};
12
13 pub const Capacity = struct {
14 limits: Limits,
15 questions_offset: usize,
16 question_bytes: usize,
17 options_offset: usize,
18 option_bytes: usize,
19 joined_text_offset: usize,
20 storage_bytes: usize,
21
22 pub fn derive(limits: Limits) DeriveError!Capacity {
23 const questions = try placed(model.Question, 0, limits.max_questions);
24 const options = try placed(model.Option, questions.end, limits.max_options);
25 return .{
26 .limits = limits,
27 .questions_offset = questions.start,
28 .question_bytes = questions.bytes,
29 .options_offset = options.start,
30 .option_bytes = options.bytes,
31 .joined_text_offset = options.end,
32 .storage_bytes = try added(options.end, limits.max_joined_text_bytes),
33 };
34 }
35 };
36
37 const Region = struct {
38 start: usize,
39 bytes: usize,
40 end: usize,
41 };
42
43 fn placed(comptime T: type, offset: usize, count: usize) DeriveError!Region {
44 const mask: usize = @alignOf(T) - 1;
45 const start = (try added(offset, mask)) & ~mask;
46 const bytes = std.math.mul(usize, count, @sizeOf(T)) catch {
47 return error.CapacityOverflow;
48 };
49 return .{ .start = start, .bytes = bytes, .end = try added(start, bytes) };
50 }
51
52 fn added(left: usize, right: usize) DeriveError!usize {
53 return std.math.add(usize, left, right) catch error.CapacityOverflow;
54 }
55
56 fn independent(limits: Limits) DeriveError!Capacity {
57 const question_bytes = @as(u128, limits.max_questions) * @sizeOf(model.Question);
58 const options_offset = alignForward(question_bytes, @alignOf(model.Option));
59 const option_bytes = @as(u128, limits.max_options) * @sizeOf(model.Option);
60 const joined_text_offset = options_offset + option_bytes;
61 const storage_bytes = joined_text_offset + limits.max_joined_text_bytes;
62 if (question_bytes > std.math.maxInt(usize) or
63 options_offset > std.math.maxInt(usize) or
64 option_bytes > std.math.maxInt(usize) or
65 joined_text_offset > std.math.maxInt(usize) or
66 storage_bytes > std.math.maxInt(usize))
67 {
68 return error.CapacityOverflow;
69 }
70 return .{
71 .limits = limits,
72 .questions_offset = 0,
73 .question_bytes = @intCast(question_bytes),
74 .options_offset = @intCast(options_offset),
75 .option_bytes = @intCast(option_bytes),
76 .joined_text_offset = @intCast(joined_text_offset),
77 .storage_bytes = @intCast(storage_bytes),
78 };
79 }
80
81 fn alignForward(value: u128, alignment: u128) u128 {
82 const mask = alignment - 1;
83 return (value + mask) & ~mask;
84 }
85
86 test "quiz capacity matches an independent byte model" {
87 comptime {
88 @stardustClaim(
89 @import("alloc_phase").capacity.witness(@import("./root.zig").Storage, "zen_quiz_capacity"),
90 null,
91 null,
92 null,
93 null,
94 null,
95 null,
96 );
97 }
98
99 const limits = Limits{
100 .max_questions = 2,
101 .max_options = 3,
102 .max_joined_text_bytes = 65,
103 };
104 const capacity = try Capacity.derive(limits);
105 try std.testing.expectEqual(try independent(limits), capacity);
106 try std.testing.expectEqual(@as(usize, 233), capacity.storage_bytes);
107 try std.testing.expectEqual(
108 @as(usize, 83_968),
109 (try Capacity.derive(plan_mod.default_limits)).storage_bytes,
110 );
111 }
112
113 test "quiz capacity handles zero and rejects overflow" {
114 try std.testing.expectEqual(@as(usize, 0), (try Capacity.derive(.{
115 .max_questions = 0,
116 .max_options = 0,
117 .max_joined_text_bytes = 0,
118 })).storage_bytes);
119 try std.testing.expectError(error.CapacityOverflow, Capacity.derive(.{
120 .max_questions = std.math.maxInt(usize),
121 .max_options = 0,
122 .max_joined_text_bytes = 0,
123 }));
124 try std.testing.expectError(error.CapacityOverflow, Capacity.derive(.{
125 .max_questions = 0,
126 .max_options = std.math.maxInt(usize),
127 .max_joined_text_bytes = 0,
128 }));
129 }