lib/preserves/src/constructors.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 //! Each function here builds one value from Zig data in a single call. A caller needs values that
2 //! compare, hash and encode the same way whatever order the caller listed a set's elements or a
3 //! dictionary's entries in. A caller also needs to know which memory the new value points into, so
4 //! it can free exactly what the call allocated.
5 //!
6 //! A set with a repeated element, or a dictionary with a repeated key, has no single meaning, so
7 //! building one has to fail. Copying every string would cost an allocation per atom, and an atom
8 //! built from a string literal can point into bytes that outlive the value.
9 //!
10 //! The string and symbol constructors borrow the caller's bytes, and the compound constructors copy
11 //! only the slice of items they are given. `set` and `dictionary` reject a repeated element or key,
12 //! then store a sorted copy, so the stored order is the package's value order (`compare`).
13 //! `freeValue` frees the one slice or cell that a compound or pattern constructor allocated, and
14 //! leaves the items and atoms it points to alone. `Constructors` takes the type of the embedded
15 //! values as a parameter, and `constructors` and `any_constructors` are its two common instances.
16 const std = @import("std");
17 const Allocator = std.mem.Allocator;
18
19 const value_mod = @import("value.zig");
20 const domain_mod = @import("domain.zig");
21 const embedded_mod = @import("embedded.zig");
22 const symbols_mod = @import("symbols.zig");
23
24 pub const Value = value_mod.Value;
25 pub const NoEmbedded = domain_mod.NoEmbedded;
26 pub const AnyEmbedded = embedded_mod.AnyEmbedded;
27
28 /// Returns a namespace of constructors for `Value(D)`, the Preserves value whose embedded values
29 /// hold a `D`. A caller whose values hold embedded values of one type calls it once at compile
30 /// time, for every constructor over those values. The JSON decoder, the protocol record builders
31 /// and the pattern conversions each call it to build their records. `D` has to provide `eql`,
32 /// `order`, `deinit` and `clone`, or the call is a compile error.
33 pub fn Constructors(comptime D: type) type {
34 domain_mod.assertIsDomain(D);
35 const V = Value(D);
36 return struct {
37 /// Returns the boolean value `v`. The call allocates nothing.
38 pub fn boolean(v: bool) V {
39 return V.initBoolean(v);
40 }
41
42 /// Returns an integer value holding `v`. The call widens `v` to 128 bits, so the value
43 /// compares and encodes like any other integer. The call allocates nothing.
44 pub fn integer(v: i64) V {
45 return V.initI128(@as(i128, v));
46 }
47
48 /// Returns the double value `v`. The call allocates nothing.
49 pub fn float(v: f64) V {
50 return V.initDouble(v);
51 }
52
53 /// Returns a string value that borrows the bytes `s`. The value points into `s`, so `s` has
54 /// to stay alive while the value is in use. The call checks nothing about `s`, including
55 /// whether it is valid UTF-8.
56 pub fn string(s: []const u8) V {
57 return .{ .string = s };
58 }
59
60 /// Returns a symbol value that borrows the bytes `name`. The value points into `name`, so
61 /// `name` has to stay alive while the value is in use.
62 pub fn symbol(name: []const u8) V {
63 return .{ .symbol = name };
64 }
65
66 /// Returns the discard pattern, which matches any value. The call allocates nothing.
67 pub fn discard() V {
68 return .{ .discard = {} };
69 }
70
71 /// Returns the symbol `null`, the value the package uses for JSON's null. The symbol's
72 /// bytes are a constant of the package, so the value is safe to keep. `isNull` recognizes
73 /// the value.
74 pub fn null_val() V {
75 return .{ .symbol = symbols_mod.SYM_NULL.name };
76 }
77
78 /// Returns a capture pattern, which matches what `inner` matches and records the matched
79 /// value. The call allocates one cell for `inner` with `alloc` and moves `inner` into it.
80 /// The only error is running out of memory.
81 pub fn capture(alloc: Allocator, inner: V) !V {
82 const p = try alloc.create(V);
83 p.* = inner;
84 return .{ .capture = p };
85 }
86
87 /// Returns a pattern that matches what `inner` matches and records the matched value under
88 /// `name`. The call allocates one cell for `inner` with `alloc` and borrows `name`. On
89 /// failure the call frees its cell and leaves `inner` with the caller.
90 pub fn bindVal(alloc: Allocator, name: []const u8, inner: V) !V {
91 const p = try alloc.create(V);
92 errdefer alloc.destroy(p);
93 p.* = inner;
94 return .{ .bind = .{ .name = name, .pattern = p } };
95 }
96
97 /// Returns a pattern that matches a sequence whose first items match `prefix` and whose
98 /// remaining items match `rest`. The call copies the `prefix` slice and allocates one cell
99 /// for `rest`, both with `alloc`. The copy is shallow: the items it holds still point to
100 /// whatever the caller's items pointed to. On failure the call frees what it allocated and
101 /// leaves `prefix` and `rest` with the caller.
102 pub fn restPattern(alloc: Allocator, prefix: []const V, rest: V) !V {
103 const prefix_copy = try alloc.dupe(V, prefix);
104 errdefer alloc.free(prefix_copy);
105 const rest_ptr = try alloc.create(V);
106 errdefer alloc.destroy(rest_ptr);
107 rest_ptr.* = rest;
108 return .{ .rest_pattern = .{ .prefix = prefix_copy, .rest = rest_ptr } };
109 }
110
111 /// Returns an embedded value that holds the pointer `ptr`. The call is a compile error
112 /// unless `D` is `AnyEmbedded`. The value carries no equality, cleanup or copy functions,
113 /// so two such values are equal only when they hold the same pointer. Freeing the value
114 /// leaves the pointed-to data alone.
115 pub fn embedded(ptr: *anyopaque) V {
116 if (D != AnyEmbedded) {
117 @compileError("Constructors(" ++ @typeName(D) ++ ").embedded(ptr) requires D == AnyEmbedded");
118 }
119 return V{ .embedded = AnyEmbedded{ .value = ptr } };
120 }
121
122 /// Returns a record with label `label` and fields `fields`. The call allocates one cell for
123 /// the label and a copy of the `fields` slice, both with `alloc`. The copy is shallow: the
124 /// new record holds the same field values the caller passed. `freeValue` frees exactly the
125 /// cell and the slice this call allocated. On failure the call frees its cell and leaves
126 /// `label` and `fields` with the caller.
127 pub fn record(alloc: Allocator, label: V, fields: []const V) !V {
128 const lp = try alloc.create(V);
129 errdefer alloc.destroy(lp);
130 lp.* = label;
131 const fs = try alloc.dupe(V, fields);
132 return .{ .record = .{ .label = lp, .fields = fs } };
133 }
134
135 /// Returns a sequence holding a copy of the `items` slice, allocated with `alloc`. The copy
136 /// is shallow: the new sequence holds the same item values the caller passed. `freeValue`
137 /// frees exactly the slice this call allocated.
138 pub fn sequence(alloc: Allocator, items: []const V) !V {
139 const s = try alloc.dupe(V, items);
140 return .{ .sequence = s };
141 }
142
143 /// Returns a set holding a sorted copy of `items`, allocated with `alloc`. The copy is
144 /// sorted by `compare`, the package's total order on values. The copy is shallow, and
145 /// `freeValue` frees exactly the slice this call allocated. The duplicate check compares
146 /// every pair of items, so its cost grows with the square of the count. The call returns
147 /// `error.DuplicateSetElement` when two items are equal. That error allocates nothing. The
148 /// duplicate check leaves `items` with the caller.
149 pub fn set(alloc: Allocator, items: []const V) !V {
150 if (!V.setElementsDistinct(items)) return error.DuplicateSetElement;
151 const sorted = try alloc.dupe(V, items);
152 const Cmp = struct {
153 fn lt(_: void, a: V, b: V) bool {
154 return a.compare(b) == .lt;
155 }
156 };
157 std.mem.sort(V, sorted, {}, Cmp.lt);
158 return .{ .set = sorted };
159 }
160
161 /// Returns a dictionary holding a copy of `entries` sorted by key, allocated with `alloc`.
162 /// The keys are sorted by `compare`, the package's total order on values. The copy is
163 /// shallow, and `freeValue` frees exactly the slice this call allocated. The duplicate
164 /// check compares every pair of keys, so its cost grows with the square of the count. The
165 /// call returns `error.DuplicateDictionaryKey` when two keys are equal. That error
166 /// allocates nothing. The duplicate check leaves `entries` with the caller.
167 pub fn dictionary(alloc: Allocator, entries: []const V.DictionaryEntry) !V {
168 if (!V.dictionaryKeysDistinct(entries)) return error.DuplicateDictionaryKey;
169 const sorted = try alloc.dupe(V.DictionaryEntry, entries);
170 const Cmp = struct {
171 fn lt(_: void, a: V.DictionaryEntry, b: V.DictionaryEntry) bool {
172 return a.key.compare(b.key) == .lt;
173 }
174 };
175 std.mem.sort(V.DictionaryEntry, sorted, {}, Cmp.lt);
176 return .{ .dictionary = sorted };
177 }
178 };
179 }
180
181 /// The constructors for `Value(NoEmbedded)`. A caller whose values are `Value(NoEmbedded)` calls
182 /// these constructors, for a namespace fixed to that type. The protocol record builders and the
183 /// pattern conversions for such values are built on it.
184 pub const constructors = Constructors(NoEmbedded);
185 /// The constructors for values whose embedded values hold any pointer (`AnyEmbedded`). The package
186 /// root re-exports each of its functions under the same name, so a caller that writes
187 /// `preserves.record` calls this instance. The JSON decoder builds its records and patterns with
188 /// it.
189 pub const any_constructors = Constructors(AnyEmbedded);
190
191 /// Returns the boolean value `v` as a `Value(NoEmbedded)`, the same as `constructors.boolean`.
192 pub fn boolean(v: bool) Value(NoEmbedded) {
193 return constructors.boolean(v);
194 }
195
196 /// Returns an integer value holding `v` as a `Value(NoEmbedded)`, the same as
197 /// `constructors.integer`.
198 pub fn integer(v: i64) Value(NoEmbedded) {
199 return constructors.integer(v);
200 }
201
202 /// Returns the double value `v` as a `Value(NoEmbedded)`, the same as `constructors.float`.
203 pub fn float(v: f64) Value(NoEmbedded) {
204 return constructors.float(v);
205 }
206
207 /// Returns a `Value(NoEmbedded)` string that borrows `s`, the same as `constructors.string`. `s`
208 /// has to stay alive while the value is in use.
209 pub fn string(s: []const u8) Value(NoEmbedded) {
210 return constructors.string(s);
211 }
212
213 /// Returns a `Value(NoEmbedded)` symbol that borrows `name`, the same as `constructors.symbol`.
214 /// `name` has to stay alive while the value is in use.
215 pub fn symbol(name: []const u8) Value(NoEmbedded) {
216 return constructors.symbol(name);
217 }
218
219 /// Returns the discard pattern as a `Value(NoEmbedded)`, the same as `constructors.discard`.
220 pub fn discard() Value(NoEmbedded) {
221 return constructors.discard();
222 }
223
224 /// Returns the symbol `null` as a `Value(NoEmbedded)`, the same as `constructors.null_val`.
225 pub fn null_val() Value(NoEmbedded) {
226 return constructors.null_val();
227 }
228
229 test "top-level constructors round-trip to classes" {
230 const integer_mod = @import("integer.zig");
231 const V = Value(NoEmbedded);
232 const b = boolean(true);
233 try std.testing.expectEqual(value_mod.AtomClass.boolean, b.atomClass().?);
234
235 const i = integer(42);
236 try std.testing.expectEqual(value_mod.AtomClass.signed_integer, i.atomClass().?);
237 try std.testing.expectEqual(@as(i64, 42), integer_mod.SignedInteger.toI64Lossy(i.signed_integer));
238
239 const f = float(3.14);
240 try std.testing.expectEqual(value_mod.AtomClass.double, f.atomClass().?);
241
242 const s = string("hi");
243 try std.testing.expectEqual(value_mod.AtomClass.string, s.atomClass().?);
244
245 const sy = symbol("tag");
246 try std.testing.expectEqual(value_mod.AtomClass.symbol, sy.atomClass().?);
247
248 const d = discard();
249 try std.testing.expectEqual(value_mod.PatternFormClass.discard, d.patternClass().?);
250
251 const n = null_val();
252 try std.testing.expectEqual(value_mod.AtomClass.symbol, n.atomClass().?);
253 try std.testing.expect(std.mem.eql(u8, n.symbol, "null"));
254 _ = V;
255 }
256
257 test "capture/bindVal/restPattern allocate and build pattern forms" {
258 const V = Value(NoEmbedded);
259 const allocator = std.testing.allocator;
260 var arena = std.heap.ArenaAllocator.init(allocator);
261 defer arena.deinit();
262 const a = arena.allocator();
263
264 const cap = try constructors.capture(a, V.initI128(7));
265 try std.testing.expectEqual(value_mod.PatternFormClass.capture, cap.patternClass().?);
266 try std.testing.expectEqual(@as(i128, 7), try cap.capture.*.signed_integer.toI128());
267
268 const bound = try constructors.bindVal(a, "x", V.initBoolean(true));
269 try std.testing.expectEqual(value_mod.PatternFormClass.bind, bound.patternClass().?);
270 try std.testing.expect(std.mem.eql(u8, bound.bind.name, "x"));
271
272 const prefix = [_]V{ V.initI128(1), V.initI128(2) };
273 const rp = try constructors.restPattern(a, &prefix, .{ .discard = {} });
274 try std.testing.expectEqual(value_mod.PatternFormClass.rest_pattern, rp.patternClass().?);
275 try std.testing.expectEqual(@as(usize, 2), rp.rest_pattern.prefix.len);
276 try std.testing.expectEqual(value_mod.PatternFormClass.discard, rp.rest_pattern.rest.*.patternClass().?);
277 }
278
279 test "record/sequence/set/dictionary constructors copy their inputs" {
280 const V = Value(NoEmbedded);
281 const allocator = std.testing.allocator;
282 var arena = std.heap.ArenaAllocator.init(allocator);
283 defer arena.deinit();
284 const a = arena.allocator();
285
286 const label = symbol("pair");
287 const fields = [_]V{ V.initI128(1), V.initI128(2) };
288 const rec = try constructors.record(a, label, &fields);
289 try std.testing.expectEqual(value_mod.CompoundClass.record, rec.compoundClass().?);
290 try std.testing.expectEqual(@as(usize, 2), rec.record.fields.len);
291 try std.testing.expect(std.mem.eql(u8, rec.record.label.*.symbol, "pair"));
292
293 const items = [_]V{ V.initBoolean(false), V.initBoolean(true) };
294 const seq = try constructors.sequence(a, &items);
295 try std.testing.expectEqual(value_mod.CompoundClass.sequence, seq.compoundClass().?);
296 try std.testing.expectEqual(@as(usize, 2), seq.sequence.len);
297
298 const set_items = [_]V{ V.initI128(3), V.initI128(4) };
299 const s = try constructors.set(a, &set_items);
300 try std.testing.expectEqual(value_mod.CompoundClass.set, s.compoundClass().?);
301 try std.testing.expectEqual(@as(usize, 2), s.set.len);
302 }
303
304 test "set rejects duplicate elements without consuming inputs" {
305 const V = Value(NoEmbedded);
306 const allocator = std.testing.allocator;
307
308 var first = try V.initString(allocator, "duplicate");
309 defer first.deinit(allocator);
310 var second = try V.initString(allocator, "duplicate");
311 defer second.deinit(allocator);
312 const items = [_]V{ first, V.initI128(3), second };
313
314 try std.testing.expectError(error.DuplicateSetElement, constructors.set(allocator, &items));
315 }
316
317 test "set sorts distinct elements" {
318 const V = Value(NoEmbedded);
319 const allocator = std.testing.allocator;
320 var arena = std.heap.ArenaAllocator.init(allocator);
321 defer arena.deinit();
322 const a = arena.allocator();
323
324 const items = [_]V{ V.initI128(4), V.initI128(3), V.initI128(1) };
325 const s = try constructors.set(a, &items);
326 try std.testing.expectEqual(@as(usize, 3), s.set.len);
327 try std.testing.expectEqual(@as(i128, 1), try s.set[0].signed_integer.toI128());
328 try std.testing.expectEqual(@as(i128, 3), try s.set[1].signed_integer.toI128());
329 try std.testing.expectEqual(@as(i128, 4), try s.set[2].signed_integer.toI128());
330 }
331
332 test "dictionary rejects duplicate keys without consuming inputs" {
333 const V = Value(NoEmbedded);
334 const allocator = std.testing.allocator;
335
336 var first = try V.initString(allocator, "duplicate");
337 defer first.deinit(allocator);
338 var second = try V.initString(allocator, "duplicate");
339 defer second.deinit(allocator);
340 const entries = [_]V.DictionaryEntry{
341 .{ .key = first, .value = V.initI128(1) },
342 .{ .key = second, .value = V.initI128(2) },
343 };
344
345 try std.testing.expectError(
346 error.DuplicateDictionaryKey,
347 constructors.dictionary(allocator, &entries),
348 );
349 }
350
351 test "dictionary sorts distinct keys" {
352 const V = Value(NoEmbedded);
353 const allocator = std.testing.allocator;
354 var arena = std.heap.ArenaAllocator.init(allocator);
355 defer arena.deinit();
356
357 const entries = [_]V.DictionaryEntry{
358 .{ .key = V.initI128(3), .value = V.initI128(30) },
359 .{ .key = V.initI128(1), .value = V.initI128(10) },
360 .{ .key = V.initI128(2), .value = V.initI128(20) },
361 };
362 const d = try constructors.dictionary(arena.allocator(), &entries);
363 try std.testing.expectEqual(value_mod.CompoundClass.dictionary, d.compoundClass().?);
364 try std.testing.expectEqual(@as(usize, 3), d.dictionary.len);
365 try std.testing.expectEqual(@as(i128, 1), try d.dictionary[0].key.signed_integer.toI128());
366 try std.testing.expectEqual(@as(i128, 10), try d.dictionary[0].value.signed_integer.toI128());
367 try std.testing.expectEqual(@as(i128, 2), try d.dictionary[1].key.signed_integer.toI128());
368 try std.testing.expectEqual(@as(i128, 3), try d.dictionary[2].key.signed_integer.toI128());
369 }
370
371 test "any_constructors.embedded wraps a raw pointer" {
372 var payload: u32 = 0xdeadbeef;
373 const e = any_constructors.embedded(&payload);
374 try std.testing.expectEqual(@as(?value_mod.AtomClass, null), e.atomClass());
375 try std.testing.expect(e == .embedded);
376 try std.testing.expectEqual(@intFromPtr(&payload), @intFromPtr(e.embedded.value));
377 }
378
379 test "integer round-trips via toI64Lossy" {
380 const integer_mod = @import("integer.zig");
381 const round = integer(-12345);
382 try std.testing.expectEqual(@as(i64, -12345), integer_mod.SignedInteger.toI64Lossy(round.signed_integer));
383 }