26. Serialization and Pretty Printing
Values print, compare, hash, and serialize as graphs: shared substructure is respected, and cycles — built through Ref assignment or in-place container mutation — are handled everywhere instead of recursing forever (see also §17).
26.1 Pretty Printing
prettyString(x) renders a value against a target line width (default 80; pass a second Int argument to change it), and prettyPrint(x) prints the result. Each container stays on one line when it fits and otherwise breaks one element per line with two-space indentation. At a very large width the output equals toString. The REPL and notebook cells display results this way automatically.
let v = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
v prettyPrint; -- fits: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
v prettyPrint(20); -- narrower target width:
-- [
-- [1, 2, 3],
-- [4, 5, 6],
-- [7, 8, 9]
-- ]
struct Point { x Float, y Float }
Point { x: 1.5, y: 2.5 } prettyPrint(12);
-- Point {
-- x: 1.5,
-- y: 2.5
-- }
Cyclic values print with ^n^ markers (“the same object n container levels up”) in both the flat and pretty printers — printing always terminates.
26.2 serialize and deserialize<T>
serialize(x) encodes any data value into a compact binary Bytes buffer; deserialize<T>(b) decodes it, with the target type as an explicit type argument. All the data types serialize — scalars, strings, Bytes, arrays, lists, maps, sets, refs, tuples, structs, enums (including recursive ones), and persistent collections. Types containing functions, coroutines, futures, actors, Any, or existentials are rejected at compile time.
struct Patch { name String, params [Float] }
let p = Patch { name: "lead", params: [440.0, 0.3] };
let b = serialize(p); -- Bytes
let q = deserialize<Patch>(b);
println(p == q); -- true
Serialization is graph-preserving: shared substructure is written once and restored as sharing; cycles are stored as references and round-trip intact. And it is deterministic: equal values produce identical bytes no matter how they were built (map and set entries are written in a canonical order), so hash(serialize(x)) is a stable content address.
-- Aliases stay aliases across a round trip
let shared = &10;
let out = deserialize<[Ref<Int>]>(serialize([shared, shared]));
out[0] <- 99;
*(out[1]) println; -- 99 (still one Ref)
-- Cycles round-trip and stay cyclic
enum Tree { node [Tree], leaf Int }
var a = [Tree.leaf(1)];
a push!(Tree.node(a));
let c = deserialize<[Tree]>(serialize(a));
println(c == a); -- true
-- Deterministic bytes: construction order doesn't matter
var m1 = ["a": 1]; m1["b"] = 2;
var m2 = ["b": 2]; m2["a"] = 1;
println(serialize(m1) == serialize(m2)); -- true
26.3 Type Safety and Errors
The buffer embeds a structural signature of the type it was serialized as — field and case names and the full recursive layout. deserialize<T> validates it against T by exact comparison, so decoding into the wrong type, or feeding truncated/corrupt bytes, raises a clean runtime error rather than producing garbage. Compatibility is structural: a structurally identical declaration (same shape and the same spelled names) in another program round-trips; renaming a field or case intentionally breaks it. Declared sizes are validated against the buffer length, so hostile input cannot cause runaway allocation.
let b = serialize(42);
-- deserialize<Float>(b); -- runtime error: type signature mismatch
Explicit type arguments
deserialize<T>(b) uses the explicit call-site type argument form f<T>(…). It is available on built-ins that need a type the arguments can't supply — typeName<T>() String is another (typeName<[Int]>() → "[Int]"). Ordinary template functions infer their type parameters from arguments as usual (§20).