lib/zen/src/diagram/document/storage.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const alloc_phase = @import("alloc_phase");
  3 
  4 const capacity_mod = @import("capacity.zig");
  5 const model = @import("model.zig");
  6 const plan_mod = @import("plan.zig");
  7 
  8 const diagram = @import("../root.zig");
  9 const spec = diagram.spec;
 10 
 11 pub const Status = struct {
 12     phase: alloc_phase.capacity.Phase,
 13     in_use: bool,
 14     storage_bytes: usize,
 15     line_scratch_bytes: usize,
 16     document_bytes: usize,
 17     source_bytes: usize,
 18     document_bytes_used: usize,
 19     high_water_source_bytes: usize,
 20     high_water_line_scratch_bytes: usize,
 21     high_water_document_bytes: usize,
 22     rejected_document_count: u64,
 23 };
 24 
 25 pub const Storage = struct {
 26     phase: alloc_phase.capacity.Phase,
 27     capacity: capacity_mod.Capacity,
 28     line_scratch: []u8,
 29     document_bytes: []u8,
 30     prepared: ?plan_mod.Plan = null,
 31     document_slot: [@sizeOf(spec.Document)]u8 align(@alignOf(spec.Document)) = undefined,
 32     document_end: usize = 0,
 33     document_peak: usize = 0,
 34     in_use: bool = false,
 35     source_bytes: usize = 0,
 36     high_water_source_bytes: usize = 0,
 37     high_water_line_scratch_bytes: usize = 0,
 38     high_water_document_bytes: usize = 0,
 39     rejected_document_count: u64 = 0,
 40 
 41     pub const Limits: type = capacity_mod.Limits;
 42     pub const Capacity: type = capacity_mod.Capacity;
 43     pub const Exhaustion: type = model.Exhaustion;
 44     pub const InitError = std.mem.Allocator.Error || capacity_mod.DeriveError;
 45     pub const ParseError: type = model.Error;
 46 
 47     pub const claim: alloc_phase.capacity.Declaration = .{
 48         .source = .{
 49             .id = "zen.diagram_document_storage",
 50             .kind = .phase_static,
 51             .limit_source = .caller,
 52             .storage = .{
 53                 .covered = &.{
 54                     .{
 55                         .id = "json_line_parse_scratch",
 56                         .lifetime = .steady,
 57                         .detail = "JSON line parse scratch",
 58                     },
 59                     .{
 60                         .id = "parsed_diagram_document_values_and_retained_strings",
 61                         .lifetime = .steady,
 62                         .detail = "parsed diagram Document values and retained strings",
 63                     },
 64                 },
 65                 .excluded = &.{
 66                     "caller-owned JSONL source",
 67                     "ASCII and SVG render scratch, output, Markdown, and filesystem owners",
 68                 },
 69             },
 70             .capacity = .{
 71                 .inputs = &.{
 72                     alloc_phase.capacity.bindInput(Limits, "max_line_scratch_bytes", "max_line_scratch_bytes"),
 73                     alloc_phase.capacity.bindInput(Limits, "max_document_bytes", "max_document_bytes"),
 74                 },
 75                 .type_selectors = &.{},
 76                 .nodes = &.{
 77                     .{ .input = 0 },
 78                     .{ .input = 1 },
 79                     .{ .add = .{ .left = 0, .right = 1 } },
 80                 },
 81                 .assertions = &.{.{
 82                     .scope = .closure_total,
 83                     .measure = .retained,
 84                     .relation = .exact,
 85                     .expression = 2,
 86                 }},
 87             },
 88             .overload = .{
 89                 .kind = .drop,
 90                 .detail = "an in-use, oversized, or invalid attempted source is dropped while the two reusable regions remain eligible for later input",
 91             },
 92             .risks = .{
 93                 .transitive = .{
 94                     .status = .witnessed,
 95                     .detail = "survey and parsing use only caller source plus the two activated regions, with a stable owner-backed allocator context",
 96                 },
 97                 .foreign = .{
 98                     .status = .excluded,
 99                     .detail = "diagram Document parsing crosses no operating-system or foreign callback boundary",
100                 },
101             },
102             .obligations = &.{
103                 .{ .key = "zen_diagram_document_capacity", .role = .capacity_model },
104                 .{ .key = "zen_diagram_document_acquisition", .role = .custom },
105                 .{ .key = "zen_diagram_document_oom", .role = .custom },
106                 .{ .key = "zen_diagram_document_boundaries", .role = .overload },
107                 .{ .key = "zen_diagram_document_reuse", .role = .overload },
108                 .{ .key = "zen_diagram_document_differential", .role = .custom },
109                 .{ .key = "zen_diagram_document_sealed", .role = .transitive_risk },
110                 .{ .key = "zen_diagram_document_root", .role = .custom },
111                 .{ .key = "zen_diagram_document_consumer", .role = .foreign_risk },
112             },
113         },
114         .bindings = .{
115             .owner = @This(),
116             .seal = .{
117                 .family = alloc_phase.capacity.selector(@This().activate),
118                 .premise = .{
119                     .class = .checked_semantic_fact,
120                     .authority = .checker,
121                 },
122             },
123             .teardown = .{
124                 .family = alloc_phase.capacity.selector(@This().deinit),
125                 .premise = .{
126                     .class = .checked_semantic_fact,
127                     .authority = .checker,
128                 },
129             },
130         },
131     };
132 
133     pub fn init(allocator: std.mem.Allocator, limits: Limits) InitError!Storage {
134         const capacity = try Capacity.derive(limits);
135         const line_scratch = try allocateRegion(allocator, capacity.line_scratch_bytes);
136         errdefer allocator.free(line_scratch);
137         const document_bytes = try allocateRegion(allocator, capacity.document_bytes);
138         return .{
139             .phase = .initialization,
140             .capacity = capacity,
141             .line_scratch = line_scratch,
142             .document_bytes = document_bytes,
143         };
144     }
145 
146     pub fn initExact(allocator: std.mem.Allocator, jsonl: []const u8) !Storage {
147         const line_scratch_bytes = try plan_mod.Plan.requiredLineScratchBytes(jsonl);
148         const line_scratch = try allocateRegion(allocator, line_scratch_bytes);
149         errdefer allocator.free(line_scratch);
150         const plan = try plan_mod.Plan.inspect(line_scratch, jsonl);
151         const capacity = try Capacity.derive(plan.exactLimits());
152         const document_bytes = try allocateRegion(allocator, capacity.document_bytes);
153         return .{
154             .phase = .initialization,
155             .capacity = capacity,
156             .line_scratch = line_scratch,
157             .document_bytes = document_bytes,
158             .prepared = plan,
159             .high_water_line_scratch_bytes = @intCast(plan.surveyed.scratch_high_water),
160         };
161     }
162 
163     pub fn activate(self: *Storage) void {
164         std.debug.assert(self.phase == .initialization);
165         self.assertStorage();
166         self.phase = .steady;
167     }
168 
169     pub fn parse(self: *Storage, jsonl: []const u8) ParseError!*const spec.Document {
170         std.debug.assert(self.phase == .steady);
171         if (self.in_use) return self.reject(error.DiagramDocumentStorageInUse);
172         const required_line_scratch = plan_mod.Plan.requiredLineScratchBytes(jsonl) catch |err| {
173             self.rejected_document_count +|= 1;
174             return err;
175         };
176         if (required_line_scratch > self.capacity.line_scratch_bytes) {
177             return self.reject(error.DiagramLineScratchCapacityExceeded);
178         }
179         const plan = if (self.prepared) |prepared|
180             if (prepared.matches(jsonl)) prepared else plan_mod.Plan.inspect(self.line_scratch, jsonl) catch |err| {
181                 self.rejected_document_count +|= 1;
182                 return err;
183             }
184         else
185             plan_mod.Plan.inspect(self.line_scratch, jsonl) catch |err| {
186                 self.rejected_document_count +|= 1;
187                 return err;
188             };
189         plan.require(self.capacity.limits) catch |err| return self.reject(err);
190 
191         self.document_end = 0;
192         self.document_peak = 0;
193         const parsed = spec.Document.parse(
194             documentAllocator(self),
195             self.line_scratch[0..plan.line_scratch_bytes],
196             jsonl,
197         ) catch |err| {
198             self.observe(plan, jsonl.len);
199             self.document_end = 0;
200             self.document_peak = 0;
201             self.prepared = null;
202             self.rejected_document_count +|= 1;
203             return err;
204         };
205         documentSlot(self).* = parsed;
206         self.in_use = true;
207         self.source_bytes = jsonl.len;
208         self.prepared = null;
209         self.observe(plan, jsonl.len);
210         self.assertStorage();
211         return documentSlot(self);
212     }
213 
214     pub fn reset(self: *Storage) void {
215         std.debug.assert(self.phase == .steady);
216         std.debug.assert(self.in_use);
217         documentSlot(self).deinit();
218         self.document_end = 0;
219         self.document_peak = 0;
220         self.in_use = false;
221         self.source_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             .line_scratch_bytes = self.capacity.line_scratch_bytes,
231             .document_bytes = self.capacity.document_bytes,
232             .source_bytes = self.source_bytes,
233             .document_bytes_used = self.document_end,
234             .high_water_source_bytes = self.high_water_source_bytes,
235             .high_water_line_scratch_bytes = self.high_water_line_scratch_bytes,
236             .high_water_document_bytes = self.high_water_document_bytes,
237             .rejected_document_count = self.rejected_document_count,
238         };
239     }
240 
241     pub fn deinit(self: *Storage, allocator: std.mem.Allocator) void {
242         std.debug.assert(self.phase != .teardown);
243         std.debug.assert(!self.in_use);
244         self.assertStorage();
245         self.phase = .teardown;
246         allocator.free(self.document_bytes);
247         allocator.free(self.line_scratch);
248         self.document_bytes = &.{};
249         self.line_scratch = &.{};
250         self.prepared = null;
251     }
252 
253     fn reject(self: *Storage, err: model.Exhaustion) model.Exhaustion {
254         self.rejected_document_count +|= 1;
255         return err;
256     }
257 
258     fn observe(self: *Storage, plan: plan_mod.Plan, source_bytes: usize) void {
259         self.high_water_source_bytes = @max(self.high_water_source_bytes, source_bytes);
260         self.high_water_line_scratch_bytes = @max(
261             self.high_water_line_scratch_bytes,
262             @as(usize, @intCast(plan.surveyed.scratch_high_water)),
263         );
264         self.high_water_document_bytes = @max(
265             self.high_water_document_bytes,
266             @max(self.document_end, self.document_peak),
267         );
268     }
269 
270     fn assertStorage(self: *const Storage) void {
271         std.debug.assert(self.line_scratch.len == self.capacity.line_scratch_bytes);
272         std.debug.assert(self.document_bytes.len == self.capacity.document_bytes);
273         std.debug.assert(self.capacity.storage_bytes == self.line_scratch.len + self.document_bytes.len);
274         std.debug.assert(self.document_end <= self.document_bytes.len);
275         std.debug.assert(self.document_peak <= self.document_bytes.len);
276         std.debug.assert(self.high_water_line_scratch_bytes <= self.line_scratch.len);
277         std.debug.assert(self.high_water_document_bytes <= self.document_bytes.len);
278         if (!self.in_use) std.debug.assert(self.source_bytes == 0);
279     }
280 };
281 
282 fn allocateRegion(allocator: std.mem.Allocator, bytes: usize) std.mem.Allocator.Error![]u8 {
283     return if (bytes == 0) &.{} else try allocator.alloc(u8, bytes);
284 }
285 
286 fn documentAllocator(storage: *Storage) std.mem.Allocator {
287     return .{
288         .ptr = storage,
289         .vtable = &.{
290             .alloc = documentAlloc,
291             .resize = documentResize,
292             .remap = documentRemap,
293             .free = documentFree,
294         },
295     };
296 }
297 
298 fn documentSlot(storage: *Storage) *spec.Document {
299     return @ptrCast(@alignCast(&storage.document_slot));
300 }
301 
302 fn documentAlloc(
303     context: *anyopaque,
304     len: usize,
305     alignment: std.mem.Alignment,
306     return_address: usize,
307 ) ?[*]u8 {
308     const storage: *Storage = @ptrCast(@alignCast(context));
309     var fixed = documentFixed(storage);
310     const result = std.heap.FixedBufferAllocator.alloc(
311         &fixed,
312         len,
313         alignment,
314         return_address,
315     );
316     storage.document_end = fixed.end_index;
317     storage.document_peak = @max(storage.document_peak, storage.document_end);
318     return result;
319 }
320 
321 fn documentResize(
322     context: *anyopaque,
323     memory: []u8,
324     alignment: std.mem.Alignment,
325     new_len: usize,
326     return_address: usize,
327 ) bool {
328     const storage: *Storage = @ptrCast(@alignCast(context));
329     var fixed = documentFixed(storage);
330     const resized = std.heap.FixedBufferAllocator.resize(
331         &fixed,
332         memory,
333         alignment,
334         new_len,
335         return_address,
336     );
337     storage.document_end = fixed.end_index;
338     storage.document_peak = @max(storage.document_peak, storage.document_end);
339     return resized;
340 }
341 
342 fn documentRemap(
343     context: *anyopaque,
344     memory: []u8,
345     alignment: std.mem.Alignment,
346     new_len: usize,
347     return_address: usize,
348 ) ?[*]u8 {
349     const storage: *Storage = @ptrCast(@alignCast(context));
350     var fixed = documentFixed(storage);
351     const result = std.heap.FixedBufferAllocator.remap(
352         &fixed,
353         memory,
354         alignment,
355         new_len,
356         return_address,
357     );
358     storage.document_end = fixed.end_index;
359     storage.document_peak = @max(storage.document_peak, storage.document_end);
360     return result;
361 }
362 
363 fn documentFree(
364     context: *anyopaque,
365     memory: []u8,
366     alignment: std.mem.Alignment,
367     return_address: usize,
368 ) void {
369     const storage: *Storage = @ptrCast(@alignCast(context));
370     var fixed = documentFixed(storage);
371     std.heap.FixedBufferAllocator.free(
372         &fixed,
373         memory,
374         alignment,
375         return_address,
376     );
377     storage.document_end = fixed.end_index;
378 }
379 
380 fn documentFixed(storage: *Storage) std.heap.FixedBufferAllocator {
381     var fixed = std.heap.FixedBufferAllocator.init(storage.document_bytes);
382     fixed.end_index = storage.document_end;
383     return fixed;
384 }
385 
386 fn checkInitFailures(allocator: std.mem.Allocator) !void {
387     var storage = try Storage.init(allocator, .{
388         .max_line_scratch_bytes = 7_744,
389         .max_document_bytes = 4_594,
390     });
391     storage.deinit(allocator);
392 }
393 
394 fn checkExactInitFailures(allocator: std.mem.Allocator) !void {
395     var storage = try Storage.initExact(
396         allocator,
397         "{\"kind\":\"point\",\"x\":1,\"y\":2}",
398     );
399     storage.deinit(allocator);
400 }
401 
402 test "diagram Document storage acquires two exact regions" {
403     comptime {
404         @stardustClaim(
405             @import("alloc_phase").capacity.witness(Storage, "zen_diagram_document_acquisition"),
406             null,
407             null,
408             null,
409             null,
410             null,
411             null,
412         );
413     }
414 
415     var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
416     const limits = capacity_mod.Limits{
417         .max_line_scratch_bytes = 7_744,
418         .max_document_bytes = 4_594,
419     };
420     const capacity = try capacity_mod.Capacity.derive(limits);
421     var storage = try Storage.init(counting.allocator(), limits);
422     defer storage.deinit(counting.allocator());
423     try std.testing.expectEqual(@as(usize, 2), counting.alloc_index);
424     try std.testing.expectEqual(capacity.storage_bytes, counting.allocated_bytes);
425     try std.testing.expectEqual(alloc_phase.capacity.Phase.initialization, storage.status().phase);
426 }
427 
428 test "diagram Document storage retries after every allocation failure" {
429     comptime {
430         @stardustClaim(
431             @import("alloc_phase").capacity.witness(Storage, "zen_diagram_document_oom"),
432             null,
433             null,
434             null,
435             null,
436             null,
437             null,
438         );
439     }
440 
441     try std.testing.checkAllAllocationFailures(std.testing.allocator, checkInitFailures, .{});
442     try std.testing.checkAllAllocationFailures(
443         std.testing.allocator,
444         checkExactInitFailures,
445         .{},
446     );
447 }
448 
449 test "exact diagram Document storage exposes both allocated regions" {
450     const jsonl = "{\"kind\":\"point\",\"x\":1,\"y\":2}";
451     var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
452     var storage = try Storage.initExact(counting.allocator(), jsonl);
453     defer storage.deinit(counting.allocator());
454     try std.testing.expectEqual(@as(usize, 2), counting.alloc_index);
455     try std.testing.expectEqual(storage.capacity.storage_bytes, counting.allocated_bytes);
456 }
457 
458 comptime {
459     alloc_phase.capacity.requireAllocatorRejectingOwnerShape(Storage);
460 }