lib/zen/src/footnote/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 pub const Regions = struct {
8 plan: plan_mod.Plan,
9 definitions: []model.Definition,
10 joined_text: []u8,
11 };
12
13 pub const Status = struct {
14 phase: alloc_phase.capacity.Phase,
15 in_use: bool,
16 storage_bytes: usize,
17 max_definitions: usize,
18 max_joined_text_bytes: usize,
19 definition_count: usize,
20 joined_text_bytes: usize,
21 high_water_definitions: usize,
22 high_water_joined_text_bytes: usize,
23 rejected_source_count: u64,
24 };
25
26 pub const Storage = struct {
27 phase: alloc_phase.capacity.Phase,
28 capacity: capacity_mod.Capacity,
29 bytes: []align(capacity_mod.storage_alignment) u8,
30 definitions: []model.Definition,
31 joined_text: []u8,
32 in_use: bool = false,
33 definition_count: usize = 0,
34 joined_text_bytes: usize = 0,
35 high_water_definitions: usize = 0,
36 high_water_joined_text_bytes: usize = 0,
37 rejected_source_count: u64 = 0,
38
39 pub const Limits: type = capacity_mod.Limits;
40 pub const Capacity: type = capacity_mod.Capacity;
41 pub const Exhaustion: type = model.Exhaustion;
42 pub const InitError = std.mem.Allocator.Error || capacity_mod.DeriveError;
43 pub const AcquireError: type = model.Error;
44
45 pub const claim: alloc_phase.capacity.Declaration = .{
46 .source = .{
47 .id = "zen.footnote_storage",
48 .kind = .phase_static,
49 .limit_source = .caller,
50 .storage = .{
51 .covered = &.{
52 .{
53 .id = "footnote_definition_descriptors",
54 .lifetime = .steady,
55 .detail = "footnote definition descriptors",
56 },
57 .{
58 .id = "joined_footnote_continuation_text",
59 .lifetime = .steady,
60 .detail = "joined footnote continuation text",
61 },
62 },
63 .excluded = &.{
64 "caller-owned Markdown source and borrowed keys or single-line text",
65 "rendered HTML and inline-rendering owners",
66 "heading, quiz, site, theme, and filesystem owners",
67 },
68 },
69 .capacity = .{
70 .inputs = &.{
71 alloc_phase.capacity.bindInput(Limits, "max_definitions", "max_definitions"),
72 alloc_phase.capacity.bindInput(Limits, "max_joined_text_bytes", "max_joined_text_bytes"),
73 },
74 .type_selectors = &.{
75 alloc_phase.capacity.bindType(model.Definition, "definition"),
76 },
77 .nodes = &.{
78 .{ .input = 0 },
79 .{ .scale = .{ .node = 0, .coefficient = .{ .size_of_concrete_type = 0 } } },
80 .{ .input = 1 },
81 .{ .add = .{ .left = 1, .right = 2 } },
82 },
83 .assertions = &.{.{
84 .scope = .closure_total,
85 .measure = .retained,
86 .relation = .exact,
87 .expression = 3,
88 }},
89 },
90 .overload = .{
91 .kind = .reject_before_mutation,
92 .detail = "max plus one and in-use collection reject before the backing region or active result changes; only rejection telemetry advances",
93 },
94 .risks = .{
95 .transitive = .{
96 .status = .witnessed,
97 .detail = "scanning, planning, descriptor filling, duplicate filtering, joining, lookup, and numbering use borrowed input and acquired slices only",
98 },
99 .foreign = .{
100 .status = .excluded,
101 .detail = "footnote collection crosses no operating-system or foreign callback boundary",
102 },
103 },
104 .obligations = &.{
105 .{ .key = "zen_footnote_capacity", .role = .capacity_model },
106 .{ .key = "zen_footnote_acquisition", .role = .custom },
107 .{ .key = "zen_footnote_oom", .role = .custom },
108 .{ .key = "zen_footnote_boundaries", .role = .overload },
109 .{ .key = "zen_footnote_reuse", .role = .overload },
110 .{ .key = "zen_footnote_sealed", .role = .transitive_risk },
111 .{ .key = "zen_footnote_root", .role = .custom },
112 .{ .key = "zen_footnote_consumer", .role = .foreign_risk },
113 },
114 },
115 .bindings = .{
116 .owner = @This(),
117 .seal = .{
118 .family = alloc_phase.capacity.selector(@This().activate),
119 .premise = .{
120 .class = .checked_semantic_fact,
121 .authority = .checker,
122 },
123 },
124 .teardown = .{
125 .family = alloc_phase.capacity.selector(@This().deinit),
126 .premise = .{
127 .class = .checked_semantic_fact,
128 .authority = .checker,
129 },
130 },
131 },
132 };
133
134 pub fn init(allocator: std.mem.Allocator, limits: Limits) InitError!Storage {
135 const capacity = try Capacity.derive(limits);
136 const bytes = if (capacity.storage_bytes == 0)
137 @as([]align(capacity_mod.storage_alignment) u8, &.{})
138 else
139 try allocator.alignedAlloc(
140 u8,
141 .fromByteUnits(capacity_mod.storage_alignment),
142 capacity.storage_bytes,
143 );
144 return .{
145 .phase = .initialization,
146 .capacity = capacity,
147 .bytes = bytes,
148 .definitions = typedSlice(
149 model.Definition,
150 bytes,
151 capacity.definitions_offset,
152 limits.max_definitions,
153 ),
154 .joined_text = bytes[capacity.joined_text_offset..][0..limits.max_joined_text_bytes],
155 };
156 }
157
158 pub fn activate(self: *Storage) void {
159 std.debug.assert(self.phase == .initialization);
160 self.assertStorage();
161 self.phase = .steady;
162 }
163
164 pub fn acquire(self: *Storage, source: []const u8) AcquireError!Regions {
165 std.debug.assert(self.phase == .steady);
166 if (self.in_use) return self.reject(error.FootnoteStorageInUse);
167 const plan = plan_mod.Plan.inspect(source, self.capacity.limits) catch |err| {
168 self.rejected_source_count +|= 1;
169 return err;
170 };
171 self.in_use = true;
172 self.definition_count = plan.definitions;
173 self.joined_text_bytes = plan.joined_text_bytes;
174 self.high_water_definitions = @max(self.high_water_definitions, plan.definitions);
175 self.high_water_joined_text_bytes = @max(
176 self.high_water_joined_text_bytes,
177 plan.joined_text_bytes,
178 );
179 self.assertStorage();
180 return .{
181 .plan = plan,
182 .definitions = self.definitions[0..plan.definitions],
183 .joined_text = self.joined_text[0..plan.joined_text_bytes],
184 };
185 }
186
187 pub fn reset(self: *Storage) void {
188 std.debug.assert(self.phase == .steady);
189 std.debug.assert(self.in_use);
190 self.in_use = false;
191 self.definition_count = 0;
192 self.joined_text_bytes = 0;
193 self.assertStorage();
194 }
195
196 pub fn status(self: *const Storage) Status {
197 return .{
198 .phase = self.phase,
199 .in_use = self.in_use,
200 .storage_bytes = self.capacity.storage_bytes,
201 .max_definitions = self.capacity.limits.max_definitions,
202 .max_joined_text_bytes = self.capacity.limits.max_joined_text_bytes,
203 .definition_count = self.definition_count,
204 .joined_text_bytes = self.joined_text_bytes,
205 .high_water_definitions = self.high_water_definitions,
206 .high_water_joined_text_bytes = self.high_water_joined_text_bytes,
207 .rejected_source_count = self.rejected_source_count,
208 };
209 }
210
211 pub fn deinit(self: *Storage, allocator: std.mem.Allocator) void {
212 std.debug.assert(self.phase != .teardown);
213 std.debug.assert(!self.in_use);
214 self.assertStorage();
215 self.phase = .teardown;
216 allocator.free(self.bytes);
217 self.bytes = &.{};
218 self.definitions = &.{};
219 self.joined_text = &.{};
220 }
221
222 fn reject(self: *Storage, err: model.Exhaustion) model.Exhaustion {
223 self.rejected_source_count +|= 1;
224 return err;
225 }
226
227 fn assertStorage(self: *const Storage) void {
228 std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
229 std.debug.assert(self.definitions.len == self.capacity.limits.max_definitions);
230 std.debug.assert(self.joined_text.len == self.capacity.limits.max_joined_text_bytes);
231 std.debug.assert(self.definition_count <= self.definitions.len);
232 std.debug.assert(self.joined_text_bytes <= self.joined_text.len);
233 std.debug.assert(self.high_water_definitions <= self.definitions.len);
234 std.debug.assert(self.high_water_joined_text_bytes <= self.joined_text.len);
235 if (!self.in_use) {
236 std.debug.assert(self.definition_count == 0);
237 std.debug.assert(self.joined_text_bytes == 0);
238 }
239 }
240 };
241
242 fn typedSlice(
243 comptime T: type,
244 bytes: []align(capacity_mod.storage_alignment) u8,
245 offset: usize,
246 count: usize,
247 ) []T {
248 const byte_count = count * @sizeOf(T);
249 const region: []align(@alignOf(T)) u8 = @alignCast(bytes[offset..][0..byte_count]);
250 return std.mem.bytesAsSlice(T, region);
251 }
252
253 fn checkInitFailures(allocator: std.mem.Allocator) !void {
254 var storage = try Storage.init(allocator, .{
255 .max_definitions = 3,
256 .max_joined_text_bytes = 42,
257 });
258 storage.deinit(allocator);
259 }
260
261 test "footnote storage acquires one exact aligned region" {
262 comptime {
263 @stardustClaim(
264 @import("alloc_phase").capacity.witness(Storage, "zen_footnote_acquisition"),
265 null,
266 null,
267 null,
268 null,
269 null,
270 null,
271 );
272 }
273
274 var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
275 const limits = capacity_mod.Limits{
276 .max_definitions = 3,
277 .max_joined_text_bytes = 42,
278 };
279 const capacity = try capacity_mod.Capacity.derive(limits);
280 var storage = try Storage.init(counting.allocator(), limits);
281 defer storage.deinit(counting.allocator());
282
283 try std.testing.expectEqual(@as(usize, 1), counting.alloc_index);
284 try std.testing.expectEqual(capacity.storage_bytes, counting.allocated_bytes);
285 try std.testing.expectEqual(alloc_phase.capacity.Phase.initialization, storage.status().phase);
286 storage.activate();
287 const source = "[^one]: First\n continued\n[^two]: Second\n[^three]: Third\n";
288 const regions = try storage.acquire(source);
289 defer storage.reset();
290 const base = @intFromPtr(storage.bytes.ptr);
291 try std.testing.expectEqual(base + capacity.definitions_offset, @intFromPtr(regions.definitions.ptr));
292 try std.testing.expectEqual(base + capacity.joined_text_offset, @intFromPtr(regions.joined_text.ptr));
293 }
294
295 test "footnote storage retries after every allocation failure" {
296 comptime {
297 @stardustClaim(
298 @import("alloc_phase").capacity.witness(Storage, "zen_footnote_oom"),
299 null,
300 null,
301 null,
302 null,
303 null,
304 null,
305 );
306 }
307
308 try std.testing.checkAllAllocationFailures(std.testing.allocator, checkInitFailures, .{});
309 }
310
311 comptime {
312 alloc_phase.capacity.requireAllocatorRejectingOwnerShape(Storage);
313 }