lib/ui/src/fact/store.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const alloc_phase = @import("alloc_phase");
3 const records = @import("records.zig");
4
5 const Allocator = std.mem.Allocator;
6 const Fact = records.Fact;
7
8 pub const Limits = struct {
9 nodes: u32 = 16_384,
10 };
11
12 pub const Capacity = struct {
13 fact_bytes: usize,
14 total_bytes: usize,
15
16 pub fn derive(limits: Limits) error{CapacityOverflow}!Capacity {
17 const fact_bytes = std.math.mul(usize, limits.nodes, @sizeOf(Fact)) catch
18 return error.CapacityOverflow;
19 const total_bytes = std.math.mul(usize, fact_bytes, 2) catch
20 return error.CapacityOverflow;
21 return .{ .fact_bytes = fact_bytes, .total_bytes = total_bytes };
22 }
23 };
24
25 pub const Store = struct {
26 pub const claim: alloc_phase.capacity.Declaration = .{
27 .source = .{
28 .id = "ui.fact_store",
29 .kind = .phase_static,
30 .limit_source = .caller,
31 .storage = .{
32 .covered = &.{.{
33 .id = "retained_fact_arrays",
34 .lifetime = .steady,
35 .detail = "two arrays of Limits.nodes Fact records; the current and prior frame occupy one array each",
36 }},
37 .excluded = &.{
38 "the tree store's node identity map and the damage owner's changed-node list",
39 "paint, image, text, and clip tables owned by their respective emitters",
40 },
41 },
42 .capacity = .{
43 .inputs = &.{alloc_phase.capacity.bindInput(Limits, "nodes", "nodes")},
44 .type_selectors = &.{alloc_phase.capacity.bindType(Fact, "fact")},
45 .nodes = &.{
46 .{ .input = 0 },
47 .{ .scale = .{ .node = 0, .coefficient = .{ .size_of_concrete_type = 0 } } },
48 .{ .add = .{ .left = 1, .right = 1 } },
49 },
50 .assertions = &.{.{
51 .scope = .closure_total,
52 .measure = .retained,
53 .relation = .exact,
54 .expression = 2,
55 }},
56 },
57 .overload = .{
58 .kind = .reject_before_seal,
59 .detail = "publish rejects a fact count above Limits.nodes before writing either retained array",
60 },
61 .risks = .{
62 .transitive = .{
63 .status = .open,
64 .detail = "publish copies caller facts without an allocator; a machine certificate of the call graph remains open",
65 },
66 .foreign = .{
67 .status = .excluded,
68 .detail = "fact emission is an in-process copy and calls no foreign runtime",
69 },
70 },
71 .obligations = &.{
72 .{ .key = "ui_fact_capacity", .role = .capacity_model },
73 .{ .key = "ui_fact_acquisition", .role = .acquisition },
74 .{ .key = "ui_fact_oom", .role = .initialization_failure },
75 .{ .key = "ui_fact_teardown", .role = .teardown },
76 },
77 },
78 .bindings = .{
79 .owner = @This(),
80 .seal = .{
81 .family = alloc_phase.capacity.selector(@This().activate),
82 .premise = .{ .class = .checked_semantic_fact, .authority = .checker },
83 },
84 .teardown = .{
85 .family = alloc_phase.capacity.selector(@This().deinit),
86 .premise = .{ .class = .checked_semantic_fact, .authority = .checker },
87 },
88 },
89 };
90
91 phase: alloc_phase.capacity.Phase,
92 limits: Limits,
93 capacity: Capacity,
94 storage: []Fact,
95 arrays: [2][]Fact,
96 counts: [2]u32 = @splat(0),
97 live: u1 = 0,
98
99 pub fn init(allocator: Allocator, limits: Limits) (error{CapacityOverflow} || Allocator.Error)!Store {
100 const capacity = try Capacity.derive(limits);
101 const count = std.math.mul(usize, limits.nodes, 2) catch return error.CapacityOverflow;
102 const storage = try allocator.alloc(Fact, count);
103 return .{
104 .phase = .initialization,
105 .limits = limits,
106 .capacity = capacity,
107 .storage = storage,
108 .arrays = .{ storage[0..limits.nodes], storage[limits.nodes..] },
109 };
110 }
111
112 pub fn activate(self: *Store) void {
113 std.debug.assert(self.phase == .initialization);
114 self.phase = .steady;
115 }
116
117 pub fn publish(self: *Store, facts: []const Fact) error{FactCapacityExceeded}!void {
118 std.debug.assert(self.phase == .steady);
119 if (facts.len > self.limits.nodes) return error.FactCapacityExceeded;
120 const next = self.live ^ 1;
121 @memcpy(self.arrays[next][0..facts.len], facts);
122 self.counts[next] = @intCast(facts.len);
123 self.live = next;
124 }
125
126 pub fn retained(self: *const Store) []const Fact {
127 std.debug.assert(self.phase == .steady);
128 return self.arrays[self.live][0..self.counts[self.live]];
129 }
130
131 pub fn prior(self: *const Store) []const Fact {
132 std.debug.assert(self.phase == .steady);
133 const other = self.live ^ 1;
134 return self.arrays[other][0..self.counts[other]];
135 }
136
137 pub fn deinit(self: *Store, allocator: Allocator) void {
138 std.debug.assert(self.phase != .teardown);
139 self.phase = .teardown;
140 allocator.free(self.storage);
141 self.storage = &.{};
142 }
143 };
144
145 test "ui_fact_capacity derives two exact arrays" {
146 comptime {
147 @stardustClaim(alloc_phase.capacity.witness(Store, "ui_fact_capacity"), null, null, null, null, null, null);
148 }
149 const capacity = try Capacity.derive(.{});
150 try std.testing.expectEqual(@as(usize, 1_048_576), capacity.fact_bytes);
151 try std.testing.expectEqual(@as(usize, 2_097_152), capacity.total_bytes);
152 }
153
154 test "ui_fact_acquisition retains the old frame on overload" {
155 comptime {
156 @stardustClaim(alloc_phase.capacity.witness(Store, "ui_fact_acquisition"), null, null, null, null, null, null);
157 }
158 var store = try Store.init(std.testing.allocator, .{ .nodes = 1 });
159 defer store.deinit(std.testing.allocator);
160 store.activate();
161 try store.publish(&.{.{ .id = 7 }});
162 try std.testing.expectError(error.FactCapacityExceeded, store.publish(&.{ .{ .id = 8 }, .{ .id = 9 } }));
163 try std.testing.expectEqual(@as(u64, 7), store.retained()[0].id);
164 try std.testing.expectEqual(@as(usize, 0), store.prior().len);
165 try store.publish(&.{.{ .id = 10 }});
166 try std.testing.expectEqual(@as(u64, 10), store.retained()[0].id);
167 try std.testing.expectEqual(@as(u64, 7), store.prior()[0].id);
168 }
169
170 test "ui_fact_oom leaves no partial acquisition" {
171 comptime {
172 @stardustClaim(alloc_phase.capacity.witness(Store, "ui_fact_oom"), null, null, null, null, null, null);
173 }
174 var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 0 });
175 try std.testing.expectError(error.OutOfMemory, Store.init(failing.allocator(), .{}));
176 try std.testing.expectEqual(@as(usize, 0), failing.allocated_bytes - failing.freed_bytes);
177 }
178
179 test "ui_fact_teardown releases both arrays" {
180 comptime {
181 @stardustClaim(alloc_phase.capacity.witness(Store, "ui_fact_teardown"), null, null, null, null, null, null);
182 }
183 var store = try Store.init(std.testing.allocator, .{ .nodes = 2 });
184 store.activate();
185 store.deinit(std.testing.allocator);
186 try std.testing.expectEqual(alloc_phase.capacity.Phase.teardown, store.phase);
187 }