lib/preserves/src/predicates.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 //! Predicates that test a value's shape. None of them allocates.
  2 //!
  3 //! Code that dispatches on values needs one tested answer for each shape question, so every caller
  4 //! draws the line between atoms, compound values and patterns the same way.
  5 const std = @import("std");
  6 
  7 const value_mod = @import("value.zig");
  8 const domain_mod = @import("domain.zig");
  9 const symbols_mod = @import("symbols.zig");
 10 
 11 pub const Value = value_mod.Value;
 12 pub const NoEmbedded = domain_mod.NoEmbedded;
 13 
 14 /// Returns the entries of a record's attribute dictionary. The JSON encoder calls this function for
 15 /// the attributes of a record it writes as a `{"type": …}` object. A record with zero fields has an
 16 /// empty attribute list. A record whose one field is a dictionary with string keys has that
 17 /// dictionary's entries. The call returns null for any other value, including a record whose
 18 /// dictionary has a key other than a string. The entries point into `value`, and the call allocates
 19 /// nothing.
 20 pub fn recordAttributes(comptime D: type, value: Value(D)) ?[]const Value(D).DictionaryEntry {
 21     domain_mod.assertIsDomain(D);
 22     switch (value) {
 23         .record => |r| {
 24             if (r.fields.len == 0) return &.{};
 25             if (r.fields.len != 1) return null;
 26             switch (r.fields[0]) {
 27                 .dictionary => |d| {
 28                     for (d) |entry| {
 29                         switch (entry.key) {
 30                             .string => {},
 31                             else => return null,
 32                         }
 33                     }
 34                     return d;
 35                 },
 36                 else => return null,
 37             }
 38         },
 39         else => return null,
 40     }
 41 }
 42 
 43 /// Returns true when `value` is a record. When `label` is given, its label is the symbol `label`,
 44 /// and when `arity` is given, it has `arity` fields. The `Observe` field readers call this function
 45 /// for a label and field-count check, and so does `isObserve`. A record with a label other than a
 46 /// symbol matches only when `label` is null. The package root's version takes the label as a
 47 /// `Symbol`.
 48 pub fn isRecord(comptime D: type, value: Value(D), label: ?[]const u8, arity: ?usize) bool {
 49     domain_mod.assertIsDomain(D);
 50     switch (value) {
 51         .record => |r| {
 52             if (label) |l| {
 53                 switch (r.label.*) {
 54                     .symbol => |s| if (!std.mem.eql(u8, s, l)) return false,
 55                     else => return false,
 56                 }
 57             }
 58             if (arity) |a| {
 59                 if (r.fields.len != a) return false;
 60             }
 61             return true;
 62         },
 63         else => return false,
 64     }
 65 }
 66 
 67 /// Returns true when `value` is an `<Observe pattern observer>` record: labeled `Observe`, with two
 68 /// fields.
 69 pub fn isObserve(comptime D: type, value: Value(D)) bool {
 70     return isRecord(D, value, symbols_mod.SYM_OBSERVE.name, 2);
 71 }
 72 
 73 /// Returns true when `value` is an embedded value.
 74 pub fn isEmbedded(comptime D: type, value: Value(D)) bool {
 75     domain_mod.assertIsDomain(D);
 76     return value == .embedded;
 77 }
 78 
 79 /// Returns true when `value` is a symbol.
 80 pub fn isSymbol(comptime D: type, value: Value(D)) bool {
 81     domain_mod.assertIsDomain(D);
 82     return value == .symbol;
 83 }
 84 
 85 /// Returns true when `value` is the symbol `null`, the value the package uses for JSON's null.
 86 pub fn isNull(comptime D: type, value: Value(D)) bool {
 87     domain_mod.assertIsDomain(D);
 88     return switch (value) {
 89         .symbol => |s| std.mem.eql(u8, s, symbols_mod.SYM_NULL.name),
 90         else => false,
 91     };
 92 }
 93 
 94 /// Returns true for a record, sequence, set or dictionary. The call returns false for patterns and
 95 /// embedded values, even a capture that holds a record.
 96 pub fn isCompound(comptime D: type, value: Value(D)) bool {
 97     domain_mod.assertIsDomain(D);
 98     return switch (value) {
 99         .record, .sequence, .set, .dictionary => true,
100         else => false,
101     };
102 }
103 
104 /// Returns true for a discard, capture, bind or rest pattern.
105 pub fn isPatternForm(comptime D: type, value: Value(D)) bool {
106     domain_mod.assertIsDomain(D);
107     return switch (value) {
108         .discard, .capture, .bind, .rest_pattern => true,
109         else => false,
110     };
111 }
112 
113 /// Returns true for a boolean, double, integer, string, byte string or symbol. The call returns
114 /// false for an embedded value.
115 pub fn isAtom(comptime D: type, value: Value(D)) bool {
116     domain_mod.assertIsDomain(D);
117     return switch (value) {
118         .boolean, .double, .signed_integer, .string, .byte_string, .symbol => true,
119         else => false,
120     };
121 }
122 
123 test "isRecord matches label and arity optionally" {
124     const V = Value(NoEmbedded);
125     const allocator = std.testing.allocator;
126     var arena = std.heap.ArenaAllocator.init(allocator);
127     defer arena.deinit();
128     const a = arena.allocator();
129 
130     const label = try V.initSymbol(a, "Foo");
131     const fields = try a.alloc(V, 2);
132     fields[0] = V.initI128(1);
133     fields[1] = V.initI128(2);
134     const rec = try V.initRecord(a, label, fields);
135 
136     try std.testing.expect(isRecord(NoEmbedded, rec, "Foo", 2));
137     try std.testing.expect(isRecord(NoEmbedded, rec, null, 2));
138     try std.testing.expect(isRecord(NoEmbedded, rec, "Foo", null));
139     try std.testing.expect(isRecord(NoEmbedded, rec, null, null));
140     try std.testing.expect(!isRecord(NoEmbedded, rec, "Bar", 2));
141     try std.testing.expect(!isRecord(NoEmbedded, rec, "Foo", 3));
142     try std.testing.expect(!isRecord(NoEmbedded, V.initBoolean(true), null, null));
143 }
144 
145 test "isObserve matches `<Observe _ _>` records" {
146     const V = Value(NoEmbedded);
147     const allocator = std.testing.allocator;
148     var arena = std.heap.ArenaAllocator.init(allocator);
149     defer arena.deinit();
150     const a = arena.allocator();
151 
152     const label = try V.initSymbol(a, "Observe");
153     const fields = try a.alloc(V, 2);
154     fields[0] = V{ .discard = {} };
155     fields[1] = V.initBoolean(true);
156     const rec = try V.initRecord(a, label, fields);
157     try std.testing.expect(isObserve(NoEmbedded, rec));
158 
159     const wrong_arity = try V.initRecord(a, try V.initSymbol(a, "Observe"), try a.alloc(V, 0));
160     try std.testing.expect(!isObserve(NoEmbedded, wrong_arity));
161 }
162 
163 test "isNull, isSymbol, isEmbedded, isAtom, isCompound, isPatternForm" {
164     const V = Value(NoEmbedded);
165     const allocator = std.testing.allocator;
166     var arena = std.heap.ArenaAllocator.init(allocator);
167     defer arena.deinit();
168     const a = arena.allocator();
169 
170     const null_v = V{ .symbol = "null" };
171     try std.testing.expect(isNull(NoEmbedded, null_v));
172     try std.testing.expect(isSymbol(NoEmbedded, null_v));
173     try std.testing.expect(isAtom(NoEmbedded, null_v));
174     try std.testing.expect(!isCompound(NoEmbedded, null_v));
175     try std.testing.expect(!isPatternForm(NoEmbedded, null_v));
176     try std.testing.expect(!isEmbedded(NoEmbedded, null_v));
177 
178     const not_null = V{ .symbol = "hello" };
179     try std.testing.expect(!isNull(NoEmbedded, not_null));
180 
181     const seq = V.initSequence(try a.alloc(V, 0));
182     try std.testing.expect(isCompound(NoEmbedded, seq));
183     try std.testing.expect(!isAtom(NoEmbedded, seq));
184 
185     const discard_v: V = .{ .discard = {} };
186     try std.testing.expect(isPatternForm(NoEmbedded, discard_v));
187     try std.testing.expect(!isAtom(NoEmbedded, discard_v));
188     try std.testing.expect(!isCompound(NoEmbedded, discard_v));
189 }
190 
191 test "recordAttributes extracts dict entries only for single-dict-field records" {
192     const V = Value(NoEmbedded);
193     const allocator = std.testing.allocator;
194     var arena = std.heap.ArenaAllocator.init(allocator);
195     defer arena.deinit();
196     const a = arena.allocator();
197 
198     const empty_entries: V.Dictionary = try a.alloc(V.DictionaryEntry, 0);
199     const d_empty = V.initDictionary(empty_entries);
200     const fields_empty = try a.alloc(V, 1);
201     fields_empty[0] = d_empty;
202     const rec_empty = try V.initRecord(a, try V.initSymbol(a, "Label"), fields_empty);
203     const got_empty = recordAttributes(NoEmbedded, rec_empty).?;
204     try std.testing.expectEqual(@as(usize, 0), got_empty.len);
205 
206     const entries = try a.alloc(V.DictionaryEntry, 1);
207     entries[0] = .{ .key = try V.initString(a, "k"), .value = V.initI128(1) };
208     const d_one = V.initDictionary(entries);
209     const fields_one = try a.alloc(V, 1);
210     fields_one[0] = d_one;
211     const rec_one = try V.initRecord(a, try V.initSymbol(a, "Label"), fields_one);
212     const got_one = recordAttributes(NoEmbedded, rec_one).?;
213     try std.testing.expectEqual(@as(usize, 1), got_one.len);
214 
215     try std.testing.expect(recordAttributes(NoEmbedded, V.initI128(0)) == null);
216 
217     const bad_entries = try a.alloc(V.DictionaryEntry, 1);
218     bad_entries[0] = .{ .key = V.initI128(7), .value = V.initI128(1) };
219     const d_bad = V.initDictionary(bad_entries);
220     const fields_bad = try a.alloc(V, 1);
221     fields_bad[0] = d_bad;
222     const rec_bad = try V.initRecord(a, try V.initSymbol(a, "Label"), fields_bad);
223     try std.testing.expect(recordAttributes(NoEmbedded, rec_bad) == null);
224 
225     const fields_two = try a.alloc(V, 2);
226     fields_two[0] = V.initI128(1);
227     fields_two[1] = V.initI128(2);
228     const rec_two = try V.initRecord(a, try V.initSymbol(a, "Label"), fields_two);
229     try std.testing.expect(recordAttributes(NoEmbedded, rec_two) == null);
230 }