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 }