// A declared field's TYPE.
//
// ONE type lattice. There were two — this one, `TyInfo`, which the runtime
// walks, and `@tutuca.Ty`, which the schema parses into and the
// generators emit from — described as a "projection" of each other. They were
// not: they had the same sixteen shapes, and the projection between them was a
// hand-maintained table in `statedef/types.mbt` with a hand-maintained twin in
// `statedef/info/info.mbt`, one emitting SOURCE and the other building VALUES.
// A type added to the schema had to be added in four places, and the one that
// was forgotten was the one nobody read until a manifest lost a field's
// element type.
//
// What made them look different was one honest asymmetry: a closed set's
// members and a record's fields need a DECLARATION to resolve, and the runtime
// has none. That is answered by carrying what only the schema knows — a
// `TyFlags` carries its members inline — and by keeping a name where the shape
// is the instance's to open: a record, an enum and a variant keep a name and
// nothing else, which is enough to label them and not enough to open them.
//
// `TyInfo` is the older name and still reads.
///|
/// What a declared field carries.
#alias(TyInfo)
pub(all) enum Ty {
TyBool
/// Every `Int8`..`Int`/`UInt8`..`UInt`. Width and signedness survive so a
/// decode can range-check, even though every one of them emits a MoonBit
/// `Int` — by the time a value reaches the runtime the generated codec has
/// already applied the check.
TyInt(width~ : Int, signed~ : Bool)
TyFloat
TyText
TyList(Ty)
TyTuple(Array[Ty])
TyOption(Ty)
/// A named record. The name labels it; its fields are the instance's to
/// answer.
TyRecord(String)
TyEnum(String)
TyVariant(String)
/// A set with CLOSED membership, carrying the members the schema declared —
/// the one thing about a set that only the schema knows, since the encoded
/// value is just a Map.
TyFlags(String, Array[String])
/// A set with OPEN membership.
TySet
/// An ordered map. WIT's own `map` is unordered by definition, so this is a
/// refinement of it rather than a spelling for it.
TyOMap(Ty)
/// A child-component slot; None when the schema said only `component`.
TyComp(String?)
/// A dynamically composed child constrained by canonical protocol ids.
///
/// Deliberately distinct from `TyComp`: a component name is a construction
/// hint, while protocols are capabilities checked when an instance is
/// installed in the slot.
TyCompProtocols(Array[String])
/// Tabular data: typed columns of equal width (`tables.table`).
///
/// The one named shape whose contents the schema DOES know. `TyRecord` keeps
/// a name and nothing else because a record's fields are the instance's to
/// answer; a table is standard, so the projection can describe it fully, a
/// validator can check it, and an agent can be told how to write one. That
/// is the whole reason it is a case here rather than `TyRecord("table")`.
TyTable
/// An arbitrary `Value`.
TyAny
} derive(Eq, Debug)
///|
/// How the author wrote it. The WIT spelling, because that is what is in the
/// file and what they will edit.
pub fn Ty::show_source(self : Ty) -> String {
match self {
TyBool => "Bool"
TyInt(width~, signed~) =>
match (width, signed) {
(32, true) => "Int"
(8, true) => "Int8"
(16, true) => "Int16"
(32, false) => "UInt"
(8, false) => "UInt8"
_ => "UInt16"
}
TyFloat => "Double"
TyText => "String"
TyList(e) => "Array[" + e.show_source() + "]"
TyTuple(ts) => "(" + ts.map(t => t.show_source()).join(", ") + ")"
TyOption(e) => e.show_source() + "?"
TyRecord(n) | TyEnum(n) | TyVariant(n) => n
// A closed set names its enum, which is where the membership is written.
TyFlags(n, _) => "Set[" + n + "]"
TySet => "Set[String]"
TyOMap(v) => "Map[String, " + v.show_source() + "]"
TyComp(Some(c)) => "Instance[" + c + "]"
TyComp(None) => "Instance"
TyCompProtocols(ids) => "Instance[protocol " + ids.join(" & ") + "]"
TyTable => "Table"
TyAny => "Any"
}
}
///|
/// What this type CONTAINS, or None when it contains nothing.
///
/// Strict, and deliberately not the same question `iterable_elem` answers.
/// That one asks "may a view `@each` over this?", where `any` and a child slot
/// must both say yes — a dynamic value may turn out to be a list and a
/// component iterates its own entries, neither knowable at generation time.
/// Answering the two with one method put a meaningless `elem: any` on 117
/// fields of the corpus and showed it in the inspector, where the question is
/// the other one: what is in here?
pub fn Ty::elem(self : Ty) -> Ty? {
match self {
TyList(e) | TyOMap(e) => Some(e)
// A set iterates its members, which are text either way.
TySet | TyFlags(_, _) => Some(TyText)
_ => None
}
}
///|
/// The component a child slot holds, or None for a data field. `Some("")` is a
/// slot whose component the schema did not name.
pub fn Ty::slot(self : Ty) -> String? {
match self {
TyComp(Some(c)) => Some(c)
TyComp(None) => Some("")
TyCompProtocols(_) => Some("")
_ => None
}
}
///|
/// A closed set's declared members. Empty for every other type, and for a set
/// with open membership — which is a real answer, not a missing one.
pub fn Ty::members(self : Ty) -> Array[String] {
match self {
TyFlags(_, ms) => ms
_ => []
}
}
///|
/// The runtime kind: how a value of this type is REPRESENTED, once the shape
/// that distinguishes it no longer matters.
///
/// A projection rather than a stored field, which is the point.
pub fn Ty::kind(self : Ty) -> FieldKind {
match self {
TyBool => FBool
TyInt(..) => FInt
TyFloat => FFloat
// An enum is payload-free, so it travels as its case name.
TyText | TyEnum(_) => FText
TyList(_) | TyTuple(_) => FList
// A table travels as `{"columns": [...]}` — a Map, like a record. What
// makes it different is what the SCHEMA can say about it, not how the
// value is carried.
TyRecord(_) | TyTable => FMap
// A variant travels as EITHER a bare case name or a case name followed by
// its payload — `Str("Ping")` or `List(["SetTo", 7])`. No single narrow
// kind covers both, and the old table said `FMap`, which covers neither: a
// write of a perfectly good variant value hit `coerce_or_default`, failed
// the `Map` shape test and was replaced by the field's default.
TyVariant(_) => FAny
TySet | TyFlags(_, _) => FSet
TyOMap(_) => FOMap
// The slot's construction arguments are not part of its TYPE; the component
// layer fills them in from what the author wired.
TyComp(c) => FComp(comp=c.unwrap_or(""), args=Map([]))
TyCompProtocols(_) => FComp(comp="", args=Map([]))
// An `option` is its value or absent, so the kind is the widest one: the
// field may hold the inner shape or nothing.
TyOption(_) | TyAny => FAny
}
}
///|
/// The empty value of this type, as the runtime carries it.
///
/// Type-directed, like the generated `State::zero()` — which is the point:
/// a default is not an independent fact about a field, so storing one beside
/// the type would be a second thing to keep true. The one default a TYPE cannot
/// give is a child slot's construction arguments, and those are a value the
/// author chooses rather than anything the schema knows.
///
/// This is what an inspector shows in a field's "default" column, reachable
/// from a bare `Value`.
pub fn Ty::zero(self : Ty) -> Value {
match self {
TyBool => Bool(false)
TyInt(..) | TyFloat => Num(0.0)
TyText | TyEnum(_) => Str("")
TyList(_) | TyTuple(_) => List([])
TyRecord(_) | TyOMap(_) => Map(Map([]))
// No columns and therefore no rows. Spelled out rather than left as an
// empty Map because `{"columns": []}` is a VALID table and `{}` is not:
// the zero of a table should decode, not fail the first check made of it.
TyTable => Map({ "columns": List([]) })
// Both sets start EMPTY: the CONTAINER decides the zero and the enum
// decides the membership, which is one idea in each place.
TyFlags(_, _) | TySet => Map(Map([]))
// A variant's zero is its first case, which this cannot name; an option
// is absent; a slot is filled by the registration scope at make time.
TyVariant(_) | TyOption(_) | TyComp(_) | TyCompProtocols(_) | TyAny => Null
}
}
///|
/// Whether a runtime value has this declared outer shape. Named records and
/// variants remain permissive because their structural declaration is not
/// carried by `Ty`; generated decoders perform the deeper check.
pub fn Ty::accepts(self : Ty, value : Value) -> Bool {
match (self, value) {
(TyBool, Bool(_)) => true
(TyInt(..) | TyFloat, Num(_)) => true
(TyText | TyEnum(_), Str(_)) => true
(TyList(_) | TyTuple(_), List(_)) => true
(TyRecord(_) | TyOMap(_) | TySet | TyFlags(_, _) | TyTable, Map(_)) => true
(TyComp(_) | TyCompProtocols(_), Obj(_)) => true
(TyVariant(_), Str(_) | List(_)) => true
(TyOption(_), Null) => true
(TyOption(inner), other) => inner.accepts(other)
(TyAny, _) => true
_ => false
}
}
///|
/// What a view may `@each` OVER, and what one item of it is.
///
/// Deliberately more permissive than `elem`, and a different question: this one
/// asks "may a view iterate this?", where `any` and a child slot must both say
/// yes — a dynamic value may turn out to be a list at run time, and a component
/// iterates its own `seq_entries` — while `elem` asks what is actually IN here
/// and answers nothing for both.
pub fn Ty::iterable_elem(self : Ty) -> Ty? {
match self {
TyList(e) | TyOMap(e) => Some(e)
// A set iterates its members, which are text either way: a closed set has
// declared membership, an open one does not.
TySet | TyFlags(_, _) => Some(TyText)
TyTuple(_) => Some(TyAny)
// A table iterates its columns.
TyTable => Some(TyAny)
TyAny | TyComp(_) | TyCompProtocols(_) => Some(TyAny)
_ => None
}
}
///|
/// True when this field lives in the state STRUCT — which is every field.
///
/// A slot is a `Value` field like any other, seeded with `Null` and filled at
/// make time.
pub fn Ty::in_struct(_self : Ty) -> Bool {
true
}
///|
/// Whether writing this type in a boolean position can ever decide anything.
///
/// Everything has a truthiness at run time; this asks whether the test can
/// fail. `Value::is_truthy` answers `true` for List, Map and Obj
/// unconditionally, so a container in an `@show` is a test that cannot fail —
/// almost always a forgotten `.length` or field read.
///
/// A string, a number and an enum are NOT in that set even though they are not
/// booleans: `@show=".role"` beside `@text=".role"` is the idiomatic way to
/// hide an empty badge, and `@show=".count"` hides a zero. Flagging those would
/// cost more in false failures than the real ones are worth — the rule stated
/// at the head of `viewgen/check_state.mbt`.
pub fn Ty::is_boolish(self : Ty) -> Bool {
match self {
TyList(_)
| TyTuple(_)
| TyRecord(_)
| TyVariant(_)
| TyFlags(_, _)
| TySet
| TyOMap(_)
| TyTable => false
_ => true
}
}
///|
/// The component this type names, when it names one.
///
/// `greeting?` is a slot the way `greeting` is — `is_renderable` unwraps an
/// option, so the question "which component" has to unwrap it the same way or
/// the two disagree about the same field.
pub fn Ty::component_named(self : Ty) -> String? {
match self {
TyComp(c) => c
TyCompProtocols(_) => None
TyOption(inner) => inner.component_named()
_ => None
}
}
///|
/// Whether `` can render a value of this type.
pub fn Ty::is_renderable(self : Ty) -> Bool {
match self {
TyComp(_) | TyCompProtocols(_) | TyAny => true
TyOption(inner) => inner.is_renderable()
_ => false
}
}
///|
/// The MoonBit source of the expression that rebuilds this type.
///
/// What `gen` splices into a generated `SchemaInfo`. It is a method on
/// the type rather than a table in the generator, and that is the whole point
/// of there being one `Ty`: the source form and the value form cannot describe
/// different shapes if one of them IS the shape.
///
/// The constructors are written UNQUALIFIED, and have to be: an enum
/// constructor does not resolve through the `pub using` re-export that keeps
/// `@component.Ty` spelling correct, nor through the module-root facade the
/// playground compiles examples against. Bare works in both, because the only
/// position the generator writes one into — a `FieldInfo::new` argument —
/// already knows the type.
pub fn Ty::to_mbt_source(self : Ty) -> String {
fn quoted(s : String) -> String {
"\"" + s + "\""
}
match self {
TyBool => "TyBool"
TyInt(width~, signed~) => "TyInt(width=\{width}, signed=\{signed})"
TyFloat => "TyFloat"
TyText => "TyText"
TyList(e) => "TyList(" + e.to_mbt_source() + ")"
TyTuple(ts) =>
"TyTuple([" + ts.map(t => t.to_mbt_source()).join(", ") + "])"
TyOption(e) => "TyOption(" + e.to_mbt_source() + ")"
TyOMap(v) => "TyOMap(" + v.to_mbt_source() + ")"
TyRecord(n) => "TyRecord(" + quoted(n) + ")"
TyEnum(n) => "TyEnum(" + quoted(n) + ")"
TyVariant(n) => "TyVariant(" + quoted(n) + ")"
// A closed set's members are INLINED rather than named: this is read at
// run time, where the declaration they would refer to does not exist.
TyFlags(n, ms) =>
"TyFlags(" + quoted(n) + ", [" + ms.map(quoted).join(", ") + "])"
TySet => "TySet"
TyTable => "TyTable"
TyComp(Some(c)) => "TyComp(Some(" + quoted(c) + "))"
TyComp(None) => "TyComp(None)"
// Protocol-constrained slots retain their constraint in SchemaInfo; the
// runtime uses it when a dynamic instance is installed.
TyCompProtocols(ids) =>
"TyCompProtocols([" + ids.map(quoted).join(", ") + "])"
TyAny => "TyAny"
}
}