Wire
The opt-in serialization layer.
Serialization is deliberately out of the always-on prelude: a program that never persists a value imports none of this. Everything here is pure total functions over Bytes; writing those bytes to a file, a socket, or a replay log is a capability routed through the effect system, never performed here.
The surface has three layers. The Serialize class and its instances are the codec: a value’s compact positional body, derived structurally by deriving (Serialize) and bottoming out in the primitive instances below. The envelope (wire_encode_stable/wire_decode_stable, or the explicit-digest *_with_digest escape hatches) wraps a body in the frame [scheme tag][kind][contract digest][body?], checked before the body so a stale layout is rejected up front. The version ladder (compose_upgrade/ compose_downgrade) spans frozen versions with O(n) adjacent converters.
The frame
Every serialized thing is one envelope, read left to right, each header part checked before the next is touched:
+------------+------+-----------------+ - - - - - - +
| scheme tag | kind | contract digest | body? |
+------------+------+-----------------+ - - - - - - +
scheme tag length-prefixed string, always "prism-core-hash-v1";
a foreign scheme is rejected before anything else
kind uvarint naming what the digest identifies:
value 0, def 1, protocol 2, kont 3, cert 4
contract digest length-prefixed hex shape digest of the expected
layout, checked before the body is decoded
body? the compact positional encoding below; an absent
body is a pure reference, named by its digest
body encoding Int and I64 as zigzag LEB128 varints, U64 and
constructor tags as plain LEB128, strings and
lists length-prefixed with the length bounded on
both encode and decode, a sum as its constructor
tag followed by its arguments, a product as its
fields in declaration order, no padding
The Bytes representation
Bytes is an unboxed byte buffer (Buf, runtime/prism_buffer.c) plus a read cursor: Bytes(buf, off) is the bytes buf[off ..]. The buffer holds raw u8 with no UTF-8 interpretation, so it threads byte-for-byte identically on both backends (the parity contract a String-of-bytes would break, since reconstructing a String from raw bytes repairs invalid UTF-8 lossily). The cursor makes decode cheap: peeling a byte off the front advances the offset in O(1) with no copy, so a whole decode is linear. Encode accumulates into a growable buffer builder (buf_push, amortized O(1)), so building a container or a string body stays linear rather than paying the quadratic cost a right-nested immutable wire_cat would incur. The concrete representation is owned by the codec; a derived instance only ever threads a Bytes through the builders below, never inspects it. bytes_of_list/bytes_to_list bridge to the older List(Int) view for callers that still want it.
Totality
decode is total. Every read consumes at least one byte from a finite, strictly shrinking list, so decode always terminates; a truncated or hostile input runs out of bytes and fails through Fail rather than looping or panicking. Varints are length-capped and container and string lengths are bounded on both encode and decode, so a hostile length prefix cannot force unbounded work. The frame’s total decoder checks the scheme, kind, and digest before touching the body and rejects trailing bytes after the value.
Deriving
deriving (Serialize) writes the codec: a product encodes its fields in declaration order, a sum prefixes a constructor tag, and each field defers to its own instance. deriving (Stable) succeeds only when every component is itself Stable, so the derivability is the proof that a frozen format contains no unserializable value.
Types
Bytes
type Bytes = Bytes(Buf, Int)
A compact, positional byte body: an unboxed byte buffer and a read cursor, Bytes(buf, off) denoting the bytes buf[off ..]. The concrete representation is owned by the codec; a derived instance only ever threads a Bytes through the builders below, never inspects it.
Loss
type Loss = Loss(List(String))
What a downgrade could not carry down: the names of the fields dropped when lowering a value to an older frozen version. A downgrade never silently discards, it reports every field it had to drop.
Policy
type Policy = Reject | LargestSafeSubset
How to reconcile a higher-version value that a reader cannot represent at its own version: Reject refuses it, LargestSafeSubset downgrades it and keeps the Loss. A version mismatch always has one of these defined outcomes, never undefined behavior.
Type Classes
Serialize
class Serialize(a)
encode : (a) -> Bytes
decode : (Bytes) -> (a, Bytes) ! {Fail | e}
The codec, derived structurally by deriving (Serialize). encode writes a value’s compact positional body; decode reads one back, returning the value alongside the bytes that follow it so a reader can thread a frame’s fields in declaration order. Decode is total: it fails through Fail on a truncated or malformed body rather than panicking, so hostile input is one ordinary failure channel.
Stable
class Stable(a)
shape_digest_of : (a) -> String
The structural witness that a type’s format is frozen-serializable: a record is Stable when all its fields are, a sum when all its variants’ arguments are. The derivability is the whole content of the proof: persisting a type with a non-stable component (a function, an effectful thunk) fails at the deriving (Stable) site, at compile time, naming the offending field.
The one method, shape_digest_of, is the type’s contract digest: the shape digest of its frozen layout. A derived instance’s body is that digest injected by the compiler from its single shape-digest computation, so shape_digest_of is a per-type constant no source can forge (Stable is derive-only; a hand-written instance is rejected). The argument is used only to resolve the instance by value; the digest does not depend on it. wire_encode_stable/wire_decode_stable read it, so ordinary code never hand-threads a digest string into the envelope.
Instances
serializeInt
instance serializeInt : Serialize(Int)
serializeI64
instance serializeI64 : Serialize(I64)
serializeU64
instance serializeU64 : Serialize(U64)
serializeBool
instance serializeBool : Serialize(Bool)
serializeUnit
instance serializeUnit : Serialize(Unit)
serializeChar
instance serializeChar : Serialize(Char)
serializeString
instance serializeString : Serialize(String)
serializeList
instance serializeList : Serialize(List(a))
serializeBytes
instance serializeBytes : Serialize(Bytes)
serializeOption
instance serializeOption : Serialize(Option(a))
serializePair
instance serializePair : Serialize((a, b))
serializeTriple
instance serializeTriple : Serialize((a, b, c))
serializeMap
instance serializeMap : Serialize(Map(k, v, ord))
Functions and Values
bytes_of_buf
bytes_of_buf : (Buf) -> Wire.Bytes
bytes_buf
bytes_buf : (Wire.Bytes) -> Buf
bytes_at
bytes_at : (Wire.Bytes, Int) -> Int
wire_empty
wire_empty : Wire.Bytes
wire_cat
wire_cat : (Wire.Bytes, Wire.Bytes) -> Wire.Bytes
Concatenate two byte bodies into a fresh buffer.
wire_is_empty
wire_is_empty : (Wire.Bytes) -> Bool
True when a body has no bytes left. The reader uses it to reject trailing bytes after a value, and a peer uses it to tell a reference frame (no body) from an inline one.
wire_len
wire_len : (Wire.Bytes) -> Int
The number of bytes in a body.
bytes_of_list
bytes_of_list : (List(Int)) -> Wire.Bytes
Build a body from a List(Int) of byte values (each masked into 0..255), and read a body back out as that list. These bridge the buffer representation to the older list view for callers that thread bytes as ordinary data.
bytes_to_list
bytes_to_list : (Wire.Bytes) -> List(Int)
wire_tag
wire_tag : (Int) -> Wire.Bytes
Encode a constructor tag (a small non-negative integer) as a varint. The derived Serialize for a sum prefixes its body with this.
wire_get_tag
wire_get_tag : (Wire.Bytes) -> (Int, Wire.Bytes) ! {Fail}
Peel a constructor tag off the front of a body, returning it and the rest. The derived Serialize for a sum reads this before dispatching on the tag.
wire_scheme_tag
wire_scheme_tag : String
wire_kind_value
wire_kind_value : Int
wire_kind_def
wire_kind_def : Int
wire_kind_protocol
wire_kind_protocol : Int
wire_kind_kont
wire_kind_kont : Int
wire_kind_cert
wire_kind_cert : Int
wire_frame
wire_frame : (Int, String, Wire.Bytes) -> Wire.Bytes
Build a frame around a body. wire_ref builds the bodyless reference form.
wire_ref
wire_ref : (Int, String) -> Wire.Bytes
A pure reference: a frame that carries its contract digest and no body. Its identity is the digest; a peer resolves the body from its store or requests it.
wire_open
wire_open : (Wire.Bytes, Int, String) -> Wire.Bytes ! {Fail}
Open a frame, checking the scheme, kind, and digest before the body and returning the body bytes that follow. Any header mismatch fails through Fail, so a stale layout is rejected on a cheap comparison, never by a corrupt field.
wire_is_reference
wire_is_reference : (Wire.Bytes, Int, String) -> Bool ! {Fail}
True when a well-formed frame for kind/digest carries no body (a pure reference). Fails if the header itself does not match.
wire_open_value_any
wire_open_value_any : (Wire.Bytes) -> (String, Wire.Bytes) ! {Fail}
Open a value-kind frame without knowing its contract digest up front, returning that digest alongside the body bytes. It checks the scheme and the value kind before the digest. An empty body is legal and returned as-is: a record with no fields encodes to zero bytes, so emptiness cannot be read as “reference” here without an explicit marker in the frame. Trailing-byte discipline is left to the body decoder, exactly as wire_open leaves it. A version-dispatched reader uses the returned digest to pick which frozen rung the body decodes as.
wire_encode_value_with_digest
wire_encode_value_with_digest : forall a. (String, a) -> Wire.Bytes
Encode a value as a value-kind frame carrying an explicitly supplied contract digest. This is the escape hatch: it trusts the caller’s digest verbatim, so it is for code that already holds a compiler-computed digest (a stable block’s generated frame helpers) or is exercising a hand-built frame in a test. Ordinary Stable code uses wire_encode_stable, which supplies the digest from the type.
wire_decode_value_with_digest
wire_decode_value_with_digest : forall a. (Wire.Bytes, String) -> a ! {Fail}
Decode a value-kind frame against an explicitly supplied contract digest: check the header, decode the body, and reject trailing bytes. A bodyless reference frame fails here, because a value cannot be materialized without its bytes. The digest-supplying counterpart to wire_encode_value_with_digest; ordinary Stable code uses wire_decode_stable.
wire_encode_stable
wire_encode_stable : forall a. (a) -> Wire.Bytes
Encode a Stable value as a value frame under its own contract digest. The digest is shape_digest_of, a per-type constant the deriving (Stable) instance carries, so no digest string is hand-threaded. This is the ordinary encode for a frozen-serializable value.
wire_decode_stable
wire_decode_stable : forall a. (Wire.Bytes) -> a ! {Fail}
Decode a Stable value from a value frame, checking the frame’s digest against the type’s own shape_digest_of and rejecting trailing bytes. The result type comes from the use site; an unannotated call is ambiguous and asks for an annotation. A wrong digest, a wrong kind, a truncated body, and trailing bytes are all decode failures through Fail. The frame is opened without assuming its digest, the body decoded as the annotated type, and only then is the frame’s digest required to equal that type’s own, so a frame minted for another shape is refused.
no_loss
no_loss : Wire.Loss
The empty Loss: a downgrade that dropped nothing.
dropped
dropped : (List(String)) -> Wire.Loss
A Loss naming the fields a downgrade dropped.
loss_names
loss_names : (Wire.Loss) -> List(String)
The field names inside a Loss.
lossless
lossless : (Wire.Loss) -> Bool
True when a Loss dropped nothing, so the downgrade was lossless. The safe subset of a version step is exactly the values whose downgrade is lossless.
loss_union
loss_union : (Wire.Loss, Wire.Loss) -> Wire.Loss
Merge two losses along a composed downgrade.
compose_upgrade
compose_upgrade : forall a b c. ((a) -> b, (b) -> c) -> (a) -> c
Compose two adjacent upgrades into one spanning upgrade.
compose_downgrade
compose_downgrade : forall a b c. ((a) -> (b, Wire.Loss), (b) -> (c, Wire.Loss)) -> (a) -> (c, Wire.Loss)
Compose two adjacent downgrades into one spanning downgrade, unioning the losses each step reported.
reconcile
reconcile : forall a b. (Wire.Policy, (a) -> (b, Wire.Loss), a) -> (b, Wire.Loss) ! {Fail}
Apply a mismatch policy to a downgrade. Reject fails through Fail; LargestSafeSubset runs the downgrade and returns the lowered value with the fields it had to drop.