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

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const alloc_phase = @import("alloc_phase");
  3 
  4 const diagram = @import("../root.zig");
  5 const capacity_mod = @import("capacity.zig");
  6 const model = @import("model.zig");
  7 const plan_mod = @import("plan.zig");
  8 
  9 pub const Regions = struct {
 10     plan: plan_mod.Plan,
 11     categories: [][]const u8,
 12     stack_totals: []diagram.bounds.StackTotal,
 13     canvas: []u8,
 14     output: []u8,
 15 };
 16 
 17 /// Observable capacity, current demand, high-water demand, and rejections.
 18 pub const Status = struct {
 19     /// Initialization, steady, or teardown lifecycle phase.
 20     phase: alloc_phase.capacity.Phase,
 21     /// True while returned output still borrows this storage.
 22     in_use: bool,
 23     /// Total bytes acquired during initialization.
 24     storage_bytes: usize,
 25     max_categories: usize,
 26     max_stack_totals: usize,
 27     max_canvas_bytes: usize,
 28     max_output_bytes: usize,
 29     categories: usize,
 30     stack_totals: usize,
 31     canvas_bytes: usize,
 32     output_bytes: usize,
 33     high_water_categories: usize,
 34     high_water_stack_totals: usize,
 35     high_water_canvas_bytes: usize,
 36     high_water_output_bytes: usize,
 37     /// Saturating count of capacity and in-use rejections.
 38     rejected_render_count: u64,
 39 };
 40 
 41 /// One reusable aligned region for ASCII render scratch and output.
 42 ///
 43 /// Call `init`, then `activate`. Output borrows the region until `reset`.
 44 /// Call `deinit` with the allocator passed to `init` after the last reset.
 45 pub const Storage = struct {
 46     phase: alloc_phase.capacity.Phase,
 47     capacity: capacity_mod.Capacity,
 48     bytes: []align(capacity_mod.storage_alignment) u8,
 49     categories: [][]const u8,
 50     stack_totals: []diagram.bounds.StackTotal,
 51     canvas: []u8,
 52     output: []u8,
 53     in_use: bool = false,
 54     category_count: usize = 0,
 55     stack_total_count: usize = 0,
 56     canvas_bytes: usize = 0,
 57     output_bytes: usize = 0,
 58     high_water_categories: usize = 0,
 59     high_water_stack_totals: usize = 0,
 60     high_water_canvas_bytes: usize = 0,
 61     high_water_output_bytes: usize = 0,
 62     rejected_render_count: u64 = 0,
 63 
 64     pub const Limits: type = capacity_mod.Limits;
 65     pub const Capacity: type = capacity_mod.Capacity;
 66     pub const Exhaustion: type = model.Exhaustion;
 67     pub const InitError = std.mem.Allocator.Error || capacity_mod.DeriveError;
 68     pub const AcquireError: type = model.Exhaustion;
 69 
 70     pub const claim: alloc_phase.capacity.Declaration = .{
 71         .source = .{
 72             .id = "zen.ascii_render_storage",
 73             .kind = .phase_static,
 74             .limit_source = .caller,
 75             .storage = .{
 76                 .covered = &.{
 77                     .{
 78                         .id = "borrowed_categorical_bound_slots",
 79                         .lifetime = .steady,
 80                         .detail = "borrowed categorical bound slots",
 81                     },
 82                     .{
 83                         .id = "stacked_category_bound_totals",
 84                         .lifetime = .steady,
 85                         .detail = "stacked-category bound totals",
 86                     },
 87                     .{
 88                         .id = "ascii_canvas_bytes",
 89                         .lifetime = .steady,
 90                         .detail = "ASCII canvas bytes",
 91                     },
 92                     .{
 93                         .id = "compacted_ascii_output_bytes",
 94                         .lifetime = .steady,
 95                         .detail = "compacted ASCII output bytes",
 96                     },
 97                 },
 98                 .excluded = &.{
 99                     "caller-owned parsed diagram Document and retained strings",
100                     "JSON line scratch, Document parsing, Markdown output, and filesystem owners",
101                 },
102             },
103             .capacity = .{
104                 .inputs = &.{
105                     alloc_phase.capacity.bindInput(Limits, "max_categories", "max_categories"),
106                     alloc_phase.capacity.bindInput(Limits, "max_stack_totals", "max_stack_totals"),
107                     alloc_phase.capacity.bindInput(Limits, "max_canvas_bytes", "max_canvas_bytes"),
108                     alloc_phase.capacity.bindInput(Limits, "max_output_bytes", "max_output_bytes"),
109                 },
110                 .type_selectors = &.{
111                     alloc_phase.capacity.bindType([]const u8, "const_u8"),
112                     alloc_phase.capacity.bindType(diagram.bounds.StackTotal, "stacktotal"),
113                 },
114                 .nodes = &.{
115                     .{ .input = 0 },
116                     .{ .scale = .{ .node = 0, .coefficient = .{ .size_of_concrete_type = 0 } } },
117                     .{ .alignment = .{ .node = 1, .alignment = .{ .literal = 16 } } },
118                     .{ .input = 1 },
119                     .{ .scale = .{ .node = 3, .coefficient = .{ .size_of_concrete_type = 1 } } },
120                     .{ .input = 2 },
121                     .{ .input = 3 },
122                     .{ .add = .{ .left = 2, .right = 4 } },
123                     .{ .add = .{ .left = 7, .right = 5 } },
124                     .{ .add = .{ .left = 8, .right = 6 } },
125                 },
126                 .assertions = &.{.{
127                     .scope = .closure_total,
128                     .measure = .retained,
129                     .relation = .exact,
130                     .expression = 9,
131                 }},
132             },
133             .overload = .{
134                 .kind = .reject_before_mutation,
135                 .detail = "every max plus one plan and in-use acquisition reject before backing bytes or the active result change; only rejection telemetry advances",
136             },
137             .risks = .{
138                 .transitive = .{
139                     .status = .witnessed,
140                     .detail = "bounds derivation, categorical lookup, canvas drawing, and row compaction use borrowed Document values and acquired slices only",
141                 },
142                 .foreign = .{
143                     .status = .excluded,
144                     .detail = "parsed-document ASCII rendering crosses no operating-system or foreign callback boundary",
145                 },
146             },
147             .obligations = &.{
148                 .{ .key = "zen_ascii_render_capacity", .role = .capacity_model },
149                 .{ .key = "zen_ascii_render_acquisition", .role = .custom },
150                 .{ .key = "zen_ascii_render_oom", .role = .custom },
151                 .{ .key = "zen_ascii_render_boundaries", .role = .overload },
152                 .{ .key = "zen_ascii_render_reuse", .role = .overload },
153                 .{ .key = "zen_ascii_render_sealed", .role = .transitive_risk },
154                 .{ .key = "zen_ascii_render_root", .role = .custom },
155                 .{ .key = "zen_ascii_render_consumer", .role = .foreign_risk },
156             },
157         },
158         .bindings = .{
159             .owner = @This(),
160             .seal = .{
161                 .family = alloc_phase.capacity.selector(@This().activate),
162                 .premise = .{
163                     .class = .checked_semantic_fact,
164                     .authority = .checker,
165                 },
166             },
167             .teardown = .{
168                 .family = alloc_phase.capacity.selector(@This().deinit),
169                 .premise = .{
170                     .class = .checked_semantic_fact,
171                     .authority = .checker,
172                 },
173             },
174         },
175     };
176 
177     pub fn init(allocator: std.mem.Allocator, limits: Limits) InitError!Storage {
178         const capacity = try Capacity.derive(limits);
179         const bytes = if (capacity.storage_bytes == 0)
180             @as([]align(capacity_mod.storage_alignment) u8, &.{})
181         else
182             try allocator.alignedAlloc(
183                 u8,
184                 .fromByteUnits(capacity_mod.storage_alignment),
185                 capacity.storage_bytes,
186             );
187         return .{
188             .phase = .initialization,
189             .capacity = capacity,
190             .bytes = bytes,
191             .categories = typedSlice(
192                 []const u8,
193                 bytes,
194                 capacity.categories_offset,
195                 limits.max_categories,
196             ),
197             .stack_totals = typedSlice(
198                 diagram.bounds.StackTotal,
199                 bytes,
200                 capacity.stack_totals_offset,
201                 limits.max_stack_totals,
202             ),
203             .canvas = bytes[capacity.canvas_offset..][0..limits.max_canvas_bytes],
204             .output = bytes[capacity.output_offset..][0..limits.max_output_bytes],
205         };
206     }
207 
208     pub fn activate(self: *Storage) void {
209         std.debug.assert(self.phase == .initialization);
210         self.assertStorage();
211         self.phase = .steady;
212     }
213 
214     /// Acquires exact plan regions or rejects before backing bytes change.
215     pub fn acquire(self: *Storage, plan: plan_mod.Plan) AcquireError!Regions {
216         std.debug.assert(self.phase == .steady);
217         if (self.in_use) return self.reject(error.AsciiRenderStorageInUse);
218         plan.require(self.capacity.limits) catch |err| return self.reject(err);
219         self.in_use = true;
220         self.category_count = plan.categories;
221         self.stack_total_count = plan.stack_totals;
222         self.canvas_bytes = plan.canvas_bytes;
223         self.output_bytes = plan.output_bytes;
224         self.high_water_categories = @max(self.high_water_categories, plan.categories);
225         self.high_water_stack_totals = @max(self.high_water_stack_totals, plan.stack_totals);
226         self.high_water_canvas_bytes = @max(self.high_water_canvas_bytes, plan.canvas_bytes);
227         self.high_water_output_bytes = @max(self.high_water_output_bytes, plan.output_bytes);
228         self.assertStorage();
229         return .{
230             .plan = plan,
231             .categories = self.categories[0..plan.categories],
232             .stack_totals = self.stack_totals[0..plan.stack_totals],
233             .canvas = self.canvas[0..plan.canvas_bytes],
234             .output = self.output[0..plan.output_bytes],
235         };
236     }
237 
238     /// Ends the active output borrow so the same region can render again.
239     pub fn reset(self: *Storage) void {
240         std.debug.assert(self.phase == .steady);
241         std.debug.assert(self.in_use);
242         self.in_use = false;
243         self.category_count = 0;
244         self.stack_total_count = 0;
245         self.canvas_bytes = 0;
246         self.output_bytes = 0;
247         self.assertStorage();
248     }
249 
250     /// Returns current, high-water, and rejected demand without mutation.
251     pub fn status(self: *const Storage) Status {
252         return .{
253             .phase = self.phase,
254             .in_use = self.in_use,
255             .storage_bytes = self.capacity.storage_bytes,
256             .max_categories = self.capacity.limits.max_categories,
257             .max_stack_totals = self.capacity.limits.max_stack_totals,
258             .max_canvas_bytes = self.capacity.limits.max_canvas_bytes,
259             .max_output_bytes = self.capacity.limits.max_output_bytes,
260             .categories = self.category_count,
261             .stack_totals = self.stack_total_count,
262             .canvas_bytes = self.canvas_bytes,
263             .output_bytes = self.output_bytes,
264             .high_water_categories = self.high_water_categories,
265             .high_water_stack_totals = self.high_water_stack_totals,
266             .high_water_canvas_bytes = self.high_water_canvas_bytes,
267             .high_water_output_bytes = self.high_water_output_bytes,
268             .rejected_render_count = self.rejected_render_count,
269         };
270     }
271 
272     pub fn deinit(self: *Storage, allocator: std.mem.Allocator) void {
273         std.debug.assert(self.phase != .teardown);
274         std.debug.assert(!self.in_use);
275         self.assertStorage();
276         self.phase = .teardown;
277         allocator.free(self.bytes);
278         self.bytes = &.{};
279         self.categories = &.{};
280         self.stack_totals = &.{};
281         self.canvas = &.{};
282         self.output = &.{};
283     }
284 
285     fn reject(self: *Storage, err: model.Exhaustion) model.Exhaustion {
286         self.rejected_render_count +|= 1;
287         return err;
288     }
289 
290     fn assertStorage(self: *const Storage) void {
291         std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
292         std.debug.assert(self.categories.len == self.capacity.limits.max_categories);
293         std.debug.assert(self.stack_totals.len == self.capacity.limits.max_stack_totals);
294         std.debug.assert(self.canvas.len == self.capacity.limits.max_canvas_bytes);
295         std.debug.assert(self.output.len == self.capacity.limits.max_output_bytes);
296         std.debug.assert(self.category_count <= self.categories.len);
297         std.debug.assert(self.stack_total_count <= self.stack_totals.len);
298         std.debug.assert(self.canvas_bytes <= self.canvas.len);
299         std.debug.assert(self.output_bytes <= self.output.len);
300         std.debug.assert(self.high_water_categories <= self.categories.len);
301         std.debug.assert(self.high_water_stack_totals <= self.stack_totals.len);
302         std.debug.assert(self.high_water_canvas_bytes <= self.canvas.len);
303         std.debug.assert(self.high_water_output_bytes <= self.output.len);
304         if (!self.in_use) {
305             std.debug.assert(self.category_count == 0);
306             std.debug.assert(self.stack_total_count == 0);
307             std.debug.assert(self.canvas_bytes == 0);
308             std.debug.assert(self.output_bytes == 0);
309         }
310     }
311 };
312 
313 fn typedSlice(
314     comptime T: type,
315     bytes: []align(capacity_mod.storage_alignment) u8,
316     offset: usize,
317     count: usize,
318 ) []T {
319     if (count == 0) return &.{};
320     const byte_count = count * @sizeOf(T);
321     const region: []align(@alignOf(T)) u8 = @alignCast(bytes[offset..][0..byte_count]);
322     return std.mem.bytesAsSlice(T, region);
323 }
324 
325 fn checkInitFailures(allocator: std.mem.Allocator) !void {
326     var storage = try Storage.init(allocator, .{
327         .max_categories = 3,
328         .max_stack_totals = 2,
329         .max_canvas_bytes = 72 * 22,
330         .max_output_bytes = 73 * 22,
331     });
332     storage.deinit(allocator);
333 }
334 
335 test "ASCII render storage acquires one exact aligned region" {
336     comptime {
337         @stardustClaim(
338             @import("alloc_phase").capacity.witness(Storage, "zen_ascii_render_acquisition"),
339             null,
340             null,
341             null,
342             null,
343             null,
344             null,
345         );
346     }
347 
348     var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
349     const limits = capacity_mod.Limits{
350         .max_categories = 3,
351         .max_stack_totals = 2,
352         .max_canvas_bytes = 72 * 22,
353         .max_output_bytes = 73 * 22,
354     };
355     const capacity = try capacity_mod.Capacity.derive(limits);
356     var storage = try Storage.init(counting.allocator(), limits);
357     defer storage.deinit(counting.allocator());
358     try std.testing.expectEqual(@as(usize, 1), counting.alloc_index);
359     try std.testing.expectEqual(capacity.storage_bytes, counting.allocated_bytes);
360     try std.testing.expectEqual(alloc_phase.capacity.Phase.initialization, storage.status().phase);
361     storage.activate();
362     const regions = try storage.acquire(.{
363         .width = 72,
364         .height = 22,
365         .categories = 3,
366         .stack_totals = 2,
367         .canvas_bytes = 72 * 22,
368         .output_bytes = 73 * 22,
369     });
370     defer storage.reset();
371     const base = @intFromPtr(storage.bytes.ptr);
372     try std.testing.expectEqual(
373         base + capacity.categories_offset,
374         @intFromPtr(regions.categories.ptr),
375     );
376     try std.testing.expectEqual(
377         base + capacity.stack_totals_offset,
378         @intFromPtr(regions.stack_totals.ptr),
379     );
380     try std.testing.expectEqual(base + capacity.canvas_offset, @intFromPtr(regions.canvas.ptr));
381     try std.testing.expectEqual(base + capacity.output_offset, @intFromPtr(regions.output.ptr));
382 }
383 
384 test "ASCII render storage retries after every allocation failure" {
385     comptime {
386         @stardustClaim(
387             @import("alloc_phase").capacity.witness(Storage, "zen_ascii_render_oom"),
388             null,
389             null,
390             null,
391             null,
392             null,
393             null,
394         );
395     }
396 
397     try std.testing.checkAllAllocationFailures(std.testing.allocator, checkInitFailures, .{});
398 }
399 
400 comptime {
401     alloc_phase.capacity.requireAllocatorRejectingOwnerShape(Storage);
402 }