lib/preserves/src/records.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 //! Builders for ten kinds of record, each with a fixed label and a fixed number of fields. A caller
2 //! building one of these records needs the label spelled the one way readers match it, and needs to
3 //! know what the record takes from its arguments.
4 const std = @import("std");
5 const Allocator = std.mem.Allocator;
6
7 const value_mod = @import("value.zig");
8 const domain_mod = @import("domain.zig");
9 const symbols_mod = @import("symbols.zig");
10 const embedded_mod = @import("embedded.zig");
11 const constructors_mod = @import("constructors.zig");
12
13 pub const Value = value_mod.Value;
14 pub const NoEmbedded = domain_mod.NoEmbedded;
15 pub const AnyEmbedded = embedded_mod.AnyEmbedded;
16
17 /// Returns a namespace of protocol record builders for `Value(D)`. Code whose values hold embedded
18 /// values of one type calls it once at compile time, for every protocol record builder over those
19 /// values. `D` has to provide `eql`, `order`, `deinit` and `clone`, or the call is a compile error.
20 /// Every builder allocates a copy of the label, one label cell and a copy of the field slice, all
21 /// with `alloc`. The record takes the field values the caller passes, and `deinit` on the record
22 /// frees the label, the fields and the record's own storage. On failure a builder frees its label
23 /// and leaves the fields with the caller.
24 pub fn Records(comptime D: type) type {
25 domain_mod.assertIsDomain(D);
26 const V = Value(D);
27 const H = constructors_mod.Constructors(D);
28 return struct {
29 fn recordWithSymbol(alloc: Allocator, symbol_name: []const u8, fields: []const V) !V {
30 var label = try V.initSymbol(alloc, symbol_name);
31 errdefer label.deinit(alloc);
32 return try H.record(alloc, label, fields);
33 }
34
35 /// Returns `<Observe pattern observer>`, for code that asks to observe a pattern. For
36 /// `AnyEmbedded` values, an observer other than an embedded value is moved into a new cell
37 /// and wrapped as an embedded value that owns it. The wrapped observer compares, hashes,
38 /// copies and frees through the value it holds. An observer that is already an embedded
39 /// value goes in as it is. When the embedded values have a type other than `AnyEmbedded`,
40 /// every observer goes in as it is. On failure the call frees the wrapper cell and leaves
41 /// `pattern` and `observer` with the caller.
42 pub fn observeRecord(alloc: Allocator, pattern: V, observer: V) !V {
43 var wrapped_ptr: ?*V = null;
44 const obs: V = observer_wrap: {
45 if (D == AnyEmbedded) {
46 switch (observer) {
47 .embedded => break :observer_wrap observer,
48 else => {
49 const val_ptr = try alloc.create(V);
50 val_ptr.* = observer;
51 wrapped_ptr = val_ptr;
52 break :observer_wrap V{ .embedded = AnyEmbedded{
53 .value = @ptrCast(val_ptr),
54 .semantic_ops = embedded_mod.parsedEmbeddedOps(V),
55 .deinit_fn = embedded_mod.parsedEmbeddedDeinit(V),
56 .clone_fn = embedded_mod.parsedEmbeddedClone(V),
57 } };
58 },
59 }
60 }
61 break :observer_wrap observer;
62 };
63 errdefer if (wrapped_ptr) |ptr| alloc.destroy(ptr);
64 return recordWithSymbol(alloc, symbols_mod.SYM_OBSERVE.name, &.{ pattern, obs });
65 }
66
67 /// Returns `<Synced>`, a record with zero fields.
68 pub fn syncedRecord(alloc: Allocator) !V {
69 return recordWithSymbol(alloc, symbols_mod.SYM_SYNCED.name, &.{});
70 }
71
72 /// Returns `<RequireService name>`.
73 pub fn requireServiceRecord(alloc: Allocator, name: V) !V {
74 return recordWithSymbol(alloc, symbols_mod.SYM_REQUIRE_SERVICE.name, &.{name});
75 }
76
77 /// Returns `<RunService name>`.
78 pub fn runServiceRecord(alloc: Allocator, name: V) !V {
79 return recordWithSymbol(alloc, symbols_mod.SYM_RUN_SERVICE.name, &.{name});
80 }
81
82 /// Returns `<ServiceState name state>`.
83 pub fn serviceStateRecord(alloc: Allocator, name: V, state: V) !V {
84 return recordWithSymbol(alloc, symbols_mod.SYM_SERVICE_STATE.name, &.{ name, state });
85 }
86
87 /// Returns `<ServiceObject name obj>`.
88 pub fn serviceObjectRecord(alloc: Allocator, name: V, obj: V) !V {
89 return recordWithSymbol(alloc, symbols_mod.SYM_SERVICE_OBJECT.name, &.{ name, obj });
90 }
91
92 /// Returns `<ReactorError stage facet reactor err>`, a record with four fields.
93 pub fn reactorErrorRecord(alloc: Allocator, stage: V, facet: V, reactor: V, err: V) !V {
94 return recordWithSymbol(alloc, symbols_mod.SYM_REACTOR_ERROR.name, &.{ stage, facet, reactor, err });
95 }
96
97 /// Returns `<EntityRuntime kind observe during>`.
98 pub fn entityRuntimeRecord(alloc: Allocator, kind: V, observe: V, during: V) !V {
99 return recordWithSymbol(alloc, symbols_mod.SYM_ENTITY_RUNTIME.name, &.{ kind, observe, during });
100 }
101
102 /// Returns `<ServiceDependency depender dependee>`.
103 pub fn serviceDependencyRecord(alloc: Allocator, depender: V, dependee: V) !V {
104 return recordWithSymbol(alloc, symbols_mod.SYM_SERVICE_DEPENDENCY.name, &.{ depender, dependee });
105 }
106
107 /// Returns `<RestartService name>`.
108 pub fn restartServiceRecord(alloc: Allocator, name: V) !V {
109 return recordWithSymbol(alloc, symbols_mod.SYM_RESTART_SERVICE.name, &.{name});
110 }
111 };
112 }
113
114 /// The protocol record builders for `Value(NoEmbedded)`. Code whose values are `Value(NoEmbedded)`
115 /// calls these builders, for a namespace fixed to that type.
116 pub const records = Records(NoEmbedded);
117 /// The protocol record builders for values whose embedded values hold any pointer (`AnyEmbedded`).
118 /// The package root re-exports each builder under the same name, so a caller of
119 /// `preserves.observeRecord` calls this instance.
120 pub const any_records = Records(AnyEmbedded);
121
122 test "syncedRecord builds <Synced>" {
123 const V = Value(NoEmbedded);
124 const allocator = std.testing.allocator;
125 var arena = std.heap.ArenaAllocator.init(allocator);
126 defer arena.deinit();
127 const a = arena.allocator();
128
129 const r = try records.syncedRecord(a);
130 try std.testing.expect(r == .record);
131 try std.testing.expect(std.mem.eql(u8, r.record.label.*.symbol, "Synced"));
132 try std.testing.expectEqual(@as(usize, 0), r.record.fields.len);
133 _ = V;
134 }
135
136 test "requireServiceRecord and runServiceRecord take a single name" {
137 const V = Value(NoEmbedded);
138 const allocator = std.testing.allocator;
139 var arena = std.heap.ArenaAllocator.init(allocator);
140 defer arena.deinit();
141 const a = arena.allocator();
142
143 const name = try V.initString(a, "svc-a");
144 const req = try records.requireServiceRecord(a, name);
145 try std.testing.expect(std.mem.eql(u8, req.record.label.*.symbol, "RequireService"));
146 try std.testing.expectEqual(@as(usize, 1), req.record.fields.len);
147
148 const run = try records.runServiceRecord(a, name);
149 try std.testing.expect(std.mem.eql(u8, run.record.label.*.symbol, "RunService"));
150 }
151
152 test "observeRecord (NoEmbedded) keeps observer unchanged" {
153 const V = Value(NoEmbedded);
154 const allocator = std.testing.allocator;
155 var arena = std.heap.ArenaAllocator.init(allocator);
156 defer arena.deinit();
157 const a = arena.allocator();
158
159 const pattern: V = .{ .discard = {} };
160 const observer = V.initBoolean(true);
161 const rec = try records.observeRecord(a, pattern, observer);
162 try std.testing.expect(std.mem.eql(u8, rec.record.label.*.symbol, "Observe"));
163 try std.testing.expectEqual(@as(usize, 2), rec.record.fields.len);
164 try std.testing.expect(rec.record.fields[1] == .boolean);
165 }
166
167 test "observeRecord (AnyEmbedded) auto-wraps non-embedded observers" {
168 const V = Value(AnyEmbedded);
169 const allocator = std.testing.allocator;
170 var arena = std.heap.ArenaAllocator.init(allocator);
171 defer arena.deinit();
172 const a = arena.allocator();
173
174 const pattern: V = .{ .discard = {} };
175 const observer = V.initBoolean(true);
176 const rec = try any_records.observeRecord(a, pattern, observer);
177 try std.testing.expect(std.mem.eql(u8, rec.record.label.*.symbol, "Observe"));
178 try std.testing.expect(rec.record.fields[1] == .embedded);
179 try std.testing.expect(
180 rec.record.fields[1].embedded.semantic_ops == embedded_mod.parsedEmbeddedOps(V),
181 );
182
183 var probe: u32 = 7;
184 const pre_embedded = V{ .embedded = AnyEmbedded{ .value = &probe } };
185 const rec2 = try any_records.observeRecord(a, pattern, pre_embedded);
186 try std.testing.expect(rec2.record.fields[1].embedded.semantic_ops == null);
187 }
188
189 test "protocol record labels are owned by returned values" {
190 const V = Value(NoEmbedded);
191 const allocator = std.testing.allocator;
192
193 var name = try V.initString(allocator, "svc-a");
194 var name_moved = false;
195 errdefer if (!name_moved) name.deinit(allocator);
196
197 var observed = try records.observeRecord(allocator, .{ .discard = {} }, V.initBoolean(true));
198 defer observed.deinit(allocator);
199
200 var required = try records.requireServiceRecord(allocator, name);
201 name_moved = true;
202 defer required.deinit(allocator);
203 }
204
205 test "observeRecord owns auto-wrapped embedded observer payload" {
206 const V = Value(AnyEmbedded);
207 const allocator = std.testing.allocator;
208
209 var observed = try any_records.observeRecord(allocator, .{ .discard = {} }, V.initBoolean(true));
210 defer observed.deinit(allocator);
211 }
212
213 fn checkObserveRecordAllocationFailures(allocator: Allocator) !void {
214 const V = Value(AnyEmbedded);
215 var observer = try V.initString(allocator, "observer");
216 var observer_owned = true;
217 defer if (observer_owned) observer.deinit(allocator);
218
219 var observed = try any_records.observeRecord(
220 allocator,
221 .{ .discard = {} },
222 observer,
223 );
224 observer_owned = false;
225 defer observed.deinit(allocator);
226
227 try std.testing.expect(observed.record.fields[1] == .embedded);
228 try std.testing.expect(observed.record.fields[1].embedded.deinit_fn != null);
229 try std.testing.expect(observed.record.fields[1].embedded.clone_fn != null);
230 }
231
232 test "observeRecord preserves caller ownership on every allocation failure" {
233 try std.testing.checkAllAllocationFailures(
234 std.testing.allocator,
235 checkObserveRecordAllocationFailures,
236 .{},
237 );
238 }
239
240 test "protocol record builders deinit under owning allocator" {
241 const V = Value(NoEmbedded);
242 const allocator = std.testing.allocator;
243
244 var synced = try records.syncedRecord(allocator);
245 defer synced.deinit(allocator);
246
247 var require_service = try records.requireServiceRecord(allocator, V.initI128(1));
248 defer require_service.deinit(allocator);
249
250 var run_service = try records.runServiceRecord(allocator, V.initI128(2));
251 defer run_service.deinit(allocator);
252
253 var service_state = try records.serviceStateRecord(allocator, V.initI128(3), V.initI128(4));
254 defer service_state.deinit(allocator);
255
256 var service_object = try records.serviceObjectRecord(allocator, V.initI128(5), V.initI128(6));
257 defer service_object.deinit(allocator);
258
259 var reactor_error = try records.reactorErrorRecord(
260 allocator,
261 V.initI128(7),
262 V.initI128(8),
263 V.initI128(9),
264 V.initI128(10),
265 );
266 defer reactor_error.deinit(allocator);
267
268 var entity_runtime = try records.entityRuntimeRecord(allocator, V.initI128(11), V.initI128(12), V.initI128(13));
269 defer entity_runtime.deinit(allocator);
270
271 var service_dependency = try records.serviceDependencyRecord(allocator, V.initI128(14), V.initI128(15));
272 defer service_dependency.deinit(allocator);
273
274 var restart_service = try records.restartServiceRecord(allocator, V.initI128(16));
275 defer restart_service.deinit(allocator);
276 }
277
278 test "reactorErrorRecord has correct arity" {
279 const V = Value(NoEmbedded);
280 const allocator = std.testing.allocator;
281 var arena = std.heap.ArenaAllocator.init(allocator);
282 defer arena.deinit();
283 const a = arena.allocator();
284
285 const re = try records.reactorErrorRecord(
286 a,
287 V.initI128(1),
288 V.initI128(2),
289 V.initI128(3),
290 V.initI128(4),
291 );
292 try std.testing.expect(std.mem.eql(u8, re.record.label.*.symbol, "ReactorError"));
293 try std.testing.expectEqual(@as(usize, 4), re.record.fields.len);
294 }
295
296 test "entityRuntime, serviceDependency, restartService, serviceState, serviceObject" {
297 const V = Value(NoEmbedded);
298 const allocator = std.testing.allocator;
299 var arena = std.heap.ArenaAllocator.init(allocator);
300 defer arena.deinit();
301 const a = arena.allocator();
302
303 const er = try records.entityRuntimeRecord(a, V.initI128(1), V.initI128(2), V.initI128(3));
304 try std.testing.expect(std.mem.eql(u8, er.record.label.*.symbol, "EntityRuntime"));
305
306 const sd = try records.serviceDependencyRecord(a, V.initI128(1), V.initI128(2));
307 try std.testing.expect(std.mem.eql(u8, sd.record.label.*.symbol, "ServiceDependency"));
308
309 const rs = try records.restartServiceRecord(a, V.initI128(1));
310 try std.testing.expect(std.mem.eql(u8, rs.record.label.*.symbol, "RestartService"));
311
312 const ss = try records.serviceStateRecord(a, V.initI128(1), V.initI128(2));
313 try std.testing.expect(std.mem.eql(u8, ss.record.label.*.symbol, "ServiceState"));
314
315 const so = try records.serviceObjectRecord(a, V.initI128(1), V.initI128(2));
316 try std.testing.expect(std.mem.eql(u8, so.record.label.*.symbol, "ServiceObject"));
317 }