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 }