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 }