lib/xkb/src/compose/workspace/storage.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const alloc_phase = @import("alloc_phase");
  3 const capacity_mod = @import("capacity.zig");
  4 
  5 pub const StorageExhaustion = error{
  6     ComposeScratchInUse,
  7     LineByteCapacityExceeded,
  8     PathByteCapacityExceeded,
  9 };
 10 
 11 pub const Status = struct {
 12     phase: alloc_phase.capacity.Phase,
 13     in_use: bool,
 14     storage_bytes: usize,
 15     line_bytes: usize,
 16     path_bytes: usize,
 17 };
 18 
 19 pub const Storage = struct {
 20     phase: alloc_phase.capacity.Phase,
 21     capacity: capacity_mod.Capacity,
 22     bytes: []u8,
 23     lines: []u8,
 24     paths: []u8,
 25     in_use: bool = false,
 26 
 27     pub const Limits: type = capacity_mod.Limits;
 28     pub const Capacity: type = capacity_mod.Capacity;
 29     pub const Exhaustion: type = StorageExhaustion;
 30     pub const InitError = std.mem.Allocator.Error || capacity_mod.DeriveError;
 31     pub const AcquireError = Storage.Exhaustion;
 32 
 33     pub const claim: alloc_phase.capacity.Declaration = .{
 34         .source = .{
 35             .id = "xkb.compose_scratch_storage",
 36             .kind = .phase_static,
 37             .limit_source = .caller,
 38             .storage = .{
 39                 .covered = &.{
 40                     .{
 41                         .id = "recursive_compose_and_registry_line_input",
 42                         .lifetime = .steady,
 43                         .detail = "recursive Compose and registry line input",
 44                     },
 45                     .{
 46                         .id = "compose_discovery_and_include_paths",
 47                         .lifetime = .steady,
 48                         .detail = "Compose discovery and include paths",
 49                     },
 50                 },
 51                 .excluded = &.{
 52                     "caller-owned root Compose input bytes",
 53                     "Compose trie and result storage",
 54                     "diagnostic callbacks, operating-system file handles, and teardown storage",
 55                 },
 56             },
 57             .capacity = .{
 58                 .inputs = &.{
 59                     alloc_phase.capacity.bindInput(Limits, "line_bytes", "line_bytes"),
 60                     alloc_phase.capacity.bindInput(Limits, "path_bytes", "path_bytes"),
 61                 },
 62                 .type_selectors = &.{},
 63                 .nodes = &.{
 64                     .{ .input = 0 },
 65                     .{ .constant = 1 },
 66                     .{ .add = .{ .left = 0, .right = 1 } },
 67                     .{ .scale = .{ .node = 2, .coefficient = .{ .literal = 7 } } },
 68                     .{ .input = 1 },
 69                     .{ .scale = .{ .node = 4, .coefficient = .{ .literal = 8 } } },
 70                     .{ .add = .{ .left = 3, .right = 5 } },
 71                 },
 72                 .assertions = &.{.{
 73                     .scope = .closure_total,
 74                     .measure = .retained,
 75                     .relation = .exact,
 76                     .expression = 6,
 77                 }},
 78             },
 79             .overload = .{
 80                 .kind = .terminal,
 81                 .detail = "Line or path overload terminates compilation and resets private scratch and table state.",
 82             },
 83             .risks = .{
 84                 .transitive = .{
 85                     .status = .witnessed,
 86                     .detail = "File, registry, locale, include, and path processing use only fixed scratch slots after activation.",
 87                 },
 88                 .foreign = .{
 89                     .status = .excluded,
 90                     .detail = "filesystem handles and platform I/O internals remain foreign initialization effects",
 91                 },
 92             },
 93             .obligations = &.{
 94                 .{ .key = "xkb_compose_scratch_capacity", .role = .capacity_model },
 95                 .{ .key = "xkb_compose_scratch_acquisition", .role = .custom },
 96                 .{ .key = "xkb_compose_scratch_oom", .role = .custom },
 97                 .{ .key = "xkb_compose_scratch_boundaries", .role = .overload },
 98                 .{ .key = "xkb_compose_scratch_reuse", .role = .overload },
 99                 .{ .key = "xkb_compose_scratch_depth", .role = .overload },
100                 .{ .key = "xkb_compose_scratch_sealed", .role = .transitive_risk },
101                 .{ .key = "xkb_compose_scratch_root", .role = .custom },
102                 .{ .key = "xkb_compose_scratch_consumer", .role = .foreign_risk },
103                 .{ .key = "xkb_compose_scratch_windowing_root", .role = .custom },
104             },
105         },
106         .bindings = .{
107             .owner = @This(),
108             .seal = .{
109                 .family = alloc_phase.capacity.selector(@This().activate),
110                 .premise = .{
111                     .class = .checked_semantic_fact,
112                     .authority = .checker,
113                 },
114             },
115             .teardown = .{
116                 .family = alloc_phase.capacity.selector(@This().deinit),
117                 .premise = .{
118                     .class = .checked_semantic_fact,
119                     .authority = .checker,
120                 },
121             },
122         },
123     };
124 
125     pub fn init(allocator: std.mem.Allocator, limits: Limits) InitError!Storage {
126         const capacity = try Capacity.derive(limits);
127         const bytes = try allocator.alloc(u8, capacity.storage_bytes);
128         return .{
129             .phase = .initialization,
130             .capacity = capacity,
131             .bytes = bytes,
132             .lines = bytes[capacity.line_offset..][0..capacity.line_storage_bytes],
133             .paths = bytes[capacity.path_offset..][0..capacity.path_storage_bytes],
134         };
135     }
136 
137     pub fn activate(self: *Storage) void {
138         std.debug.assert(self.phase == .initialization);
139         std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
140         self.phase = .steady;
141     }
142 
143     pub fn acquire(self: *Storage) Storage.Exhaustion!void {
144         std.debug.assert(self.phase == .steady);
145         if (self.in_use) return error.ComposeScratchInUse;
146         self.in_use = true;
147     }
148 
149     pub fn reset(self: *Storage) void {
150         std.debug.assert(self.phase == .steady);
151         std.debug.assert(self.in_use);
152         self.in_use = false;
153     }
154 
155     pub fn fileLine(self: *Storage, depth: usize) []u8 {
156         std.debug.assert(self.in_use);
157         std.debug.assert(depth < capacity_mod.file_line_slot_count);
158         return self.lineSlot(depth);
159     }
160 
161     pub fn registryLine(self: *Storage) []u8 {
162         std.debug.assert(self.in_use);
163         return self.lineSlot(capacity_mod.registry_line_slot_index);
164     }
165 
166     pub fn filePath(self: *Storage, depth: usize) []u8 {
167         std.debug.assert(self.in_use);
168         std.debug.assert(depth < capacity_mod.file_path_slot_count);
169         return self.pathSlot(depth);
170     }
171 
172     pub fn auxiliaryPath(self: *Storage, index: usize) []u8 {
173         std.debug.assert(self.in_use);
174         std.debug.assert(index < capacity_mod.auxiliary_path_slot_count);
175         return self.pathSlot(capacity_mod.file_path_slot_count + index);
176     }
177 
178     pub fn lineLimit(self: *const Storage) usize {
179         return self.capacity.limits.line_bytes;
180     }
181 
182     pub fn status(self: *const Storage) Status {
183         return .{
184             .phase = self.phase,
185             .in_use = self.in_use,
186             .storage_bytes = self.capacity.storage_bytes,
187             .line_bytes = self.capacity.limits.line_bytes,
188             .path_bytes = self.capacity.limits.path_bytes,
189         };
190     }
191 
192     pub fn deinit(self: *Storage, allocator: std.mem.Allocator) void {
193         std.debug.assert(self.phase != .teardown);
194         std.debug.assert(!self.in_use);
195         std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
196         self.phase = .teardown;
197         allocator.free(self.bytes);
198         self.bytes = &.{};
199         self.lines = &.{};
200         self.paths = &.{};
201     }
202 
203     fn lineSlot(self: *Storage, index: usize) []u8 {
204         const width = self.capacity.line_bytes_per_slot;
205         const start = index * width;
206         return self.lines[start..][0..width];
207     }
208 
209     fn pathSlot(self: *Storage, index: usize) []u8 {
210         const width = self.capacity.limits.path_bytes;
211         const start = index * width;
212         return self.paths[start..][0..width];
213     }
214 };
215 
216 comptime {
217     alloc_phase.capacity.requireAllocatorRejectingOwnerShape(Storage);
218 }
219 
220 fn checkInitFailures(allocator: std.mem.Allocator) !void {
221     var storage = try Storage.init(allocator, capacity_mod.default_limits);
222     storage.deinit(allocator);
223 }
224 
225 test "Compose scratch storage acquires one exact region" {
226     comptime {
227         @stardustClaim(
228             @import("alloc_phase").capacity.witness(Storage, "xkb_compose_scratch_acquisition"),
229             null,
230             null,
231             null,
232             null,
233             null,
234             null,
235         );
236     }
237 
238     var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
239     const capacity = try capacity_mod.Capacity.derive(capacity_mod.default_limits);
240     var storage = try Storage.init(counting.allocator(), capacity_mod.default_limits);
241     defer storage.deinit(counting.allocator());
242     try std.testing.expectEqual(@as(usize, 1), counting.alloc_index);
243     try std.testing.expectEqual(capacity.storage_bytes, counting.allocated_bytes);
244     storage.activate();
245     try storage.acquire();
246     defer storage.reset();
247     const base = @intFromPtr(storage.bytes.ptr);
248     try std.testing.expectEqual(base + capacity.line_offset, @intFromPtr(storage.lines.ptr));
249     try std.testing.expectEqual(base + capacity.path_offset, @intFromPtr(storage.paths.ptr));
250 }
251 
252 test "Compose scratch storage retries after every allocation failure" {
253     comptime {
254         @stardustClaim(
255             @import("alloc_phase").capacity.witness(Storage, "xkb_compose_scratch_oom"),
256             null,
257             null,
258             null,
259             null,
260             null,
261             null,
262         );
263     }
264 
265     try std.testing.checkAllAllocationFailures(
266         std.testing.allocator,
267         checkInitFailures,
268         .{},
269     );
270 }