lib/zen/src/document/storage.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const alloc_phase = @import("alloc_phase");
3 const capacity_mod = @import("capacity.zig");
4 const model = @import("model.zig");
5 const plan_mod = @import("plan.zig");
6
7 /// One acquired view into heading storage.
8 pub const Regions = struct {
9 plan: plan_mod.Plan,
10 headings: []model.Heading,
11 text: []u8,
12 ids: []u8,
13 };
14
15 /// Observable capacity, current demand, high-water demand, and rejections.
16 pub const Status = struct {
17 /// Initialization, steady, or teardown lifecycle phase.
18 phase: alloc_phase.capacity.Phase,
19 /// True while a returned document still borrows this storage.
20 in_use: bool,
21 /// Total bytes acquired during initialization.
22 storage_bytes: usize,
23 max_headings: usize,
24 max_text_bytes: usize,
25 max_id_bytes: usize,
26 heading_count: usize,
27 text_bytes: usize,
28 id_bytes: usize,
29 /// Largest heading count admitted since activation.
30 high_water_headings: usize,
31 /// Largest visible-text demand admitted since activation.
32 high_water_text_bytes: usize,
33 /// Largest ID-byte demand admitted since activation.
34 high_water_id_bytes: usize,
35 /// Saturating count of capacity and in-use rejections.
36 rejected_source_count: u64,
37 };
38
39 /// One reusable aligned region for an inspected Markdown document.
40 ///
41 /// Call `init`, then `activate`. A document borrows the region until `reset`.
42 /// Call `deinit` with the allocator passed to `init` after the last reset.
43 pub const Storage = struct {
44 phase: alloc_phase.capacity.Phase,
45 capacity: capacity_mod.Capacity,
46 bytes: []align(capacity_mod.storage_alignment) u8,
47 headings: []model.Heading,
48 text: []u8,
49 ids: []u8,
50 in_use: bool = false,
51 heading_count: usize = 0,
52 text_bytes: usize = 0,
53 id_bytes: usize = 0,
54 high_water_headings: usize = 0,
55 high_water_text_bytes: usize = 0,
56 high_water_id_bytes: usize = 0,
57 rejected_source_count: u64 = 0,
58
59 pub const Limits: type = capacity_mod.Limits;
60 pub const Capacity: type = capacity_mod.Capacity;
61 pub const Exhaustion: type = model.Exhaustion;
62 pub const InitError = std.mem.Allocator.Error || capacity_mod.DeriveError;
63 pub const AcquireError: type = model.Exhaustion;
64
65 pub const claim: alloc_phase.capacity.Declaration = .{
66 .source = .{
67 .id = "zen.heading_storage",
68 .kind = .phase_static,
69 .limit_source = .caller,
70 .storage = .{
71 .covered = &.{
72 .{
73 .id = "markdown_heading_descriptors",
74 .lifetime = .steady,
75 .detail = "Markdown heading descriptors",
76 },
77 .{
78 .id = "source_independent_heading_text",
79 .lifetime = .steady,
80 .detail = "source-independent heading text",
81 },
82 .{
83 .id = "stable_heading_identifiers",
84 .lifetime = .steady,
85 .detail = "stable heading identifiers",
86 },
87 },
88 .excluded = &.{
89 "caller-owned Markdown source",
90 "rendered HTML and inline-rendering owners",
91 "site catalog, theme, and filesystem output owners",
92 },
93 },
94 .capacity = .{
95 .inputs = &.{
96 alloc_phase.capacity.bindInput(Limits, "max_headings", "max_headings"),
97 alloc_phase.capacity.bindInput(Limits, "max_text_bytes", "max_text_bytes"),
98 alloc_phase.capacity.bindInput(Limits, "max_id_bytes", "max_id_bytes"),
99 },
100 .type_selectors = &.{
101 alloc_phase.capacity.bindType(model.Heading, "heading"),
102 },
103 .nodes = &.{
104 .{ .input = 0 },
105 .{ .scale = .{ .node = 0, .coefficient = .{ .size_of_concrete_type = 0 } } },
106 .{ .input = 1 },
107 .{ .input = 2 },
108 .{ .add = .{ .left = 1, .right = 2 } },
109 .{ .add = .{ .left = 4, .right = 3 } },
110 },
111 .assertions = &.{.{
112 .scope = .closure_total,
113 .measure = .retained,
114 .relation = .exact,
115 .expression = 5,
116 }},
117 },
118 .overload = .{
119 .kind = .reject_before_mutation,
120 .detail = "max plus one and in-use acquisition reject before the backing region or active result changes; only rejection telemetry advances",
121 },
122 .risks = .{
123 .transitive = .{
124 .status = .witnessed,
125 .detail = "heading scanning, planning, copying, and identifier resolution use borrowed input and acquired slices only",
126 },
127 .foreign = .{
128 .status = .excluded,
129 .detail = "heading inspection crosses no operating-system or foreign callback boundary",
130 },
131 },
132 .obligations = &.{
133 .{ .key = "zen_heading_capacity", .role = .capacity_model },
134 .{ .key = "zen_heading_acquisition", .role = .custom },
135 .{ .key = "zen_heading_oom", .role = .custom },
136 .{ .key = "zen_heading_boundaries", .role = .overload },
137 .{ .key = "zen_heading_reuse", .role = .overload },
138 .{ .key = "zen_heading_sealed", .role = .transitive_risk },
139 .{ .key = "zen_heading_root", .role = .custom },
140 .{ .key = "zen_heading_consumer", .role = .foreign_risk },
141 },
142 },
143 .bindings = .{
144 .owner = @This(),
145 .seal = .{
146 .family = alloc_phase.capacity.selector(@This().activate),
147 .premise = .{
148 .class = .checked_semantic_fact,
149 .authority = .checker,
150 },
151 },
152 .teardown = .{
153 .family = alloc_phase.capacity.selector(@This().deinit),
154 .premise = .{
155 .class = .checked_semantic_fact,
156 .authority = .checker,
157 },
158 },
159 },
160 };
161
162 pub fn init(allocator: std.mem.Allocator, limits: Limits) InitError!Storage {
163 const capacity = try Capacity.derive(limits);
164 const bytes = if (capacity.storage_bytes == 0)
165 @as([]align(capacity_mod.storage_alignment) u8, &.{})
166 else
167 try allocator.alignedAlloc(
168 u8,
169 .fromByteUnits(capacity_mod.storage_alignment),
170 capacity.storage_bytes,
171 );
172 return .{
173 .phase = .initialization,
174 .capacity = capacity,
175 .bytes = bytes,
176 .headings = typedSlice(
177 model.Heading,
178 bytes,
179 capacity.headings_offset,
180 limits.max_headings,
181 ),
182 .text = bytes[capacity.text_offset..][0..limits.max_text_bytes],
183 .ids = bytes[capacity.id_offset..][0..limits.max_id_bytes],
184 };
185 }
186
187 pub fn activate(self: *Storage) void {
188 std.debug.assert(self.phase == .initialization);
189 self.assertStorage();
190 self.phase = .steady;
191 }
192
193 /// Acquires exact plan regions or rejects before backing bytes change.
194 pub fn acquire(self: *Storage, plan: plan_mod.Plan) AcquireError!Regions {
195 std.debug.assert(self.phase == .steady);
196 if (self.in_use) return self.reject(error.DocumentStorageInUse);
197 plan.require(self.capacity.limits) catch |err| return self.reject(err);
198 self.in_use = true;
199 self.heading_count = plan.headings;
200 self.text_bytes = plan.text_bytes;
201 self.id_bytes = plan.id_bytes;
202 self.high_water_headings = @max(self.high_water_headings, plan.headings);
203 self.high_water_text_bytes = @max(self.high_water_text_bytes, plan.text_bytes);
204 self.high_water_id_bytes = @max(self.high_water_id_bytes, plan.id_bytes);
205 self.assertStorage();
206 return .{
207 .plan = plan,
208 .headings = self.headings[0..plan.headings],
209 .text = self.text[0..plan.text_bytes],
210 .ids = self.ids[0..plan.id_bytes],
211 };
212 }
213
214 /// Ends the active borrow so the same region can inspect another document.
215 pub fn reset(self: *Storage) void {
216 std.debug.assert(self.phase == .steady);
217 std.debug.assert(self.in_use);
218 self.in_use = false;
219 self.heading_count = 0;
220 self.text_bytes = 0;
221 self.id_bytes = 0;
222 self.assertStorage();
223 }
224
225 /// Returns current, high-water, and rejected demand without mutation.
226 pub fn status(self: *const Storage) Status {
227 return .{
228 .phase = self.phase,
229 .in_use = self.in_use,
230 .storage_bytes = self.capacity.storage_bytes,
231 .max_headings = self.capacity.limits.max_headings,
232 .max_text_bytes = self.capacity.limits.max_text_bytes,
233 .max_id_bytes = self.capacity.limits.max_id_bytes,
234 .heading_count = self.heading_count,
235 .text_bytes = self.text_bytes,
236 .id_bytes = self.id_bytes,
237 .high_water_headings = self.high_water_headings,
238 .high_water_text_bytes = self.high_water_text_bytes,
239 .high_water_id_bytes = self.high_water_id_bytes,
240 .rejected_source_count = self.rejected_source_count,
241 };
242 }
243
244 pub fn deinit(self: *Storage, allocator: std.mem.Allocator) void {
245 std.debug.assert(self.phase != .teardown);
246 std.debug.assert(!self.in_use);
247 self.assertStorage();
248 self.phase = .teardown;
249 allocator.free(self.bytes);
250 self.bytes = &.{};
251 self.headings = &.{};
252 self.text = &.{};
253 self.ids = &.{};
254 }
255
256 fn reject(self: *Storage, err: model.Exhaustion) model.Exhaustion {
257 self.rejected_source_count +|= 1;
258 return err;
259 }
260
261 fn assertStorage(self: *const Storage) void {
262 std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
263 std.debug.assert(self.headings.len == self.capacity.limits.max_headings);
264 std.debug.assert(self.text.len == self.capacity.limits.max_text_bytes);
265 std.debug.assert(self.ids.len == self.capacity.limits.max_id_bytes);
266 std.debug.assert(self.heading_count <= self.headings.len);
267 std.debug.assert(self.text_bytes <= self.text.len);
268 std.debug.assert(self.id_bytes <= self.ids.len);
269 std.debug.assert(self.high_water_headings <= self.headings.len);
270 std.debug.assert(self.high_water_text_bytes <= self.text.len);
271 std.debug.assert(self.high_water_id_bytes <= self.ids.len);
272 if (!self.in_use) {
273 std.debug.assert(self.heading_count == 0);
274 std.debug.assert(self.text_bytes == 0);
275 std.debug.assert(self.id_bytes == 0);
276 }
277 }
278 };
279
280 fn typedSlice(
281 comptime T: type,
282 bytes: []align(capacity_mod.storage_alignment) u8,
283 offset: usize,
284 count: usize,
285 ) []T {
286 const byte_count = count * @sizeOf(T);
287 const region: []align(@alignOf(T)) u8 = @alignCast(bytes[offset..][0..byte_count]);
288 return std.mem.bytesAsSlice(T, region);
289 }
290
291 fn checkInitFailures(allocator: std.mem.Allocator) !void {
292 var storage = try Storage.init(allocator, .{
293 .max_headings = 2,
294 .max_text_bytes = 11,
295 .max_id_bytes = 11,
296 });
297 storage.deinit(allocator);
298 }
299
300 test "heading storage acquires one exact aligned region" {
301 comptime {
302 @stardustClaim(
303 @import("alloc_phase").capacity.witness(Storage, "zen_heading_acquisition"),
304 null,
305 null,
306 null,
307 null,
308 null,
309 null,
310 );
311 }
312
313 var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
314 const limits = capacity_mod.Limits{
315 .max_headings = 2,
316 .max_text_bytes = 11,
317 .max_id_bytes = 11,
318 };
319 const capacity = try capacity_mod.Capacity.derive(limits);
320 var storage = try Storage.init(counting.allocator(), limits);
321 defer storage.deinit(counting.allocator());
322
323 try std.testing.expectEqual(@as(usize, 1), counting.alloc_index);
324 try std.testing.expectEqual(capacity.storage_bytes, counting.allocated_bytes);
325 try std.testing.expectEqual(alloc_phase.capacity.Phase.initialization, storage.status().phase);
326 storage.activate();
327 const regions = try storage.acquire(.{
328 .source_bytes = 20,
329 .headings = 2,
330 .text_bytes = 11,
331 .id_bytes = 11,
332 });
333 defer storage.reset();
334 const base = @intFromPtr(storage.bytes.ptr);
335 try std.testing.expectEqual(base + capacity.headings_offset, @intFromPtr(regions.headings.ptr));
336 try std.testing.expectEqual(base + capacity.text_offset, @intFromPtr(regions.text.ptr));
337 try std.testing.expectEqual(base + capacity.id_offset, @intFromPtr(regions.ids.ptr));
338 }
339
340 test "heading storage retries after every allocation failure" {
341 comptime {
342 @stardustClaim(
343 @import("alloc_phase").capacity.witness(Storage, "zen_heading_oom"),
344 null,
345 null,
346 null,
347 null,
348 null,
349 null,
350 );
351 }
352
353 try std.testing.checkAllAllocationFailures(std.testing.allocator, checkInitFailures, .{});
354 }
355
356 comptime {
357 alloc_phase.capacity.requireAllocatorRejectingOwnerShape(Storage);
358 }