The Rust binding of the language-independent schema model, and the check it performs at open.

Two layers

The split is deliberate and worth preserving in a port.

kladde-schema — the model. The descriptor types, the canonical encoding, the fingerprint computation, and a SHA-256 implementation. It has no connection to the store, to Persistable, or to any Rust type system machinery, which is what makes it portable and testable on its own: the conformance vectors for encoding and fingerprints exercise this layer and nothing else.

The binding — where Rust types meet the model. A Persistable type declares its own descriptor in describe_local, and the derive macro generates that declaration.

The binding

A type contributes its descriptor by describing itself into a SchemaBuilder, which interns descriptors into a table and hands back references:

  • Recursion terminates. A type that refers to itself reserves its table slot, keyed by its TypeId, before describing its fields.
  • Descriptors are deduplicated by type, so a type used in twenty places occupies one table entry.
  • The table is built once, not per value.

What a type declares

A struct declares a Struct descriptor with its fields in declaration order; an enum declares an Enum with its variants canonically ordered by discriminant.

A hand-written implementation declares whatever descriptor matches the bytes it actually reads and writes — which is the rule that most often trips people up. A hand-written implementation of a plain field-sum declares Struct, not Opaque; “hand-written” and “opaque” are different axes, see the guiding principle.

The containers declare Opaque, carrying their library name, type name, version, inline size, and element types as parameters. Whether they should — as opposed to declaring structural descriptors so that tools can walk them — is an open format question.

At create and at open

Kladde::create encodes the root type’s descriptor table into an allocation that the header’s schema_table names, and writes the root’s fingerprint into the header. Kladde::open compares the header’s fingerprint with the application’s and fails closed on a mismatch, with an error carrying both fingerprints. It never parses the descriptor table on the matching path, which is the fast path the fingerprint exists for.

Resolution — reading at a writer’s offsets — does not exist yet, so a mismatch cannot be bridged; see Evolution.

Design invariants

Worth keeping in mind when changing anything here:

  • The fingerprint must not depend on table layout. References are encoded structurally, so any renumbering or reordering produces the same hash; a test permutes the table and asserts equality.
  • A type’s own name is not fingerprinted; its fields’ and variants’ names are.
  • Determinism over everything. No hash-map iteration order, no addresses, nothing incidental may reach the hash.
  • Encoding and fingerprinting are separate functions over the same model. They share the canonical order but not the code, because the fingerprint deliberately omits things the encoding includes.