// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// A value that knows how to become bytes, and how to come back from them.
///
/// The contract is one line: **`write_data` and `read_data` are everything after
/// the type ID.** The type ID belongs to whoever knows the schema — a struct
/// field to its `FieldMeta`, a list to its header, a map to its chunk header —
/// and what follows belongs to the value, which is why a struct or an enum
/// writes its user type ID here and not there.
///
/// Everything this package ships implements this trait, and a caller's own type
/// implements it the same way: there is no second path for the types that come
/// with the library.
pub trait Codec {
/// The type ID: the number that begins a value of this type.
fn ty(Self) -> TypeId
/// Everything after the type ID.
fn write_data(Self, Writer) -> Unit raise
/// Everything after the type ID, read back. `ty` is what the wire said, which
/// is not always what the writer would have chosen: a peer's `1` may come as
/// `VARINT64`, and an integer asked for as thirty-two bits may arrive as
/// sixty-four.
fn read_data(Reader, ty : TypeId) -> Self raise
}
///|
pub impl Codec for Bool with fn ty(_self) -> TypeId {
Bool
}
///|
pub impl Codec for Bool with fn write_data(self, w : Writer) -> Unit raise {
w.write_bool(self)
}
///|
pub impl Codec for Bool with fn read_data(r : Reader, ty : TypeId) -> Bool raise {
if ty != Bool {
raise Refused::WantType(
at=r.offset(),
want=TypeId::Bool.code(),
got=ty.code(),
)
}
r.read_bool()
}
///|
pub impl Codec for Int32 with fn ty(_self) -> TypeId {
Int32
}
///|
pub impl Codec for Int32 with fn write_data(self, w : Writer) -> Unit raise {
w.write_i32(self)
}
///|
pub impl Codec for Int32 with fn read_data(r : Reader, ty : TypeId) -> Int32 raise {
narrow(r, signed(r, ty))
}
///|
pub impl Codec for Int64 with fn ty(_self) -> TypeId {
Int64
}
///|
pub impl Codec for Int64 with fn write_data(self, w : Writer) -> Unit raise {
w.write_i64(self)
}
///|
pub impl Codec for Int64 with fn read_data(r : Reader, ty : TypeId) -> Int64 raise {
signed(r, ty)
}
///|
pub impl Codec for UInt32 with fn ty(_self) -> TypeId {
UInt32
}
///|
pub impl Codec for UInt32 with fn write_data(self, w : Writer) -> Unit raise {
w.write_u32(self)
}
///|
pub impl Codec for UInt32 with fn read_data(r : Reader, ty : TypeId) -> UInt32 raise {
let v = unsigned(r, ty)
if v > 4_294_967_295UL {
raise Refused::TooLarge(
at=r.offset(),
limit=4_294_967_295L,
got=v.reinterpret_as_int64(),
)
}
v.to_uint()
}
///|
pub impl Codec for UInt64 with fn ty(_self) -> TypeId {
UInt64
}
///|
pub impl Codec for UInt64 with fn write_data(self, w : Writer) -> Unit raise {
w.write_u64(self)
}
///|
pub impl Codec for UInt64 with fn read_data(r : Reader, ty : TypeId) -> UInt64 raise {
unsigned(r, ty)
}
///|
pub impl Codec for Byte with fn ty(_self) -> TypeId {
UInt8
}
///|
pub impl Codec for Byte with fn write_data(self, w : Writer) -> Unit raise {
w.write_u8(self.to_int())
}
///|
pub impl Codec for Byte with fn read_data(r : Reader, ty : TypeId) -> Byte raise {
let v = unsigned(r, ty)
if v > 255UL {
raise Refused::TooLarge(
at=r.offset(),
limit=255L,
got=v.reinterpret_as_int64(),
)
}
v.to_int().to_byte()
}
///|
pub impl Codec for Float with fn ty(_self) -> TypeId {
Float32
}
///|
pub impl Codec for Float with fn write_data(self, w : Writer) -> Unit raise {
w.write_f32(self)
}
///|
pub impl Codec for Float with fn read_data(r : Reader, ty : TypeId) -> Float raise {
Float::from_double(numbers(r, ty))
}
///|
pub impl Codec for Double with fn ty(_self) -> TypeId {
Float64
}
///|
pub impl Codec for Double with fn write_data(self, w : Writer) -> Unit raise {
w.write_f64(self)
}
///|
pub impl Codec for Double with fn read_data(r : Reader, ty : TypeId) -> Double raise {
numbers(r, ty)
}
///|
pub impl Codec for String with fn ty(_self) -> TypeId {
String
}
///|
pub impl Codec for String with fn write_data(self, w : Writer) -> Unit raise {
w.write_string(self)
}
///|
pub impl Codec for String with fn read_data(r : Reader, ty : TypeId) -> String raise {
if ty != String {
raise Refused::WantType(
at=r.offset(),
want=TypeId::String.code(),
got=ty.code(),
)
}
r.read_string()
}
///|
pub impl Codec for Bytes with fn ty(_self) -> TypeId {
Binary
}
///|
pub impl Codec for Bytes with fn write_data(self, w : Writer) -> Unit raise {
w.write_binary(self)
}
///|
pub impl Codec for Bytes with fn read_data(r : Reader, ty : TypeId) -> Bytes raise {
if ty != Binary {
raise Refused::WantType(
at=r.offset(),
want=TypeId::Binary.code(),
got=ty.code(),
)
}
r.read_binary()
}
///|
/// Read a signed integer of whatever width the wire chose.
///
/// A peer's small integer is not always the width this side would have picked:
/// Python writes `VARINT64`, Go writes `VARINT64`, and a schema may say
/// `TAGGED_INT64`. The signedness is the contract; the width is not.
fn signed(r : Reader, ty : TypeId) -> Int64 raise {
match ty {
Int8 => r.read_i8().to_int64()
Int16 => r.read_i16().to_int64()
Int32 => r.read_i32().to_int64()
VarInt32 => r.read_var_i32().to_int64()
Int64 => r.read_i64()
VarInt64 => r.read_var_i64()
TaggedInt64 => r.read_tagged_i64()
_ =>
raise Refused::WantType(
at=r.offset(),
want=TypeId::Int32.code(),
got=ty.code(),
)
}
}
///|
/// Read an unsigned integer of whatever width the wire chose.
fn unsigned(r : Reader, ty : TypeId) -> UInt64 raise {
match ty {
UInt8 => r.read_u8().reinterpret_as_uint().to_uint64()
UInt16 => r.read_u16().reinterpret_as_uint().to_uint64()
UInt32 => r.read_u32().to_uint64()
VarUInt32 => r.read_var_u32().to_uint64()
UInt64 => r.read_u64()
VarUInt64 => r.read_var_u64()
TaggedUInt64 => r.read_tagged_u64()
_ =>
raise Refused::WantType(
at=r.offset(),
want=TypeId::UInt64.code(),
got=ty.code(),
)
}
}
///|
/// Read a float of whatever width the wire chose, as a double.
fn numbers(r : Reader, ty : TypeId) -> Double raise {
match ty {
Float16 => half(r.read_u16())
BFloat16 => bfloat(r.read_u16())
Float32 => r.read_f32().to_double()
Float64 => r.read_f64()
_ =>
raise Refused::WantType(
at=r.offset(),
want=TypeId::Float64.code(),
got=ty.code(),
)
}
}
///|
/// A sixty-four-bit integer asked for as thirty-two: refused rather than cut,
/// because a value that silently loses its top half is worse than an error.
fn narrow(r : Reader, v : Int64) -> Int32 raise {
if v < -2_147_483_648L || v > 2_147_483_647L {
raise Refused::TooLarge(at=r.offset(), limit=2_147_483_647L, got=v)
}
v.to_int()
}
///|
/// A value that may be written more than once, or that takes part in a cycle.
///
/// MoonBit has no identity to infer sharing from — `physical_equal` is a hint
/// and not a promise — so the key is the caller's, the way `Rc` is the caller's
/// in Rust: two writes of the same key send the value once and point at it
/// afterwards.
pub(all) struct Shared[T] {
/// The name the caller gives the value.
key : Int32
/// The value itself.
value : T
} derive(Eq, Debug)
///|
pub extend Shared with Debug::{to_repr}
///|
pub extend Shared with Eq::{not_equal, equal}
///|
/// Mark a value as one that may be shared.
pub fn[T] shared(key : Int32, value : T) -> Shared[T] {
{ key, value, }
}
///|
pub impl[T : Codec] Codec for Shared[T] with fn ty(self) -> TypeId {
self.value.ty()
}
///|
pub impl[T : Codec] Codec for Shared[T] with fn write_data(
self : Shared[T],
w : Writer,
) -> Unit raise {
// Tracking is the configuration's business and not the value's: with it off,
// the flag is the plain not-null one and the body follows, which is what a
// peer that is not tracking reads.
if w.config.track_ref {
if w.write_tracked(self.key) {
self.value.write_data(w)
}
} else {
w.write_byte(0xFF)
self.value.write_data(w)
}
}
///|
pub impl[T : Codec] Codec for Shared[T] with fn read_data(
r : Reader,
ty : TypeId,
) -> Shared[T] raise {
match r.read_ref_flag() {
Null => raise Refused::BadEncoding(at=r.offset(), got=-3)
Ref(id~) => {
let saved = r.enter_ref(id)
let value = T::read_data(r, ty)
r.leave_ref(saved)
{ key: id, value, }
}
NotNull => { key: -1, value: T::read_data(r, ty), }
RefValue(id~) => { key: id, value: T::read_data(r, ty), }
}
}
///|
pub impl[T : Codec] Codec for Array[T] with fn ty(_self) -> TypeId {
List
}
///|
/// A list: a count, a header byte, the element type, then the elements.
///
/// The element type is written rather than left to the schema, so that a peer
/// reading without one can still read it back; an empty list writes no type at
/// all, having nothing to say about it.
pub impl[T : Codec] Codec for Array[T] with fn write_data(
self : Array[T],
w : Writer,
) -> Unit raise {
w.write_var_u32(self.length().reinterpret_as_uint())
if self.length() == 0 {
return
}
w.write_byte(0x08)
w.write_type_id(self[0].ty())
for item in self {
item.write_data(w)
}
}
///|
pub impl[T : Codec] Codec for Array[T] with fn read_data(
r : Reader,
ty : TypeId,
) -> Array[T] raise {
if ty != List && ty != Set {
raise Refused::WantType(
at=r.offset(),
want=TypeId::List.code(),
got=ty.code(),
)
}
let n = r.read_var_u32().reinterpret_as_int()
r.spend(n)
let out : Array[T] = []
if n == 0 {
return out
}
let header = r.read_byte()
if (header & 0x02) != 0 {
raise Refused::Unsupported(kind="list with null elements")
}
if (header & 0x04) != 0 {
raise Refused::Unsupported(kind="list of a declared type")
}
if (header & 0x08) != 0 {
let element = r.read_type_id()
for _ in 0.. TypeId {
Map
}
///|
/// A map: a count, then chunks of at most two hundred and fifty-five pairs,
/// each chunk saying the two types it holds.
pub impl[K : Codec + Hash + Eq, V : Codec] Codec for Map[K, V] with fn write_data(
self : Map[K, V],
w : Writer,
) -> Unit raise {
let pairs : Array[(K, V)] = []
for pair in self.iter() {
pairs.push(pair)
}
w.write_var_u32(pairs.length().reinterpret_as_uint())
let mut at = 0
while at < pairs.length() {
let size = if pairs.length() - at > 255 { 255 } else { pairs.length() - at }
let (_, value) = pairs[at]
w.write_byte(0x00)
w.write_byte(size)
w.write_type_id(Codec::ty(pairs[at].0))
w.write_type_id(value.ty())
for i in at..<(at + size) {
let (k, v) = pairs[i]
Codec::write_data(k, w)
Codec::write_data(v, w)
}
at = at + size
}
}
///|
pub impl[K : Codec + Hash + Eq, V : Codec] Codec for Map[K, V] with fn read_data(
r : Reader,
ty : TypeId,
) -> Map[K, V] raise {
if ty != Map {
raise Refused::WantType(
at=r.offset(),
want=TypeId::Map.code(),
got=ty.code(),
)
}
let total = r.read_var_u32().reinterpret_as_int()
r.spend(total)
let out : Map[K, V] = Map([])
while out.length() < total {
let header = r.read_byte()
if (header & 0x02) != 0 || (header & 0x10) != 0 {
raise Refused::Unsupported(kind="map with a null key or value")
}
if (header & 0x04) != 0 || (header & 0x20) != 0 {
raise Refused::Unsupported(kind="map of a declared type")
}
let size = r.read_byte()
let key_ty = r.read_type_id()
let val_ty = r.read_type_id()
for _ in 0.. Unit raise {
let ty = value.ty()
if ty == TypeId::NoneType {
w.write_byte(0xFD)
return
}
if w.config.track_ref {
let _ = w.write_tracked(root_key)
} else {
w.write_byte(0xFF)
}
w.write_type_id(ty)
value.write_data(w)
}
///|
/// Read one complete message: the root's flag first, then the type ID.
///
/// A value that is not there comes back as this type read under `NONE`, which
/// every type answers in its own way: `Value` answers `Null`, and a type that
/// cannot be absent refuses. A reference where the root belongs is a message
/// that begins by pointing at itself, and is refused.
pub fn[T : Codec] read_message(r : Reader) -> T raise {
match r.read_ref_flag() {
Null => T::read_data(r, TypeId::NoneType)
Ref(id~) => raise Refused::BadEncoding(at=r.offset(), got=id)
NotNull | RefValue(_) => {
let ty = r.read_type_id()
T::read_data(r, ty)
}
}
}
///|
pub extend Shared with Codec::{write_data, ty, read_data}
///|
/// A value whose type travels with it: the type ID is written by whoever knows
/// the schema, and everything after it — the user type ID for the kinds that
/// carry one, then the body — is here.
pub impl Codec for Value with fn ty(self) -> TypeId {
self.ty()
}
///|
pub impl Codec for Value with fn write_data(self, w : Writer) -> Unit raise {
write_value_data(w, self)
}
///|
pub impl Codec for Value with fn read_data(r : Reader, ty : TypeId) -> Value raise {
read_body(r, ty)
}
///|
pub extend Value with Codec::{write_data, read_data}
///|
/// One value as one message: the head, the type ID, and the value.
///
/// The configuration is the record, then this call's flat mirrors: the library
/// preset is beaten by the record, and the record by the flat mirrors, unless
/// `wins` says otherwise. A flat mirror that contradicts the record beside it is
/// what `clash` arbitrates: taken silently by `wins`, refused, or handed to the
/// caller's function as (the record, the two as one).
pub fn[T : Codec] dumps(
value : T,
config? : Config,
xlang? : Bool,
track_ref? : Bool,
check_string? : Bool,
depth? : Int32,
fields? : Int32,
graph? : Int32,
wins? : Wins,
clash? : OnClash[Config],
) -> Bytes raise {
let w = Writer::new(
config=resolve(
config, xlang, track_ref, check_string, depth, fields, graph, wins, clash,
),
)
w.write_head()
write_message(w, value)
w.bytes()
}
///|
/// The same, into a buffer the caller keeps: for a stream of messages written
/// one after another, or for a buffer that already holds a header of its own.
///
/// The configuration is the record, then this call's flat mirrors: the library
/// preset is beaten by the record, and the record by the flat mirrors, unless
/// `wins` says otherwise. A flat mirror that contradicts the record beside it is
/// what `clash` arbitrates: taken silently by `wins`, refused, or handed to the
/// caller's function as (the record, the two as one).
pub fn[T : Codec] dump(
value : T,
buffer : Buffer,
config? : Config,
xlang? : Bool,
track_ref? : Bool,
check_string? : Bool,
depth? : Int32,
fields? : Int32,
graph? : Int32,
wins? : Wins,
clash? : OnClash[Config],
) -> Unit raise {
let w = Writer::new(
config=resolve(
config, xlang, track_ref, check_string, depth, fields, graph, wins, clash,
),
buffer~,
)
w.write_head()
write_message(w, value)
}
///|
/// Bytes as a value: the head is checked, the value is read, and the bytes left
/// over are refused, because a message with two values in it is two messages.
///
/// `schemas` is what the reader knows about structs: a struct arrives as a user
/// type ID and nothing else. The configuration is the record, then this call's
/// flat mirrors: the library preset is beaten by the record, and the record by
/// the flat mirrors, unless `wins` says otherwise. A flat mirror that
/// contradicts the record beside it is what `clash` arbitrates: taken silently
/// by `wins`, refused, or handed to the caller's function as (the record, the
/// two as one).
pub fn[T : Codec] loads(
bytes : Bytes,
schemas~ : Array[StructMeta] = [],
config? : Config,
xlang? : Bool,
track_ref? : Bool,
check_string? : Bool,
depth? : Int32,
fields? : Int32,
graph? : Int32,
wins? : Wins,
clash? : OnClash[Config],
) -> T raise {
let r = Reader::new(
bytes,
schemas~,
config=resolve(
config, xlang, track_ref, check_string, depth, fields, graph, wins, clash,
),
)
r.read_head()
let value = read_message(r)
r.end()
value
}
///|
/// One value read out of a reader the caller owns, which is how several
/// messages written one after another are read back: the reader keeps the
/// position, and its configuration is the reader's.
pub fn[T : Codec] load(reader : Reader) -> T raise {
read_message(reader)
}