lib/zen/src/theme/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 output: []u8,
10 };
11
12 pub const Status = struct {
13 phase: alloc_phase.capacity.Phase,
14 in_use: bool,
15 storage_bytes: usize,
16 max_output_bytes: usize,
17 output_bytes: usize,
18 high_water_output_bytes: usize,
19 rejected_page_count: u64,
20 };
21
22 pub const Storage = struct {
23 phase: alloc_phase.capacity.Phase,
24 capacity: capacity_mod.Capacity,
25 bytes: []u8,
26 in_use: bool = false,
27 output_bytes: usize = 0,
28 high_water_output_bytes: usize = 0,
29 rejected_page_count: u64 = 0,
30
31 pub const Limits: type = capacity_mod.Limits;
32 pub const Capacity: type = capacity_mod.Capacity;
33 pub const Exhaustion: type = model.Exhaustion;
34 pub const InitError: type = std.mem.Allocator.Error;
35 pub const AcquireError: type = model.Error;
36
37 pub const claim: alloc_phase.capacity.Declaration = .{
38 .source = .{
39 .id = "zen.theme_render_storage",
40 .kind = .phase_static,
41 .limit_source = .caller,
42 .storage = .{
43 .covered = &.{
44 .{
45 .id = "rendered_theme_page_bytes",
46 .lifetime = .steady,
47 .detail = "rendered theme page bytes",
48 },
49 },
50 .excluded = &.{
51 "caller-owned theme layout, page values, metadata, and includes",
52 "Markdown rendering, site catalog, loaded theme files, and filesystem owners",
53 },
54 },
55 .capacity = .{
56 .inputs = &.{
57 alloc_phase.capacity.bindInput(Limits, "max_output_bytes", "max_output_bytes"),
58 },
59 .type_selectors = &.{},
60 .nodes = &.{
61 .{ .input = 0 },
62 },
63 .assertions = &.{.{
64 .scope = .closure_total,
65 .measure = .retained,
66 .relation = .exact,
67 .expression = 0,
68 }},
69 },
70 .overload = .{
71 .kind = .reject_before_mutation,
72 .detail = "invalid templates, max plus one output, and in-use rendering reject before output bytes or the active result change; only rejection telemetry advances",
73 },
74 .risks = .{
75 .transitive = .{
76 .status = .witnessed,
77 .detail = "template scanning, placeholder resolution, escaped-length planning, and exact filling use borrowed inputs and the acquired byte slice only",
78 },
79 .foreign = .{
80 .status = .excluded,
81 .detail = "theme page rendering crosses no operating-system or foreign callback boundary",
82 },
83 },
84 .obligations = &.{
85 .{ .key = "zen_theme_render_capacity", .role = .capacity_model },
86 .{ .key = "zen_theme_render_acquisition", .role = .custom },
87 .{ .key = "zen_theme_render_oom", .role = .custom },
88 .{ .key = "zen_theme_render_boundaries", .role = .overload },
89 .{ .key = "zen_theme_render_reuse", .role = .overload },
90 .{ .key = "zen_theme_render_sealed", .role = .transitive_risk },
91 .{ .key = "zen_theme_render_root", .role = .custom },
92 .{ .key = "zen_theme_render_consumer", .role = .foreign_risk },
93 },
94 },
95 .bindings = .{
96 .owner = @This(),
97 .seal = .{
98 .family = alloc_phase.capacity.selector(@This().activate),
99 .premise = .{
100 .class = .checked_semantic_fact,
101 .authority = .checker,
102 },
103 },
104 .teardown = .{
105 .family = alloc_phase.capacity.selector(@This().deinit),
106 .premise = .{
107 .class = .checked_semantic_fact,
108 .authority = .checker,
109 },
110 },
111 },
112 };
113
114 pub fn init(allocator: std.mem.Allocator, limits: Limits) InitError!Storage {
115 const capacity = Capacity.derive(limits);
116 const bytes = if (capacity.storage_bytes == 0)
117 @as([]u8, &.{})
118 else
119 try allocator.alloc(u8, capacity.storage_bytes);
120 return .{
121 .phase = .initialization,
122 .capacity = capacity,
123 .bytes = bytes,
124 };
125 }
126
127 pub fn activate(self: *Storage) void {
128 std.debug.assert(self.phase == .initialization);
129 self.assertStorage();
130 self.phase = .steady;
131 }
132
133 pub fn acquire(self: *Storage, active: model.Theme, page: model.Page) AcquireError!Regions {
134 std.debug.assert(self.phase == .steady);
135 if (self.in_use) return self.reject(error.ThemeStorageInUse);
136 const plan = plan_mod.Plan.inspect(active, page, self.capacity.limits) catch |err| {
137 self.rejected_page_count +|= 1;
138 return err;
139 };
140 self.in_use = true;
141 self.output_bytes = plan.output_bytes;
142 self.high_water_output_bytes = @max(self.high_water_output_bytes, plan.output_bytes);
143 self.assertStorage();
144 return .{
145 .plan = plan,
146 .output = self.bytes[self.capacity.output_offset..][0..plan.output_bytes],
147 };
148 }
149
150 pub fn reset(self: *Storage) void {
151 std.debug.assert(self.phase == .steady);
152 std.debug.assert(self.in_use);
153 self.in_use = false;
154 self.output_bytes = 0;
155 self.assertStorage();
156 }
157
158 pub fn status(self: *const Storage) Status {
159 return .{
160 .phase = self.phase,
161 .in_use = self.in_use,
162 .storage_bytes = self.capacity.storage_bytes,
163 .max_output_bytes = self.capacity.limits.max_output_bytes,
164 .output_bytes = self.output_bytes,
165 .high_water_output_bytes = self.high_water_output_bytes,
166 .rejected_page_count = self.rejected_page_count,
167 };
168 }
169
170 pub fn deinit(self: *Storage, allocator: std.mem.Allocator) void {
171 std.debug.assert(self.phase != .teardown);
172 std.debug.assert(!self.in_use);
173 self.assertStorage();
174 self.phase = .teardown;
175 allocator.free(self.bytes);
176 self.bytes = &.{};
177 }
178
179 fn reject(self: *Storage, err: model.Exhaustion) model.Exhaustion {
180 self.rejected_page_count +|= 1;
181 return err;
182 }
183
184 fn assertStorage(self: *const Storage) void {
185 std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
186 std.debug.assert(self.output_bytes <= self.bytes.len);
187 std.debug.assert(self.high_water_output_bytes <= self.bytes.len);
188 if (!self.in_use) std.debug.assert(self.output_bytes == 0);
189 }
190 };
191
192 fn checkInitFailures(allocator: std.mem.Allocator) !void {
193 var storage = try Storage.init(allocator, .{ .max_output_bytes = 583 });
194 storage.deinit(allocator);
195 }
196
197 test "theme render storage acquires one exact byte region" {
198 comptime {
199 @stardustClaim(
200 @import("alloc_phase").capacity.witness(Storage, "zen_theme_render_acquisition"),
201 null,
202 null,
203 null,
204 null,
205 null,
206 null,
207 );
208 }
209
210 var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
211 const limits = capacity_mod.Limits{ .max_output_bytes = 583 };
212 var storage = try Storage.init(counting.allocator(), limits);
213 defer storage.deinit(counting.allocator());
214 try std.testing.expectEqual(@as(usize, 1), counting.alloc_index);
215 try std.testing.expectEqual(@as(usize, 583), counting.allocated_bytes);
216 try std.testing.expectEqual(alloc_phase.capacity.Phase.initialization, storage.status().phase);
217 storage.activate();
218 const content = @as([(583) * ("x").len]u8, @bitCast(@as([583][("x").len]u8, @splat(("x")[0..("x").len].*))));
219 const regions = try storage.acquire(
220 .{ .layout = "{{content}}" },
221 .{ .title = "Title", .content = &content },
222 );
223 defer storage.reset();
224 try std.testing.expectEqual(@intFromPtr(storage.bytes.ptr), @intFromPtr(regions.output.ptr));
225 }
226
227 test "theme render storage retries after every allocation failure" {
228 comptime {
229 @stardustClaim(
230 @import("alloc_phase").capacity.witness(Storage, "zen_theme_render_oom"),
231 null,
232 null,
233 null,
234 null,
235 null,
236 null,
237 );
238 }
239
240 try std.testing.checkAllAllocationFailures(std.testing.allocator, checkInitFailures, .{});
241 }
242
243 comptime {
244 alloc_phase.capacity.requireAllocatorRejectingOwnerShape(Storage);
245 }