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 }