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 }