lib/zen/src/quiz/plan.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const model = @import("model.zig");
  3 const scan = @import("scan.zig");
  4 
  5 pub const Limits = struct {
  6     max_questions: usize,
  7     max_options: usize,
  8     max_joined_text_bytes: usize,
  9 };
 10 
 11 pub const default_limits: Limits = .{
 12     .max_questions = 128,
 13     .max_options = 512,
 14     .max_joined_text_bytes = 64 * 1024,
 15 };
 16 
 17 pub const Plan = struct {
 18     input_bytes: usize,
 19     questions: usize,
 20     options: usize,
 21     joined_text_bytes: usize,
 22 
 23     pub fn inspect(source: []const u8, limits: Limits) model.Error!Plan {
 24         var state: State = .{};
 25         var iterator = scan.Iterator.init(source);
 26         while (iterator.next()) |token| switch (token) {
 27             .prompt => |text| {
 28                 if (state.current.started()) try state.current.finish();
 29                 state.questions = try added(state.questions, 1);
 30                 if (state.questions > limits.max_questions) {
 31                     return error.QuestionCapacityExceeded;
 32                 }
 33                 state.current = .{
 34                     .element = .prompt,
 35                     .element_bytes = text.len,
 36                 };
 37             },
 38             .option => |option| {
 39                 if (!state.current.started() or state.current.element == .explanation) {
 40                     return error.InvalidQuizDirective;
 41                 }
 42                 state.options = try added(state.options, 1);
 43                 if (state.options > limits.max_options) return error.OptionCapacityExceeded;
 44                 state.current.options = try added(state.current.options, 1);
 45                 if (option.correct) {
 46                     state.current.correct = try added(state.current.correct, 1);
 47                 }
 48                 state.current.element = .option;
 49                 state.current.element_bytes = option.text.len;
 50                 state.current.element_joined = false;
 51             },
 52             .explanation => |text| {
 53                 if (!state.current.started()) return error.InvalidQuizDirective;
 54                 if (state.current.element == .explanation) {
 55                     try state.join(text, limits.max_joined_text_bytes);
 56                 } else {
 57                     state.current.element = .explanation;
 58                     state.current.element_bytes = text.len;
 59                     state.current.element_joined = false;
 60                     state.current.explanation_bytes = text.len;
 61                 }
 62             },
 63             .continuation => |text| {
 64                 if (!state.current.started()) return error.InvalidQuizDirective;
 65                 try state.join(text, limits.max_joined_text_bytes);
 66             },
 67         };
 68         if (state.current.started()) try state.current.finish();
 69         if (state.questions == 0) return error.InvalidQuizDirective;
 70         std.debug.assert(state.questions <= limits.max_questions);
 71         std.debug.assert(state.options <= limits.max_options);
 72         std.debug.assert(state.joined_text_bytes <= limits.max_joined_text_bytes);
 73         return .{
 74             .input_bytes = source.len,
 75             .questions = state.questions,
 76             .options = state.options,
 77             .joined_text_bytes = state.joined_text_bytes,
 78         };
 79     }
 80 
 81     pub fn exactLimits(self: Plan) Limits {
 82         return .{
 83             .max_questions = self.questions,
 84             .max_options = self.options,
 85             .max_joined_text_bytes = self.joined_text_bytes,
 86         };
 87     }
 88 };
 89 
 90 const Element = enum {
 91     none,
 92     prompt,
 93     option,
 94     explanation,
 95 };
 96 
 97 const Current = struct {
 98     element: Element = .none,
 99     element_bytes: usize = 0,
100     element_joined: bool = false,
101     options: usize = 0,
102     correct: usize = 0,
103     explanation_bytes: usize = 0,
104 
105     fn started(self: Current) bool {
106         return self.element != .none;
107     }
108 
109     fn finish(self: Current) model.ParseError!void {
110         std.debug.assert(self.started());
111         std.debug.assert(self.element_bytes != 0);
112         if (self.options == 1) return error.InvalidQuizDirective;
113         if (self.options != 0 and self.correct != 1) return error.InvalidQuizDirective;
114         if (self.options == 0 and self.explanation_bytes == 0) {
115             return error.InvalidQuizDirective;
116         }
117     }
118 };
119 
120 const State = struct {
121     questions: usize = 0,
122     options: usize = 0,
123     joined_text_bytes: usize = 0,
124     current: Current = .{},
125 
126     fn join(self: *State, text: []const u8, max_joined_text_bytes: usize) model.Error!void {
127         std.debug.assert(self.current.started());
128         std.debug.assert(self.current.element_bytes != 0);
129         if (!self.current.element_joined) {
130             self.joined_text_bytes = try added(
131                 self.joined_text_bytes,
132                 self.current.element_bytes,
133             );
134             self.current.element_joined = true;
135         }
136         self.joined_text_bytes = try added(self.joined_text_bytes, 1);
137         self.joined_text_bytes = try added(self.joined_text_bytes, text.len);
138         if (self.joined_text_bytes > max_joined_text_bytes) {
139             return error.JoinedTextByteCapacityExceeded;
140         }
141         self.current.element_bytes = try added(self.current.element_bytes, 1);
142         self.current.element_bytes = try added(self.current.element_bytes, text.len);
143         if (self.current.element == .explanation) {
144             self.current.explanation_bytes = self.current.element_bytes;
145         }
146     }
147 };
148 
149 fn added(left: usize, right: usize) error{CapacityOverflow}!usize {
150     return std.math.add(usize, left, right) catch error.CapacityOverflow;
151 }
152 
153 test "quiz plan counts descriptors and only joined text" {
154     const source =
155         "? Which pass inserts safety checks?\n" ++
156         "- AstGen\n" ++
157         "* Sema\n" ++
158         "- Liveness\n" ++
159         "! Sema lowers ZIR to AIR and materializes checks\n" ++
160         "  as instructions.\n" ++
161         "\n" ++
162         "? What owns the quiz grammar?\n" ++
163         "! The zen quiz module.\n";
164     const plan = try Plan.inspect(source, default_limits);
165     try std.testing.expectEqual(@as(usize, 2), plan.questions);
166     try std.testing.expectEqual(@as(usize, 3), plan.options);
167     try std.testing.expectEqual(
168         "Sema lowers ZIR to AIR and materializes checks as instructions.".len,
169         plan.joined_text_bytes,
170     );
171 }
172 
173 test "quiz plan borrows every uncontinued text field" {
174     const plan = try Plan.inspect(
175         "? Prompt\n* Correct\n- Other\n! Explanation\n",
176         default_limits,
177     );
178     try std.testing.expectEqual(@as(usize, 0), plan.joined_text_bytes);
179 }