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 }