lib/zen/src/frontmatter/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 pairs: []model.Pair,
10 continuation: []u8,
11 };
12
13 pub const Status = struct {
14 phase: alloc_phase.capacity.Phase,
15 in_use: bool,
16 storage_bytes: usize,
17 max_pairs: usize,
18 max_continuation_bytes: usize,
19 };
20
21 pub const Storage = struct {
22 phase: alloc_phase.capacity.Phase,
23 capacity: capacity_mod.Capacity,
24 bytes: []align(capacity_mod.storage_alignment) u8,
25 pairs: []model.Pair,
26 continuation: []u8,
27 in_use: bool = false,
28
29 pub const Limits: type = capacity_mod.Limits;
30 pub const Capacity: type = capacity_mod.Capacity;
31 pub const Exhaustion: type = model.Exhaustion;
32 pub const InitError = std.mem.Allocator.Error || capacity_mod.DeriveError;
33 pub const AcquireError: type = model.Error;
34
35 pub const claim: alloc_phase.capacity.Declaration = .{
36 .source = .{
37 .id = "zen.frontmatter_storage",
38 .kind = .phase_static,
39 .limit_source = .caller,
40 .storage = .{
41 .covered = &.{
42 .{
43 .id = "frontmatter_pair_descriptors",
44 .lifetime = .steady,
45 .detail = "frontmatter pair descriptors",
46 },
47 .{
48 .id = "joined_continuation_values",
49 .lifetime = .steady,
50 .detail = "joined continuation values",
51 },
52 },
53 .excluded = &.{
54 "caller-owned Markdown source and borrowed ordinary metadata values",
55 "site catalog, rendered page, theme, and filesystem output owners",
56 },
57 },
58 .capacity = .{
59 .inputs = &.{
60 alloc_phase.capacity.bindInput(Limits, "max_pairs", "max_pairs"),
61 alloc_phase.capacity.bindInput(Limits, "max_continuation_bytes", "max_continuation_bytes"),
62 },
63 .type_selectors = &.{
64 alloc_phase.capacity.bindType(model.Pair, "pair"),
65 },
66 .nodes = &.{
67 .{ .input = 0 },
68 .{ .scale = .{ .node = 0, .coefficient = .{ .size_of_concrete_type = 0 } } },
69 .{ .alignment = .{ .node = 1, .alignment = .{ .literal = 16 } } },
70 .{ .input = 1 },
71 .{ .add = .{ .left = 2, .right = 3 } },
72 },
73 .assertions = &.{.{
74 .scope = .closure_total,
75 .measure = .retained,
76 .relation = .exact,
77 .expression = 4,
78 }},
79 },
80 .overload = .{
81 .kind = .reject_before_mutation,
82 .detail = "unclosed input, pair overflow, continuation overflow, and concurrent acquisition fail before storage mutation",
83 },
84 .risks = .{
85 .transitive = .{
86 .status = .witnessed,
87 .detail = "section discovery, field scanning, and continuation joining use only borrowed input and acquired slices",
88 },
89 .foreign = .{
90 .status = .excluded,
91 .detail = "frontmatter parsing crosses no operating-system or foreign callback boundary",
92 },
93 },
94 .obligations = &.{
95 .{ .key = "zen_frontmatter_capacity", .role = .capacity_model },
96 .{ .key = "zen_frontmatter_acquisition", .role = .custom },
97 .{ .key = "zen_frontmatter_oom", .role = .custom },
98 .{ .key = "zen_frontmatter_boundaries", .role = .overload },
99 .{ .key = "zen_frontmatter_reuse", .role = .overload },
100 .{ .key = "zen_frontmatter_sealed", .role = .transitive_risk },
101 .{ .key = "zen_frontmatter_root", .role = .custom },
102 .{ .key = "zen_frontmatter_consumer", .role = .foreign_risk },
103 },
104 },
105 .bindings = .{
106 .owner = @This(),
107 .seal = .{
108 .family = alloc_phase.capacity.selector(@This().activate),
109 .premise = .{
110 .class = .checked_semantic_fact,
111 .authority = .checker,
112 },
113 },
114 .teardown = .{
115 .family = alloc_phase.capacity.selector(@This().deinit),
116 .premise = .{
117 .class = .checked_semantic_fact,
118 .authority = .checker,
119 },
120 },
121 },
122 };
123
124 pub fn init(allocator: std.mem.Allocator, limits: Limits) InitError!Storage {
125 const capacity = try Capacity.derive(limits);
126 const bytes = try allocator.alignedAlloc(
127 u8,
128 .fromByteUnits(capacity_mod.storage_alignment),
129 capacity.storage_bytes,
130 );
131 return .{
132 .phase = .initialization,
133 .capacity = capacity,
134 .bytes = bytes,
135 .pairs = typedSlice(
136 model.Pair,
137 bytes,
138 capacity.pairs_offset,
139 limits.max_pairs,
140 ),
141 .continuation = bytes[capacity.continuation_offset..][0..limits.max_continuation_bytes],
142 };
143 }
144
145 pub fn activate(self: *Storage) void {
146 std.debug.assert(self.phase == .initialization);
147 std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
148 self.phase = .steady;
149 }
150
151 pub fn acquire(self: *Storage, markdown: []const u8) AcquireError!Regions {
152 std.debug.assert(self.phase == .steady);
153 if (self.in_use) return error.FrontmatterStorageInUse;
154 const plan = try plan_mod.Plan.inspect(markdown, self.capacity.limits);
155 self.in_use = true;
156 return .{
157 .plan = plan,
158 .pairs = self.pairs[0..plan.pairs],
159 .continuation = self.continuation[0..plan.continuation_bytes],
160 };
161 }
162
163 pub fn reset(self: *Storage) void {
164 std.debug.assert(self.phase == .steady);
165 std.debug.assert(self.in_use);
166 self.in_use = false;
167 }
168
169 pub fn status(self: *const Storage) Status {
170 return .{
171 .phase = self.phase,
172 .in_use = self.in_use,
173 .storage_bytes = self.capacity.storage_bytes,
174 .max_pairs = self.capacity.limits.max_pairs,
175 .max_continuation_bytes = self.capacity.limits.max_continuation_bytes,
176 };
177 }
178
179 pub fn deinit(self: *Storage, allocator: std.mem.Allocator) void {
180 std.debug.assert(self.phase != .teardown);
181 std.debug.assert(!self.in_use);
182 std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
183 self.phase = .teardown;
184 allocator.free(self.bytes);
185 self.bytes = &.{};
186 self.pairs = &.{};
187 self.continuation = &.{};
188 }
189 };
190
191 fn typedSlice(
192 comptime T: type,
193 bytes: []align(capacity_mod.storage_alignment) u8,
194 offset: usize,
195 count: usize,
196 ) []T {
197 const byte_count = count * @sizeOf(T);
198 const region: []align(@alignOf(T)) u8 = @alignCast(bytes[offset..][0..byte_count]);
199 return std.mem.bytesAsSlice(T, region);
200 }
201
202 fn checkInitFailures(allocator: std.mem.Allocator) !void {
203 var storage = try Storage.init(allocator, .{
204 .max_pairs = 5,
205 .max_continuation_bytes = 38,
206 });
207 storage.deinit(allocator);
208 }
209
210 test "frontmatter storage acquires one exact aligned region" {
211 comptime {
212 @stardustClaim(
213 @import("alloc_phase").capacity.witness(Storage, "zen_frontmatter_acquisition"),
214 null,
215 null,
216 null,
217 null,
218 null,
219 null,
220 );
221 }
222
223 var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
224 const limits = capacity_mod.Limits{
225 .max_pairs = 5,
226 .max_continuation_bytes = 38,
227 };
228 const capacity = try capacity_mod.Capacity.derive(limits);
229 var storage = try Storage.init(counting.allocator(), limits);
230 defer storage.deinit(counting.allocator());
231
232 try std.testing.expectEqual(@as(usize, 1), counting.alloc_index);
233 try std.testing.expectEqual(capacity.storage_bytes, counting.allocated_bytes);
234 try std.testing.expectEqual(alloc_phase.capacity.Phase.initialization, storage.status().phase);
235 storage.activate();
236 const source = "---\ndescription:\n bounded\n---\n# Body\n";
237 const regions = try storage.acquire(source);
238 defer storage.reset();
239 const base = @intFromPtr(storage.bytes.ptr);
240 try std.testing.expectEqual(base + capacity.pairs_offset, @intFromPtr(regions.pairs.ptr));
241 try std.testing.expectEqual(base + capacity.continuation_offset, @intFromPtr(regions.continuation.ptr));
242 }
243
244 test "frontmatter storage retries after every allocation failure" {
245 comptime {
246 @stardustClaim(
247 @import("alloc_phase").capacity.witness(Storage, "zen_frontmatter_oom"),
248 null,
249 null,
250 null,
251 null,
252 null,
253 null,
254 );
255 }
256
257 try std.testing.checkAllAllocationFailures(std.testing.allocator, checkInitFailures, .{});
258 }
259
260 comptime {
261 alloc_phase.capacity.requireAllocatorRejectingOwnerShape(Storage);
262 }