lib/preserves/src/json.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 //! Converts the package's values to JSON text, and JSON text back to values. A caller exchanging
  2 //! values with JSON tools needs a JSON form for records, patterns and embedded values too, and
  3 //! needs to know which details a round trip loses.
  4 //!
  5 //! JSON has objects, arrays, strings, numbers, booleans and null only. It has no records, symbols,
  6 //! byte strings, sets or patterns. The standard library's JSON reader holds each number as a 64-bit
  7 //! integer or a double. The package keeps the JSON representation of the
  8 //! [Preserves](https://preserves.dev/) data language, alongside its text and binary syntaxes.
  9 //!
 10 //! Each value JSON lacks becomes a JSON object whose reserved member names it: `__record__`,
 11 //! `__discard__`, `__embedded__`, `$capture`, `$bind` and `$rest`. A record labeled with one of the
 12 //! ten labels in the file's protocol table becomes an object with a `"type"` member and named
 13 //! fields, and the object decodes back to that record. `Observe` and `Synced` are two of those ten
 14 //! labels. The mapping loses detail: symbols, byte strings and sets come back as strings and
 15 //! sequences, integers outside 64 bits change, and non-finite doubles become null. The decoder
 16 //! copies every string it keeps, so the decoded value owns all its memory and frees with `deinit`.
 17 const std = @import("std");
 18 const Allocator = std.mem.Allocator;
 19 const pretty = @import("pretty");
 20 
 21 const value_mod = @import("value.zig");
 22 const integer_mod = @import("integer.zig");
 23 const embedded_mod = @import("embedded.zig");
 24 const symbols_mod = @import("symbols.zig");
 25 const constructors_mod = @import("constructors.zig");
 26 const predicates_mod = @import("predicates.zig");
 27 const text_format = @import("text/root.zig");
 28 const parse_error_mod = @import("error.zig");
 29 
 30 pub const AnyEmbedded = embedded_mod.AnyEmbedded;
 31 /// The value type this file reads and writes: Preserves values whose embedded values hold any
 32 /// pointer (`AnyEmbedded`), for code that builds values for `toJsonString` or reads what
 33 /// `fromJsonString` returns. The type is the same type as `Value(AnyEmbedded)` from the package
 34 /// root.
 35 pub const Value = value_mod.Value(AnyEmbedded);
 36 pub const SignedInteger = integer_mod.SignedInteger;
 37 pub const ParseError = parse_error_mod.ParseError;
 38 /// The errors `toJsonString` returns, for a caller switching on these errors. The error set covers
 39 /// running out of memory, and a set or dictionary anywhere in the value that holds two equal
 40 /// elements or keys.
 41 pub const EncodeError = Allocator.Error || error{
 42     DuplicateSetElement,
 43     DuplicateDictionaryKey,
 44 };
 45 
 46 const JsonWriter = pretty.json.Writer;
 47 const WriteError = EncodeError || std.Io.Writer.Error;
 48 const any_constructors = constructors_mod.any_constructors;
 49 
 50 const ProtocolEntry = struct {
 51     symbol_name: []const u8,
 52     json_type: []const u8,
 53     fields: []const []const u8,
 54 };
 55 
 56 const protocol_records = [_]ProtocolEntry{
 57     .{ .symbol_name = "Observe", .json_type = "observe", .fields = &.{ "pattern", "observer" } },
 58     .{ .symbol_name = "ReactorError", .json_type = "reactor.error", .fields = &.{ "stage", "facet", "reactor", "error" } },
 59     .{ .symbol_name = "EntityRuntime", .json_type = "entity.runtime", .fields = &.{ "kind", "observe", "during" } },
 60     .{ .symbol_name = "RequireService", .json_type = "require.service", .fields = &.{"name"} },
 61     .{ .symbol_name = "RunService", .json_type = "run.service", .fields = &.{"name"} },
 62     .{ .symbol_name = "ServiceState", .json_type = "service.state", .fields = &.{ "name", "state" } },
 63     .{ .symbol_name = "ServiceObject", .json_type = "service.object", .fields = &.{ "name", "object" } },
 64     .{ .symbol_name = "ServiceDependency", .json_type = "service.dependency", .fields = &.{ "depender", "dependee" } },
 65     .{ .symbol_name = "RestartService", .json_type = "restart.service", .fields = &.{"name"} },
 66     .{ .symbol_name = "Synced", .json_type = "syndicate.sync.synced", .fields = &.{} },
 67 };
 68 
 69 fn lookupProtocol(symbol_name: []const u8) ?ProtocolEntry {
 70     for (&protocol_records) |entry| {
 71         if (std.mem.eql(u8, entry.symbol_name, symbol_name)) return entry;
 72     }
 73     return null;
 74 }
 75 
 76 fn lookupProtocolByJsonType(json_type: []const u8) ?ProtocolEntry {
 77     for (&protocol_records) |entry| {
 78         if (std.mem.eql(u8, entry.json_type, json_type)) return entry;
 79     }
 80     return null;
 81 }
 82 
 83 /// Returns `value` as minified JSON text, allocated with `alloc`, for code that logs or sends a
 84 /// value to a JSON consumer. The caller owns the returned bytes and frees them with `alloc`.
 85 /// Booleans, strings and sequences become their JSON counterparts, and sets become arrays. The
 86 /// symbol `null` becomes JSON null, and every other symbol becomes a JSON string. A byte string
 87 /// becomes a string of lowercase hexadecimal digits. If a double is non-finite, it becomes null. An
 88 /// integer too wide for 128 bits becomes a 64-bit number that differs from it. A dictionary becomes
 89 /// an object: string and symbol keys become member names, and other keys are written in the text
 90 /// syntax. A record whose label is a protocol name becomes `{"type": …}` with named fields, and
 91 /// each field past the named ones takes the name `f` plus its index. If a record has a symbol label
 92 /// and either zero fields or one dictionary field with string keys, the record becomes
 93 /// `{"type": label}` plus the dictionary's entries. Any other record becomes
 94 /// `{"__record__": label, "fields": […]}`. The discard pattern becomes `{"__discard__": true}`, and
 95 /// captures, binds and rest patterns become `$capture`, `$bind` and `$rest` objects. An embedded
 96 /// value becomes `{"__embedded__": "…"}` holding its pointer as a decimal string. The call frees
 97 /// what it wrote so far on any error.
 98 pub fn toJsonString(alloc: Allocator, value: Value) EncodeError![]const u8 {
 99     var out: std.Io.Writer.Allocating = .init(alloc);
100     errdefer out.deinit();
101     var json = JsonWriter.init(&out.writer, .minified);
102     writeJson(alloc, &json, value) catch |err| switch (err) {
103         error.WriteFailed => return error.OutOfMemory,
104         else => return @errorCast(err),
105     };
106     return try out.toOwnedSlice();
107 }
108 
109 fn writeJson(alloc: Allocator, json: *JsonWriter, value: Value) WriteError!void {
110     switch (value) {
111         .discard => {
112             const object = try json.object();
113             try object.field("__discard__", true);
114             try object.end();
115         },
116         .capture => |inner| {
117             const object = try json.object();
118             try object.writer.objectField("$capture");
119             try writeJson(alloc, object.writer, inner.*);
120             try object.end();
121         },
122         .bind => |binding| {
123             const object = try json.object();
124             try object.field("$bind", binding.name);
125             if (binding.pattern.* != .discard) {
126                 try object.writer.objectField("pattern");
127                 try writeJson(alloc, object.writer, binding.pattern.*);
128             }
129             try object.end();
130         },
131         .rest_pattern => |rest_pattern| {
132             const object = try json.object();
133             const rest = try object.object("$rest");
134             const prefix = try rest.array("prefix");
135             for (rest_pattern.prefix) |item| {
136                 try writeJson(alloc, prefix.writer, item);
137             }
138             try prefix.end();
139             try rest.writer.objectField("rest");
140             try writeJson(alloc, rest.writer, rest_pattern.rest.*);
141             try rest.end();
142             try object.end();
143         },
144         .boolean => |actual| try json.write(actual),
145         .signed_integer => |actual| try writeSignedInteger(json, actual),
146         .double => |actual| try writeDouble(json, actual),
147         .string => |actual| try json.write(actual),
148         .byte_string => |actual| try json.hexString(actual),
149         .symbol => |name| {
150             if (std.mem.eql(u8, name, "null")) {
151                 try json.write(null);
152             } else {
153                 try json.write(name);
154             }
155         },
156         .record => |record| try writeRecord(alloc, json, record),
157         .sequence => |items| try writeArray(alloc, json, items),
158         .set => |items| {
159             if (!Value.setElementsDistinct(items)) return error.DuplicateSetElement;
160             try writeArray(alloc, json, items);
161         },
162         .dictionary => |entries| {
163             if (!Value.dictionaryKeysDistinct(entries)) {
164                 return error.DuplicateDictionaryKey;
165             }
166             try writeDictionary(alloc, json, entries);
167         },
168         .embedded => |embedded| {
169             const object = try json.object();
170             var tmp: [24]u8 = undefined;
171             const pointer = std.fmt.bufPrint(
172                 &tmp,
173                 "{d}",
174                 .{@intFromPtr(embedded.value)},
175             ) catch unreachable;
176             try object.field("__embedded__", pointer);
177             try object.end();
178         },
179     }
180 }
181 
182 fn writeRecord(alloc: Allocator, json: *JsonWriter, record: Value.Record) WriteError!void {
183     const label_symbol: ?[]const u8 = switch (record.label.*) {
184         .symbol => |symbol| symbol,
185         else => null,
186     };
187 
188     if (label_symbol) |label_name| {
189         if (lookupProtocol(label_name)) |protocol| {
190             const object = try json.object();
191             try object.field("type", protocol.json_type);
192             for (record.fields, 0..) |field, index| {
193                 if (index < protocol.fields.len) {
194                     try object.writer.objectField(protocol.fields[index]);
195                 } else {
196                     var tmp: [16]u8 = undefined;
197                     const field_name = std.fmt.bufPrint(
198                         &tmp,
199                         "f{d}",
200                         .{index},
201                     ) catch unreachable;
202                     try object.writer.objectField(field_name);
203                 }
204                 try writeJson(alloc, object.writer, field);
205             }
206             try object.end();
207             return;
208         }
209         if (predicates_mod.recordAttributes(AnyEmbedded, .{ .record = record })) |attributes| {
210             const object = try json.object();
211             try object.field("type", label_name);
212             for (attributes) |entry| {
213                 try object.writer.objectField(entry.key.string);
214                 try writeJson(alloc, object.writer, entry.value);
215             }
216             try object.end();
217             return;
218         }
219     }
220 
221     const object = try json.object();
222     try object.writer.objectField("__record__");
223     try writeJson(alloc, object.writer, record.label.*);
224     const fields = try object.array("fields");
225     for (record.fields) |field| {
226         try writeJson(alloc, fields.writer, field);
227     }
228     try fields.end();
229     try object.end();
230 }
231 
232 fn writeArray(alloc: Allocator, json: *JsonWriter, items: []const Value) WriteError!void {
233     const array = try json.array();
234     for (items) |item| {
235         try writeJson(alloc, array.writer, item);
236     }
237     try array.end();
238 }
239 
240 fn writeDictionary(
241     alloc: Allocator,
242     json: *JsonWriter,
243     entries: []const Value.DictionaryEntry,
244 ) WriteError!void {
245     const object = try json.object();
246     for (entries) |entry| {
247         switch (entry.key) {
248             .string => |key| {
249                 try object.writer.objectField(key);
250                 try writeJson(alloc, object.writer, entry.value);
251             },
252             .symbol => |key| {
253                 try object.writer.objectField(key);
254                 try writeJson(alloc, object.writer, entry.value);
255             },
256             else => {
257                 const key = text_format.toText(alloc, entry.key) catch |err| switch (err) {
258                     error.OutOfMemory => return error.OutOfMemory,
259                     error.DuplicateSetElement => return error.DuplicateSetElement,
260                     error.DuplicateDictionaryKey => return error.DuplicateDictionaryKey,
261                 };
262                 defer alloc.free(key);
263                 try object.writer.objectField(key);
264                 try writeJson(alloc, object.writer, entry.value);
265             },
266         }
267     }
268     try object.end();
269 }
270 
271 fn writeSignedInteger(json: *JsonWriter, value: SignedInteger) std.Io.Writer.Error!void {
272     switch (value.repr) {
273         .i128 => |actual| try json.print("{d}", .{actual}),
274         .u128 => |actual| try json.print("{d}", .{actual}),
275         .big => try json.print("{d}", .{value.toI64Lossy()}),
276     }
277 }
278 
279 fn writeDouble(json: *JsonWriter, value: f64) std.Io.Writer.Error!void {
280     if (std.math.isNan(value) or std.math.isInf(value)) {
281         try json.write(null);
282         return;
283     }
284     try json.print("{d}", .{value});
285 }
286 
287 /// Parses `json_text` and returns the Preserves value it describes, allocated with `alloc`, for
288 /// code that receives JSON text. The value owns all its memory, and the caller frees it with
289 /// `deinit`. The mapping is the one `fromJsonValue` applies to the parsed JSON. Malformed JSON
290 /// returns `error.OutOfMemory`, the only error the call reports. If memory runs out inside a
291 /// `__record__`, `$rest`, `$capture`, `$bind` or `"type"` object, the call leaks the parts already
292 /// built.
293 pub fn fromJsonString(alloc: Allocator, json_text: []const u8) !Value {
294     const parsed = std.json.parseFromSlice(std.json.Value, alloc, json_text, .{}) catch return error.OutOfMemory;
295     defer parsed.deinit();
296     return jsonToValue(alloc, parsed.value);
297 }
298 
299 /// Returns the Preserves value that `json_value` describes, allocated with `alloc`, for code that
300 /// already holds a parsed `std.json.Value`, so the JSON is parsed once. The result copies every
301 /// string it keeps, so it owns all its memory and borrows nothing from `json_value`. JSON null
302 /// becomes the symbol `null`, an integer that fits 64 bits becomes an integer, any other finite
303 /// number becomes a double, and a number outside those ranges becomes the symbol `NaN`. Strings
304 /// stay strings, and arrays become sequences. The reserved member names are tried in order:
305 /// `__discard__`, `__record__`, `__embedded__`, `$rest`, `$capture`, `$bind`, then `"type"`. Any
306 /// other object becomes a dictionary with string keys. A `"type"` naming a protocol record becomes
307 /// that record, and each missing field becomes the discard pattern. Any other `"type"` becomes a
308 /// record labeled with that name and one dictionary field holding the other members. A `__record__`
309 /// string label becomes a symbol label. An `__embedded__` member becomes an embedded value that
310 /// owns the decoded member, and copying or freeing the outer value copies or frees it. Decoding the
311 /// output of `toJsonString` for an embedded value yields an embedded value that holds the pointer's
312 /// decimal string.
313 pub fn fromJsonValue(alloc: Allocator, json_value: std.json.Value) !Value {
314     return jsonToValue(alloc, json_value);
315 }
316 
317 fn jsonToValue(alloc: Allocator, jv: std.json.Value) Allocator.Error!Value {
318     return switch (jv) {
319         .null => Value{ .symbol = try alloc.dupe(u8, symbols_mod.SYM_NULL.name) },
320         .bool => |v| Value{ .boolean = v },
321         .integer => |v| Value{ .signed_integer = SignedInteger.fromI128(@as(i128, v)) },
322         .float => |v| Value{ .double = v },
323         .string => |s| blk: {
324             const dup = try alloc.dupe(u8, s);
325             break :blk Value{ .string = dup };
326         },
327         .array => |arr| blk: {
328             const items = try alloc.alloc(Value, arr.items.len);
329             var built: usize = 0;
330             errdefer {
331                 for (items[0..built]) |*item| item.deinit(alloc);
332                 alloc.free(items);
333             }
334             for (arr.items, 0..) |item, i| {
335                 items[i] = try jsonToValue(alloc, item);
336                 built = i + 1;
337             }
338             break :blk Value{ .sequence = items };
339         },
340         .object => |obj| try objectToValue(alloc, obj),
341         .number_string => Value{ .symbol = try alloc.dupe(u8, "NaN") },
342     };
343 }
344 
345 fn objectToValue(alloc: Allocator, obj: std.json.ObjectMap) Allocator.Error!Value {
346     if (obj.get("__discard__")) |_| {
347         return Value{ .discard = {} };
348     }
349     if (obj.get("__record__")) |label_json| {
350         const label_val = try jsonToValue(alloc, label_json);
351         const label = switch (label_val) {
352             .string => |s| Value{ .symbol = s },
353             else => label_val,
354         };
355         const fields_json = obj.get("fields") orelse {
356             return any_constructors.record(alloc, label, &.{}) catch |err| switch (err) {
357                 error.OutOfMemory => return error.OutOfMemory,
358             };
359         };
360         switch (fields_json) {
361             .array => |arr| {
362                 const fields = try alloc.alloc(Value, arr.items.len);
363                 defer alloc.free(fields);
364                 for (arr.items, 0..) |item, i| {
365                     fields[i] = try jsonToValue(alloc, item);
366                 }
367                 return any_constructors.record(alloc, label, fields) catch |err| switch (err) {
368                     error.OutOfMemory => return error.OutOfMemory,
369                 };
370             },
371             else => return any_constructors.record(alloc, label, &.{}) catch |err| switch (err) {
372                 error.OutOfMemory => return error.OutOfMemory,
373             },
374         }
375     }
376     if (obj.get("__embedded__")) |inner_json| {
377         var inner = try jsonToValue(alloc, inner_json);
378         errdefer inner.deinit(alloc);
379         const ptr = try alloc.create(Value);
380         ptr.* = inner;
381         return Value{ .embedded = AnyEmbedded{
382             .value = @ptrCast(ptr),
383             .semantic_ops = embedded_mod.parsedEmbeddedOps(Value),
384             .deinit_fn = embedded_mod.parsedEmbeddedDeinit(Value),
385             .clone_fn = embedded_mod.parsedEmbeddedClone(Value),
386         } };
387     }
388     if (obj.get("$rest")) |rest_json| {
389         switch (rest_json) {
390             .object => |rest_obj| {
391                 if (rest_obj.get("prefix")) |prefix_json| {
392                     if (rest_obj.get("rest")) |rest_val_json| {
393                         switch (prefix_json) {
394                             .array => |arr| {
395                                 const prefix = try alloc.alloc(Value, arr.items.len);
396                                 errdefer alloc.free(prefix);
397                                 for (arr.items, 0..) |item, i| {
398                                     prefix[i] = try jsonToValue(alloc, item);
399                                 }
400                                 const rest_val = try jsonToValue(alloc, rest_val_json);
401                                 const rest_ptr = try alloc.create(Value);
402                                 rest_ptr.* = rest_val;
403                                 return Value{ .rest_pattern = .{ .prefix = prefix, .rest = rest_ptr } };
404                             },
405                             else => {},
406                         }
407                     }
408                 }
409             },
410             else => {},
411         }
412     }
413     if (obj.get("$capture")) |cap_json| {
414         const inner = try jsonToValue(alloc, cap_json);
415         return any_constructors.capture(alloc, inner) catch |err| switch (err) {
416             error.OutOfMemory => return error.OutOfMemory,
417         };
418     }
419     if (obj.get("$bind")) |bind_json| {
420         switch (bind_json) {
421             .string => |name| {
422                 const name_dup = try alloc.dupe(u8, name);
423                 const pat: Value = if (obj.get("pattern")) |pj|
424                     try jsonToValue(alloc, pj)
425                 else
426                     Value{ .discard = {} };
427                 return any_constructors.bindVal(alloc, name_dup, pat) catch |err| switch (err) {
428                     error.OutOfMemory => return error.OutOfMemory,
429                 };
430             },
431             else => {},
432         }
433     }
434     if (obj.get("type")) |type_json| {
435         switch (type_json) {
436             .string => |type_name| {
437                 if (lookupProtocolByJsonType(type_name)) |proto| {
438                     const fields = try alloc.alloc(Value, proto.fields.len);
439                     defer alloc.free(fields);
440                     for (proto.fields, 0..) |fname, i| {
441                         if (obj.get(fname)) |fv| {
442                             fields[i] = try jsonToValue(alloc, fv);
443                         } else {
444                             fields[i] = Value{ .discard = {} };
445                         }
446                     }
447                     const label_name = try alloc.dupe(u8, proto.symbol_name);
448                     return any_constructors.record(alloc, Value{ .symbol = label_name }, fields) catch |err| switch (err) {
449                         error.OutOfMemory => return error.OutOfMemory,
450                     };
451                 }
452                 const attr_count = if (obj.count() > 0) obj.count() - 1 else 0;
453                 const entries = try alloc.alloc(Value.DictionaryEntry, attr_count);
454                 errdefer alloc.free(entries);
455                 var iter = obj.iterator();
456                 var i: usize = 0;
457                 while (iter.next()) |entry| {
458                     if (std.mem.eql(u8, entry.key_ptr.*, "type")) continue;
459                     const key_str = try alloc.dupe(u8, entry.key_ptr.*);
460                     entries[i] = .{
461                         .key = Value{ .string = key_str },
462                         .value = try jsonToValue(alloc, entry.value_ptr.*),
463                     };
464                     i += 1;
465                 }
466                 const tagged = Value{ .dictionary = entries };
467                 const label_name = try alloc.dupe(u8, type_name);
468                 const fields = [_]Value{tagged};
469                 return any_constructors.record(alloc, Value{ .symbol = label_name }, &fields) catch |err| switch (err) {
470                     error.OutOfMemory => return error.OutOfMemory,
471                 };
472             },
473             else => {},
474         }
475     }
476     const entries = try alloc.alloc(Value.DictionaryEntry, obj.count());
477     var built: usize = 0;
478     errdefer {
479         for (entries[0..built]) |*done| {
480             done.key.deinit(alloc);
481             done.value.deinit(alloc);
482         }
483         alloc.free(entries);
484     }
485     var iter = obj.iterator();
486     var i: usize = 0;
487     while (iter.next()) |entry| {
488         const key_str = try alloc.dupe(u8, entry.key_ptr.*);
489         errdefer alloc.free(key_str);
490         entries[i] = .{
491             .key = Value{ .string = key_str },
492             .value = try jsonToValue(alloc, entry.value_ptr.*),
493         };
494         i += 1;
495         built = i;
496     }
497     return Value{ .dictionary = entries };
498 }
499 
500 test "toJsonString: primitives" {
501     const alloc = std.testing.allocator;
502 
503     const b = try toJsonString(alloc, Value{ .boolean = true });
504     defer alloc.free(b);
505     try std.testing.expectEqualStrings("true", b);
506 
507     const i = try toJsonString(alloc, Value{ .signed_integer = SignedInteger.fromI128(-42) });
508     defer alloc.free(i);
509     try std.testing.expectEqualStrings("-42", i);
510 
511     const s = try toJsonString(alloc, Value{ .string = "hi" });
512     defer alloc.free(s);
513     try std.testing.expectEqualStrings("\"hi\"", s);
514 
515     const n = try toJsonString(alloc, Value{ .symbol = "null" });
516     defer alloc.free(n);
517     try std.testing.expectEqualStrings("null", n);
518 }
519 
520 test "toJsonString: NaN and Inf round to null" {
521     const alloc = std.testing.allocator;
522     const nan = try toJsonString(alloc, Value{ .double = std.math.nan(f64) });
523     defer alloc.free(nan);
524     try std.testing.expectEqualStrings("null", nan);
525 
526     const inf = try toJsonString(alloc, Value{ .double = std.math.inf(f64) });
527     defer alloc.free(inf);
528     try std.testing.expectEqualStrings("null", inf);
529 }
530 
531 test "toJsonString: byte_string as hex-packed string" {
532     const alloc = std.testing.allocator;
533     const bytes = [_]u8{ 0xde, 0xad, 0xbe, 0xef };
534     const j = try toJsonString(alloc, Value{ .byte_string = &bytes });
535     defer alloc.free(j);
536     try std.testing.expectEqualStrings("\"deadbeef\"", j);
537 }
538 
539 test "toJsonString: nested dictionaries preserve wide values and escaping" {
540     const alloc = std.testing.allocator;
541     const bytes = [_]u8{ 0x00, 0xff };
542     var sequence = [_]Value{
543         .{ .byte_string = &bytes },
544         .{ .signed_integer = SignedInteger.fromU128(std.math.maxInt(u128)) },
545         .{ .double = 1.5 },
546     };
547     var set = [_]Value{
548         .{ .boolean = false },
549         .{ .boolean = true },
550     };
551     var entries = [_]Value.DictionaryEntry{
552         .{
553             .key = .{ .string = "nested\"\n\x01" },
554             .value = .{ .sequence = &sequence },
555         },
556         .{
557             .key = Value.initI128(7),
558             .value = .{ .set = &set },
559         },
560     };
561 
562     const rendered = try toJsonString(alloc, Value.initDictionary(&entries));
563     defer alloc.free(rendered);
564     try std.testing.expectEqualStrings(
565         "{\"nested\\\"\\n\\u0001\":[\"00ff\",340282366920938463463374607431768211455,1.5],\"7\":[false,true]}",
566         rendered,
567     );
568 }
569 
570 test "toJsonString: __discard__ envelope" {
571     const alloc = std.testing.allocator;
572     const j = try toJsonString(alloc, Value{ .discard = {} });
573     defer alloc.free(j);
574     try std.testing.expectEqualStrings("{\"__discard__\":true}", j);
575 }
576 
577 test "toJsonString: Observe record uses protocol mapping" {
578     const alloc = std.testing.allocator;
579     var arena = std.heap.ArenaAllocator.init(alloc);
580     defer arena.deinit();
581     const a = arena.allocator();
582 
583     const label = Value{ .symbol = "Observe" };
584     const fields = [_]Value{
585         .{ .discard = {} },
586         .{ .boolean = true },
587         .{ .string = "extra" },
588     };
589     const rec = try any_constructors.record(a, label, &fields);
590     const j = try toJsonString(alloc, rec);
591     defer alloc.free(j);
592     try std.testing.expectEqualStrings(
593         "{\"type\":\"observe\",\"pattern\":{\"__discard__\":true},\"observer\":true,\"f2\":\"extra\"}",
594         j,
595     );
596 }
597 
598 test "toJsonString: embedded values use decimal pointer strings" {
599     const alloc = std.testing.allocator;
600     var marker: u8 = 0;
601     const rendered = try toJsonString(alloc, .{
602         .embedded = .{ .value = &marker },
603     });
604     defer alloc.free(rendered);
605 
606     const parsed = try std.json.parseFromSlice(std.json.Value, alloc, rendered, .{});
607     defer parsed.deinit();
608     const pointer = parsed.value.object.get("__embedded__").?.string;
609     var expected_buffer: [24]u8 = undefined;
610     const expected = try std.fmt.bufPrint(
611         &expected_buffer,
612         "{d}",
613         .{@intFromPtr(&marker)},
614     );
615     try std.testing.expectEqualStrings(expected, pointer);
616 }
617 
618 test "toJsonString: unknown symbol-label record uses __record__ envelope" {
619     const alloc = std.testing.allocator;
620     var arena = std.heap.ArenaAllocator.init(alloc);
621     defer arena.deinit();
622     const a = arena.allocator();
623 
624     const label = Value{ .symbol = "Weird" };
625     const fields = [_]Value{ Value{ .signed_integer = SignedInteger.fromI128(1) }, Value{ .signed_integer = SignedInteger.fromI128(2) } };
626     const rec = try any_constructors.record(a, label, &fields);
627     const j = try toJsonString(alloc, rec);
628     defer alloc.free(j);
629     try std.testing.expectEqualStrings(
630         "{\"__record__\":\"Weird\",\"fields\":[1,2]}",
631         j,
632     );
633 }
634 
635 test "fromJsonString: primitives" {
636     const alloc = std.testing.allocator;
637 
638     var b = try fromJsonString(alloc, "true");
639     defer b.deinit(alloc);
640     try std.testing.expect(b == .boolean and b.boolean == true);
641 
642     var n = try fromJsonString(alloc, "null");
643     defer n.deinit(alloc);
644     try std.testing.expect(n == .symbol and std.mem.eql(u8, n.symbol, "null"));
645 
646     var i = try fromJsonString(alloc, "42");
647     defer i.deinit(alloc);
648     try std.testing.expect(i == .signed_integer);
649     try std.testing.expectEqual(@as(i128, 42), try i.signed_integer.toI128());
650 
651     var s = try fromJsonString(alloc, "\"hello\"");
652     defer s.deinit(alloc);
653     try std.testing.expect(s == .string);
654     try std.testing.expectEqualStrings("hello", s.string);
655 }
656 
657 test "fromJsonString: __discard__ round-trip" {
658     const alloc = std.testing.allocator;
659     var v = try fromJsonString(alloc, "{\"__discard__\":true}");
660     defer v.deinit(alloc);
661     try std.testing.expect(v == .discard);
662 }
663 
664 test "fromJsonString: protocol type lookup" {
665     const alloc = std.testing.allocator;
666     var v = try fromJsonString(alloc, "{\"type\":\"observe\",\"pattern\":{\"__discard__\":true},\"observer\":true}");
667     defer v.deinit(alloc);
668     try std.testing.expect(v == .record);
669     try std.testing.expect(v.record.label.* == .symbol);
670     try std.testing.expectEqualStrings("Observe", v.record.label.*.symbol);
671     try std.testing.expectEqual(@as(usize, 2), v.record.fields.len);
672     try std.testing.expect(v.record.fields[0] == .discard);
673     try std.testing.expect(v.record.fields[1] == .boolean);
674     try std.testing.expectEqual(true, v.record.fields[1].boolean);
675 }
676 
677 test "fromJsonString: unknown type becomes tagged dict record" {
678     const alloc = std.testing.allocator;
679     var v = try fromJsonString(alloc, "{\"type\":\"custom\",\"k\":1}");
680     defer v.deinit(alloc);
681     try std.testing.expect(v == .record);
682     try std.testing.expectEqualStrings("custom", v.record.label.*.symbol);
683     try std.testing.expectEqual(@as(usize, 1), v.record.fields.len);
684     try std.testing.expect(v.record.fields[0] == .dictionary);
685     try std.testing.expectEqual(@as(usize, 1), v.record.fields[0].dictionary.len);
686     try std.testing.expectEqualStrings("k", v.record.fields[0].dictionary[0].key.string);
687 }
688 
689 test "fromJsonString: array maps to sequence" {
690     const alloc = std.testing.allocator;
691     var v = try fromJsonString(alloc, "[1,2,3]");
692     defer v.deinit(alloc);
693     try std.testing.expect(v == .sequence);
694     try std.testing.expectEqual(@as(usize, 3), v.sequence.len);
695 }
696 
697 test "fromJsonString: plain object becomes dictionary" {
698     const alloc = std.testing.allocator;
699     var v = try fromJsonString(alloc, "{\"a\":1,\"b\":2}");
700     defer v.deinit(alloc);
701     try std.testing.expect(v == .dictionary);
702     try std.testing.expectEqual(@as(usize, 2), v.dictionary.len);
703 }
704 
705 test "fromJsonString: __record__ round-trip" {
706     const alloc = std.testing.allocator;
707     var v = try fromJsonString(alloc, "{\"__record__\":\"Weird\",\"fields\":[1,2]}");
708     defer v.deinit(alloc);
709     try std.testing.expect(v == .record);
710     try std.testing.expectEqualStrings("Weird", v.record.label.*.symbol);
711     try std.testing.expectEqual(@as(usize, 2), v.record.fields.len);
712 }
713 
714 test "toJsonString + fromJsonString: sequence round-trip" {
715     const alloc = std.testing.allocator;
716     var arena = std.heap.ArenaAllocator.init(alloc);
717     defer arena.deinit();
718     const a = arena.allocator();
719 
720     const items = try a.alloc(Value, 3);
721     items[0] = Value{ .boolean = true };
722     items[1] = Value{ .signed_integer = SignedInteger.fromI128(99) };
723     items[2] = Value{ .string = "x" };
724     const seq = Value{ .sequence = items };
725 
726     const text = try toJsonString(alloc, seq);
727     defer alloc.free(text);
728     try std.testing.expectEqualStrings("[true,99,\"x\"]", text);
729 
730     var parsed = try fromJsonString(alloc, text);
731     defer parsed.deinit(alloc);
732     try std.testing.expect(parsed == .sequence);
733     try std.testing.expect(parsed.sequence[0].boolean == true);
734     try std.testing.expectEqual(@as(i128, 99), try parsed.sequence[1].signed_integer.toI128());
735     try std.testing.expectEqualStrings("x", parsed.sequence[2].string);
736 }
737 
738 test "toJsonString rejects duplicate set elements and dictionary keys" {
739     const alloc = std.testing.allocator;
740     var set_items = [_]Value{ Value.initI128(1), Value.initI128(1) };
741     var entries = [_]Value.DictionaryEntry{
742         .{ .key = Value.initI128(1), .value = Value.initBoolean(true) },
743         .{ .key = Value.initI128(1), .value = Value.initBoolean(false) },
744     };
745 
746     try std.testing.expectError(
747         error.DuplicateSetElement,
748         toJsonString(alloc, Value.initSet(&set_items)),
749     );
750     try std.testing.expectError(
751         error.DuplicateDictionaryKey,
752         toJsonString(alloc, Value.initDictionary(&entries)),
753     );
754 }
755 
756 fn checkJsonEncodingAllocationFailures(allocator: Allocator) !void {
757     var values = [_]Value{
758         .{ .string = "owned\"\n" },
759         .{ .byte_string = "bytes" },
760     };
761     var entries = [_]Value.DictionaryEntry{
762         .{
763             .key = Value.initI128(7),
764             .value = .{ .sequence = &values },
765         },
766     };
767     const rendered = try toJsonString(
768         allocator,
769         Value.initDictionary(&entries),
770     );
771     defer allocator.free(rendered);
772 }
773 
774 test "toJsonString releases every allocation failure path" {
775     try std.testing.checkAllAllocationFailures(
776         std.testing.allocator,
777         checkJsonEncodingAllocationFailures,
778         .{},
779     );
780 }
781 
782 test "fromJsonValue handles pre-parsed std.json.Value" {
783     const alloc = std.testing.allocator;
784     const parsed = try std.json.parseFromSlice(std.json.Value, alloc, "[true,false]", .{});
785     defer parsed.deinit();
786     var v = try fromJsonValue(alloc, parsed.value);
787     defer v.deinit(alloc);
788     try std.testing.expect(v == .sequence);
789     try std.testing.expectEqual(@as(usize, 2), v.sequence.len);
790     try std.testing.expectEqual(true, v.sequence[0].boolean);
791     try std.testing.expectEqual(false, v.sequence[1].boolean);
792 }
793 
794 test "fromJsonString: __embedded__ payloads are owned and cloneable" {
795     const alloc = std.testing.allocator;
796 
797     var v = try fromJsonString(alloc, "{\"__embedded__\":{\"name\":\"demo\"}}");
798     defer v.deinit(alloc);
799 
800     try std.testing.expect(v == .embedded);
801     try std.testing.expect(v.embedded.semantic_ops == embedded_mod.parsedEmbeddedOps(Value));
802     try std.testing.expect(v.embedded.deinit_fn != null);
803     try std.testing.expect(v.embedded.clone_fn != null);
804 
805     var cloned = try v.embedded.clone(alloc);
806     defer cloned.deinit(alloc);
807 
808     try std.testing.expect(v.embedded.value != cloned.value);
809     const cloned_payload: *const Value = @ptrCast(@alignCast(cloned.value));
810     try std.testing.expect(cloned_payload.* == .dictionary);
811     try std.testing.expectEqual(@as(usize, 1), cloned_payload.dictionary.len);
812     try std.testing.expectEqualStrings("name", cloned_payload.dictionary[0].key.string);
813     try std.testing.expectEqualStrings("demo", cloned_payload.dictionary[0].value.string);
814 }
815 
816 fn checkEmbeddedJsonAllocationFailures(allocator: Allocator) !void {
817     var value = try fromJsonString(
818         allocator,
819         "{\"__embedded__\":{\"name\":\"demo\"}}",
820     );
821     defer value.deinit(allocator);
822 }
823 
824 test "fromJsonString embedded payload releases every allocation failure path" {
825     try std.testing.checkAllAllocationFailures(
826         std.testing.allocator,
827         checkEmbeddedJsonAllocationFailures,
828         .{},
829     );
830 }
831 
832 test "$capture / $bind / $rest round-trip through JSON" {
833     const alloc = std.testing.allocator;
834     var arena = std.heap.ArenaAllocator.init(alloc);
835     defer arena.deinit();
836     const a = arena.allocator();
837 
838     const cap = try any_constructors.capture(a, Value{ .discard = {} });
839     const cap_json = try toJsonString(alloc, cap);
840     defer alloc.free(cap_json);
841     try std.testing.expectEqualStrings("{\"$capture\":{\"__discard__\":true}}", cap_json);
842 
843     const bnd = try any_constructors.bindVal(a, "x", Value{ .discard = {} });
844     const bnd_json = try toJsonString(alloc, bnd);
845     defer alloc.free(bnd_json);
846     try std.testing.expectEqualStrings("{\"$bind\":\"x\"}", bnd_json);
847 
848     const rest = try any_constructors.restPattern(
849         a,
850         &.{ Value.initI128(1), .{ .string = "tail" } },
851         .{ .discard = {} },
852     );
853     const rest_json = try toJsonString(alloc, rest);
854     defer alloc.free(rest_json);
855     try std.testing.expectEqualStrings(
856         "{\"$rest\":{\"prefix\":[1,\"tail\"],\"rest\":{\"__discard__\":true}}}",
857         rest_json,
858     );
859 
860     var parsed_cap = try fromJsonString(alloc, "{\"$capture\":{\"__discard__\":true}}");
861     defer parsed_cap.deinit(alloc);
862     try std.testing.expect(parsed_cap == .capture);
863     try std.testing.expect(parsed_cap.capture.* == .discard);
864 
865     var parsed_bnd = try fromJsonString(alloc, "{\"$bind\":\"y\"}");
866     defer parsed_bnd.deinit(alloc);
867     try std.testing.expect(parsed_bnd == .bind);
868     try std.testing.expectEqualStrings("y", parsed_bnd.bind.name);
869     try std.testing.expect(parsed_bnd.bind.pattern.* == .discard);
870 
871     var parsed_rest = try fromJsonString(alloc, rest_json);
872     defer parsed_rest.deinit(alloc);
873     try std.testing.expect(parsed_rest == .rest_pattern);
874     try std.testing.expectEqual(@as(usize, 2), parsed_rest.rest_pattern.prefix.len);
875     try std.testing.expect(parsed_rest.rest_pattern.rest.* == .discard);
876 }