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 }