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 }