// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// Which end of a number comes first.
#derive(Eq, Debug)
pub(all) enum Order {
Little
/// Most significant byte first, which is what a network order is.
Big
}
///|
pub extend Order with Eq::{equal, not_equal}
///|
pub extend Order with Debug::{to_repr}
///|
/// A value as a byte format lays it out.
///
/// A document says what a value is. A format that writes bytes also fixes how
/// wide it is, whether it carries a sign, which byte comes first, and whether
/// it is written where it stands or as an offset to somewhere else. The four
/// belong to the type rather than to the value: every value of the type is
/// laid out the same way, and a reader has to know before it looks at one.
///
/// A projection that writes a document reads `kind` and says nothing about the
/// rest, because a document has no width and no offset. The width alone is
/// still worth writing as a `format`, which is how the JSON family already
/// distinguishes `int32` from `int64`.
#derive(Eq, Debug)
pub(all) struct Layout {
/// The value's own kind.
kind : Type
/// The width a format fixes, in bits — 8, 16, 32 or 64. Absent leaves the
/// width to the format's own table, which is what a plain `Int` says.
///
/// It agrees with `kind` where `kind` has a width of its own: `Int` is 32
/// bits and `Big` 64. A layout that says otherwise is written at the width
/// it states; the word and the projections trust `bits` over `kind`.
bits : Int32?
/// Whether the value has no sign bit.
unsigned : Bool
/// Which byte of a number comes first.
order : Order
/// Whether the value is written as an offset into the buffer rather than
/// where it stands — which a format that sends a shared value once and
/// points at it twice has to know before it reads.
reference : Bool
}
///|
pub extend Layout with Eq::{equal, not_equal}
///|
pub extend Layout with Debug::{to_repr}
///|
/// A layout: the kind, and how a format writes it.
///
/// Every part but the kind has the common answer as its default, so a format
/// whose numbers are little-endian and written in place says only the width.
pub fn Layout::new(
kind : Type,
bits? : Int32,
unsigned~ : Bool = false,
order~ : Order = Little,
reference~ : Bool = false,
) -> Layout {
{ kind, bits, unsigned, order, reference, }
}
///|
/// The `format` this layout asks for: `int8`, `uint16`, `float32`.
///
/// `None` when the layout fixes no width of its own, or when the kind is not
/// a number — a width on anything else is not a width, and writing one would
/// make a reader think the format had said something it did not.
pub fn Layout::format(self : Layout) -> String? {
match self.bits {
Some(bits) =>
match self.kind {
Real =>
match bits {
32 => Some("float32")
64 => Some("float64")
_ => None
}
Int | Big => width_word(bits, self.unsigned)
_ => None
}
None => None
}
}
///|
/// The word for a width, which is the one every schema family already uses for
/// the two widths it has: `int32` and `int64`.
fn width_word(bits : Int32, unsigned : Bool) -> String? {
match bits {
8 => Some(if unsigned { "uint8" } else { "int8" })
16 => Some(if unsigned { "uint16" } else { "int16" })
32 => Some(if unsigned { "uint32" } else { "int32" })
64 => Some(if unsigned { "uint64" } else { "int64" })
_ => None
}
}
///|
/// The word a scalar type is written with: what `#model.field(kind=…)` takes,
/// and what a report calls it.
///
/// `None` for a type with parts inside — a list, a mapping, a model, a schema
/// written out — because one word cannot carry what is in it. A layout's word
/// adds `-be` for a big-endian number and `-ref` for one written by reference;
/// both are read back by `of_word`, so a word and a type round-trip.
pub fn Type::word(self : Type) -> String? {
match self {
Bool => Some("bool")
Int => Some("int")
Big => Some("big")
Real => Some("real")
Text => Some("text")
Bytes => Some("bytes")
Stamp => Some("stamp")
Any => Some("any")
Wire(layout) => {
let base = match layout.bits {
Some(bits) =>
match layout.kind {
Real =>
match bits {
32 => Some("float32")
64 => Some("float64")
_ => None
}
Int | Big => width_word(bits, layout.unsigned)
other => other.word()
}
None => layout.kind.word()
}
base.map(word => {
let mut word = word
if layout.order is Big {
word = word + "-be"
}
if layout.reference {
word = word + "-ref"
}
word
})
}
_ => None
}
}
///|
/// The type a word stands for, when it is one of the words `word` writes.
///
/// Read the suffixes from the right first: `uint16-be-ref` is a `uint16` in
/// big-endian order, written by reference. A word fixes what the kind and the
/// width agree on, so a layout that contradicts its own kind — an `Int` at 64
/// bits — is written at the width it states and read back as the kind that
/// width belongs to.
pub fn Type::of_word(word : String) -> Type? {
let mut rest = word.trim(chars=" ").to_lower()
let reference = rest.has_suffix("-ref")
if reference {
rest = rest.exact_view(end=rest.length() - 4).to_owned()
}
let order = if rest.has_suffix("-be") {
rest = rest.exact_view(end=rest.length() - 3).to_owned()
Order::Big
} else {
Order::Little
}
let base = match rest {
"bool" => Some(Bool)
"int" => Some(Int)
"big" => Some(Big)
"real" => Some(Real)
"text" => Some(Text)
"bytes" => Some(Bytes)
"stamp" => Some(Stamp)
"any" => Some(Any)
"int8" => Some(Wire(Layout::new(Int, bits=8)))
"int16" => Some(Wire(Layout::new(Int, bits=16)))
"int32" => Some(Wire(Layout::new(Int, bits=32)))
"int64" => Some(Wire(Layout::new(Big, bits=64)))
"uint8" => Some(Wire(Layout::new(Int, bits=8, unsigned=true)))
"uint16" => Some(Wire(Layout::new(Int, bits=16, unsigned=true)))
"uint32" => Some(Wire(Layout::new(Int, bits=32, unsigned=true)))
"uint64" => Some(Wire(Layout::new(Big, bits=64, unsigned=true)))
"float32" => Some(Wire(Layout::new(Real, bits=32)))
"float64" => Some(Wire(Layout::new(Real, bits=64)))
_ => None
}
match base {
None => None
Some(inner) =>
match inner {
Wire(layout) => Some(Wire({ ..layout, order, reference, }))
// A plain scalar takes a layout only when one of the two says
// something the plain kind does not.
plain =>
if order is Little && !reference {
Some(plain)
} else {
Some(Wire(Layout::new(plain, order~, reference~)))
}
}
}
}