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 }