Type reflection

pub enum TypeKind {
  IntegerKind,
  LongKind,
  SingleKind,
  FloatKind,
  BooleanKind,
  TextKind,
  CharacterKind,
  /// A struct — its fields are in `fields`.
  RecordKind,
  /// An enum — its variants are in `variants`.
  EnumKind,
  /// One variant of a struct-enum; it has fields of its own.
  VariantKind,
  /// A vector — `element` names what it holds.
  VectorKind,
  /// A keyed collection (hash / index / sorted / ordered / radix / trie; a
  /// `spatial` reports as radix) — walked by cursor rather than by layout, so it
  /// has no fields of its own. Which of the six it is, and on which fields, are
  /// in `collection` and `keys`.
  KeyedKind,
  /// A stored reference to another record.
  RefKind,
  /// A kind this loft version has no name for. Never guessed at.
  OtherKind,
}

What a type IS — the discriminant a caller matches on before reading the fields that only some kinds have.

These are STORAGE kinds, because storage is what the descriptor records. A narrow i32 field reports IntegerKind; the declared width is visible in size, not in a separate kind.

pub struct FieldInfo {
  pub name: text,
  /// The field type's name, as the store records it.
  pub type_name: text,
  /// Byte offset of the field within its record.
  pub position: integer,
  pub kind: TypeKind,
  /// Was the field DECLARED nullable (`text?` rather than `text`)?
  ///
  /// Not a layout fact — a nullable field occupies the same bytes and spells an
  /// absent value with a sentinel — so nothing in the stored bytes implies it,
  /// and it reaches you only because the compiler records it. It is what a
  /// generated `CREATE TABLE` needs for `NOT NULL`.
  pub nullable: boolean,
}

One field of a record or of a struct-enum variant.

pub enum CollectionKind {
  /// Not a keyed collection. Every other kind of type answers this, so a
  /// caller can read `collection` without matching `kind` first.
  NotKeyed,
  KeyedHash,
  KeyedIndex,
  KeyedSorted,
  KeyedOrdered,
  KeyedRadix,
  KeyedTrie,
}

WHICH keyed collection a type is — the shape of its lookup.

KeyedKind says a type is walked by cursor; this says what by. The distinction decides what a query over it can be: a hash answers equality only, sorted / ordered / index also answer a range in their declared direction, a radix is a Morton-order structure that no SQL shape means the same thing as, and a trie answers equality, byte order, and a PREFIX — which is LIKE 'x%' and nothing narrower.

pub struct KeyInfo {
  pub name: text,
  /// Byte offset of this field within the ELEMENT record — the same number the
  /// element type's `FieldInfo.position` carries, so a caller joins the two by
  /// VALUE rather than by matching names. A name is a display fact; the
  /// position is what the collection actually keys on.
  pub position: integer,
  /// Does the collection order this key ascending?
  ///
  /// `true` for a kind that has no order of its own (`hash`, `radix`), and for a
  /// `trie`, whose single key IS ordered ascending by byte. Only a kind that can
  /// be declared descending ever answers `false`. Match `collection` first where the
  /// difference between "ascending" and "unordered" matters.
  pub ascending: boolean,
}

One key field of a keyed collection.

pub struct VariantInfo {
  pub name: text,
  /// The discriminant this variant is stored as. Never 0 — 0 means absent.
  pub tag: integer,
}

One variant of an enum.

pub struct TypeInfo {
  pub name: text,
  pub kind: TypeKind,
  /// Bytes one record of this type occupies; 0 for a type with no record.
  pub size: integer,
  pub const fields: vector<FieldInfo>,
  pub const variants: vector<VariantInfo>,
  /// For a vector or a keyed collection, the element type's name.
  pub element: text,
  /// For a keyed collection, which of the six it is; `NotKeyed` otherwise.
  pub collection: CollectionKind,
  /// The key fields of a keyed collection, in KEY ORDER — the order a composite
  /// lookup binds them in, which is why it is a vector and not a set.
  ///
  /// Empty for every type that is not keyed, and empty for the one keyed shape
  /// whose keys are not its element's own fields: a `__nullable<S>` element
  /// keys through its `Some` payload. Empty rather than partial is deliberate —
  /// a query built from half a composite key is a WRONG query, not a narrower
  /// one, so a key list is delivered whole or not at all.
  pub const keys: vector<KeyInfo>,
}

The declared shape of one type.

fields is empty for everything but a record and a struct-enum variant; variants for everything but an enum; element is "" unless the type holds one. Empty is the honest answer for a kind that has no such thing — a caller matches kind first.

pub fn reflect_type(kt: integer) -> TypeInfo

The declared shape of value's type.

**The argument is read for its TYPE and is not evaluated** — the same contract C's sizeof has, and for the same reason: nothing about the answer depends on the value. Pass a variable, a field or a parameter; an expression with a side effect will not have it.

loft t = type_of(row); println("{t.name}:"); for f in t.fields { println(" {f.name}: {f.type_name} @{f.position}") }

**Not inside a generic.** A generic body is parsed ONCE against its type variable, so type_of(v) there answers a type-variable placeholder (__typevar_T…) — the same reason "{v:j}" in a generic body renders {}. Call it where the concrete type is known.

Describes a TYPE. To read what a VALUE holds at one of these positions, use field_value. Reflection offers no way to WRITE a value by field.

pub fn type_named(name: text) -> TypeInfo?

The declared shape of the type called name, or null if this program has no such type — reflection with no value in hand.

This is what an ORM or a schema check needs: the name arrives from a config file, a database catalogue or a command line, so there is nothing to call type_of on.

loft t = type_named("Row"); if t == null { println("no such type") } else { println("{t.name}") }

**TypeInfo?, and null means the name is not a type here.** A type that does not exist has no shape, and saying so in the type is what makes a caller handle it rather than read a plausible-looking empty answer.

A name is matched exactly as the store records it, which is the loft type name — Point, text, vector<Point>. A composite name such as vector<Point> exists only when the program uses that type somewhere; a program that never does answers null for it.

Unlike type_of, no parser intercept: the name is a RUNTIME value, so the lookup is a runtime one. It works on --native because the generated init() replays the type registrations — names included — and Stores::name is a total lookup that answers "absent" rather than minting a type for a typo.

pub struct ValueInfo {
  /// What the type's own descriptor says lives at that position — never what
  /// the caller expected to find there.
  pub kind: TypeKind,
  /// The SCALAR at that position holds loft's NULL.
  ///
  /// Separate from the payload because a null is not a value a payload can
  /// spell: `0`, `""` and `false` are ordinary answers a field can genuinely
  /// hold, and an ORM that could not tell them from SQL NULL would write the
  /// wrong row. Test this before reading `i` / `f` / `t`.
  ///
  /// `false` for a `kind` that has no scalar reading and for `OtherKind` — a
  /// field that was never read is not a field that read as null, and those are
  /// three answers rather than two.
  pub is_null: boolean,
  pub i: integer,
  pub f: float,
  pub t: text,
}

One field's VALUE, read out of a record at the position reflection reported.

kind says which payload carries the answer, so a caller matches it once rather than asking a different question per type:

| kind | read | |---|---| | IntegerKind · LongKind | i | | BooleanKind | i — 1 or 0 | | CharacterKind | i — the code point | | FloatKind · SingleKind | f | | TextKind | t | | RecordKind · VectorKind · KeyedKind · RefKind | nothing — no scalar reading | | OtherKind | nothing — see field_value |

A boolean and a character ride in i for the reason a bound SQL value does: one integer path is one thing to get right, and the kind beside it is what keeps them apart.

pub fn reflect_field(value: const TypeInfo, position: integer, kt: integer) -> ValueInfo

The value value holds at byte position — the VALUE half of reflection.

type_of says a record has a text at byte 16; this reads it. Together they are what a generic serialiser, an ORM write or a diff needs, and neither half is enough alone:

loft t = type_of(row); for f in t.fields { v = field_value(row, f.position); if v.is_null { println("{f.name}=null") } else { println("{f.name}={v.t}") } }

**The position is CHECKED against the type's own descriptor, never trusted.** A number that does not begin a field — a hand-made offset, a stale one, a position from a different type — answers OtherKind. A field whose type has no scalar reading (a nested record, a vector, a keyed collection, a stored reference) answers its own kind with no payload. So there is no offset a caller can pass that reads bytes belonging to something else, which is what makes this ordinary loft rather than a pointer.

kind is the DESCRIPTOR's answer, not the caller's: passing the position of an integer field does not make the reading an integer, it makes it whatever that field is. That is why the kind is reported rather than requested.

**REFUSED inside a generic**, and refused rather than merely unsupported. A generic body is parsed once against its type variable, so there is no concrete type to read positions out of — the same limit type_of has. Left to answer, every call there reports OtherKind, which for an ORM's write half is an EMPTY ROW rather than an error: the write succeeds and the columns are missing. So it is a compile error naming the type variable. Call it one frame out, where the type is known, and pass the values in.

pub fn reflect_field_path(value: const TypeInfo, path: const vector<integer>, kt: integer) -> ValueInfo

The value at the end of a PATH of positions — field_value(x, [8, 0]).

type_of reports a nested record's fields at positions relative to THAT record, so one number cannot name origin.x — and reflection hands back no handle to a nested record to call field_value on again. A path closes that: each element is a position in the record the previous element landed in.

**The offsets must not be added up by the caller.** field_value(doc, 8 + 0) asks for a field BEGINNING at byte 8 of Doc, which is origin itself — the check that makes this ordinary loft rather than a pointer is that a position must begin a field of the type it is read against. So the walk happens inside, one descriptor step per element, each checked the same way.

loft // Doc.origin @8, Point.y @8 v = field_value(doc, [8, 8]); // doc.origin.y

Every step but the last must land on an INLINE record. A step onto a scalar, a vector, a keyed collection or a stored reference answers OtherKind and reads nothing: a stored reference names a record with its own identity, and following it would be a pointer chase rather than a field read. An empty path answers OtherKind for the same reason a bad position does — nothing names a field there.

A one-element path is the single-position form, and answers identically.