tson.io

Revision 39Current working draft·All revisions

Part 2 of 2

TSON Part 2: Type System and Schema

Draft 2026·Working Draft·view raw ↗

TSON Part 2: Type System and Schema

2026 Revision 39

Status: Working revision. The 2026 revision series is subject to change without compatibility guarantees. When finalised, this specification will be published as TSON version 1 and frozen; until then, revisions are released under the 2026 series.

Series: TSON Specification, Part 2 of 3

Copyright: © 2026 Litterat Pty Ltd. This document is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License (CC BY-SA 4.0): https://creativecommons.org/licenses/by-sa/4.0/

1. Introduction

TSON is a schema system, and this document is its centre: a type system of immutable, hash-pinned schemas whose definitions are themselves data, resolving down a verified chain of imports — document → schema → the schemas it imports → the pre-loaded kernel — so that one hash authenticates a document together with its entire contract. The text format of [TSON-DATA] is this system’s notation and its reference encoding: schemas are written in it, resolver output is serialized in it, and §7 states its encoding rules — the first encoding to carry the type system’s values, with others defined against the same encoding-independent model.

This document defines the TSON type system and schema layer: the schema grammar and its type-definition forms, the type system and its operations, the import chain and its resolution model, the resolver output contract, the text encoding rules, and the schema-layer operation of the import directive.

[TSON-DATA] defines the lexer, the data grammar, base type resolution, and the built-in type vocabulary. This document introduces no lexical changes: a schema document is parsed by a second body grammar over the same lexer, selected by the root value’s tag ([TSON-DATA] §2.2), and every operator it uses is a token that lexer already emits — the reserved special tokens of [TSON-DATA] §7.2.5 receive their meaning here. The schema grammar imports [TSON-DATA]‘s value grammar at exactly two points — the constructor-application payload, a core-value, and the refinement body, its braced subset (§5.5, §5.6, §12.1), the second desugaring into the first; every other value position is deliberately narrower (a record-refinement body is a braced record-def, §5.7; a field-modifier value is a bare token, or the selector =? with none, §5.2) — and the coupling is one-directional: nothing in the data grammar depends on this document. One production runs the other way: a reference carrying an argument list is [TSON-DATA] §7.4’s, and the schema grammar’s type expressions reuse it (§12.1).

1.1 The TSON Specification Series

  • Part 1: Text Data Format [TSON-DATA] — the notation and reference encoding: the lexer, the data grammar, base type resolution, and the built-in type vocabulary.
  • Part 2: Type System and Schema (this document) — the centre of the series: the schema grammar, the type system, the import chain, and the operation of the import directive.
  • Part 3: JSON Encoding [TSON-JSON] — the JSON encoding of the same model: the second encoding to carry the type system’s values, with its own encoding rules stated against §7’s shape, and the TSON-Schema transport field.

[TSON-DATA] §1.3 gives the series architecture: the import chain — the type system — on one axis, and the notation and encodings that carry it on the other.

1.2 Design Principles

In addition to the principles of [TSON-DATA] §1.2:

  1. Schema-value separation — A data document has no definitions of its own: every tag (!name) in it resolves against the namespace its !!import directives bind (§3), and a schema document’s declarations exist only for the documents that import it.

  2. Purpose-built representation — A schema document shares the data format’s lexicon — the lexer, its tokens, annotations, and directives — but has its own body grammar, selected by the root’s !schema tag (§12.1). The semantics remain shared: a schema document resolves to an ordinary TSON value (a schema value, §2.1), and resolver output is an ordinary data document (§8).

  3. Permanent stability — The schema grammar, the kernel, the bundled libraries, and the resolver output contract are frozen once published, on the same terms as [TSON-DATA] §1.2 principle 6. New types are added through new type libraries, not through changes to this document.

1.3 Conformance

[TSON-DATA] §1.5 defines the series’ two conformance classes. Class 1 (data-format processor) is defined there in full; its sole schema-layer obligation is to reject schema documents with a categorized diagnostic. This document defines Class 2.

A Class 2 processor (schema-aware processor) conforms to [TSON-DATA] and additionally implements the schema grammar (§4–§5, §12), the directive operation (§2.2), name resolution and the schema library (§3, §10), schema compilation and resolver output (§8), atom token parsing (§7.4), and validation. Such a processor:

  • MUST pre-load the kernel (§3.4, §10.1) and SHOULD pre-load the bundled libraries;
  • MUST resolve every name a document carries — a tag, a constructor head, a type position, a reference’s of, a template bound, an annotation — in the one namespace §3.3 defines for that document, and MUST NOT apply the [TSON-DATA] §5 built-in vocabulary or [TSON-DATA] §4 base type resolution in schema scope: under a schema every value is typed by its position or by its tag (§7.1, §7.3);
  • MUST resolve each schema in the wait order of §3.4.1, consulting the order its declarations are written in for nothing, and MUST refuse a wait cycle as a resolver error;
  • MUST produce, for every valid schema document, a resolved schema value — a value of the kernel’s meta.schema, a map from each name to the value its entry resolves to (§8) — with every type’s chain computed (§8.1). Serializing the resolved schema value as a data document is OPTIONAL; output, when produced, MUST conform to §8’s serialization contract;
  • MAY read a resolved form as data (§8.1, §10.1); an implementation that does MUST apply §8.1’s derived-field treatment and §10.1’s source rules;
  • MUST resolve annotations (@name, @name:value) in the annotating document’s namespace (§3.3.3) and validate each annotation’s value against the named type’s contract (§6); an annotation whose name does not resolve is a resolver error (§6), and annotations are preserved in resolver output (§8.1);
  • MUST enforce the identity-agreement rule (§10.1), verify hash-pinned references per [TSON-DATA] §2.2.1 (§10.2), and refuse two pins of one identity in one closure (§2.2.3);
  • MUST match every declared type name, parameter name, constructor head and each segment of every referenced name against the identifier grammar ([TSON-DATA] §7.7) and reject a failure as a parse error (§12.1); and MUST hold every declared field name and enum member to its record’s or enum’s name type, rejecting a failure as a resolver error (§5.2, §7.4);
  • MUST implement the name-hygiene checks of [TSON-DATA] §8.2 over the schema-layer scopes (§11.4), each schema map among them, enforce them by default, and report a refusal as [TSON-DATA] §8.1’s refused outcome — distinguishable from a verdict, in the same report; and MUST report a schema it could not obtain as the undetermined outcome, and one it will not obtain as a refusal (§10.1, §11.2);
  • MUST enforce the schema-side resource limits of §11.5 on [TSON-DATA] §9.1’s terms;
  • MUST report errors in the categories and phrasings of [TSON-DATA] §8.1.

Resolved-output consumers. A consumer that reads only resolved schema values (§8, §10.1) — never schema source — is fully conforming with no support for parameters or substitution (§5.10). An open entry appears in output as its value, in one of two forms (§5.10, §8.1). A record template is a !meta.record_template: a record body over parameters, whose fields are meta.record_template_fields and label a parameter that stands for a field’s whole type (type_param) or whole value (value_param), with its discriminators stated as the family’s and its chain.parents the parents as written. Every other open entry is a !meta.template — the kernel constructor whose instances are held bodies: the parameters, each with the type an argument for it is read as, and the application as text. A closed record is a !meta.record, and no field of it carries a parameter: a reader or writer built from one never meets substitution machinery, and a consumer that builds one from a !meta.record_template has made a category error the vocabulary refuses. The closed-entry rule (§5.10) has one deliberate exception: a type position may name a record template as a family base, since no value is ever read against it — a value there is a value of some member, and every member is closed. Such a consumer MUST therefore support naming a record template at a type position, recognising it by its constructor and dispatching on its discriminators, and MUST never need to read a field that carries a parameter: the selector names are stated on the record template for exactly that reason. It can check an application against either form on the same terms: each parameter’s recorded type, and its bound where it has one, says what an argument must be (§5.10). Every other entry a data document’s type can reach is closed, so the wire never presents substitution machinery to a data-path consumer, and a consumer that reads a !meta.template’s held text needs the schema parser §8.1’s resolved-form reading already requires of it.

1.4 Terminology

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.

  • A schema document is the document kind whose root value is tagged !schema ([TSON-DATA] §2.2) and whose body is a schema map (§2.1, §12.1) — the source artifact that is authored, published, hash-pinned, and resolved.
  • A schema is what a schema document defines: a named, immutable collection of declarations, identified by URL and referenced by the !!import directive.
  • A schema value is the resolved form of a schema: a value of the kernel’s meta.schema type, the map from meta.type_name to meta.declared (§4.1, §9) — each entry mapped to the value it resolves to — produced by resolution and optionally serialized as a data document (§8). Schema values are derived artifacts, never schema sources (§10.1).
  • A namespace, of a document, is the set of names it can resolve: its own entries and every entry of its import closure (§3.3). meta.namespace is the kernel’s constructor of the map types keyed by names (§4.1); which is meant is always clear from the spelling.
  • A kind is, of an entry, ATOM, PRODUCT, SUM or ALIAS, derived from its value (§4.1, §8.1); of a reference over a constructor, the set of types it admits (§4.2): meta.type_ref names every type a value can have, meta.identifier_ref every identifier family, meta.ref<meta.record> every record type.
  • A reference is a name held in a slot: a type’s name, a value’s qualified name, an application, or a sibling reference (§4.2, §5.12). An entry whose value points to another entry is an alias (§8.3), not a reference.
  • The kernel is the pre-loaded schema document meta.tn (§3.4, §9): the type system’s vocabulary and the atoms it needs.
  • The schema grammar is the body grammar of schema documents (§12.1); the type-definition grammar is its declaration right-hand side — the type-def production that each name => type-def declaration activates.

This document names a kernel entry by its path from the kernel’s root, as a schema writes it (§3.3): meta.record, unicode.text_type, numeric.count. The kernel’s root entries — void, boolean, integer, int64, text, bytes, iri, iri_reference, regex, doc, enum, set and schema — are written as they are declared. Where a sentence names a kernel constructor by its last segment alone — record, array, map, choice, alias, template — it abbreviates the path. A declaration written meta.x => … in prose stands for the member x => … of the map meta; no grammar declares a dotted name (§2.2.3).

1.5 Companion Artifacts

This document is published with eleven companion artifacts. The normative artifacts are pinned by content hash at publication (§13.2). Per §3.4, implementations pre-load the kernel as an in-memory structure; the artifact document is a description of that structure, and the in-memory model is authoritative.

ArtifactStatusContent
meta.tnNormativeThe kernel: the type system, pre-loaded, and the atoms it needs (§3.4, §9)
numeric.tnNormativeThe numeric library: the exact and approximate tiers and the sized integers (§9)
temporal.tnNormativeThe temporal library: the five RFC 3339 families (§9)
net.tnNormativeThe network and identifier library: the address, network, host-name, host, media-type, URI, UUID and email families (§9, [TSON-DATA] §5.5)
core.tnNormativeThe bundle: imports the three libraries, and declares the author annotations (§6, §9)
policy.tnNormativeThe processor policy’s vocabulary: the identifier and token policies and the resource limits ([TSON-DATA] §8.2, §9.1; §11.5)
meta-resolved.tnNon-normativeResolver-output fixture for the kernel (§8)
numeric-resolved.tnNon-normativeResolver-output fixture for the numeric library (§8)
temporal-resolved.tnNon-normativeResolver-output fixture for the temporal library (§8)
net-resolved.tnNon-normativeResolver-output fixture for the network library (§8)
core-resolved.tnNon-normativeResolver-output fixture for core (§8)

1.6 A Complete Example

A schema document declaring four types, importing core.tn:

!!id:"https://example.com/task.tn"
!!import:"https://tson.io/2026/39/m/core.tn"
@doc:"Task-tracking example schema."
!schema {
  priority   => !integer ^ { min: 1  max: 5 }
  title_text => !text ^ { min_length: 1 }
  status     => !enum [OPEN ACTIVE DONE]
  flagged    => <T, N> { entry: T  priority?: priority ~ N }
  task => {
    id:        uuid
    title:     title_text
    priority?: priority ~ 3
    status?:   status ~ OPEN
    due?:      date
    tags?:     [text]
    history?:  [flagged<status, 2>]
  }
}

priority and title_text refine the kernel’s integer and text instances (§5.5) — no bundled library declares a bounded refinement of either, a bound being a line the schema that wants it writes for itself (§9); status applies the enum constructor, the kernel’s root entry, reached through the core import’s closure (§3.3); task is a fresh record whose field types resolve in the same namespace — uuid and date from net.tn and temporal.tn through core, priority, title_text and status from the local declarations — and id and title must be written, priority and status may be omitted and are then injected, due, tags and history may be omitted and are then missing (§5.2). flagged is a record template — a meta.record_template with two parameters, one a type parameter and one a value parameter (§5.10): a fresh record whose priority field is defaulted by parameter (~ N on a marked name, as a literal default is written, §5.7), built entirely from names in the schema’s own namespace, and a family base a position may name. flagged<status, 2> is its fully-bound application, which the resolver materialises as its expansion once per distinct application (§8.2); history wraps it in the plain array sugar. A data document imports the schema with !!import (§7.1) and instantiates its types:

!!import:"https://example.com/task.tn"
!task {
  id:       550e8400-e29b-41d4-a716-446655440000
  title:    "Ship revision 39"
  priority: 3
  status:   OPEN
  due:      2026-08-01
  tags:     [spec editorial]
  history:  [{ entry: OPEN }  { entry: ACTIVE  priority: 4 }]
}

Resolution derives a schema value from the schema document, optionally serialized as resolver output — a data document with the source’s header, in which every entry is its value, desugared to the canonical !C { bindings } form with its chain computed and its record fields a map keyed by name (§5.6, §8): priority => !numeric.integer_type { chain: { supertypes: [integer] } min: 1 max: 5 }, task => !meta.record { fields: { id => !meta.record_field { type: uuid } … } }, flagged => !meta.record_template { … }. The companion fixtures (§1.5) and [TSON-GUIDE] give a full worked resolver-output example.

2. Schema Documents

A schema document is the source artifact of the schema layer (§1.4). This section defines the document’s structure and the operation of its header directives. How a data document’s !!imports bind it is defined in §7.1.

2.1 Schema Document Structure

A schema document is a header followed by a braced map of declarations — the schema map — tagged !schema. The header carries the schema’s identity (!!id, first line; optional in the grammar, required for publication and hash-pinning, §2.2.1) and its dependencies (!!import, repeatable, declaration order carrying nothing); annotations that bind to the schema sit after the header, immediately before the tag. Header order and cardinality are grammar productions, not conventions ([TSON-DATA] §2.2, §3.3; §12.1):

!!id:"https://example.com/people.tn"
!!import:"https://tson.io/2026/39/m/core.tn"
@doc:"Minimal example schema."
!schema {
  person => { name: text  age: integer }
  employee => person & { department: text }
  status => !enum [ACTIVE INACTIVE SUSPENDED]
}

Entries are separated like data-map entries — whitespace or a comma, and a comma may follow the last ([TSON-DATA] §2.4) — and the map MUST contain at least one entry (§12.1). Each declaration binds a type name to a type definition with the => operator — a compound token the lexer already emits; the schema grammar introduces no reserved words — its operators are the reserved special tokens of the shared lexer ([TSON-DATA] §7.2.5).

The root tag classifies the document and names the kernel’s schema. A document is a schema document exactly when its root value is tagged !schema ([TSON-DATA] §2.2): classification reads the header, the root’s leading annotations and its tag, never the root’s value, so the schema map’s own grammar is never parsed to classify it. The tag names the pre-loaded kernel’s root entry schema, an alias of meta.schema (§4.1, §9), whatever the document imports or declares — classification and the choice of the payload’s grammar precede reading any import, and the pre-loaded kernel gives the one answer a lookup would. The tag is read as a spelling; a nested map may also be tagged !meta.schema (§4.1). A schema parser refuses an untagged root, and a Class 1 processor rejects a tagged one with a categorized diagnostic ([TSON-DATA] §1.5, §8.1).

The body is a map, in syntax and in semantics. The document resolves to a value of the kernel’s meta.schema type — the map from meta.type_name to meta.declared, each entry mapped to the value it resolves to (§4.1, §8, §9) — and the braces carry the document’s annotation anchor: annotations precede the value they bind to ([TSON-DATA] §3.1), and the schema map is the value the document-level annotations bind to. A schema map may be nested as an entry: geo => !schema { address => { … } } declares its members as entries of the document, each named by its path from the root, geo.address (§4.1, §5.12). Resolver output writes the root !schema and every nested map !meta.schema (§8.1).

Annotation binding. Annotations immediately before the !schema tag bind to the schema — the header directives carry no annotations ([TSON-DATA] §7.4 gives them no annotation slot). Inside the braces the data-map convention applies unchanged ([TSON-DATA] §3.1): an annotation immediately preceding a declaration’s name binds to the key — the name itself (§6) — and annotations after => and before the type-def bind to the type definition. Resolver output preserves the placement (§8).

2.2 Directives

[TSON-DATA] §3.3 defines the directive set: two names — id and import — each legal only at fixed positions in the header, with order and cardinality enforced by the grammar. The set is closed: any other directive name, or a legal name outside its position, is a parse error. There is no directive registry and no unknown-directive category.

This document defines the directive operations — what each directive means to the resolver. Every directive value is an IRI-reference ([TSON-DATA] §3.3); these are logical identifiers resolved through the schema library (§10), never fetch instructions.

OperationDirectivePlacement ([TSON-DATA] §3.3)Defined in
Identity declaration!!idfirst header line, either kind; optional in the grammar — required for published schemas§2.2.1
Import!!importheader, after the optional !!id; repeatable; either kind§2.2.3 (schema documents), §7.1 (data documents)

2.2.1 The !!id Directive

!!id declares the authoritative identity of a document — the name other documents use to reference it, compared as a canonical identity ([TSON-DATA] §2.2.1). It connects the file’s content to its logical name in the schema library (§10) and anchors content addressing: the hash input for a document’s content hash is every byte after the id line ([TSON-DATA] §2.2.1; §10.2).

!!id is optional in the grammar and, when present, must be the first line. For schema documents it is required by policy, not grammar: publishing a schema — registering it under its own name for reference by other documents, or pinning it by content hash — REQUIRES !!id. An id-less schema is a development artifact; it may be registered under an application-supplied URL (§10.1) but has no published identity and cannot be hash-pinned.

2.2.2 Retired: the governing relation

Revisions before 39 bound a schema document to a governing meta-schema with a !!meta directive, and the kernel to itself; constructors could be declared only in a document so governed, and a document’s names resolved in two namespaces by position. The relation is gone: a schema document imports the schemas whose entries it names, the kernel among them, in the one namespace §3.3 defines, and any schema may declare a constructor (§4.2). !!meta is a parse error ([TSON-DATA] §3.3). This number is kept so that citations into §2.2.3 stand.

2.2.3 The !!import Directive

!!import imports a schema’s entries into the importing document. The directive value is an IRI-reference identifying a published schema document; every target MUST be a schema document — one whose root is tagged !schema — and a target that is not fails to parse as one, so the importer fails to load. The directive loads the referenced schema and makes its entries available as if they were declared in the importing document: imported entries are available to all local declarations (including for recursive references), and local declarations may refine, compose with, apply or instantiate imported entries.

Imports are transitive. An !!import contributes the imported schema’s entire namespace — the entries it declares and the entries it imported. A document’s namespace is therefore flat: one name denotes one entry throughout a resolution, and every name in the import closure is reachable by the data documents that import the schema, so the types §7.2’s subsumption admits at a position are always nameable where the position is used.

Multiple !!import directives are permitted. Collisions are decided by entry identity, not by name occurrence. A declared entry’s identity is its declaring document and its path from that document’s root (§4.1, §8.2); a synthetic entry’s is its form (§8.2). One schema reached through more than one route contributes its entries once — re-arrival unifies, so the diamond (two imports that each import meta.tn) is ordinary rather than an error, and naming one schema twice, or under two spellings of one canonical identity ([TSON-DATA] §2.2.1), is redundant rather than a conflict. A ?sha256= pin is verification metadata ([TSON-DATA] §2.2.1, §10.2), never part of identity: routes that disagree about pinning still name one schema, and a pin disagreeing with that schema’s content is a verification failure, not a second identity; two distinct pins for one identity in one closure are a resolver error, since at most one matches the content. Order carries nothing: the collision rule leaves no name for an earlier import to win. It is a resolver error for two different schemas to declare the same name anywhere in the import closure, or for a local declaration to reuse a name the closure already binds — a schema map excepted, which is open (below), though a map’s name collides with any entry of that name that is no schema map — no hiding and no redefinition: the schema fails to load, and the diagnostic names both declaring schemas. One synthetic form minted by two documents is one entry of a closure holding both (§8.2).

A nested schema map is open. A schema map of one path declared by several documents of a closure (geo => !schema { … } in two imports) is one map, the union of their members: each document contributes the members it declares, and a member path two of them declare collides as any name does, by identity. The annotations written on a map’s entry are its declaring document’s. A document contributes by declaring the map itself; no grammar declares a dotted name. The kernel’s own maps are open on the same terms: numeric.tn adds its members to the kernel’s numeric and net.tn to the kernel’s net, and an importer of either sees one map holding both documents’ members (§9).

Two costs of the flat namespace are deliberate and worth knowing in advance. Every name in the closure is reserved — a schema importing core.tn reserves the kernel’s thirteen root names, the libraries’ and core.tn’s roots and the map names meta, atom, numeric, unicode, net, binary and temporal, with no aliasing to escape with; that is the price of a bare name meaning one thing everywhere, and it is why the bundled documents keep their roots to the vocabulary an author writes and their machinery under those maps (§9). And some conflicts are unfixable by the importer: if two imported schemas transitively reach different schemas that both declare id, the importing schema is rejected over names it never wrote and can only be repaired upstream. One consequence follows without a separate rule: schema identities carry the specification revision, so an import closure reaching two revisions of one library is rejected at namespace construction where the two declare a common name — the collision rule firing on genuinely different documents — rather than allowing two identically-spelled types to coexist and surface later as an inexplicable field conflict; nothing defines a revision for an arbitrary identity, so no rule refuses two revisions otherwise.

Merged entries keep their home resolution. An imported entry’s internal references — alias targets, field types, the parents in its chain, its name_type — are names in its defining schema’s namespace, fixed when that schema was resolved; the merge does not re-resolve them against the importer. Operationally, the library holds one resolved namespace per schema, and a consumer following an imported entry’s references follows them in the defining schema’s resolution (§3.4.1).

Import references MUST be acyclic. A schema’s reference closure — every document reachable through !!import — is resolved dependencies-first (§3.4.1), and a cycle admits no such order: an !!import reaching, directly or transitively, a schema whose resolution is in progress is a resolver error — import cycle — reported with the cycle path. The prohibition costs nothing in practice: a hash-pinned cycle is unauthorable, since each document’s pin would have to be computed over bytes that already contain the other’s, so a cycle can arise only among unpinned development references. Mutual coupling has one sanctioned route: shared types belong in a third schema both partners import.

The imported schema MUST itself be a valid schema document, resolved independently.

3. Schemas and Namespaces

A data document never resolves tags against definitions of its own: it has none, and every tag (!name) in it resolves against the namespace its !!import directives bind. A schema document resolves every name it carries — the constructor heads it applies, the types it names at type positions, the sources it refines and the operands it composes, a reference’s of, a template bound, an annotation — in one namespace: its own entries and every entry of its import closure (§2.2.3). There is no second store consulted by grammar position, and no document on a rung above another: the kernel is a schema document like any other, reached by import, and pre-loaded only because its own declarations wait on each other (§3.4).

3.1 The Import Chain

Every TSON document that uses tags sits on a chain of imports. Each rung binds the same relation — resolve my names against X — with the one directive:

data document
  └─ !!import → schema
                  └─ !!import → library (numeric.tn, temporal.tn, net.tn)
                                  └─ !!import → kernel (meta.tn, pre-loaded)

A data document’s !!imports name the schemas its tags resolve against. A schema’s !!imports bring in the entries its declarations name — a library’s, or the kernel’s directly. The bundled libraries each import the kernel alone; core.tn imports the three libraries so that one import brings every bundled type (§9). The kernel imports nothing and resolves to a pre-loaded schema object (§3.4).

3.2 Schema-Value Separation

A published schema is an immutable artifact: once published at a URL with a content hash, its declarations do not change (§3.5). Because a data document’s tags always resolve against imported schemas, !text in a given document means exactly what the imported closure declares it to mean — two documents importing the same schemas have identical type vocabularies. A document with no !!import directive has no type vocabulary: base type resolution and the built-in annotations of [TSON-DATA] apply there, and only there (§7.1).

3.3 One Namespace

The namespace of a document is its own entries and the entries of its import closure (§2.2.3) — nothing more, and one set. Every name a document carries resolves there, and the only lookup that precedes it is a template’s own parameter list (§3.3.2). A name resolves from the root: a one-segment name names an entry of the root namespace, and each further segment of a qualified name selects a member of the nested schema map reached so far (§5.12). Inside a nested map a sibling is written by its path, geo.address, and a bare address names the root’s address or fails to resolve; there is no relative lookup, so there is no shadowing rule.

3.3.1 Constructor Roles

Some grammar positions are constructor roles: the name there MUST resolve to a constructor or an ordinary type (§4.1), and nothing else is admitted.

  • Constructor-application heads — the name after ! when no ^ follows (!enum [...], !numeric.integer_type {}, !net.uuid_type {}), resolved in the document’s namespace: a miss is an unresolved-constructor error, and the found entry MUST be meta.top or IS-A it, or an ordinary type (§4.1) — a hit whose entry is any other type builds a value of that type (§4.1, §5.5), which is an entry no type position names — a braced payload at an atom instance, no value of it, being a resolver error naming the refinement form, !name ^ { ... }. IS-A stops at construction, so the predicate separates a constructor from a part of one: meta.top, meta.type, the base kinds, every constructor beneath them, meta.namespace, and the kernel’s meta.alias and meta.template are constructors by that measure and may be applied — !meta.alias { target: T } is the explicit spelling of the alias T (§8.3), open or closed; !meta.template { … } and !meta.record_template { … } are resolver vocabulary a source declaration never applies directly (§8.1); !schema { … } and !meta.schema { … } are the application of a namespace type that declares (§2.1, §4.1); meta.top, meta.type and the base kinds are abstract (§4.1), so a direct application of one is refused as any abstract record’s is, naming the subtypes that would satisfy it — while meta.record_field, meta.record_template_field, meta.template_param, meta.tuple_element, meta.field_group, numeric.integer_size, atom.specification and meta.type_chain are records that are no constructor, and an application of one is a value (§4.1).
  • An alias of a constructor is applied as its target is: the resolver’s read of a declaration follows its head’s alias chain and reads the value against the constructor at the end, in a held template body as anywhere (<T> !my_enum [a T] over my_enum => enum builds an enumeration).
  • The implicit desugar targets of the sugar forms — [T] and the sized forms to meta.array, [T, U] to meta.tuple, (A | B) to meta.choice, {K => V} to meta.map, { … } to meta.record, a partial application to meta.alias (§5.3, §5.6, §8.3) — are the kernel’s entries by path, grammar-supplied and never author-written, and are targets whatever the document imports: a document importing nothing may write { a: b } and apply nothing else.
  • A refinement source — I in !I ^ { values } — resolves in the document’s namespace and MUST be a value whose type states refinement rules (§5.5, §5.7): an atom-family instance (integer, carrying !numeric.integer_type {}), the kernel’s reference instances, and nothing whose type states none.

Local and imported declarations participate alike at every role: a document may apply a constructor it declares itself, in the wait order of §3.4.1. A generic-application head (name<args>) resolves in the same namespace — parameters, then the document’s entries — and an unresolved head is an unresolved-type error; a head resolving to a parameter is a resolver error (§5.10’s no-head-abstraction boundary), diagnosed rather than applied. Implementations SHOULD emit a migration diagnostic when a generic head fails resolution but matches a parameterless constructor, suggesting the sugar spelling or the !C { … } form (map<text, text> → “did you mean {text => text}?“).

3.3.2 Lookup Order

A name used at a type position — a field type, a type argument, a choice variant, a composition target, a refinement source, a reference’s of, a template bound — and a generic-application head resolve by walking, in order:

  1. Parameters of the enclosing template (§5.10).
  2. The document’s namespace: its own entries and its import closure, one flat set (§2.2.3), a qualified name walked from the root (§3.3, §5.12).

Name collisions across the import closure and with local entries are decided by entry identity (§2.2.3): one schema reached twice contributes once; two different schemas binding one name reject the load. A kernel name is reachable only where the closure holds the kernel: an import-free schema (!schema { a => { b: b } b => {} }) is valid, sees its own names, and may declare int64 or schema as entries of its own, though its root tag still names the pre-loaded kernel’s (§2.1).

3.3.3 Annotation Resolution

An annotation @name or @name:value resolves in the annotating document’s namespace — for a data document, the closure its !!imports bind; for a schema document, its own entries and its import closure — resolved from the root as any name is (@doc the kernel’s root entry, @atom.ordering a member of the kernel’s atom map, @title core’s root entry). An annotation declared locally in a schema document is usable in that schema document and in the data documents that import it alike: there is one namespace, and no rung for the one to be on that the other is not.

Where annotation types live follows: the kernel declares doc, meta.annotation, meta.synthetic, meta.application, meta.disjoint and the classifying atom.ordering, atom.bounded, atom.exact and atom.numeric; core.tn declares the author annotations deprecated, title, comment, examples, read_only and write_only; a schema declares any other it wants, under a name its closure leaves free (§6, §9).

3.3.4 Data Documents and Namespaces

A data document’s !!imports bind one namespace — the union of the imported schemas’ closures, collisions decided as §2.2.3 decides them — and that namespace is the document’s entire vocabulary. Tags and annotations resolve against it; nothing else is reachable. If a data document needs a kernel type like text, or uuid from net.tn, some schema it imports must have those in its closure.

A consequence: the kernel’s vocabulary is visible from data exactly where it is imported. A data document importing a schema that imports the kernel may write !meta.record { … } — and resolver output is exactly such a document (§8), read under the kernel it was resolved against. A data document importing a schema whose closure lacks the kernel cannot.

A family is open across schemas, and is judged over the closure that holds it. A schema’s namespace is its declarations plus its imports, so a record a schema imports may gain subtypes there: the subtypes index is computed over the namespace of the schema being resolved (§8.1), so an importing schema that composes onto an imported record declares a further subtype of it, a further member of its family where it is the base of one (§5.2). Nothing closes a family at its declaration. What a family is is therefore relative to a closure, and its checks run over every member the closure holds: two members brought together only by an import merge — each from a schema that linked cleanly — are judged where they meet, and a pair that collides is the importing schema’s resolver error, reported with both origins named. Judging only the families with a member declared locally would let that schema load and dispatch on a mapping that is not a function.

3.3.5 Retired: duplicate names across layers

Revision 38 admitted a type name declared in both the meta layer and a type library, the two being consulted at different grammar positions, and core.tn declared same-named copies of kernel entries. With one namespace (§3.3) there is no position at which two entries of one name are told apart: two same-named entries in one closure are §2.2.3’s collision, and the bundled documents declare no name twice (§9). This number is kept so that citations stand.

3.4 The Kernel and the Resolution Order

The chain of imports grounds in the kernel, meta.tn: one schema document whose header is its !!id alone, which imports nothing, and whose declarations reference each other — meta.record is declared as a record. Its entries are pre-loaded into the schema library by the implementation: they exist as in-memory structures before any document is parsed, and the kernel is never resolved from a document. A document claiming the kernel’s identity is checked against the bytes the implementation ships (§10.2), and the kernel a processor pre-loads MUST be the revision it was built for. When the kernel document is read, its tags resolve against those pre-loaded structures through the normal library lookup, and the document’s description must match the implementation’s in-memory model; if they disagree, the document is invalid — the in-memory model is authoritative.

The kernel’s names reach a schema by import only: a schema whose closure holds the kernel writes text, enum, meta.record and unicode.text_type as any other imported names (§3.3), and an import-free schema sees none of them.

What is built by hand is the kernel’s one wait cycle. Under the order below, a constructor whose declaration applies a type of its own document, and a type whose declaration reads a value of that constructor, wait on each other; the kernel holds exactly one such cycle, the entries around meta.record, meta.map, meta.field_map, meta.record_field, enum, boolean, integer, text, numeric.count and the records, sets and references their declarations read (§9). The hand-built set is that cycle; every other kernel entry is read by the rules below, and the kernel’s resolved form MUST come out of them unchanged.

3.4.1 Resolution Order

Schema resolution proceeds per schema as Parse → Desugar → Pass 1 → Pass 2, with imports fully resolved before any phase runs on the importing schema.

Desugar is purely syntactic and per-declaration: sugar forms rewrite to canonical constructor applications, and every nested form lifts to a synthetic declaration — closed for a concrete form, open for a parameter-bearing one (§5.3) — entering Pass 1 alongside declared names; a nested !schema map’s members enter as entries named by path (§4.1). A template declaration contributes its own name plus one synthetic per open form it holds.

Pass 1 — Name population. The resolver collects all declaration names in the schema document, synthetic declarations included. The schema’s namespace is populated with skeleton entries keyed by path. Bodies are not yet validated.

Pass 2 — Body resolution and validation. The resolver resolves each entry’s body against the populated namespace, in the wait order below. It validates that references resolve, that composition and refinement rules hold, that type arguments match parameter arities and types (§5.10), that every record’s and enum’s names are values of its name type (§5.2, §7.4), and that every name at a reference-typed slot is admitted by the reference (§4.2). It computes each type’s transitive IS-A chain (chain.supertypes) and derives the inverse (chain.subtypes) (§8.1).

The wait order. Within a schema, the order in which entries are read is fixed by what each needs, and never by the order its declarations are written in. Terms: a form is a structure serialized data takes, read by an algorithm the processor owns — an atom (one token), a record or tuple, an array or map, a choice; a constructor links a name to the data a form needs to be read; a declaration is an entry’s value, the data its constructor describes; an entry is ready when its declaration has been read, and a type can be read against exactly when it is ready. Seven rules, the first the rule and the rest its consequences:

  1. The rule. Reading a value against a type T waits on T being ready, and on each type a part of the value is read against as it is reached: a field’s type, an element’s, a nested ! tag’s, an annotation’s. An entry’s declaration is read against its head (C in !C { … }, the source’s constructor in !I ^ { … }) and waits as well on each refinement source and & operand, since it starts from their declarations. An alias is ready when its target is; a closed application when its materialised body is.
  2. A reference orders nothing. A name at a slot typed by a reference (§4.2) — a field’s type, an argument, a reference’s of — is read by the atom’s grammar alone; what it names is resolved and judged once every entry is ready (§4.2, §8.3). So tree => { t: tree }, mutual recursion, and a constructor whose facet is typed by its own instance (numeric.decimal_type => meta.atom & { min?: number … } beside number => !numeric.decimal_type {}) are sound.
  3. A held value orders nothing. A field’s default or pin, a dependent field’s value (§5.2), and a template’s body hold what a schemaless read gives ([TSON-DATA] §4), and bind as written: a pin or default is not decoded through its type when the reader carrying it is built, but judged against its type once every entry is ready, still at load (rule 6). Where a declaration names the type a form reads against, a read reaches that type by rule 1, the name itself ordering nothing: a value of meta.field_map reaches meta.record_field, which field_map pins as its value_type.
  4. A cycle is refused. Entries waiting on each other through rule 1 are a resolver error naming the entries on the cycle in wait order ('a' waits on 'b' waits on 'a'). A composition cycle (a => b & {}, b => a & {}), a constructor applying a constructor that applies it (c1 => !c2 {}, c2 => !c1 {}) and a type annotated with itself on its value (note => @note:"x" text) are each this rule. A loop of aliases is not a cycle under this rule; §8.3 refuses an alias chain with no terminal. The entries built by hand (§3.4) are ready before any declaration is read.
  5. Order independence. Every order consistent with rule 1 gives one verdict and one resolved output, and the order entries are written in is not consulted. Forward references within a schema need no rule: a definition may reference any other declaration in the same schema, declared earlier or later.
  6. Everything closes at load. Every entry is ready, every reference and held value judged and every reader built before the schema is used. No wait is left for a data read to discharge, and every type is known open or closed and usable in data once the schema has loaded. A judgement that walks a name to what it names — §5.8’s one name type over a composition’s supertypes, a reference’s membership — waits for every entry to be ready, so walking it never makes an entry wait on one that waits on it.
  7. Limits. The wait depth of an entry is none where it waits on nothing of its own document and one more than the deepest entry it waits on otherwise, an import being ready before the resolution starts; §11.5’s wait-depth row bounds the longest chain in one document, default 64, a refusal and not a verdict. Template expansion needs no bound of its own: §5.10.1’s regular recursion and §11.5’s materialisation depth bound it.

With count => !integer ^ { min: 0 }:

declarationwaitsverdict
tree => { t: tree }meta.record; t’s type is a referencesound
uint8 => !integer ^ { min: 0 max: 255 }integer (source); min and max read on integersound
point => { x: int32 next?: point ~ { x: 1 } }the default is heldsound
integer_type.multiple_of?: integera step read on integersound: the kernel
integer_type.min?: countcount’s min: 0 read on count, on integer, on integer_typecycle: refused
a => b & {}, b => a & {}a on b on acycle: refused
c1 => !c2 {}, c2 => !c1 {}c1 on c2 on c1cycle: refused
a => b, b => anoneno cycle; refused by §8.3

Imports run first. Resolution is defined over the schema’s reference closure: the set of schema documents reachable through !!import references, collected transitively. The order is:

  1. Collect the closure. Starting from the schema under resolution, follow every !!import reference, deduplicating by canonical identity ([TSON-DATA] §2.2.1): each canonical identity names one schema, loaded once, however many references reach it.
  2. Verify per identity. Verification attaches to canonical identities, not to individual references: for each identity in the closure, collect the set of digests declared across all references reaching it. An empty set resolves without verification. A single digest is verified against the identity’s content per §10.2 — once, at this step, before any schema in the closure resolves — and every reference to the identity, pinned and plain alike, thereafter resolves to the verified instance; a verification failure is a resolver error for the identity across the whole resolution, and no reference falls back to the rejected content. Two or more distinct digests for one identity is a resolver error (§10.2) — at most one can describe the real bytes.
  3. Order the closure. The reference relation over the deduplicated closure MUST be acyclic (§2.2.3); the resolver computes a topological order — dependencies first — and reports the cycle path as a resolver error where none exists.
  4. Resolve in order. Each schema in the closure is resolved exactly once, in topological order, by its own passes; when a schema’s turn arrives, every schema it references is already resolved. For each schema: merge each !!import’s entries into the accumulating namespace — merged entries stay bound to their defining schema’s namespace (§2.2.3), and collisions between imports are resolver errors (§2.2.3); then Desugar rewrites sugar and lifts synthetics, then Pass 1 collects local entry names, a collision between a local name and an already-merged import being a resolver error (synthetic names are resolver-chosen and fresh by construction, so they never collide with declared names; a synthetic arriving through an import whose identity an own application re-derives denotes the merged entry, §8.2); then Pass 2 resolves bodies in wait order.

The pre-loaded kernel (§3.4) enters the closure already resolved and occupies the leaf of every order. Collection follows references only into schemas not yet resolved: a reference to an already-resolved schema terminates there. Digest sets (step 2) are collected over the references observed during this collection: a terminating reference still contributes its digest to its target identity’s set, while an already-resolved schema contributes no outgoing references — its dependencies were verified when it was resolved (or shipped, for the kernel, whose pin is checked against the implementation-held digest per §10.2). Whether a dependency happens to be pre-loaded therefore never changes whether a closure loads.

3.5 Schema Evolution

Each published schema version is immutable; a schema’s identity is its exact byte content. Version N and version N+1 are independent artifacts with different content hashes and different URLs. TSON defines no version negotiation, migration rules, or compatibility checks; accepting data validated against multiple schema versions is a deployment concern. See §7.2 for the records-are-closed rule.

4. The Type System

The type system is defined by the kernel’s vocabulary — meta.top, meta.type and the base kinds, the constructors, and the supporting records — and every construct of the type-definition grammar (§5) is notation over it: each declaration form resolves to its value, the canonical constructor form !C { bindings } (§5.6, §8), and a schema map is a map from each name to the value its entry resolves to.

4.1 Kinds

The kernel defines meta.top as the structural root, meta.type as the kind of the kinds that describe data values, three base kinds — meta.atom, meta.product and meta.sum — each composing with meta.type, two constructors that compose meta.top directly, and one refinement of meta.map:

top       => abstract {}
type      => abstract meta.top & { chain?: meta.type_chain }
atom      => abstract meta.type & {}
product   => abstract meta.type & { access_pattern: meta.product_access_type  size_type: meta.product_size_type }
sum       => abstract meta.type & {}
alias     => meta.top & { target: meta.type_ref }
template  => meta.top & { parameters: [meta.template_param]  template: text }
namespace => meta.map ^ { key_type: meta.identifier_ref  voidable?: = false }

An entry is a type exactly when its value is built by a constructor IS-A meta.type. Every constructor of a data value’s shape composes, directly or through another constructor, with one base kind, and each base kind IS-A meta.type IS-A meta.top — so every such constructor transitively IS-A meta.type and IS-A meta.top. Two kernel constructors compose meta.top directly and not meta.type, so their instances are not types: meta.alias, whose instances are hops (§8.3), and meta.template, whose instances are held bodies (§5.10). IS-A does not extend below construction: !T {} transfers kind, not supertypes (§5.5), so constructor instances (integer, meta.declared) and fresh records (person) carry no supertypes and are not IS-A meta.top. That boundary is what makes IS-A meta.top, or being meta.top, the predicate for being a constructor: an entry that is meta.top or IS-A it describes a type and may be applied with ! unless it is abstract (§3.3.1, §4.2). Any other entry’s value is a value, and the entry is a value entry: name => !T { … } with T a constructor IS-A meta.type builds a type, with T meta.alias an alias (§8.3), and with T an ordinary type — a record, a namespace type, any type of the closure — builds a value of T, read against T as a data value is and marked by nothing but its tag. orders => !api { … } and create_order => !operation { … } are value entries; a value entry is admitted by no reference over a constructor (meta.type_ref excludes it unmarked), nothing is typed by it, and a reference over T names it where its type IS-A T (§4.2, §5.12). A value entry’s payload lifts and mints nothing: a reference-typed slot in it holds one bare name, and an application there is refused, the alias spelling named (§5.6). A template over an ordinary type closes to a value entry.

A constructor is an ordinary record type. A constructor’s declaration is a record — net.uuid_type => meta.atom & atom.specification & { … } is a value of meta.record whose instances are declarations — so at a slot that names a type nothing sets it apart: meta.type_ref, meta.ref<meta.record> and meta.ref<meta.product> admit net.uuid_type, and meta.record, meta.top, meta.alias and meta.template with it (§4.2). A field typed by a constructor holds a declaration’s body as data, judged by its record shape alone — it is not resolved, so a derived field such as chain is admitted as written and no coherence rule runs.

meta.top, meta.type and the base kinds are abstract. None has instances of its own, so a direct application of one — !meta.top {}, !meta.atom {} — is refused as any abstract record’s is (§5.2, §12.1), the diagnostic naming the instantiable subtypes that would satisfy the position. Constructors beneath them stay OPEN: !meta.record { … } and !numeric.integer_type {} are direct instances, as are !meta.alias { … } and !meta.template { … }.

meta.namespace is a map constructor. A namespace type is an instance of it, as a map type is one of meta.map: meta.schema => !meta.namespace { key_type: meta.type_name value_type: meta.declared }, or a schema’s own api => !meta.namespace { key_type: meta.identifier value_type: method }. A value of one is the map its facets describe — every key a name of the identifier family key_type states, judged a member of meta.identifier_ref at link, no value void, ordered and the size bounds meta.map’s — and never a record of those facets. key_type is required: no host supplies a namespace’s key family. A namespace type is a product like any map type, admitted by meta.type_ref, meta.ref<meta.product>, meta.ref<meta.map> and meta.ref<meta.namespace>; and a qualified name enters a value of a namespace type and of no other map (§5.12). The kernel’s one is meta.schema.

Every value of schema declares. The rules for a schema document’s root map apply to every value of meta.schema: geo => !schema { address => { … } } is a schema map nested as an entry, whose members are declarations that lift, mint, link and compile exactly as root entries do — types, constructors, values, aliases, templates — each named by its path from the document’s root, geo.address. It declares at least one member and takes no parameters; its tag is !schema or !meta.schema, read as a spelling (§2.1). The map’s own entry is a value of meta.schema keyed by its members’ own names; a map of one path declared by several documents is the union of their members (§2.2.3); synthetics mint at the root (§8.2). A value of any other namespace type (!api { … }) is a value map, nothing in it lifting or minting.

A record’s fields are a plain map. meta.field_map => !meta.map { key_type: text value_type: meta.record_field ordered: true }: a field name is text within its own record, judged by the record’s name_type as discriminators and a group’s members are (§5.2), and the map is no namespace — no qualified name selects a field declaration (§5.12).

A type carries its IS-A chain. meta.type’s one field is chain, a meta.type_chain:

type_chain => {
  parents?:    [meta.type_ref]   -- the parents as written; a record template's, one of which may be an application
  supertypes?: [meta.type_ref]   -- the closed chain: every entry this one IS-A, complete, each a name without arguments
  subtypes?:   [meta.type_ref]   -- the inverse, over the namespace of the schema being resolved (§3.3.4)
}

parents is what was written and is the only list that ever holds an application: it is a record template’s slot (§5.10), kept on the template entry once its parents resolve — a closed instantiation keeps none — since <T> result<T> & { … } reaches an IS-A edge to result<text> and not to result<int32>. supertypes is the closed chain, complete to a fixed point through applied parents (§5.8), sorted, each a path without arguments, and empty for a fresh record, an instance and a subtracted record alike. subtypes is the inverse, over the entries of the schema being resolved. A declaration never writes chain — !meta.record { chain: { … } } in source is a resolver error, as writing meta.choice.disjoint is — and a constructor never declares a field of that name: a schema’s widget => meta.atom & { chain?: … } is refused where it is declared, the name being meta.type’s. The resolver computes the chain from composition and refinement and the linker inverts it; on reading a resolved form it is read as stated and never recomputed, because a flat body and its chain cannot be recomputed from each other (§8.1, §10.1). Provenance is not recorded: which operator a refinement used, and what a subtracted record was subtracted from, are what was written, nothing reads them after resolution, and the source document has them.

What is universal is kind membership — every closed type or alias entry has exactly one kind, derived from where its value’s constructor reaches the kernel’s order:

  • Atom — scalar types. An atom constructor composes with meta.atom; its record of constraint fields describes the narrowable vocabulary available to instances. The kernel and the libraries define the series’ atom constructors (§9); meta.void_type is the atom with no constraint vocabulary, and meta.reference_type the atom whose values are names (§4.2). Atom instances are produced by constructor application — !<ctor> {} (empty) or !<ctor> { values } — and refined with !<instance> ^ { values } (§5.5).
  • Product — structural types. meta.record, meta.record_template, meta.array, meta.set_type, meta.map, meta.namespace and meta.tuple compose with meta.product, fixing access_pattern and size_type; the container constructors expose their type slots as required meta.type_ref-typed fields (§4.2). Bare {...} definitions without explicit composition resolve to PRODUCT by structural default, and so does an entry composing with meta.type or meta.top alone.
  • Sum — types in which the value names its own type. meta.choice is the closed sum, enumerating its variants; meta.declared_type is the open sum, whose one instance meta.declared admits every type of the reading document’s closure (§7.8).
  • Alias — an entry whose value is a pointer to another entry: !meta.alias { target: T }, where target names an entry (§8.3). Aliases are aliasing relationships, not IS-A: an alias is a hop, stated as written in resolver output and collapsed only when a processor compiles for reading (§8.3). meta.alias composes with meta.top directly and is applicable (§3.3.1); its entries take the ALIAS kind from the alias form rather than from a base kind, and carry no chain.

A kind is derived, never recorded. An entry’s kind is its value’s type’s kind — a fact the value’s tag and chain already determine — and resolver output does not write it (§8.1): a consumer derives it by the rule stated there, reading meta.atom, meta.product or meta.sum off the chain by which entry of the kernel it reaches, each counted only as the kernel’s own entry, so that a schema’s own record named product, composed into payload => product & { … }, is PRODUCT by structural default — a value conforms to where it is defined. An open entry — a meta.record_template, or a held meta.template (§5.10) — is a type where it is a record template and otherwise has no kind until it closes; the derivation names a held body TEMPLATE for the resolver’s own bookkeeping, and a closed instantiation takes its expansion’s kind. A value entry takes no kind of its own: it is a value, and the question a kind answers is asked of its type.

4.2 Type Construction

Type constructors are factories that produce types. A constructor is meta.top or an entry that IS-A it (§4.1): the root, or one that composes with meta.type, a base kind or another constructor, or refines one — the position in the IS-A chain is what makes it applicable with !, and nothing is asserted by an author beyond the composition itself. There is no marker and no constructor field: constructorness is inherited through composition, derivable from a type’s chain wherever it is wanted (§8.1), and never a property a declaration states apart from its chain. Any schema may declare a constructor — a document whose closure holds the kernel composes meta.atom, meta.product or meta.sum, or refines or composes a constructor it imports, and the entry is a constructor by its chain; a document whose closure lacks the kernel has nothing to compose and declares none. There is no construction in data: !C { bindings } produces a new type only in the schema grammar (§7.2). The kernel’s container constructors are parameterless: a type slot (meta.array’s element_type, meta.map’s key_type and value_type) is an ordinary required field typed meta.type_ref (below), filled by the construction — or by the sugar’s desugaring (§5.3) — the way data supplies any required field, so a closed application’s binding record is ordinary data of the constructor’s vocabulary (§7.2). Where a parameter must route into a slot, the route arises at the application site inside a template body (§5.10), never in a constructor’s own declaration. A constructor’s parameters are read as any template’s are (§5.10): each takes its type from the slot it stands in, so a parameter in a slot typed meta.type_ref takes a type where one in an atom-typed slot takes a value, and no rule confines a constructor’s parameters to one channel.

References: one atom whose values are names. The kernel declares one atom whose values are names, and every slot that names a type or a value is typed by an instance of it:

reference_type => meta.atom & { of: meta.type_ref  allow_sibling?: boolean ~ true }
ref            => <T> !meta.reference_type { of: T  allow_sibling: false }   -- the inline spelling: meta.ref<method>, meta.ref<meta.record>
field_type_ref => !meta.reference_type { of: meta.type }                     -- a record field's type: a type's name, or a sibling reference
type_ref       => !meta.field_type_ref ^ { allow_sibling: false }            -- the names of the types a value can have
identifier_ref => !meta.type_ref ^ { of: unicode.identifier_type }           -- the names of the identifier families
  • The predicate. A value of a reference whose of is T is a name, and conforms exactly when the entry it names, past the entry’s alias hops (§8.3), holds a value whose type is T or IS-A it. Every entry is a name and a value, and the value has a type, its tag: order => { … } holds a value of meta.record, int32 => !integer ^ { … } a value of numeric.integer_type, orders => !api { … } a value of api. A field’s type position, a meta.ref<meta.record> slot and a meta.ref<method> slot all admit a name by that one predicate, and differ only in of.
  • The slot’s meaning comes from of, never from a spelling. Where of is a constructor — meta.top or IS-A it — the values it admits are types, and the slot holds a type’s name: judged a member over the reading document’s closure, and taking part in structural identity (§8.2). Such a slot’s admitted set is a kind: meta.type_ref is the kind of the types a value can have, meta.identifier_ref the kind of the identifier families, meta.ref<net.uuid_type> the types built with uuid_type. A record written in braces is built with meta.record, and so is meta.top itself and every constructor whose declaration is a record, so meta.ref<meta.record> holds all of them; a record template is built with meta.record_template and is not among them. A template over another constructor is no type and no member; an application of one is the type it expands to (§8.2); an alias of a member is admitted beside it. Where of is any other type, the values it admits are values, and the slot holds a value’s qualified name (§5.12): the first segment resolves in the writer’s namespace and names an entry, and each further segment selects a key of a namespace value or a field of a record value; a map that is no namespace is not entered. So meta.ref<bar> over a record type names values of bar, meta.ref<meta.record> names types, and meta.ref<api> names myapi. A value is the name, never a value of the type it names: meta.ref<net.uuid_type> takes stock_id and refuses a UUID, and meta.ref<order> takes o1 and refuses a copy of the order.
  • of names any type or constructor of the writer’s namespace, resolved from the root — meta.ref<geo.address> — and never a value entry, an open entry, or a name that resolves to nothing; each is a resolver error at the declaration. meta.ref<meta.top> holds every type; meta.ref<meta.alias> holds nothing, no entry past its hops being an alias, and a required field typed by it is refused by §5.10.1’s productivity rule. A refinement narrows of: the refined of is the source’s or IS-A it, so every name the refinement admits the source admits too, and the refinement is IS-A its source (§5.7) — meta.identifier_ref is meta.type_ref narrowed to the identifier families, and a record refinement may narrow a meta.type_ref field to meta.identifier_ref by that edge (§5.8).
  • A value may be an application. geo.address<T>, pair<text integer>: after the qualified name, an adjacent <, one or more arguments, and > — the data production of [TSON-DATA] §7.4, written unquoted wherever a value stands, a quoted token never being a reference. An argument is a reference or a token, positional against the head’s parameters and typed where it lands: a type argument is judged as the parameter’s bound says, a value argument is a token of the parameter’s type (§5.10). The canonical text has no whitespace but one space between arguments; identity is the parsed form (§8.2), so whitespace inside the list is free. Every application denotes an entry: a closed one lifts to the entry it denotes and is named bare where it stands (§5.3), so a value carries arguments only where an application is still open — a record template’s parents and field types, held text, and the meta.application annotation (§8.1) — and in a data document an application at a reference-typed slot names the entry the closure closed it to, resolved bottom-up by structural identity against every application the schema closed; one nothing closed names no type the schema closes and is a validation error, a schema closing every application it holds before it loads.
  • A value may be a sibling reference. kind! — one identifier with an adjacent ! followed by whitespace ([TSON-DATA] §7.4) — names the field kind of the same record instance, whose value names the type (§5.2). allow_sibling says whether a reference admits one: a permission (§5.7), true by default, which a refinement may withdraw and never grant back. It is true at one type, meta.field_type_ref, which types meta.record_field.type and meta.record_template_field.type and nothing else; meta.type_ref, meta.identifier_ref and meta.ref<T> set it false and refuse a sibling reference, as a validation error in data and a resolver error in a schema, and a reference over a type that is no constructor holds none whatever its facet says, a sibling naming a field and no value. Resolved output writes a sibling reference as written, and the facet where it is false.
  • Where a name is judged. In a schema document the grammar is read where the name is written, and the name is judged when that schema links; the admitted set is the closure’s, derived and never written — a resolved form states of alone, and an author writing a members list is refused as an unknown field. In a data document a type’s name is judged by membership in the reading document’s closure, and a value’s qualified name by what it reaches there, §5.12’s walk over the namespace the document’s imports bind, the referent’s type IS-A of; a name reaching nothing, a type, or a value of another type is a validation error. A host receives the referent with the name. A name in a schema document — a construction’s or a value entry’s payload, or a resolved form read as data — resolves in that schema, which a reading closure may not hold: its grammar is read, and its membership is judged where that schema links, or when the resolved form is read (§8.1). A field’s default or pin at a reference naming values is a value’s qualified name, judged at link by the same walk, a refusal being the declaring entry’s resolver error (§5.2).
  • Every slot that names the type of a value is typed meta.type_ref and, in a closed entry, holds a bare name: meta.tuple_element.element_type, meta.array.element_type, meta.map.key_type and value_type, meta.choice.variants, meta.template_param.type and bound, meta.alias.target, meta.type_chain’s three lists, meta.reference_type.of and meta.application’s value. A record field’s type — meta.record_field.type and meta.record_template_field.type — is meta.field_type_ref, the one type that admits a sibling reference. meta.enum_type.name_type, meta.record.name_type, meta.record_template.name_type and meta.namespace.key_type are meta.identifier_ref. meta.map.key_type is meta.type_ref and not an atom kind: a map’s keys may be records, tuples or arrays. A type parameter is one whose type is meta.type_ref (§5.10). An ordinary schema reaches meta.ref<T> through any closure holding the kernel, and types a field meta.ref<net.uuid_type> to hold a type’s name there — stock_id, never a UUID — or meta.ref<method> to hold a method’s qualified name. The value-level case an author may want — a slot taking a value of any type built with uuid_type — is a choice whose variant set is a kind, not a reference standing as a type, and the series does not yet define it.
  • A bound is not a kind. <T: text> names a type and walks supertypes (meta.template_param.bound, §5.10); a reference over a constructor names a constructor and walks construction. Two mechanisms, and a bound refuses a constructor.

The ~ character has one use in the type-definition grammar: the default-value modifier in a field definition after a type expression (port?: integer ~ 8080, §5.2), beside = for a fixed value and =? for a selector the members pin (§5.2). Constructor refinement — a constructor refining another, as meta.set_type refines meta.array — is written with ^ like any refinement and follows §5.7’s rules (§5.7).

Constraint-vocabulary atom pairs. Atom families whose instances can be narrowed with constraint values are defined as pairs: a constructor carrying the constraint vocabulary (meta.atom & {...} listing the family’s narrowable fields) and a canonical empty instance (!<constructor> {}) that carries the constructor as its tag but establishes no IS-A. Refinements use the ^ operator — age => !integer ^ { min: 0 max: 150 } — which DOES establish IS-A with the refined instance (§5.5). Spec-bound constructors additionally compose with the kernel’s atom.specification mixin and pin their spec field. §9 inventories the families; each constructor’s facets holding a value of the constrained family are typed by that family’s own instance, declared beside it (§5.5, §7.4).

The atom with no constraint vocabulary. One kernel atom has nothing to narrow, and it has a constructor of its own with an empty vocabulary — meta.void_type => meta.atom & {}. Every atom in the series is thereby a constructor/instance pair, and a processor recognises void as it recognises every other family: by the constructor the resolved body names, never by the entry’s name.

  • void => !meta.void_type {} — the type of a slot that holds no value: its sole value is the void sentinel _, and nothing else is admitted at a void-typed position (§7.3). void is the target type for bare annotations (§6). As a field type it means “no value”, and the field’s own marks say the rest (§5.2): a?: void? may be missing or written _, a: void? must be written and as _, a?: void may only be missing, and a: void admits no document at all and is refused (§5.10.1). It is not a valid choice variant (§5.4): whether a slot may be missing or void is a property of the position, not a type.

A schema that wants a void type of its own writes !meta.void_type {} and has the kernel’s contract; there is no atom a schema can construct without a contract behind it. The escape hatch of earlier revisions, value, is gone: a field’s default or pin is read against the field’s own type (§5.2), and every facet is typed by its family (§7.4).

meta.identifier is a text family. The type of every naming position the grammar itself reads — type names and parameter names, through the roles meta.type_name and meta.param_name — is meta.identifier => !unicode.identifier_type { continue_add: "-" }: an instance of a constructor that composes unicode.text_type and states a UAX #31 identifier profile as data (§7.4). It is declared the way iri and regex are, so a rule stated on it reaches every naming position, it carries the text facets, and a schema may construct another instance to type names of its own. A record’s field names and an enum’s members are values of a name type — meta.record.name_type and meta.enum_type.name_type, each a meta.identifier_ref — which is meta.field_name, the kernel’s identifier, unless a record or enum states another (§5.2, §7.4): !unicode.identifier_type { start_add: "@$" } for a vocabulary translated from JSON-LD or JSON Schema, !unicode.identifier_type { medial: " " } for words. No bundled library declares an identifier of its own; the name is a schema’s to take.

4.3 Operations

TSON’s type operations fall into two families. Construction operations compute a new value and declare their own contract: a bare record claims none, constructor application transfers kind only, composition grants IS-A per parent, and subtraction revokes IS-A for every parent, leaving no supertypes (§5.9 states why the break is total). Refinement inherits an existing value and tightens within it, under the rules its type states, always preserving IS-A.

Both families consume values a type states rules for. The source of a refinement MUST, after following its alias chain (§8.3), be a value whose type states refinement rules (§5.5, §5.7): a record (meta.record states the field rules of §5.7), an atom instance (its constructor states the facet rules of §5.5), the kernel’s reference instances. Every operand of a composition or subtraction MUST be a definition whose body is a !meta.record — a shape with fields to merge. A value whose type states no rule for a field — a container instance, a choice, a namespace value — is finished and admits neither operator. The test is on the value and on nothing else: a record template’s instantiation (§8.2) closes to a !meta.record with fields and composes and refines like any record, however it was spelled, while vector<text, 3> closes to !meta.array and is refused for having none. A FINAL record (§5.2) has fields and is refused all the same, by a rule about the IS-A edge rather than the body: composition and refinement both mint one, and FINAL forbids it. §5.7 and §5.8 apply these rules to their own operand forms.

Each operation is defined with its grammar form in §5:

OperationSyntaxSectionIS-AAdds fieldsRemoves fieldsTightens fields
Record construction{ ... }§5.2noneyesn/an/a
Constructor application!C value§5.5none (kind transfers)n/an/an/a
CompositionA & B & { ... }§5.8preserved (each parent); refused on a FINAL parentyesnoyes
Subtractionhead - { fields }§5.9broken (no supertypes); admissible on a FINAL headvia the & bodyyesvia the & body
RefinementT ^ { ... }§5.7preserved (source); refused on a FINAL sourcenonoyes
Value refinement!I ^ { values }§5.5, §5.7preserved (source)nonoyes, under the type’s rules
Instantiation!T value in data§7.2n/a (data)n/an/an/a

Instantiation is data-level and terminal. A refined definition remains a definition: it can be refined further or instantiated. All parameters in a definition MUST be bound before instantiation (§5.10).

5. The Type Definition Grammar

Each declaration in a schema document binds a type name to a type definition: name => type-def (§2.1). Everything to the right of => is parsed by the type-definition grammar; the complete ABNF is in §12.1. Each construct is defined by its syntax, its rules, and the canonical value it resolves to — a form’s meaning is its resolution (§8).

Inside the type-definition grammar, type positions are determined by grammar context — the ! prefix marks constructor application and value refinement (§5.5), not a reference to a type. Type expressions resolve against the document’s namespace (§3.3.2).

5.1 Declarations

The right-hand side of a declaration takes one of the following forms; each resolves to the entry’s value (§8) as defined in the sections below.

person => { name: text  age: integer }                    ; record construction (§5.2, §5.3)
employee => person & contact & { department: text }        ; supertype composition (§5.8)
account_public => account - { password }                   ; subtraction (§5.9)
production => config ^ { host: = "prod.example.com" }      ; record refinement (§5.7)
pet     => abstract { pet_type: text =?  name: text }     ; abstract base with a selector (§5.2)
dog     => pet & { pet_type: = "dog"  breed: text }       ; a member, pinning the selector (§5.2, §5.8)
status  => !enum [ACTIVE INACTIVE SUSPENDED]               ; constructor application (§5.5)
age     => !integer ^ { min: 0  max: 150 }                 ; atom refinement (§5.5)
set_type => meta.array ^ { ordered?: = false }             ; constructor refinement (§5.7), in the kernel
id      => uuid                                            ; alias (§8.3)
scores  => [integer; 1..]                                  ; array type (§5.3)
point   => [number, number]                                ; tuple type (§5.3)
contact_method => (email | phone | address)                ; choice type (§5.4)
translations   => {text => text}                           ; map type sugar (§5.3)
geo     => !schema { address => { city: text } }          ; a nested schema map (§4.1)
orders  => !api { place_order => { … } }                   ; a value entry (§4.1)

5.2 Field Definitions

A field in a record definition answers three independent questions, and the spelling has one slot for each:

name?: type? ~ default
name?: type? = fixed
name:  type  =?
  1. May the key be omitted? ? on the name. Unmarked, the key MUST be written; marked, it MAY be omitted. This is the input question and nothing else — what omission then yields is the modifier’s business (below). Required by default is the right choice for a typed notation, and the one structured-output validation wants.
  2. May a written value be _? ? on the type. It means at a field exactly what it means at every other position that admits it — an array element, a map entry value, a tuple position (§5.3, §7.6): the void sentinel is admitted here. A field whose type carries ? is voidable. The record field is the one position where an earlier revision’s ? also answered the omission question, and that overload was the source of four states no spelling could reach.
  3. What may a written value be? The modifier. No modifier — the value is free; ~ token — the value is a default, supplied when the key is omitted and overridable by a written value; = token — the value is fixed, and a written value MUST equal it; =? — the value is pinned by the members of a discriminated family, a selector, and is discussed below. Whitespace around ~, = and =? is optional.
config => {
  host:     text                  ; key written, _ refused, any value
  port?:    integer ~ 8080        ; key may be omitted; omission yields 8080
  debug?:   boolean = false       ; key may be omitted; omission yields false; a written value must be false
  label?:   text                  ; key may be omitted; _ refused
  timeout:  duration?             ; key written; _ admitted — "no timeout" is a thing this document can say
  proxy?:   uri? ~ "http://gw"    ; omitted: the default; _: none; a value: the value
  version:  text = "2.0"          ; a marker the document must write, and must write as "2.0"
}

What omission yields is derived, never stored. A field whose name is unmarked MUST have its key written — a missing key there is a validation error. A field whose name is marked yields, where its key is missing, its value if it has one (~ v or = v, injected) and nothing otherwise: the field stays missing. So the three slots read as one table:

SpellingKey in the data_ in the dataValue in the dataA missing key yields
a: Twrittenrefusedany— (an error)
a?: Tmay be omittedrefusedanynothing
a: T?writtenadmittedany— (an error)
a?: T?may be omittedadmittedanynothing
a?: T ~ vmay be omittedrefusedanyv
a?: T? ~ vmay be omittedadmittedanyv
a?: T = vmay be omittedrefusedv onlyv
a: T = vwrittenrefusedv only— (an error)
a?: void?may be omittedadmittednonenothing
a: void?writtenadmittednone— (an error)
a?: voidmay be omittedrefusednonenothing

The last three are the void type in the type slot rather than field states of their own: void admits nothing but _ (§4.2, §7.3), so a void-typed field is one that may carry no value. a?: void? is a field that may be missing or written as _ and nothing else — the spelling an earlier revision wrote = _. a?: void is a field that may be omitted and never written, which empties the field and leaves the record inhabitable; it is legal, since a declared field still participates in closure (§7.2) and a removal clause can name it (§5.9). a: void (key written, _ refused, no value admitted) empties the record — no document satisfies it — and is a resolver error under §5.10.1’s productivity rule.

Four spellings are refused, as rules rather than omissions:

  • A default on an unmarked name — a: T ~ v, a: T? ~ v. An unmarked name says the key is always written, and ~ v is a value only omission can reach; the diagnostic SHOULD suggest a?: T ~ v.
  • A pin on a voidable type — a: T? = v, a?: T? = v. A pin names the only value the field admits, and _ is not it: the _ question is settled before the pin is consulted, and admitting the pair would let question 2 admit a value question 3 refuses. Nothing in any measured corpus asks for the cell.
  • A modifier on a void-typed field, and the sentinel as a modifier value (~ _, = _). void has nothing to default or pin, and _ is what the type slot spells.
  • The selector =? on anything but an unmarked name and a non-voidable type (below).

Which fields may carry a value. A ~ or = value is admitted only on a field whose declared type resolves, after following its alias chain (§8.3), to an atom-family instance or an enum — the types a single scalar token denotes directly, a reference among them. On a field of any other declared type — a record, container, tuple, choice, void, or meta.declared — a value modifier is a resolver error at the declaration, whatever token stands beside it. The positional form (§5.6) is a spelling rule for data values and does not make a record a token: admitting p?: point ~ 3 would make a field’s eligibility for a default depend on another declaration’s field count, so that adding a second field to point silently invalidated a default written elsewhere. The refusal costs two spellings and buys a rule that fits in one line. Value modifiers are restricted to scalar tokens — quoted or unquoted — covering strings, numbers, booleans, enum members and names; complex modifier values (arrays, records, maps, applications) are not supported in v1 (§12.1).

Value conformance. A fixed or default value MUST conform to the field’s declared type: the check is the field’s own parsing contract (§7.4), and a value is accepted exactly when a read would accept the same token at that position. It applies wherever the value came from: written literally ({ first?: int32 ~ "nope" } is refused at the declaration), or routed from a template parameter and checked at materialisation (retry => <N> { attempts?: int32 ~ N } applied as retry<text> is refused identically, §5.10). The same rule is where a routed parameter’s type comes from: a parameter standing in a value slot is recorded with the field’s declared type, so conformance is checked at the application, against the parameter list, before anything is substituted (§5.10). At a field typed by a reference naming values (handler?: meta.ref<method> ~ orders.place_order), the token is a qualified name, judged at link by §5.12’s walk, and a refusal is the declaring entry’s resolver error; at one typed by a reference over a constructor, it is a type’s name, judged a member.

Eager resolution. Default and fixed value tokens are resolved and validated at schema-load time, in the wait order of §3.4.1: the token is held as written while the reader carrying it is built, and judged against the field’s type once every entry is ready — still at load, never per validation. A default or fixed value that fails parsing or the type’s constraints is a resolver error, reported at schema load. Inside a template body a value slot may instead hold a parameter (§5.7), which is resolved when the template closes.

Injection. When an optional field with a value — a?: T ~ v or a?: T = v — is omitted, the decoder injects the value into the output: decoded values are fully populated, and consumers do not consult the schema to retrieve defaults. A pin on an unmarked name is never injected: a: T = v is a marker a document states itself — a "jsonrpc": "2.0", an apiVersion, a format version — and exists for a reader that does not hold the schema, which is the reader injection cannot serve; omitting it is the missing-field error. A pin whose only information is presence — present and pinned, missing and not injected — is spelled where §5.11 says that information earns its keep: a restated member of a field group, which the group governs and never injects. A written _ at a field that is not voidable is a validation error whatever the modifier; at a?: T ~ v the fix is to omit the field, and omission remains the injection route (§7.6). At a?: T? ~ v — the one declaration where omission, _ and a value are three different outcomes — a written _ is void and a missing key is v, so timeout: _ means no timeout and an omitted timeout means the default.

Encoders. Encoders SHOULD write values for defaulted fields — a document that states its defaults reads without its schema; omitting a field whose value equals its default is a wire-size optimisation an encoder MAY offer, lossless only because the decoder injects the value back on read. Resolver output is exempt from this SHOULD: it omits fields at their default values (§8.1). Two declarations bind an encoder that has the schema, because a field with no value has one spelling to choose and the right one is the declaration’s: at a: T? a void value MUST be written as _, omission being the missing-field error; at a?: T? ~ v a void value MUST be written as _, omission decoding to the default. An encoder without the schema leaves out a field that has no value, which is right wherever the two spellings mean the same and is why those two declarations need the schema to be written at all ([TSON-DATA] §2.9).

In data, a FIXED field may be provided with a value matching the fixed value, or — where its name is marked — omitted. A written value at a FIXED field MUST be verified against the fixed value, as values (§5.5): a contradicting value is a validation error, never a value the decoder silently overwrites — an implementation that seeds fixed fields and skips the check produces decoded output that differs from the bytes with no diagnostic.

Delivery is the mode’s, and only admission is the field’s. The three slots decide what a document may say and what the schema supplies. What a reader then hands over is the mode’s: a bound value has one null, so a: T?, a?: T and a?: T? deliver alike and differ only in what they refuse; a tree keeps which spelling arrived — a field written _ is present with a void value, a field never written is missing ([TSON-DATA] §2.9) — in every encoding. Patch semantics (set, clear, leave unchanged) need the three outcomes a tree has and a bound value does not, and are a capability of the mode, not of any declaration.

Positional form. The single-required-field positional form of §5.6 counts fields whose name is unmarked, whatever their modifier: a voidable field must still be written, and so must a marker. A record has a positional form exactly when one field’s name is unmarked.

Dependent fields: a type chosen by a sibling. A field’s type may be a sibling reference, kind! — one identifier with an adjacent ! followed by whitespace ([TSON-DATA] §7.4) — naming a field of the same record whose value names the field’s type: each instance’s dependent field is read against the type that instance’s sibling names. The kernel’s own meta.record_field.value?: type! is one — a default or pin is read against the type the field’s own type names — and a schema writes the same spelling for a tagged payload, setting => { kind: meta.type_ref value?: kind! }, or a code type chosen per row, cell => { of: meta.ref<meta.atom> v: of! }.

  • The position rule. A sibling reference stands only at a record field’s type — meta.record_field.type or meta.record_template_field.type, the slots typed meta.field_type_ref, whose allow_sibling is at its default (§4.2). Anywhere else it is refused, naming the slot: in sugar where it is written, before any lift (an alias target, a refinement source, a composition operand, an array’s element type, a map’s key or value type, a tuple element, a choice variant, a template argument); in an explicit instance at the slot (!meta.array { element_type: kind! }, a resolver error naming element_type); inside a data argument list, as a parse error; and in data at a field typed meta.type_ref, where the facet refuses it as a validation error — the value is well formed and breaks the facet, as NaN breaks a numeric.float_type withdrawing allow_nan. A field whose name needs quoting cannot be a sibling, "name"! being no sibling reference.
  • The sibling’s rules, checked at the declaration against the flattened record, each a resolver error:
    • the sibling is a field of the record declared before the dependent field — the fields are a telescope, so no cycle can form; flattening puts inherited fields first (§5.8), so a base’s sibling precedes a subtype’s dependent field;
    • it is typed by a reference whose of is a constructor or meta.top, past its alias hops — meta.type_ref, meta.field_type_ref, a meta.ref<C> over a constructor, a refinement of one; int32, a reference naming values (meta.ref<method>) and a dependent field are refused. A value names no type, so a reference over values cannot type a dependent field, and reaching a type through a value is deliberately absent, in any spelling: { exchange: meta.ref<exchange> body: exchange!.request } would read body against a field of the exchange the value addresses, evaluating a path through a value at read time, so that a read would wait on value entries being resolved. The schema’s vocabulary stays names and closed forms; a body tied to its exchange is written as an envelope template applied once per exchange, with the exchange pinned in each application;
    • it is there to read whenever the dependent field is: it is required, carries a default, is pinned, or is a member of a field group with the dependent field optional — a member of a group being declared optional, so a dependent field that is itself a member counts as optional. A sibling that is optional with no default, or voidable, is a resolver error, and so is a group member beside a required dependent field, and a refinement making such a field required; the rule is asked of the flattened record;
    • its pin or default names a type a value can be read against: a template, a record template among them, is refused, and so is a sibling reference; an application cannot be written as a default or pin (the modifier grammar admits a bare token), so only a name reaches this check;
    • the dependent field is never voidable and carries no default or pin;
    • it is restated only as itself: a refinement may make it required, and v?: int32 over v?: kind! is refused, as is a typed field restated as a dependent one. Pinning the sibling is what fixes the type, and the materialised body keeps type: kind! beside the pin (§5.7);
    • a sibling may be a discriminator, the family rules asking of it what they ask of any required atom-typed field (§5.8).
  • Reading. A dependent field is read against the type its sibling’s value names in the same instance, through that type’s reader — an application resolving through the sibling’s reference to the entry the closure closed it to. The type is known when the reader reaches the field in one of three ways: a pinned sibling’s pin, wherever the sibling is written or whether it is; a discriminator’s, member dispatch selecting a member that pins it before any field is read; and a free sibling’s value, written before the field, or its default where it is not yet written. A dependent field written before a free sibling with no default is a validation error at the field naming both, and so is a defaulted free sibling written after a field read against its default, found when the sibling arrives; no lookahead is added, which is why [TSON-DATA] §2.5 asks a free sibling to be written first. A free sibling that never arrives is no order broken, so the two are told apart once the record closes: a required one is the missing-field error and nothing more, and a field group’s member, absent because another option is chosen or none, leaves a dependent field written without it a validation error at that field, saying the sibling is absent. A dependent field absent beside an absent sibling is checked for nothing. A sibling naming no type a value can be read against — a sibling reference, a template, an application the closure does not hold — is a validation error at the sibling, and the value is not read; a sibling whose own read failed leaves the value unread with nothing more reported.
  • In a schema document a dependent field holds its value as written, whatever its form — a token, a record, a list, a map, an annotated or tagged value — and in either order of its fields, so the kernel’s meta.record_field.value and meta.record_template_field.value, both type!, wait on no entry (§3.4.1). The type a sibling names is one of the schema’s own namespace, so a value entry’s held value is judged when the schema links, read through the reader of that type — the sibling’s pin, the value it holds, or its default — each refusal a resolver error naming the entry, the slot, the dependent field and its sibling: d => !settings { s => { kind: int32 value: abc } } (with settings => !meta.namespace { key_type: meta.identifier value_type: setting }) is refused, and value: { port: 8080 } under kind: server is read by server’s reader. A dependent field written without its group-member sibling is refused in a schema document as in data. A resolved form read as data holds the value as written too, and judges it when the form is read (§8.1).
  • Values by name. A qualified name’s segment selecting a dependent field reaches a value of the type its sibling names in the same record value (§5.12).

Field names: name_type. A record’s field names are values of its name type, the kernel’s meta.record.name_type?: meta.identifier_ref — an identifier family (§7.4), and the same mechanism an enum’s members are held to by meta.enum_type.name_type. The three rules are one on both constructors: the type is an identifier family, every declared name is a value of it, and no two are one value under its equality. The default is the series’ own meta.field_name, the kernel’s identifier, so nothing changes unless a record or a supertype says so; a schema translated from JSON-LD, OpenAPI or JSON Schema — whose objects are records with members such as @context, @id or $ref — names a relaxed type the translation declares for itself, and the series ships none:

json_name   => !unicode.identifier_type { start_add: "@$" }
json_record => !meta.record { name_type: json_name  extension: ABSTRACT  fields: {} }
node        => json_record & { "@id": text  label: text }
  • The brace form states a type by composition, and has no syntax of its own. A relaxed name type is a translation’s tool, not the go-to form for a TSON record, so there is no brace-form spelling for it: a translation declares its type once, on a fieldless abstract base, and its records compose it. The cost is an IS-A edge to the base, which asks nothing of a position typed by a member — node reads untagged. A declared field name is any single-line token (§12.1), quoted where it is no identifier, and is judged at link by the record’s name type: a brace-form { "first name": text } under the default is a resolver error, not a parse error.
  • Inheritance keeps one name type, as the constructor’s rule. A record’s name_type is set where it is declared and a subtype cannot change it: a record with supertypes takes their one type when it states none; one that states a type beside supertypes MUST state theirs; and supertypes with different name types — json_record & address, where address says nothing and so is meta.field_name’s — do not compose and are a resolver error (§5.8). The rule is the constructor’s and not &’s: the kernel’s name_type?: meta.identifier_ref carries no ~ meta.field_name default, since a default cannot depend on supertypes, so the explicit !meta.record { … } and a template’s closed application are held to it exactly as json_record & { … } is. Taking the non-default type instead would re-judge address’s names by a type they were never declared under; the record to compose is one declared over the same type, address => json_record & { city: text }. A refinement keeps its source’s, so the brace form can restate an inherited relaxed name (tightened => node ^ { "$ref": text }). Two name types are one by the entry they resolve to, not by spelling: jn => json_name is json_name, and the kernel’s meta.field_name is its meta.identifier, so a record stating meta.identifier composes with one stating nothing. A name type a record states resolves in the author’s own namespace, as an enum’s does (§7.4), and resolved output writes name_type exactly where it was stated.
  • A name type’s form implies NFC. A reader matches a document’s name against the declared ones in the type’s normalization (§5.5), as an enum matches its members: under a case-folding name type NAME is the field name, the two spellings in one record are a duplicate field, and a family’s selector is found however its name is cased. Under the default, NFC, nothing changes. So NONE is refused at load — two canonically equivalent spellings would be two names that render alike — and ASCII_CASEFOLD, which leaves text beyond ASCII as written, is admitted only for a type whose every character is ASCII; NFKC_CASEFOLD is the fold for any other (§7.4).
  • Hygiene follows the type, over a floor no profile lifts. Every field name and enum member meets [TSON-DATA] §8.2’s rules under its type’s own profile, whose added characters are its own (json_name’s @ and $, a medial: " " for words), and the collision relation over the scope (§11.4). A unicode.identifier_type MUST NOT add a Default_Ignorable_Code_Point, a control, or a space other than U+0020 (§7.4): without the floor a soft hyphen in continue_add makes two names no rule tells apart, and a profile derived from a hostile source’s names would carry it in.
  • A schema’s own references to its fields match exactly. A group’s members, a refinement’s or removal’s restated field, a sibling reference and a discriminators entry name a declared field by its declared spelling, under a folding name type too: the schema is one author’s text, so a reference spelled differently is a mistake to report, and only a document’s names, which another party writes, are matched in the type’s form. meta.record.discriminators is therefore [text; 1..] and meta.field_group’s member slots are text, each judged by the record’s name type (§5.11).
  • A document’s field names are judged by the record that reads them. A name the record does not declare is an unrecognised field (§7.2), at its own pointer, after the name type has judged it: one that is a value of the name type is a name, and meets the identifier policy ([TSON-DATA] §8.2) under the type’s profile before it is reported as unrecognised, so a look-alike of a declared name is refused and never reported as an unknown field; one that is not can be no declared field, since every declared name is one, and is unrecognised without reaching the policy. Both encodings report an unrecognised field with the same code. Under the default the message is the plain one any undeclared name draws; under a declared name type it names the type and its reason, since those are the rules the author has to look up. A field that is no name meets no hygiene rule, and an unrecognised field’s value is skipped unjudged — no type governs it, so the one problem is the field itself, reported once. A record nested in a relaxed one judges its own names, so the relaxation does not leak into a value it holds. A lookahead does not judge a name: a sealed family’s discriminator scan crosses a member’s names before the member is chosen, and only the member knows its type, so a name the scan crosses is judged when the member reads it. A document that imports a schema has its field names judged by that schema and only by it; a processor reading it without the schema holds them to no name rule ([TSON-DATA] §1.5, §2.5), and one that cannot obtain the schema leaves the document undetermined rather than judging it by the schemaless rule.
  • A name binds and writes as the record states it. A host component binds a relaxed name through the name it states, as it binds any other, and a writer quotes a name that is no identifier, so the document reads back; a template’s field names are judged at each application, under the closed record’s name type.

Definition marks: how a record may be realised. A record states, by one of two words written between => and its type definition — before the parameter list where there is one (§5.10) — which values it has and whether others may build on it. The fact is the kernel’s meta.record.extension, over meta.record_extension_type => !enum [ABSTRACT FINAL OPEN]:

pet     => abstract { name: text  age: integer }
dog     => pet & { breed: text }
leaf    => final { id: uuid  payload: bytes }
  • OPEN — the record has direct instances, and any schema in the closure may compose onto or refine it. Every record with no mark, and the default.
  • ABSTRACT — no direct instances: no value’s effective type is this record, and a position typed by it admits exactly its subtypes (§7.2). The tag is REQUIRED at such a position in every encoding unless the record names discriminators (below), and a tag naming the base itself is refused, since no value satisfies it.
  • FINAL — direct instances, and no subtype: composition or refinement naming it as a source is a resolver error, in the declaring schema and in any schema that imports it. Subtraction stays admissible against a FINAL record: §5.9 empties the chain and mints no IS-A edge, and the IS-A set is the only thing FINAL constrains. A record template composing or refining a FINAL record is refused at its own declaration, its written parents being closed entries (§5.10).

Two marks on one declaration are ungrammatical — the slot holds one word — and a mark on a non-record definition is a resolver error. The member is never inherited: dog above is OPEN whether pet is ABSTRACT or OPEN, and must be, or no concrete subtype of an abstract base could exist. There is no transition table for it; the whole of the rule is that nothing may compose or refine onto a FINAL record, and every other declaration states its own member. Inhabitance gains no case: an ABSTRACT record with no subtype in its own closure is the ordinary shape of a library schema whose importers supply the members (§3.3.4 makes subtypes open across schemas), and refusing it at load would make the library unpublishable for having deferred exactly what it meant to defer. A document reaching such a position gets a validation error naming the remedy — no schema in this closure declares a subtype of the base, so the schema that does is missing from the imports. extension participates in a declared entry’s meaning as any body field does, is omitted from output at its default (§8.1), and the two words are ordinary identifiers everywhere but this one slot ([TSON-DATA] §7.7): abstract => { … } declares a type of that name, and f: abstract references it; what cannot be written is a declaration whose whole body is the bare word, which is a declaration missing its definition. The mark is grammar rather than an annotation because it changes which values conform, which §6 says no annotation may do.

The selector =?: a discriminated family. A field written =? reads pinned, but not here — the members pin it: the record is the base of a family whose members each fix the field to a distinct value, and a value at a position typed by the base is placed by reading the field rather than by a tag.

pet => abstract { pet_type: text =?  name: text }
dog => pet & { pet_type: = "dog"  breed: text }
cat => pet & { pet_type: = "cat"  indoor: boolean }

The fact is the kernel’s meta.record.discriminators, the base’s own statement of which fields select its members, in declaration order; whether the list is present is what says how a subtype is selected — by reading those fields where it is, by the tag where it is not — and that reading is derived from the body where it is needed, in the manner of meta.choice.disjoint (§5.4), never a member of the extension enum. =? implies ABSTRACT: a record its members are selected from has no values of its own. Writing abstract beside a selector asserts what the body already says and is admitted, on @meta.disjoint’s terms; final beside one is refused, the members it selects being subtypes that could then never exist.

The base cannot pin the field itself: §5.7’s identity rule forbids a FIXED restatement from changing the value, so were pet to write pet_type: text = "pet" no subtype could pin its own. The base therefore declares the field free and unpinned, each subtype tightens it to FIXED — the ordinary FREE → FIXED move — and there is no other arrangement. What the arrangement buys is that the selectors’ declared types are the base’s, uniform across the family by construction, so a decoder parses them with the one set of types it knows before dispatch, and the check list is short. At the base: the marked field’s declared type resolves, after its alias chain, to an atom-family instance or an enum; its name is unmarked and its type non-voidable — a?: T =? is a selector that may be missing, selecting nothing, and a: T? =? one that may be _, likewise; it is not a member of a field group (a member reachable by refinement included, §5.11); it carries no value, =? taking none; it is no dependent field, though it may be a dependent field’s sibling. Over the family: every entry in the base’s subtypes, transitively, restates each marked field FIXED — a: = v, the marker that must be written, or a?: = v, the injected one — and the pins are pairwise distinct as values under the field type’s own equality (§5.5): = 255 and = 0xFF collide, = 1 and = 1.0 collide, and text pins compare in the field type’s normalization form, so two that fold to one value collide. A base may mark several fields; the pins are then distinct as tuples, taken in the base’s declaration order, and a combination no member pins is the ordinary unmatched-value error, per document. The family is judged over the closure that holds it (§3.3.4): an importing schema that adds a member which fails to pin, or whose pin collides with an imported one, is refused — in the importing schema, which is the schema that broke it — and so is one that declares no member and merely imports two schemas whose members collide. A family discriminates one level: a subtype of a member inherits the member’s pin and §5.7 forbids changing it, so it dispatches to its parent and relies on §7.2 and the encoding’s tag below that; an intermediate type meaning to dispatch its own members declares a selector of its own. A base whose members are its own applications — a record template — is §5.10’s case, with the same checks, except that a selector’s name mark is each member’s: a selector a parameter pins may carry either mark, the base reading it required (§5.10).

A family is open across schemas. §3.3.4 makes subtypes open, so an importing schema may declare a further member under the pinning obligation above, and the family is judged over the closure that holds it: two members an import merge brings together are the importing schema’s error (§3.3.4). A host language’s closed-set construct generated from a family — a sealed interface over records, a discriminated union — is relative to one closure and not a promise the schema makes. The word “discriminator” names the marked field in every diagnostic and index, since a symbol is not searchable; what it names is written =?.

A family member is declared; a minted record is a member of nothing. An application nothing declares is its expansion (§8.2), and a minted record whose chain would name a declared type is a resolver error at the declaration that wrote the application: over dog_of => <T> pet & { pet_type?: = "dog" breed: T }, a field written k: dog_of<text> would mint a member of pet that no declaration names, and so would a parent application a declaration owns. The diagnostic is located at the declaration that wrote the application — or, where the application sits in a template’s held body, at the declaration whose closing minted the member, kt for kt => kennel_of<text> over kennel_of => <T> { d: dog_of<T> } — and SHOULD name the fix: dogs => dog_of<text>, then k: dogs. This is what §3.3.4 and §8.2 already imply together: a member changes what every position typed by its base does, in every schema that imports it, and is read, tagged and bound by name, while a minted name may never be written (§8.2), so a minted member could be selected and then neither reported, tagged nor mapped. The rule is drawn at membership rather than at member dispatch — a minted member of a tag-dispatched family is as unreachable as one a selector picks, an untagged value at its base needing a tag it cannot carry — and it leaves every other use-site application alone: arrays, sets, choices, applications of a family’s base, which compose onto nothing, and an application of a record template written at a use site, which is a type read where it is written and no member of the template’s family (§5.10). A minted record gains no IS-A edge later, and a declaration’s operand still reaches it: vip => customer & box<int32>, beside a use site’s box<int32>, is IS-A that synthetic and stands where crate<int32> is expected, the two applications being one type (§8.2).

Resolution. A record definition resolves to !meta.record { fields: { … } } (PRODUCT-kinded by derivation, §8.1), its fields a meta.field_map — an ordered map from each field’s name to its meta.record_field, in declaration order, which is part of the value (§4.1). A field’s name is written once, by its spelling, as the key, and the name carries no slot of its own in the meta.record_field: { type optional? voidable? role? value? } is the field’s type and the four facts a reader consults (§8.1): type is a meta.field_type_ref value, the bare name as written (an alias is a hop, §8.3), or a sibling reference; optional is the name’s ?, voidable the type’s, role one of FREE, DEFAULT, FIXED for the modifier, and value the eagerly-resolved default or fixed value, typed type! — read against the type the field’s own type names — and present exactly when role is not FREE. The defaults — false, false, FREE — are omitted in output. A duplicate is refused by the layer that can see it: a spelling written twice is the map’s duplicate key ([TSON-DATA] §2.6), refused where the map is read and before any name type is consulted; two spellings that are one value of the record’s name type are the linker’s resolver error. A definition mark lowers into the body’s extension (omitted at OPEN); the selector fields lower into discriminators, the marked fields themselves staying FREE, unmarked and unpinned; a stated name type lowers into name_type, written exactly where it was stated. An empty record {} is the zero-field case, !meta.record { fields: {} } — the shape of the kernel’s meta.top, abstract.

Type-name resolution. Names used at type expression positions (field positions, type arguments, choice variants, composition targets, refinement sources) and generic-application heads resolve against the document’s namespace per §3.3.2; constructor-application heads and the desugar targets of the sugar forms resolve per §3.3.1. Bare names always refer to entries — there is no field-name shadowing of type names.

Inline refinements and bare records are prohibited. Value refinements (!number ^ { min: -273.15 max: 10000 }) and bare records ({ name: text }) MUST be introduced via named declarations and referenced by name; they MAY NOT appear inline in field-type, field-group-member, tuple-element, array-element, choice-variant, type-argument, or composition positions. The container sugar forms carry no positional restriction (§5.3): a size specifier or an element/position ? is legal wherever the bracket or map form is. Implementations MAY enforce a configurable nesting-depth limit on inline forms as a resource bound in the manner of [TSON-DATA] §9.1; where such a limit is enabled, exceeding it is an error.

5.3 Type Expressions

The type-definition grammar has one form per container, legal at every type expression position — field types, group members, tuple elements, array elements, choice variants, type arguments, and declaration bodies alike. There is one bracket form and one map form, each admitting a size specifier after ; and an element/position ? wherever the form appears; nesting is the recursion already present in the element position (§12.1). Whether a form sits as a declaration’s own body or inline at a use site changes what the resolver does with it — a declaration’s body is the construction in place, everything else lifts to a synthetic entry (below) — never what may be written.

config => {
  tags:     [text]                 ; plain array
  meta:     {text => integer}      ; map sugar
  point:    [number, number]       ; tuple, no position voidable
  contact:  (email | phone)        ; choice
  index:    {text => [order]}      ; nested forms
  aliases?: [text]                 ; the ? on the name is FIELD omission (§5.2)
  sparse2:  [text]?                ; the ? on the type admits _ at the field (§5.2)
  scores:   [integer; 1..]         ; size specifier, legal at a field
  sparse:   [text, text?]          ; voidable tuple position, legal at a field
  batch:    order_batch            ; a named declaration remains good style
}

order_batch => [order; 1..100]     ; bounded range as a declaration body
matrix9     => [number; 9]         ; exact size
opt_items   => [text?]             ; voidable elements

Array types use [type] with an optional size specifier after ;. The size specifier is a grammar production over the range token ([TSON-DATA] §7.2.4): a bound, optionally followed by .. and an optional upper bound, or .. followed by a bound (§12.1). Each bound is an unquoted token whose text MUST match the decimal-natural production of [TSON-DATA] §7.6 — a non-negative decimal integer without leading zeros — or, within a template body (§5.10), a value-parameter name. Classification is unambiguous — parameters cannot be numeric — and a non-numeric bound token that resolves to no value parameter is a resolver error. Five forms result: N (exactly N elements), N..M (bounded range), N.. (at least N), ..M (at most M), and absent (unconstrained). [T; N..N] and [T; N] are two spellings of the same binding and land on the same entry (§8.2); the N form is RECOMMENDED. A lower bound of 0 with no upper bound (0..) is a resolver error — every array satisfies it, and because identity is structural (§8.2) the spelling would mint an entry distinct from [T] that means the same thing; the diagnostic SHOULD say that the unconstrained array is written [T].

Bound coherence. A size specifier desugars to the min_items/max_items binding pair (the table below), and one rule governs the pair wherever it arises, arrays and maps identically: when both bounds are present, min_items MUST be less than or equal to max_items, checked at the point the bounds are concrete — a resolver error at schema load where the bounds are literal, at materialisation where parameter-bound (§8.2). A count facet admits any value of numeric.count — 0 to 2⁶³−1 (§9) — and a bound past what a processor’s values can reach is no schema error: a max_* past the most a host’s collections or strings hold is never reached and admits every value, and a min_* past it is met by no value, so every value fails validation. A count outside the type — max_length: 9223372036854775808 — is a resolver error at the declaration, as any value outside its field’s type is.

The element position accepts an optional ? suffix, producing voidable: true on the resolved meta.array. Under [T?], an element at any position MAY be the void sentinel _ (§7.6); void elements occupy positional slots — [a _ c] has three elements and satisfies a [T?; 3] size constraint. Without the suffix voidable is false, and a void element is a validation error when a schema is in scope. An element is never missing: a container’s part is a value or it is void, so the container constructors carry voidable and no optional (§5.2, §8.1).

Order is a facet. meta.array carries ordered, true unless a body says otherwise, and meta.map carries it too, false unless a body says otherwise (below). The facet says whether two values differing only in order are one value. It never changes what a document may write, and output keeps the order written either way (§7.5).

Sets. The kernel’s meta.set_type constructor refines meta.array and pins three facets: ordered: = false, unique_items: = true, and voidable: = false — a void member has no meaning in an unordered collection of unique members. Its bounds are meta.array’s: the empty set is a set, and a set that must be non-empty says min_items: 1. Sets have no bracket sugar of their own; the spelling an author writes is the template set<T>, the kernel’s root entry set => <T> !meta.set_type { element_type: T } (§9) — a §5.10 application, admitted at a field position where a ! construction is not (§5.2). The template carries no bound, so a bounded set at a field is a named entry — tag_set => !meta.set_type { element_type: text min_items: 1 }, then tags: tag_set — which is how the kernel and the libraries state every set they rely on being non-empty: meta.enum_set, numeric.integer_member_set, unicode.text_member_set, net.scheme_set and numeric.decimal_member_set.

Map types use {key => value}, mirroring the data notation, with an optional size specifier after ; ({text => order; 1..}) desugaring to min_items/max_items under the same grammar, coherence, and diagnostic rules as arrays. The sugar’s key position accepts a type reference — a qualified name optionally carrying type arguments — not a paren or bracket form: composite map key types deserve a named declaration, and the explicit !meta.map { key_type: … } form remains available for them. The sugar takes exactly one key => value entry — a map type has one key type and one value type; the sugar mirrors the data’s shape, not its arity, and the diagnostic for a second entry SHOULD say so. The value side admits ?, marking the value voidable exactly as [T?] marks an array element: {text => order?} produces voidable: true on the resolved meta.map, under which an entry’s value MAY be the void sentinel _ (§7.6) — the entry is present with a void value ([TSON-DATA] §2.9) and counts toward the size bounds like any other. Without the suffix a void entry value is a validation error. A map is unordered unless it says otherwise, as a JSON object is: two maps holding the same entries in different orders are one value. !meta.map { key_type: K value_type: V ordered: true } declares one whose entry order is part of its value, so those two are distinct, and a host binds it to a map that keeps its order; the sugar has no spelling for it. The key side does not admit ?: a void key is a resolver error ([TSON-DATA] §2.9), and map-key has no ? to write (§12.1). Annotations inside the sugar braces are a parse error; the declaration is the annotation anchor. Brace dispatch between the record and map readings is by one consumed token plus one of lookahead, reusing [TSON-DATA] §2.8’s machinery (§12.2).

Tuple types use [type, type, ...] with comma or whitespace separation between individually typed positions, and a comma may follow the last ([TSON-DATA] §2.4). A tuple requires at least two element type expressions: two or more type expressions separated by whitespace or comma inside brackets is always a tuple; a semicolon after a single type expression introduces an array size specifier; a single type expression with no semicolon is an unconstrained array — never a one-element tuple. [text,] is therefore an unconstrained array of text, the trailing comma following a value and changing nothing. A tuple of exactly one position has no bracket spelling and is written through two templates the kernel declares (§9): meta.tuple1<T> — meta.tuple1<text> admits [a] and refuses [], [a b] and [_] — and meta.voidable_tuple1<T>, whose one position may be void, so that meta.voidable_tuple1<text> admits [a] and [_]. There are two because a type argument cannot carry the ? that would make the position voidable, and an application carries no facet an author could set instead. The need is rare in a schema written as TSON and real in one converted from another schema language, where a converter needs a spelling it can emit at a field without minting a declaration.

A tuple position answers one question — may its value be void? — recorded as meta.tuple_element.voidable. A position, like an array’s element, is never missing, so it has no optional; a record field answers both questions (§5.2). Tuples are fixed-length: every position MUST be present in the data. A voidable position may carry the void sentinel _, but the slot itself MUST appear — a tuple value with fewer elements than the type’s position count is a validation error however many trailing positions are voidable. Given sparse_pair => [text, text?]: [a, b] and [a, _] are valid; [a] is a validation error. Authors wanting a tail that may be left off should use an array ([text; 1..]).

Choice types use (type | type | ...) — see §5.4.

Arguments use name<arg, arg>, binding positional arguments to the declared parameters of a template (§5.10); the head resolves per §3.3.2. The argument count MUST match the parameter count. An argument is a type expression — which may nest (box<[integer]>, pair<text, {text => order}>) — or, for a template with value parameters, a concrete value: vector<pixel, 1920>. Number and quoted-string arguments are unambiguously values; an unquoted token argument that satisfies the identifier grammar — true and false included — is read as its parameter’s recorded type says — a type reference where that type is meta.type_ref, a literal (an enum member, for instance) where it is anything else (§5.10). The same production is data grammar: pair<text integer> written at a reference-typed slot in a data document, or in resolved output, is the reference value [TSON-DATA] §7.4 reads, its arguments references or tokens and never sugar. Bare references to a parameterized type without <> are resolver errors — parameter binding is mandatory at every use site — except a record template named as a family base (§5.10).

Desugaring. Every sugar form desugars to a canonical constructor application (§5.6), its target one of the kernel’s entries by path, whatever the document imports (§3.3.1):

Source formDesugaring
{ fields }!meta.record { fields: { … } }
[T]!meta.array { element_type: T }
[T; N]!meta.array { element_type: T min_items: N max_items: N }
[T; N..]!meta.array { element_type: T min_items: N }
[T; ..M]!meta.array { element_type: T max_items: M }
[T; N..M]!meta.array { element_type: T min_items: N max_items: M }
[T?], [T?; ...]the corresponding form with voidable: true bound directly
[T, U, ...]!meta.tuple { elements: [...] }
(A | B)!meta.choice { variants: [A B] }
{K => V}!meta.map { key_type: K value_type: V }
{K => V ; spec}!meta.map { key_type: K value_type: V min_items/max_items: … }
{K => V?}, {K => V?; …}the corresponding map form with voidable: true bound directly
set<T>the template application !meta.set_type { element_type: T } through the kernel’s root set (§9)
C<args>a template application (§5.10, §8.2); C resolves per §3.3.2
name => T!meta.alias { target: T } (§8.3)

Array forms record element_type, voidable from the element’s ? suffix, and min_items/max_items from the size specifier; tuple forms record elements with a meta.tuple_element per position, each carrying its own voidable; map forms record key_type, value_type, voidable from the value’s ? suffix, and the bounds.

Nested forms and synthetic entries. Every sugar form lifts at desugar time to a resolver-materialised entry — a synthetic entry — except a declaration’s own body, which never lifts: it is the declaration (ids => [order] is the construction in place, not a reference to one). The lift rule’s dividing line is closed versus open: a concrete form lifts to a closed synthetic entry with an ordinary constructor body; a parameter-bearing form — one mentioning a parameter of the enclosing declaration — lifts to an open synthetic entry capturing those parameters in declaration order, whose body is the constructor application as written, held until materialisation (§5.10, §8.2). The rule needs no case for whether the enclosing declaration has parameters: a concrete [order] inside <T> { a: T b: [order] } lifts closed, like any other concrete form. Materialisation creates no synthetic entries — it closes open ones, innermost-out (§8.2). A use-site sugar form mints where a declared application does not (§8.2), and the asymmetry is the design: a non-record form — an array, set, map or tuple — is the same definition wherever it is written, nothing subtypes it and no tag dispatches to it, so a content-keyed synthetic is exactly the entry every location writing [text] should reach; a record takes part in IS-A, and a family’s subtypes index and a tag both key on its entry, so a declared name has to be the entry for pet.subtypes to read [dogs cats] and for !dogs to select it. Synthetic names follow §8.2’s internal-name rules: resolver-chosen, fresh by construction, disjoint from declared names, and unreachable from source — and the entries themselves are an implementation’s own: their shape is unconstrained beyond producing the required resolved output, and a processor is free to mint one per schema, share them, or normalise them however it likes (§8.2). Synthetics mint at the root, one entry per form across the document, sugar written inside a nested map included (§4.1, §8.2). The lift targets are grammar-supplied and never author-written, so desugaring stays purely syntactic. Because every form lifts to an entry, nesting needs no special channel — an inner form’s marks, sizes, and bindings live on its own entry, and the outer form references it by name — and the positional restrictions earlier revisions imposed on inline forms are gone: a size specifier or ? is legal wherever the form is.

A use-site application never resolves in place: an inline constructor application, sugared or explicit, resolves to a reference to its (synthetic) entry, and a fully-bound application of a template resolves to a reference to its expansion — the closed synthetic entry the substituted form lands on, keyed on its content, which any other spelling of the same form lands on too (§8.2). Nothing records the application at the use site. An application with arguments therefore appears in resolver output only where an application is still open or is the identity of an entry — inside held template bodies and a record template’s fields (§5.10), in a record template’s chain.parents (§4.1), in an open alias’s target (§8.3), and in the meta.application annotation on an entry that closes an application (§8.2) — and means “an application”, nothing else.

5.4 Choice Types

A choice type declares that a value may conform to any one of a set of alternative types:

contact_method => (email | phone | address)

A choice MUST contain at least two variants; each variant is a type expression. The choice name is then used like any other type name in field definitions, and choices MAY also appear inline in type expression positions: contact: (email | phone).

A variant MUST NOT resolve to void — judged after following the alias chain (§8.3), so an alias of void is caught too; the declaration is a resolver error, and the diagnostic SHOULD say why: optionality is not choice — whether a slot may hold no value is the position’s own fact, so the author marks the position (§5.2’s ?), or types the field void outright where a slot that only ever holds the void value is genuinely meant. (T | void) would create a second, worse spelling of T?: void is the one thing the format spells one way ([TSON-DATA] §2.9), and it is a fact of a position, never a variant.

Disjointness is a total, two-valued fact of the declarations: discrimination-class distinctness. Every type has at most one discrimination class, derived after following its alias chain (§8.3):

ClassTypes
booleanthe boolean family
numberevery numeric family — integer, decimal, and float tiers alike
stringevery text-form family — text, the identifier families, uuid, uri and uri_reference, iri and iri_reference, regex, email, the temporal families, bytes, the network-address families, the references, and their refinements
bracerecords and maps (both {...})
bracketarrays and tuples (both [...])

An enum’s class is read off its members under its name_type (§7.4), always an identifier family — enum itself, and an enum over a profile a schema declares: it is its members’ shared class read off each member’s own token ([true false] is boolean-class; a name of the series’ own profile never begins with a digit, so no member of an enum is number-class, and mixed members yield none). A closed set of text that is no name is the text family’s own members facet, ports => !text ^ { members: ["80" "443"] }, string-class as every text atom is, so ( ports | integer ) is disjoint where ( ports | text ) is not. A reference (§4.2) is string-class, its values being names. A map keyed by an identifier family is brace-class like any map keyed by an atom. A type with no single class — rational and complex (whose typed forms straddle classes), an approximate atom whose allow_nan or allow_infinity is true as written, whatever its bounds admit (float32 and float64 as declared: a NaN or an infinity has no number spelling in some encodings and a string one, so the family’s forms straddle classes exactly as rational’s do), a map whose key type, after following its alias chain, is not a type a single scalar token denotes by its content — an atom-family instance or an enum, void excepted, having no key at all (no single scalar token denotes a compound key, so an encoding without a delimiter pair for such a map spells it in the bracket class, [TSON-JSON] §6.4), a nested choice, meta.declared (§7.8), or an unresolvable or cyclic alias — has none, and a classless variant makes its choice not disjoint. meta.identifier is not among them: it is a text family and string-class, as every identifier profile is, so ( meta.identifier | int32 ) is disjoint, and ( meta.identifier | text ), or a choice over two profiles, is not. The two straddling kinds are what make disjoint class-stable: a choice’s untagged values are the same set in every encoding, at the price that ( float64 | text ) carries a tag in text too unless the float narrows allow_nan and allow_infinity to false. The rule: a choice is disjoint: true if and only if every variant has a class and no class appears twice; false otherwise. The procedure is closed and normative: a resolver MUST record exactly this — it MUST NOT prove more (value-set separation such as disjoint numeric bounds or disjoint patterns does not make a choice disjoint) or less. The classes are [TSON-DATA] §4’s own host-type partition plus the two delimiter forms — three scalar classes and no more, deliberately ([TSON-DATA] §4.5): a class for a text-form family such as date would narrow what a text variant admits untagged in every schema that never mentions a date — and every text-class encoding shares them, so the fact is portable across encodings by construction. Every declared choice records disjoint; there is no absent state for a choice.

A narrower constructor leaves values unreachable. Discrimination recovers the variant from the value’s class and nothing else, so a value dispatched to a variant that then refuses it is refused outright — there is no second attempt at another variant. A constructor narrower than its base-type class therefore leaves values of that class unreachable in any untagged choice it appears in: number is decimal-only where [TSON-DATA] §4’s number production admits 0xFF, 0b1010, 0o377, .inf and .nan, so ( number | text ) classifies 0xFF as number-class, dispatches it to number, and fails, never reaching text; ( int32 | text ) refuses 99999999999999 the same way, and a pattern-refined text beside another string-class variant refuses everything the pattern excludes. This is a consequence to know rather than a rule to add, and it is one more reason the labelled form (below) is the shape for alternatives an author expects to overlap.

Tagging. At the data level, a value matching a choice type carries a type annotation (!variant) selecting the variant. The tag is REQUIRED when the choice is not disjoint, and MAY be omitted when it is: disjoint means precisely that the encoding’s own form resolution — for TSON text, the single base-type-resolution pass of [TSON-DATA] §4, plus the brace/bracket delimiter — recovers the variant, so an omitted tag is recovered from the value’s class, never by a second, type-directed inspection of the value’s form; the once-only reading of form ([TSON-DATA] §2.4) is preserved. A value omitting the tag at a non-disjoint choice is a validation error. For an emitter the rule is two sentences: if two variants share a class, tag every value of the choice; a tag is never wrong. And: at a disjoint choice an emitter writes each untagged value in the form of its variant’s class — a text value whose content would resolve to another class (42, true) is written quoted, and a value of a boolean or number variant is written bare — since there the one consultation of form [TSON-DATA] §2.4 allows is the one that picks the variant, and the form an emitter chooses is its tag. The reader does exactly what the predicate says: an untagged value goes to the variant of its token’s class and is validated as that variant, with no fallback, so a hand-written 42 at ( int8 | text ) reads as the number and a "42" as the text.

The @meta.disjoint assertion. An author MAY annotate a choice definition with @meta.disjoint (the kernel’s void-targeted marker, written bare per §6 — presence is the assertion) to assert that its variants are distinguishable by the encoding’s form resolution — the property the derivation checks, and a weaker and more useful one than mutual exclusion of inhabitance. Overlap in inhabitance is ordinary and expected: [TSON-DATA] §2.4’s form is not meaning makes text admit every token, so 42 inhabits both variants of ( int32 | text ), and the choice is disjoint all the same because the token’s class recovers the variant. The annotation carries no decode force — the resolver computes the choice body’s disjoint whether or not it is present — and is a checked annotation (§6) with exactly two outcomes against the derived fact: verified (disjoint: true; silent), or a resolver error (disjoint: false): the author asserted a property the declarations lack, in the manner of N > M in a size range (§5.3). The derivation is total, so there is no unprovable state and no warning tier. A choice whose variants are separated only by value sets — bounded numerics, disjoint patterns, disjoint enum member sets — derives false and rejects the assertion; nothing operational is lost, since no encoding could drop those tags, and the labelled form (below) is the recommended model for such alternatives. A resolver never diagnoses a choice merely for being non-disjoint — overlap is often intended, and the tag serves it.

Resolution. (A | B | ...) desugars to !meta.choice { variants: [A B ...] disjoint: … } — a SUM-kinded entry when declared, and a synthetic entry when inline (§5.3), referenced by name from the use site, so the entry carries the disjoint fact wherever the choice appears. The fact sits in the choice body, beside the variant list it is derived over (§8.1): meta.choice is the only body that has one, so “recorded on every choice and on nothing else” is structural rather than a rule a document could break. Each variant is a type’s name (a meta.type_ref value, §4.2, §8.1) and may itself be a sugar form, which lifts to its own entry (§5.3). The resolver validates that each variant resolves to a distinct type.

A choice discriminates by variant type name; for labelled disjunction — mutually exclusive alternatives distinguished by field label, including alternatives of the same underlying type — see field groups (§5.11). A choice has no discriminator field: an author reaching for member dispatch — the value placed by a field it carries rather than by a tag — wants the composed family of §5.2, an abstract base whose members pin a field written =?, and a position typed by the base rather than by a choice over the members. The labelled form is the recommended resolution wherever the tag would otherwise be mandatory: a choice whose variants share a base-type class, or whose disjointness is unprovable, is often better written as a single-group record (§5.11), which discriminates by label and needs no derived disjointness.

5.5 Constructor Application and Value Refinement

Within the type-definition grammar, !C { … } is an instance of the named type C — in a schema as in data and in resolved output. Whether the entry it declares is a type follows from C’s chain and not from the grammar: !numeric.integer_type {} is a value of numeric.integer_type, and a type because numeric.integer_type IS-A meta.type; !operation { … } is a value of the record operation, and a value entry (§4.1). The invariant the data format teaches therefore holds in every grammar of the series: !T x describes a value shaped by T. Two forms follow the prefix, distinguished by the ^ operator; the name after ! resolves per §3.3.1.

Constructor application — !C value. Produces a constructor instance filled with specific values. The core value after !C (§12.1) is a record of bindings interpreted against the constructor’s record shape — the field list C declared as its narrowable vocabulary — or the positional form of §5.6. This form does NOT establish IS-A: construction transfers only the constructor’s kind; the result carries C as its tag and no supertypes. Applying an entry that is neither meta.top nor IS-A it nor an ordinary type after a bare ! is a resolver error (§3.3.1), and so is applying one that is abstract — meta.top, meta.type and the base kinds (§4.1). The kernel’s meta.alias is applicable like any other: !meta.alias { target: X } denotes the alias X — ALIAS-kinded, carrying no chain — and not a value of the head’s own kind, open (<B> !meta.alias { … }, §5.10) and closed alike. An application of a namespace type builds a map value: orders => !api { … } is read as the map api describes, and the entry is a value entry (§4.1); an application of schema declares (§4.1).

integer => !numeric.integer_type {}
boolean => !enum [true false]
bytes   => !binary.bytes_type {}

Kind determination. A constructor’s kind is settled at definition time by the base kind — meta.atom, meta.product or meta.sum, excluding meta.top and meta.type (§4.1) — reachable through its transitive chain, each counted only as the kernel’s own entry. Zero base kinds in the chain → PRODUCT by structural default; exactly one → that kind; two or more → resolver error, since the kinds are categorically distinct. !C {} simply inherits C’s settled kind.

Bodies are closed. A construction or refinement body is a record whose type is known — the constructor’s own constraint-field vocabulary — and is validated as an ordinary closed record (§7.2): each member binds against a declared field, and a member the vocabulary does not declare is a resolver error at the declaration, naming the member and the constructor’s real fields. This is stated here, where bodies are written, because the silent alternative is the dangerous one: an implementation that binds field-by-field and ignores unmatched members reports success while discarding the constraint the author wrote (!integer ^ { minimum: 1 maximum: 100 } — JSON Schema’s spellings — would compile clean and constrain nothing).

Value refinement — !I ^ { values }. Refines a value by merging the body over it, under the refinement rules the value’s type states for its fields (§5.7). I MUST resolve, past its alias chain, to a value whose type states refinement rules — an atom-family instance, an entry whose body is an atom application (integer carrying !numeric.integer_type {}), the kernel’s reference instances (meta.type_ref => !meta.field_type_ref ^ { allow_sibling: false }) — not a constructor, whose body is the vocabulary record describing one (numeric.integer_type carries !meta.record { … }, and is refined by the record form, §5.7). The test is on the value and on nothing else: an instance and its constructor are both ATOM-kinded, so kind does not separate them, “IS-A meta.atom” says the opposite of what a reader expects (true of the constructor, false of every instance, IS-A stopping at construction), and a plain record has no supertypes either — where integer carrying !numeric.integer_type {} against numeric.integer_type carrying !meta.record { … } is one test that neither of those makes alone. The body is the braced subset of the payload a constructor application takes — a data-grammar record or empty-brace (§12.1) read against the constructor’s vocabulary: a bare value, a second type annotation, an annotation, or a map in body position is a parse error, and the positional form of §5.6 does not apply. The body is data, not a record of field declarations: size: { bits: 8 signed: true } binds a nested value, and min: 1 binds a number, exactly as they would after !numeric.integer_type. This form DOES establish IS-A: the new value carries I’s constructor as its tag, chain.supertypes holding I and everything I IS-A, and a body in the constructor’s canonical form (§5.6). A value whose type states no refinement rule for a field the body names is refused at ^, naming the type: a container instance (names => [text]) and a namespace value (!orders ^ { … }) are finished, the rule being total. A refinement head admits no removal clause (§5.9).

age              => !integer ^ { min: 0  max: 150 }
non_empty_text   => !text ^ { min_length: 1 }
positive_integer => !integer ^ { min: 1 }

age resolves to !numeric.integer_type { chain: { supertypes: [integer] } min: 0 max: 150 } and can be refined further; a refinement of a refinement carries the complete chain (small => !int32 ^ { … } carries [int32 integer]). Founding and refining are distinguished at the head, by the operator: !numeric.integer_type {} applies the constructor (fresh family, no IS-A); !integer ^ { min: 0 } refines the instance (IS-A integer). Record refinement, T ^ { … }, is the same rule with meta.record as the type: a record type is a value of meta.record, its field-declaration body is sugar for that value, and §5.7’s rules are meta.record’s. The two forms differ only in the body’s grammar: ! before the operand means the body is data, and its absence means field declarations.

Construction creates peers, not subtypes. One constructor may found any number of nominally distinct families: dogs => !numeric.integer_type {} is a fresh atom family with the same body as integer and no relation to it, and small_dog_count => !dogs ^ { min: 0 max: 5 } refines dogs, not integer. Identity is nominal for a declaration (§8.2): stock_id => !net.uuid_type {} and other_id => !net.uuid_type {} have byte-identical resolved bodies and are two types, and !other_id is refused at a stock_id position, as is !uuid. The only IS-A the ! forms ever create is the refinement’s hop to its source — recorded in the type’s chain and deliberately nowhere else in the body: the canonical form (§5.6) erases the surface distinction, so chain.supertypes is the sole carrier of the atom family’s IS-A fact (§8.1).

Single-required-field positional form. When a constructor has exactly one field whose name is unmarked, the core value after !C fills that field directly; see §5.6. The positional form applies to constructor application only — a refinement body is always a braced record.

Facets with a stated meaning. Each constraint family’s facets are defined by the constructor’s documentation in the kernel and the libraries (§9), with the tightening rules of §5.7 applying per facet kind. A facet holding a value of the constrained family — a bound, a step, a member — is typed by that family’s own instance, a float’s bounds by number (below), and a network list by the family’s network instance, [cidr4] or [cidr6], each declared in the same document (numeric.decimal_type’s bounds are number, temporal.date_type’s are date, net.ipv4_type.within is [cidr4]), and is read by that type at schema load like any other field, a value the type refuses being a resolver error (§7.4). What every family’s facets rest on, and what the artifacts state per family, is one clause:

A type denotes a value space. An encoding defines a lexical space over it and one canonical form per value. Equality, ordering, refinement, disjointness and content addressing are defined over value spaces only, never over spellings.

Every rule in the series that compares two decoded values — the set duplicate rule (§7.5), map-key identity ([TSON-DATA] §2.6), the check of a written value against a FIXED one (§5.2), the identity of a value argument (§8.2), and the digest a pinned reference verifies (§10.2) — delegates to the element type’s equality, and this clause is what that delegation reaches. For the numeric families [TSON-DATA] §4.3 already states it (255 and 0xFF are one value); for every family whose value space differs from its lexical space the companion artifacts state it in the family’s own @doc, and the consequences that decide interoperation are these:

  • bytes is an octet sequence. The RFC 4648 alphabet a text encoding spells it in is a binary.bytes_type selector (encoding, defaulting to BASE64), which picks a spelling and can never change what two values compare as: case in hex and padding in base64 are lexical, "abcd" and "ABCD" under HEX are one value, and the length facets count octets — length: 32 is a 32-byte digest whether it arrives as 64 hex characters, 44 base64 characters, or 32 raw bytes. An encoding whose values are octets ignores the selector and writes them raw, and a round trip through it is value-preserving: the alphabet is re-spelled from the schema, not recovered from the bytes. The selector is a facet rather than an annotation because a container element has no annotation position — [hexdigest] works only if the element’s own type carries the alphabet — and it is not refinable: an alphabet narrows nothing, since every octet string is writable in every one of them, so hexbytes => !bytes ^ { encoding: HEX } would claim an IS-A carrying no narrowing at positions no base64 reader could honour, and is a resolver error (§5.7). Another alphabet is another instance, hexdigest => !binary.bytes_type { encoding: HEX length: 4 }; refining for length inherits the alphabet, sha256 => !bytes ^ { length: 32 } being a base64 sha256. binary.bytes_type carries no spec: RFC 4648 governs spellings, not octets. The kernel declares one bytes and no spelled siblings, and [TSON-DATA] §5.3’s !bytes is base64, so the name denotes one type in both conformance classes.
  • number and the exact numeric tiers are exact values with no scale: 1, 1.0 and 1.00 are one value, and whether a spelling’s trailing zeros survive a round trip is an encoding’s promise, not the type’s. rational is the fraction — 2/4 and 1/2 are one value. A bound on numeric.decimal_type is a number, and is spelled as one: !number ^ { min: 0x10 } is refused, as !number 0x10 in data is, the family admitting no based-integer spelling.
  • A float’s bounds are exact. numeric.float_type’s bounds are numbers, validated against the value as written, before rounding, so typing them by the grid would round a bound the family says is unrounded. A bound admits a value inside its interval: NaN is inside none, so a float body with any bound admits no NaN, and allow_nan decides NaN only where no bound is written; ±infinity are ordered values a bound judges as any other — max: 100 refuses +.inf, and min: 0 admits it where allow_infinity does (§5.7).
  • A text value is its token’s text put into the type’s form. unicode.text_type carries normalization, over unicode.normalization => !enum [NONE NFC NFKC NFKC_CASEFOLD ASCII_CASEFOLD]: a value is the token’s text, unquoted, unescaped, and then put into that form — rewritten, never refused for not already being in it — and every other facet judges the value and never the spelling (lengths, pattern, members, an identifier’s profile), as does every comparison of two values: pins, map keys, set uniqueness, the duplicate and look-alike rules. This is the series’ own posture, stated for text: an atom maps a spelling to a value (0x10 is 16), and a quoted token at an identifier position is normalised before it is matched ([TSON-DATA] §7.2.1). No comparison goes below NFC. Two text values are one value when, each in its type’s form, they are NFC-equal, so a composed and a decomposed É are one value under every member — NONE and NFC admit the same equality and differ in what the value is, the text as written or its composed form, which is what the other facets judge and a round trip writes. NFC can carry identity where name hygiene cannot ([TSON-DATA] §8.2): Unicode’s normalization stability policy keeps two NFC-equal texts NFC-equal under every later version, so the verdict never moves, and an identifier or token policy relaxed to nothing leaves it standing. NONE keeps the text as written. ASCII_CASEFOLD maps A–Z to a–z and changes nothing else — it runs no Unicode normalisation on the value — and is the comparison URI schemes, HTTP field names and DNS names state for themselves. NFC and NFKC are Unicode’s canonical and compatibility forms. NFKC_CASEFOLD is toNFKC_Casefold, UAX #31’s form for names compared without case: a full case fold and not lowercasing (ß folds to ss, so a length changes), which also folds compatibility characters into ASCII and removes the default ignorables, the join controls among them, so [TSON-DATA] §7.7’s context rule has nothing left to judge under it. NFD and NFKD are not members: each is its composed twin’s equivalence stored decomposed, and would change a spelling and no verdict. The default is NONE on unicode.text_type, where quoted text is otherwise kept as written, and NFC on unicode.identifier_type; unicode.regex_type, net.uri_type, net.iri_type and net.email_type fix NONE, since a pattern put into another form matches something else ([A-Z] folds to [a-z]), and past the scheme and host a URI compares with case, as an IRI does and as a mailbox’s local part does. normalization is fixed where the type is constructed (§5.7). Two consequences reach an encoding. A round trip writes the value: Content-Type under a folding type is written back as content-type, as 0x10 is written 16. And a refusal SHOULD quote the token as written and then the value it was judged as — 'PUT' (read as 'put' under ASCII_CASEFOLD) — the written spelling being what a reader, or a repair of a generated document, has to find. A schema’s own members and pins are values too, decoded as data is.
  • A URI is RFC 3986’s URI, and an IRI RFC 3987’s. net.uri_type states RFC 3986’s distinctions as facets. allow_relative admits a relative reference (RFC 3986 §4.2) beside a URI (§3), so its default is the URI-reference (§4.1) and withdrawing it requires a scheme. allow_fragment admits a fragment (§3.5); withdrawn beside allow_relative it leaves the absolute-URI (§4.3). schemes lists the admitted schemes as a set of the kernel’s net.scheme_name, an ASCII_CASEFOLD identifier, so a scheme is held folded as RFC 3986 §3.1 compares it, [HTTP http] is a duplicate, and a value with no scheme is outside every set. net.uri_type, in net.tn, has uri_reference => !net.uri_type {} and uri => !uri_reference ^ { allow_relative: false } as its pair, so uri IS-A uri_reference, every URI being a URI-reference. A URI is US-ASCII (RFC 3986 §2). net.iri_type has the same three facets with the same meanings over RFC 3987’s grammar — ucschar beyond US-ASCII, and iprivate within the query — and the kernel’s iri_reference => !net.iri_type {} and iri => !iri_reference ^ { allow_relative: false } are its pair, so iri IS-A iri_reference; an IRI is judged through the URI it maps to (RFC 3987 §3.1). net.iri_type is the kernel’s because the kernel types its own references by it: iri types atom.specification.spec and every schema identity ([TSON-DATA] §2.2.1, §3.3) — an IRI-reference whose host or path may reach beyond US-ASCII, whose remaining form rules are identity’s own and are refused where an identity is formed. uri is not IS-A iri: families do not relate, and §5.4 already treats every text-form family as overlapping. A relative reference where allow_relative is withdrawn, a scheme outside schemes, and a fragment where allow_fragment is withdrawn are validation errors — the token is inside the family’s lexical space and outside the type’s value space — and a character outside the family’s grammar is refused by the family’s parser (§7.4). The value of each is its text as written.
  • hostname is a domain name, and the value is the name ([TSON-DATA] §5.5): RFC 5890’s labels, LDH labels, A-labels or U-labels in any mix. ASCII case is lexical, and a U-label and its A-label are two spellings of one label, each recovered from the other by Punycode (RFC 3492), so Example.COM and example.com are one value and so are xn--bcher-kva.example and bücher.example. The canonical text is the lowercase U-label form — what a writer emits, and the form an identity’s host takes ([TSON-DATA] §2.2.1). net.hostname_type carries one facet beside its spec: allow_idn, a permission (§5.7) granted by default, which withdrawn admits only a name none of whose labels is a U-label or an A-label. It judges the value and not a spelling, so !hostname ^ { allow_idn: false } refuses bücher.example and xn--bcher-kva.example alike, as a validation error — the token is inside the family’s lexical space and outside the type’s value space — while a token outside RFC 5890’s grammar (uppercase beyond ASCII, a trailing dot, a last label beginning with a digit) is refused by the family’s parser (§7.4).
  • host is a host name or an IP address, never both ([TSON-DATA] §5.5): RFC 3987’s ihost profiled to DNS, whose lexical space is the union of hostname’s, ipv4’s and ipv6’s plus an IPv6 address in brackets. Each member keeps its own equality — a name compares as a hostname value, an address numerically — and a name never equals an address, nor an IPv4 address an IPv6 one, its IPv4-mapped form included. The brackets are a spelling and not a value, so [::1] and ::1 are one host. The canonical text is the member’s: lowercase U-labels, a dotted quad, or RFC 5952 text unbracketed. net.host_type has no facets beyond its spec: a position that wants only names, or only addresses, is typed by the member, hostname, ipv4 or ipv6.
  • media_type is a type, a subtype and a map of parameters ([TSON-DATA] §5.5): an RFC 6838 media type, its parameters RFC 9110 §8.3.1’s. The type, subtype and parameter names compare without ASCII case, parameter order carries no meaning, a quoted parameter value equals its token form, and a parameter value keeps its case except charset’s, which folds. The canonical text is the lowercase type, subtype and parameter names with the charset value folded, parameters sorted by name, each value a token where it is one and quoted otherwise, joined by ; with no whitespace: text/html;charset=utf-8. net.media_type_type carries three facets beside its spec. allow_parameters is a permission (§5.7), withheld by default; withheld, a value carrying a parameter is a validation error. Net’s media_type admits none, and since a refinement may withdraw a permission and never grant one, a position that takes a Content-Type value declares its own instance, content_type => !net.media_type_type { allow_parameters: true }. types lists the admitted top-level types, and suffixes RFC 6839’s structured-syntax suffixes, a suffix s admitting a subtype ending +s and the subtype s itself; each list, where it is given, must admit the value, and a value outside one is a validation error. Both are member sets (§5.7) whose entries are media-type names and compare as the type and subtype do, without ASCII case — so suffixes: [JSON] admits application/ld+json and application/json, and types: [Text] refines types: [text image]: the family’s own value identity that §5.7’s subset rule compares by is the media type’s, not that of the lists’ element type text. Each entry MUST be an RFC 6838 restricted-name, a suffix holding no +, and a list MUST NOT be empty, an empty one admitting nothing; a body breaking either is a schema-load coherence error.
  • time and datetime are instants. RFC 3339 makes the offset mandatory, and the offset is a spelling: a datetime is the instant on the UTC timeline and a time is the time of day in UTC on [00:00:00, 24:00:00), so 2026-01-01T10:00:00+01:00 and 2026-01-01T09:00:00Z are one value, -00:00 (offset unknown, RFC 3339 §4.3) is the same instant as Z, and 23:30:00-02:00 is 01:30:00Z. A leap second — second 60, which RFC 3339’s grammar admits — is neither a time of day on that interval nor an instant on that timeline, and both families’ parsers refuse it ([TSON-DATA] §5.4). Both families are totally ordered — the mandatory offset is what makes them so; XSD’s partial order is the cost of an optional one. A wall clock with no offset, and an appointment for which the zone is data, are other value spaces and not these types. TSON text preserves the offset as written; an encoding that stores the instant may drop it. A bound on temporal.time_type is a time, on temporal.datetime_type a datetime.
  • duration is a signed exact decimal number of seconds, and period a signed integer number of months. The split is what gives each a total order: a month has no fixed length beside a second that has one, so the two are two value spaces rather than one partially ordered one, and every ordered-bound facet on them is enforceable (§5.7). duration’s lexical form is RFC 3339 Appendix A’s duration production with an optional leading -, a fraction on the seconds component only, any subset of D/H/M/S in order with at least one present, and no Y or month-M; PnW stands alone, a week is 7 days and a day 86400 s, so PT90M, PT1H30M and P0DT5400S are one value and PT0S, P0D and -PT0S are one value. period’s is P with a Y component, an M component, or both, and nothing else. Both ends of duration’s space are fixed at a signed 64-bit count of nanoseconds: time-secfrac is "." 1*9DIGIT — stated once, for duration, time and datetime alike, since no host runtime represents finer than a nanosecond and a value below that floor is not exactness anyone can use — and a duration’s magnitude does not exceed 2⁶³ − 1 ns (about 292 years), as a magnitude so that negating an admitted value yields an admitted one; a processor MUST represent every value in that range and MUST reject one outside it whether or not its own representation could hold it. A span of centuries is a calendar span and is a period; a quantity finer or wider than the range is a number in the unit the schema names. The floor is lexical (a fraction reaches the value through one component and is capped at the token) and the ceiling is a value rule (the seconds count is summed from D, H, M and S, so P400000D overflows with no single component long enough to catch). A bound or step on temporal.duration_type is a duration, on temporal.period_type a period.
  • precision on temporal.time_type, temporal.datetime_type and temporal.duration_type is a constraint on the value, not on a spelling: precision: N admits a value that is a whole number of 10⁻ᴺ seconds — fraction_digits on the seconds count — so precision: 3 is millisecond resolution, precision: 0 whole seconds, and precision: 9 the nanosecond grid; reading admits any spelling of an admitted value, trailing zeros included, so 12:00:00.500 is admitted under precision: 1, and a text encoding writing the value writes at most N fractional digits, 12:00:00.5. The atom is exact — nothing is truncated; a value off the grid is rejected. precision may not exceed 9, which falls out of the fraction production rather than being stated twice; a larger value is a schema-load coherence error. The temporal families carry no timezone facet: RFC 3339 full-time and date-time, which the atoms’ spec pins, already make the offset mandatory, so a facet requiring it would be vacuous and one relaxing it would widen the atom against its own pin. All five temporal families state their bounds as §5.11 groups, ( min | exclusive_min )? and ( max | exclusive_max )?, so a half-open interval is spellable on a date exactly as on an integer.
  • The network families’ within/excluding pair MUST admit a value, and the rule is exact. net.ipv4_type, net.ipv6_type, net.cidr4_type and net.cidr6_type each carry within (a value must lie inside some listed network; a network must be a subnet of one) and excluding (an address must lie inside none; a network must not overlap any — overlap, not containment, so a wider value cannot smuggle an excluded block), each list typed by the family’s network instance, [cidr4] or [cidr6], so a malformed entry or one with nonzero host bits is refused at schema load by that reader, as any value of it would be. A body whose two lists between them admit nothing is invalid at the schema level, in the voice the family already uses for a prefix outside its range: { within: ["10.0.0.0/8"] excluding: ["10.0.0.0/8"] } is { min: 10 max: 3 } in another spelling, and one an author reaches by editing. The check is decidable exactly — CIDR blocks are nested or disjoint and never partly overlap, so an exclusion meeting a permitted block either contains it or lies wholly inside one of its halves, and the walk terminates at the address width — and a resolver MUST implement it exactly rather than pairwise, since 10.0.0.0/9 and 10.128.0.0/9 cover 10.0.0.0/8 by counting and only the tiling case arises from a real edit. For a network family the prefix bounds participate: a value must be a subnet of a within block, so its prefix is at least that block’s, and max_prefix caps it from above; { within: ["10.0.0.0/24"] excluding: ["10.0.0.5/32"] max_prefix: 24 } admits almost every address and no network at all, since the one block the prefix bounds leave overlaps the hole. The diagnostic SHOULD say which cause it found — an exclusion covering everything permitted, or the largest block the bounds leave — since the two want different edits.

5.6 Canonical Form and Desugaring

All type-definition bodies ultimately take a single canonical form:

!C { bindings }

where C names a constructor and bindings is a record literal filling the constructor’s fields. Every other form — inline type expressions, positional constructor forms, value refinements — is syntactic sugar that desugars to this form during resolution; resolver output always records the fully expanded canonical form as the entry’s value.

Positional form. When a constructor has exactly one field whose name is unmarked — a field the document must write, whatever its modifier (§5.2) — the core value after !C may be that field’s value directly:

!enum [true false]       →  !enum { members: [true false] }
!meta.array text         →  !meta.array { element_type: text }
!meta.choice [a b]       →  !meta.choice { variants: [a b] }

Fields whose name carries ? do not count, whether or not they carry a value. The positional form is invalid when the type has zero, two, or more unmarked names — the resolver MUST reject such uses with a clear error. This restriction applies to the positional form only.

The positional form is general over schema-backed data. At any data position whose declared type is a record with exactly one unmarked-name field, a non-brace-delimited value fills that field directly; a braced value is always the explicit record (so record- and map-valued fills cannot go positional — the same exclusion as above). The two spellings denote the same value; which one canonical output uses is a per-type convention (§8.1). Type-definition bodies canonicalise the other way — toward the explicit bindings record, per the end state below — a deliberate pair of conventions: bodies uniform, references minimal. In a schema document the positional form keeps a bare token where the positional field is atom-typed — a type’s name at a meta.type_ref slot, an enum member, a literal — and loses it where that field is product- or sum-typed: there the explicit record is the only spelling, since a bare token at a record- or choice-typed position in a schema document would otherwise be indistinguishable from a name, and a value written by name stands at a slot typed by a reference naming values and nowhere else (§4.2, §5.12); a bare token at a record-typed slot whose positional field is product- or sum-typed, or at one whose record has no positional form, is refused as no record. So fields: { p => text } fills meta.record_field.type, an atom-typed positional field, in a schema document as in a data document (§8.1). A data document reads every bare token at a record-typed slot positionally, whatever the positional field’s type, a value written by name standing only at a reference-typed slot (§5.12), so a data document’s positional fills read as they always have. An application is data grammar: at a reference-typed slot in a data document — a parents list in resolver output, say — result<T> is the reference value [TSON-DATA] §7.4 reads, written unquoted, and at a reference-typed slot of a value body — a field of a namespace entry’s map (§4.1) — an application is refused outright: nothing lifts or mints in a value body, so the application would have no entry to be the name of. The spelling that gives such an application a name is the alias, order_page => page<order> (§8.2).

Record-bindings form. !C { ... } is the explicit form, valid for any constructor as long as the bindings cover every field whose name is unmarked. Empty bindings !C {} are valid whenever the constructor has no unmarked-name field.

Value refinement. !I ^ { values } desugars by retargeting to the instance’s constructor, with values merged over I’s own already-bound field values: a field named in values overrides I’s value for it, and every field I itself bound that values does not mention keeps I’s value — the atom counterpart of §5.7’s body-materialisation rule, under which inherited constraints survive a refinement that does not mention them:

!integer ^ { min: 0  max: 150 }   →  !numeric.integer_type { min: 0  max: 150 }
!text ^ { min_length: 1 }         →  !unicode.text_type { min_length: 1 }

For a fresh instance (integer => !numeric.integer_type {}) the merge is a no-op — every inherited field is absent — so the single-hop desugar above is the general rule’s degenerate case. Chained refinement is where the merge matters:

int8      => !integer ^ { size: { bits: 8  signed: true } }
bigNumber => !int8 ^ { min: -500  max: 5000 }
          →  !numeric.integer_type { size: { bits: 8  signed: true }  min: -500  max: 5000 }

bigNumber retargets to numeric.integer_type (an instance’s tag is always the base constructor) and keeps int8’s size binding, which nothing in its own body mentions — replacement rather than merge would silently discard the width and yield an unconstrained-width integer with bounds. Recognition is syntactic — the ^ declares the intent — and the resolver verifies it: the target MUST resolve to a value whose type states refinement rules, and the retarget follows the value’s tag. The result carries I’s constructor as its tag and I, with everything I IS-A, in chain.supertypes — [int8 integer] for bigNumber.

End state. After desugaring, every closed non-alias type-def body in resolver output is a binding record !C { bindings }, where C is the constructor the form applies — with the container constructors parameterless (§4.2), the nearest constructor in the source chain for every container closure is the container constructor itself: !meta.array for every array form, !meta.map for the map sugar, !meta.set_type for an application of set<T> — and bindings supplies values for the vocabulary’s unmarked-name fields, plus the fields the application or refinement bound. Pins, defaults, and routes divide by where their values live: a pin or default whose value is concrete in the head’s own declaration (meta.set_type’s ordered: = false) comes from the vocabulary and does not appear in the binding record, while a value routed by a user-template parameter (§5.10) is application-supplied information and does appear — so a validator reads any closed body with a one-hop lookup of the head, never substitution. A type slot is an ordinary required meta.type_ref-typed field (§4.2), and the bound name appears in the binding record as a bare token (which, after lifting, is what every closed form holds; §5.3). Binding records are closed-world: they never contain parameter references. A definition whose parameters remain open is either a record template — a !meta.record_template, read as the vocabulary, a parameter labelled in a field’s type_param or value_param or standing in an application’s arguments (§5.10) — or a held constructor application, the same !C { … } text, unread against C’s vocabulary until materialisation substitutes the parameters away (§5.10, §8.2); so constructors carry !meta.record vocabulary bodies, record templates carry !meta.record_template bodies, every other open entry carries a held application, and every other non-alias entry carries a binding record — or, for a value entry, the value as read against its type (§4.1). The surface abbreviations exist only in source text — except the one that names an alias: a use site names whatever entry the author named, an alias included, and output states the chain as written (§8.3).

Top-level constructor applications are constructions. A declaration whose body applies a constructor — the explicit form (lookup => !meta.map { key_type: text value_type: integer }) or its sugar spelling (lookup => {text => integer}) — resolves as a construction: kind from the constructor’s family, the constructor as the value’s tag (§8.1), the binding record as the value, and no supertypes (construction transfers kind, not IS-A). Two such declarations are nominally distinct entries with structurally equal bodies. A declaration whose body is a fully-bound application of a non-constructor template resolves on the same footing: the declaration is the entry — the substituted binding record its value, closed in place, the canonical application recorded as @meta.application on its key — and no parallel entry is minted beside it (§5.10, §8.2). Two such declarations are two entries, as two hand-written records with one field list are.

Named definitions required. The refinement form (!I ^ { values }) and constructor application (!C value) are valid only as the top-level body of a declaration — the inline prohibition of §5.2. A constrained atom must be introduced with its own declaration and referenced by name.

5.7 Refinement

Refinement copies an existing value and tightens it — binding values, fixing defaults, restricting ranges — producing a new definition with its own identity that IS-A the source. It never changes the field set: no field is added and none removed. It is expressed with the ^ operator between a source and a body, and a value is refined under the rules its type states (§5.5): a record under meta.record’s rules, which this section states; an atom instance under its constructor’s facet rules (below); the kernel’s reference instances under theirs. A type that states no rule for a field the body names refuses the refinement at ^, naming the type, so the rule is total.

config => { host: text  port?: integer ~ 8080  debug: boolean }
production => config ^ { host?: = "prod.example.com"  port?: = 9090 }

The operator carries the operation: ^ always means refine, preserving IS-A, at every level — record types (config ^ { ... }), constructors (meta.array ^ { ... }, a constructor being a record, §4.1), and atom and other instances (!integer ^ { ... }, !meta.type_ref ^ { … }, §5.5). A source type name followed directly by a braced body, with no operator, is a parse error; the diagnostic SHOULD suggest ^ (refinement) or & (composition). A refinement head admits no removal clause: T ^ { ... } - { ... } is a parse error — an operator that promises IS-A cannot host the operation that revokes it (§5.9).

In a refinement, only existing fields may be modified: fields in the body MUST exist in the source definition, and adding fields is a resolver error. The guiding rule is that refinement can only restrict, never expand. The source name resolves in the document’s namespace; the same syntax refines local and imported types. A refinement whose source is a FINAL record is a resolver error (§5.2).

Field facts move along three orders, and never backwards. A field’s three questions (§5.2) each carry an order from least to most determined, and a restatement refines its source exactly when no question moves backwards:

  • omission — nothing → required → injected: what a missing key yields, read off the declaration as ? without a value, an unmarked name, and ? with a value;
  • voidable — true → false;
  • role — FREE → DEFAULT → FIXED, a default overridable until it is fixed.

So a: T → a?: T ~ v (required → injected, FREE → DEFAULT) is allowed; a?: T ~ v → a: T is refused, a default not being removable; a?: T ~ v → a?: T = v is allowed; a?: T? → a?: T (voidable true → false) and a?: T → a: T (nothing → required) are allowed, and each reverse is refused. The third of §5.2’s invariants shapes one move: a?: T? ~ v reaches a pin only as a?: T = v, voidable and role moving forward together, since a pin on a voidable type is refused. One consequence is worth stating: a marker written in a base, a: T = v, may be relaxed by a subtype to a?: T = v — required → injected — exactly as a: T may be relaxed to a?: T ~ v; a family that needs its marker written in every member restates it unmarked in each. Nothing stores the omission answer, and nothing needs to: the order is checked on the derived answer, which is optional and value read together (§8.1). These orders are meta.record’s rules and hold for every record, a constructor among them: a constructor refinement (meta.set_type => meta.array ^ { ordered?: = false }) moves a default to a pin or a required field to an injected one as any refinement does, and never replaces a pin.

A restated field’s type narrows by IS-A. A restated field’s type MUST be the inherited type, or a type IS-A it (§7.2) — alias hops followed on both sides, judged at link where every name both types hold resolves — or void at a voidable position; any other restatement is a resolver error against the restating declaration, in refinement and composition alike (§5.8’s tightening entries follow the same rule). The relation is nominal because it is the one every typed position already admits by, so a value of the refinement read at the source’s position meets the field’s type by the rule that reads it, and because it is total and decidable from names, where value-set inclusion would need a containment judgement for every pair of families that the series states nowhere. So [int8] restating [int32], a variant restating its choice, and a fresh !numeric.integer_type { size: { bits: 8 } } restating int32 each narrow a value set and are refused, no IS-A edge joining any of the three pairs; the kernel’s meta.namespace => meta.map ^ { key_type: meta.identifier_ref … } passes by meta.identifier_ref’s refinement of meta.type_ref. A structural narrowing between containers is a rule of its own for each container constructor to state.

Restatement. Each field may be restated as itself. A restatement may change a default (defaults are overridable, §5.2) and MUST NOT change a pin — the identity case exists so a body may restate a fixed field without error, not so fixed values can be replaced. This is what forces a discriminated family’s arrangement (§5.2): the base cannot pin a selector, since no member could then pin its own. A dependent field (§5.2) is restated only as itself: a refinement may make it required, and v?: int32 over v?: kind! is refused, as is a typed field restated as a dependent one; pinning its sibling is what fixes its type, and the materialised body keeps type: kind! beside the pin.

Value tightening is per facet kind. “Refinement can only restrict” is checkable per constraint field, and each field’s rule follows its facet’s kind, declared once here for every family: an ordered bound (min, max, min_length, max_items, exclusive bounds, and kin) may move only inward — a lower bound may rise, an upper bound may fall, never the reverse — and requires its family’s value space to be ordered, which every family carrying one is (§5.5; the temporal split is what made duration’s so, and a float’s bounds judge the ordered values, the infinities among them, and exclude NaN, which no interval contains); a step (multiple_of, wherever it appears) is strictly positive, tests the value’s magnitude with the sign ignored, and may tighten only to an integer multiple of the inherited step (15 under 5 tightens; 10 under 15 is an error); a permission facet (a boolean granting latitude: numeric.float_type’s allow_nan and its three companions, the allow_relative and allow_fragment of net.uri_type and net.iri_type, net.hostname_type’s allow_idn, net.media_type_type’s allow_parameters, and meta.reference_type’s allow_sibling) may go from granted to withdrawn, never the reverse; a member set (an enum’s members, the members of the exact numeric tiers, the schemes of a URI or IRI family, the types and suffixes of a media-type family) may shrink to a subset, never grow or replace, compared by the family’s own value identity (§5.5, §7.4); a settable-once facet (unicode.text_type.pattern and unicode.text_type.members) may be set by a refinement where the source left it unset, restated verbatim, or left alone, and never changed — the two text facets occupy one logical position, both specifying the admitted value set, and pattern cannot be narrowed without a regular-language containment oracle the series decides nowhere, so giving the pair two rules by which spelling the author reached for would make the narrowing relation an artifact of notation; that unicode.text_type.members is settable-once where the numeric tiers’ member sets shrink is the one departure from the member-set rule, stated so that a reader does not infer it from a diagnostic; a facet fixed at construction is pinned where it is constructed — a refinement of the constructor may pin it, which is how enum states its name_type — and a refinement of an instance restates it verbatim or leaves it alone, and never sets or moves it, because moving it would change what a token means rather than narrow which values are admitted — unicode.text_type.normalization (turning a fold on in a refinement re-reads every token the source admitted); the profile facets of unicode.identifier_type, start, continue, start_add, continue_add, medial and exclude, for which settable-once would be unsound, since setting start_add where the source left it unset widens the profile (!meta.identifier ^ { start_add: "_" } would admit _x without _x being an identifier); and an enum’s or a record’s name_type (§5.2, §7.4) — so that a different form, profile or name type is a fresh construction and never a refinement; a selector facet — a discriminant among members of a closed vocabulary (numeric.integer_type.size, numeric.complex_type.component, binary.bytes_type.encoding, numeric.float_type.format) — may move under refinement only along the narrowing relation its members carry, which each family states: a width chain for size (int8 is inside int16, so !int8 ^ { size: { bits: 16 } } widens and fails, whatever the arithmetic direction); a partial order for component (INTEGER ⊂ NUMBER ⊂ RATIONAL and FLOAT32 ⊂ FLOAT64, the exact and approximate families incomparable since binary64 carries ±inf and NaN that no exact decimal represents, so !complex ^ { component: INTEGER } narrows and !complex ^ { component: FLOAT64 } is refused, a float complex being its own instance); and no relation at all for encoding, which a refinement may neither set nor change — an alphabet is a spelling of one value space, so a change would produce an IS-A carrying no narrowing, and another alphabet is another instance of the constructor (§5.5); a reference’s of narrows to a type IS-A the source’s, so every name the refinement admits the source admits too (§4.2). A selector at the constructor’s default is not thereby free: the relation is over effective values, so an unwritten default and a written one refine identically, and there is no “set-from-default” permission apart from the relation. A fixed value follows §5.2’s rules for a pin, and a written value is compared against it as a value (§5.5). A refinement value violating its facet’s rule is a resolver error at schema load, in record refinement and value refinement alike (§5.5, §5.6).

Three ways to name a type after another, and what each buys. An author writing user_id => uuid and expecting a stock_id to be refused at a user_id position gets no protection, and nothing else tells them the other two forms exist:

SpellingRelation to uuid!uuid at its positiona peer’s tag
user_id => uuid — alias (§8.3)the same type, under another nameacceptedaccepted
user_id => !uuid ^ {} — refinementIS-A uuid; usable at a uuid positionrefusedrefused
user_id => !net.uuid_type {} — fresh instance (§5.5)unrelated in either directionrefusedrefused

Only the first is a rename, and it is what identity dereferences (§8.2): box<user_id> and box<uuid> are one application. The empty refinement is legal — !uuid ^ {} narrows nothing and is the language’s one nominal-subtype spelling, which is why “a refinement tightens” is a description of what a refinement may do and not a MUST that a body change something.

Open modifiers. In a record template body (§5.10) the value of a = or ~ modifier may be a parameter: bounded => <N> { attempts?: integer = N } routes, and retry_policy => <N> { attempts?: integer ~ N } defaults. The name’s mark is the author’s, exactly as beside a literal. The ? on a name says whether the key may be missing and says nothing about the value (§5.2), so it does not wait for the value to exist: a?: T ~ P is a default, a?: T = P an injected pin, and a: T = P a marker every document must write — and a: T ~ P is refused for the reason a: T ~ v is, a default on a key that is always written being a value nothing can reach. What the declaration does not settle is the value: it arrives at application, and every application MUST bind every parameter (§5.10), so it is always supplied. While the entry is open the parameter is the field’s value_param (§5.10, §8.1), labelled so that no literal spelled like a parameter is mistaken for one. The role is the modifier’s, as beside a literal: = P is FIXED and ~ P DEFAULT where they are written, and materialisation substitutes the argument into value without changing a fact — so the materialised entry enforces what the literal form status?: int32 = 201 one line down would (a document writing status: 999 is a validation error, and an omitted status injects 201), and the bound value is checked against the field’s type by the value-conformance rule (§5.2). A FREE field never carries a value, open or closed. In a fresh record template a field pinned to a value parameter is a selector whatever its name’s mark (§5.10): one value per application is one pin per member, the base reading the field required with the pin dropped and each member FIXED to its argument, optional as the name was marked. A literal value needs a closed type: a field whose type is a parameter, or an application over one, takes a parameter or no value (§5.10). In a record template body the modifier spellings are the only way to bind a field’s value to a parameter — min_items: S in a record body is a field declaration whose type is S, not a value binding; in a held constructor body (<S> !meta.array { element_type: text min_items: S }) the slot is a value slot of meta.array’s own vocabulary and the parameter stands in it directly (§5.10).

Elided type expressions. In a refinement or supertype-composition body, the type expression in a field definition MAY be elided: when only a modifier is present (field: = value or field: ~ value), the field’s type is inherited from the source declaration and only the value state changes. A modifier-only entry is always a tightening — it names no type, so it cannot declare a new field — and a modifier-only entry whose name matches no inherited field is a resolver error. Restating the type expression remains necessary when the tightening also narrows the field’s type. In a fresh record definition there is no inherited declaration to elide toward: every field MUST have an explicit type expression, and the resolver MUST reject modifier-only entries there.

Refinement requires a value its type states rules for (§4.3 states the rule for both operator families). The source of ^ — after following its alias chain (§8.3) — MUST be a record (a fresh or refined record, a composition, a constructor, or an applied record template — a record template named bare at an operand is a resolver error, §5.10), or an instance whose constructor states facet rules. A container instance (lookup => {text => integer}) is finished: its bindings are set, meta.map states no tightening rule for them, and ^ on it is a resolver error; a choice is likewise inadmissible (§4.3). A record instantiation is not finished: it closes to a !meta.record with fields, and whether its source was written as box<text> inline or as a name for that application is provenance, which never gates an operator (§4.3). There is no refinement of an application head: {text => text} ^ { min_items: 1 } has no head to refine, and the use case is the size specifier on the sugar itself ({text => text; 1..}, §5.3). Record refinement chains remain open because record refinement re-emits a !meta.record body; a declared construction (lookup => {text => integer}) is terminal — authors wanting a narrower relative write the bounds on the form (strict_lookup => {text => integer; 1..}). The refined-def grammar’s optional <type-args> head serves user-template heads only (§5.10). Value refinement (!I ^, §5.5) is a distinct form with its own body grammar and the same rule.

A refined record definition remains a definition: it can be refined further or instantiated. A refinement that narrows a field’s type to void — a?: void?, or a: void? at a restated group member (§5.11) — forbids the field’s value in the refined type while keeping the field in the contract, the IS-A-preserving counterpart of removal (§5.9); void refines every type at a voidable position, the value set {_} being a subset of every voidable position’s. Individual map entries cannot be refined because map keys are data, not definition fields.

Body materialisation. The refined body re-emits the complete inherited field set in source order; each field carries either its inherited state and value or the tightened ones. The materialised body is self-describing — consumers of resolver output do not walk the supertype chain to learn the field set. Inherited defaults and pins appear with their values even when the refinement did not refer to them.

Resolution. The refined entry’s chain.supertypes records the source and the IS-A chain through it (§8.1), its value carries the source’s constructor as its tag, and tightened fields appear in the materialised body. A refinement keeps its source’s name_type (§5.2).

5.8 Supertype Composition

Supertype composition (&) is a construction tool: it combines one or more parent types with new fields into a new definition, declaring an IS-A relationship with each listed supertype. New fields are permitted; existing fields may be tightened.

address => { street: text  city: text  postcode: text }
contact => { name: text  email: text }
customer => address & contact & { loyalty_tier: text }

Supertype field conflicts. The supertypes MUST contribute disjoint field sets — a field name appearing in more than one supertype path is a resolver error, including diamond cases where the field traces to the same originating type through both paths. Two names are one field under the composed record’s name type (§5.2), and supertypes with different name types do not compose: json_record & address, where address states nothing and so is meta.field_name’s, is a resolver error, and the record to compose is one declared over the same type.

The trailing body. The trailing & { ... } body is optional (customer => address & contact is valid). Body fields that match an inherited field are tightening entries and follow the refinement rules of §5.7 (including elided type expressions and the IS-A narrowing of a restated type); body fields that match no inherited field are new fields.

Field ordering. Supertypes contribute fields in left-to-right order as listed; each supertype’s fields appear in their declared order. Tightening entries replace inherited fields in place; new fields are appended after all inherited fields. This is what lets a base’s sibling precede a subtype’s dependent field (§5.2), and a sibling may be a discriminator: the family rules ask of it what they ask of any required atom-typed field.

Constructorness is inherited through composition. Composition propagates the supertype chain, so an entry composing with something IS-A meta.top is IS-A meta.top and is therefore a constructor (§4.2): net.iri_type => unicode.text_type & atom.specification & { ... } is a constructor because unicode.text_type is. A record mixin such as atom.specification lends vocabulary without conferring the level, and a composition reaching no base kind — box => <T> base & { value: T } over a plain record base — is an ordinary template, not a constructor, whatever its author intended.

atom.specification => { spec: iri }
net.iri_type => unicode.text_type & atom.specification & {
  spec?:           = "https://www.rfc-editor.org/rfc/rfc3987"
  schemes?:        net.scheme_set
  allow_relative?: boolean ~ true
  allow_fragment?: boolean ~ true
  normalization?:  = NONE
}

net.iri_type is a constructor, IS-A unicode.text_type and atom.specification directly. Its fields, in order: unicode.text_type’s six constraint fields, the last of them — normalization — tightened in place from a default to the pin NONE; atom.specification’s spec, tightened in place to an injected pin via an elided-type modifier; and the three new fields. Composition onto a FINAL record is a resolver error (§5.2), and a supertype that is ABSTRACT confers nothing on the composed entry, which states its own extension and is OPEN unless it says otherwise. A restated field that is a group member stays a member, in its own option: a ? on its name is the in-option mark, which a restatement may drop and may not add (§5.11). A declared application of a template whose body is not a record (string_triple => vector<text, 3>) closes to a binding record and has no vocabulary body to compose with; a record template’s declared application is a !meta.record, which composes like any other (§4.3, §5.7).

A restated field’s annotations are the restatement’s own followed by the inherited ones. §6 puts an annotation on a meta.record_field and §8.1 preserves it; composition copies inherited fields whole and §5.7 lets a body entry restate one, and the two paths must agree. The rule: a restated field — in a composition body or a refinement body, including §5.7’s modifier-only entry — carries the restatement’s own annotations, in source order, followed by the inherited field’s, in source order. Nothing is dropped and no name is treated as a key, because [TSON-DATA] §3.1 already makes an annotation name repeatable on one value and annotations are a list; and the restatement leads because order is already the precedence mechanism — every first-occurrence lookup reads it that way, so the nearer declaration wins at every such site without any of them knowing about composition. A restatement adds and never removes: a subtype rewriting an inherited @doc leaves the field carrying both, which is what “annotations are not removable” buys. Per-name replacement would need a cardinality on the declared annotation, which v1 does not have. The rule is what makes a count along a composed chain checkable, and it has read-side force wherever an annotation directs reading: a field that tightens presence and writes no annotation of its own inherits its source’s, rather than silently losing it. It also fixes which fields carry a selector over a chain (§5.2): meta.record.discriminators is the base’s statement and the member restates the field pinned, so no per-field mark travels and none has to be cleared.

Parameterized operands. Both & composition and ^ refinement operate on type references, which may carry arguments. A refinement of a parameterized type must re-declare its open parameters in its own <> slot (§5.10); composing with a parameterized supertype works the same way: ok => <T> result<T> & { note: text }. An operand may carry arguments, which may name the declaration’s parameters; an operand that is itself a parameter is a resolver error — <T> T & { name: text } and <T> T ^ { … } are refused under the shadowing rule (§5.10), since a parameter has no field set to flatten until the template is applied, and a same-named entry in the schema’s namespace is that rule’s to refuse. A record template’s chain.parents is a list of meta.type_ref values, which may carry arguments, because a parent may be written as an application: while the composing declaration is open, result<T> stands there with its arguments, and is substituted and closed with every other reference the record holds (§5.10) — so ok<text> reaches an IS-A edge to result<text> and not to result<int32>. The closed chain supertypes records names without arguments, being computed once every parent is a type; the closed parent’s name and its own chain are folded into it when the instantiation closes. A closed type keeps no parents and carries no arguments in its chain, so a schema with no open parent produces a chain of bare names.

An application at an operand is subsumed where it stands, and reaches the head. An application standing at a composition operand is absorbed by value: its arguments are the member’s own contribution (dog => pet<"dog", dog_type> & { … } is the hand-written dog => pet & { pet_type?: = "dog" pet: dog_type } with one IS-A edge to pet), and it mints no entry (§5.10). The edge reaches the head — the template — because the member is a declared member of the template’s family; and it reaches the entry the arguments name where one exists. A declared application of a template whose parent is an application of another record template is IS-A that template too: ok_of => ok<text> over ok => <T> result<T> & { note: text } is IS-A ok and result — and result<text> where a declaration owns it — as its hand-written expansion result<text> & { note: text } would be — the application and its expansion land in one family, and a position typed result admits both. Joining the outer family brings its obligation: the member must pin the outer family’s discriminators, which ok’s own pins, or its arguments, supply. A parent application nothing declares gives a minted record no chain edge (§5.2, §8.2). A record template named without arguments at an operand is a resolver error: a position may name the base, but an operand is flattened, and a template has fields only once its parameters are bound (§5.10).

Resolution. The composed entry’s chain.supertypes records the listed parents and, transitively, their own chains (§8.1), complete to a fixed point; inherited fields are copied into the body’s fields map in the order above, so the body is flat and a consumer never repeats the merge. Which operands were written is not recorded for a closed record: direct parents are the chain’s minimal members, and operand order is in the flattened fields. §5.8’s one name type over a composition’s supertypes is judged at link, once every entry is ready (§3.4.1).

5.9 Subtraction

Subtraction removes fields from a construction and deliberately breaks IS-A — the resulting type is no longer source-compatible. Taxonomically it is a construction operation, not a refinement: like composition, it computes a new field set; unlike composition, it disclaims the contract. It is expressed as a trailing removal clause on a construction head:

removal-set = "-" ws "{" ws field-name *( separator field-name ) ws "}"
account => { name: text  email: text  password: text }
account_public => account - { password }
account_view   => account & { email?: text ~ "n/a" } - { password }
staff_public   => account & user & { badge: text } - { password  ssn }

The clause is head-level: a reader of the declaration line knows the contract is broken without scanning the body. It attaches only to construction heads — a bare source, or an & chain with or without a trailing body; a refinement head admits no removal clause (§5.7, §12.1).

Rules:

  1. Resolution order. Supertypes merge first — §5.8’s disjointness rule fires here unchanged, so subtraction cannot be used to resolve diamond conflicts — the body applies second, and removals apply last.
  2. Removing a nonexistent field is a resolver error — symmetric with refinement’s existing-fields-only rule.
  3. Source path does not restrict removal. Removal operates on the merged field set, not on field provenance; since IS-A is already broken, there is no contract to violate.
  4. The body and the removal set are disjoint. A removal naming a field the body itself introduces is a resolver error — adding and removing a field in one declaration is incoherent — and a body entry tightening a removed field is a resolver error. Body entries tightening the remaining inherited fields follow §5.7’s rules.
  5. Removal and narrowing to void are distinct. - { field } removes the field from the contract and breaks IS-A; field?: void? (§5.2, §5.7) keeps the field in the contract, forbids its value, and preserves IS-A.
  6. Empty subtraction does not exist, by grammar: the removal set requires at least one name, and source & {} is composition-with-no-additions and preserves IS-A.
  7. Groups. A removal may name a field-group member; the member leaves its option, an emptied option leaves the group, and a group the declaration rules would then refuse is rewritten as the spelling it equals (§5.11). A removal naming a dependent field’s sibling removes the dependency’s ground: the removal names the dependent field too, or the declaration is a resolver error.
  8. Constructors and parameters. A subtracted construction whose head is a constructor is not one — it has no supertypes, so it is IS-A nothing and may not be applied — and a parameterised subtraction declares a record template whose field set is the merged set minus the removals, with no parents.
  9. FINAL does not reach subtraction. A removal off a FINAL record is admissible: FINAL constrains the IS-A set (§5.2), which subtraction never joins, so the result is an unrelated type that happens to share a field list. Composition and refinement, which both mint an IS-A edge, are the two that FINAL refuses.

Resolution. A subtracted record is a record with no supertypes: chain.supertypes is empty — the subtracted type is not source-compatible — and nothing records what it was subtracted from, provenance being what was written and the source document having it (§4.1). A removed field’s annotations are lost with the field. When a resolved form is read (§8.1), the empty supertypes is read as stated rather than inferred by diffing field sets against the parents.

5.10 Templates and Parameters

A type definition may declare parameters using <> immediately after =>. Parameters are local names referenced in the body and bound to concrete arguments when the definition is referenced with <args>. Each parameter has a type, derived from how the body uses it, and may be written with one — <T: text, N: int8> (below). A definition with parameters is a template — it cannot be instantiated directly, and references to it MUST supply arguments for all its parameters:

container => <T> { items: [T] }
pair      => <T, U> { first: T  second: U }
vector    => <T, N> !meta.array { element_type: T  min_items: N  max_items: N }
matrix    => <T, M, N> [[T; N]; M]

container<text>, vector<pixel, 1920>, and matrix<pixel, 1080, 1920> are concrete types; bare container or vector references are resolver errors, except that a record template may be named bare as a family base (below). A template application’s head resolves per §3.3.2; parameters and templates are an authoring-side feature exclusively — resolved output governing data carries none of it at any closed position (§1.3, §8.2).

Template forms, and the two kinds of template. A template declaration’s body may be a record (container), a container or sugar form (matrix), a reference (a pure application body, below), or — the fourth form — an instance: a constructor application whose bindings mention parameters (vector above). The instance form is the route to a constructor with no sugar of its own (bounded_set => <N> !meta.set_type { element_type: text min_items: N }) and the target the sugar desugars into; for the sugared constructors the compact spelling exists alongside it (vector could equally be written <T, N> [T; N]). The forms divide in two, and the division is the one C++ (alias template / class template), Haskell (type / data) and ML (abbreviation / datatype) all make. A record template — a fresh record, a composition or a refinement whose body is a record — is a family: a type a position may name, whose open form is a base and whose applications are its members (below). Every other template — over a container, an atom constructor, a choice, or an alias — is an abbreviation: its open form denotes nothing, and an application of it is its expansion (§8.2). The kernel states the split with two constructors. A record template is a meta.record_template: an ordinary record body read as the vocabulary, over parameters: [meta.template_param; 1..], whose fields are meta.record_template_fields — a record field’s four facts, where the type and the value may each be a parameter of the enclosing template, and the label says which:

record_template_field => {
  ( type: meta.field_type_ref | type_param: meta.param_name )
  optional?: boolean ~ false
  voidable?: boolean ~ false
  role?:     meta.field_role ~ FREE
  ( value: type! | value_param: meta.param_name )?
}

type_param names a parameter standing for the whole type, value_param one standing for the whole value; type and value are as on a record field. Every other open entry is a meta.template — template => meta.top & { parameters: [meta.template_param] template: text } — the constructor application as written, held as text and unread until materialisation substitutes its parameters away. One substitution rule serves both (below). A record is closed by construction — no field of a meta.record carries a parameter — which is why the family base is its own constructor and not a record with an optional list of parameters: a reader, a writer or a member dispatcher built from a record cannot be handed a template, and a consumer of resolved output tells the two apart by the tag (§1.3).

Held bodies, and where a parameter stands. A meta.template’s body is not a typed quotation of the constructor’s vocabulary, and it is not read against that vocabulary while a parameter remains: a sugar form is desugared per §5.3’s table and held; an explicit <T, N> !meta.array { … } is held as written. Because a held body is not classified by slot kind, a parameter needs no channel of its own: a parameter in a value slot (min_items: N), in a type slot (element_type: T), inside a collection (variants: [T error], members: [a b M]), or at any depth is a token like any other, and collection-valued slots are parameterizable — result => <T> ( T | error ), the sum-typed result envelope, resolves and closes to an ordinary meta.choice body. A record template is not held: <T> { x: T } is !meta.record_template { fields: { x => !meta.record_template_field { type_param: T } } parameters: [{ name: T type: meta.type_ref }] }, read as the vocabulary, and a composition or refinement template is resolved against its namespace first — a tightening entry states a modifier and no type expression, so it is not a field until the inherited field supplies one — and the flattened record is the entry, its chain.parents the parents as written (§5.8). In a record template a parameter stands in three places and no other: a field’s whole type (v: T, labelled type_param), a field’s whole default or fixed value (w?: int32 ~ N, type: text = N, labelled value_param), or an argument of an application in a field’s type or in a parent (items: box<T>, result<T> & { … } — sugar inside a field, [T; N], lifts to an application and so is the third), where it is recognised by its text resolving into the template’s parameters (the shadowing rule, below) — the one place resolved output still applies that rule. A parameter anywhere else in a record body — as a field name, a group member, optional, voidable or role, extension, name_type or discriminators — is a resolver error at the declaration naming the slot, since the family base (below) would then have no shape its members share. A parameter is never an operand of & or ^ (§5.8). The labels are the resolver’s: a token the template’s own body writes is a parameter by the shadowing rule, and is labelled where the body is normalised, its parameters in scope; nothing is classified after that. A field the template takes from a closed source keeps its literal, even one spelled like the template’s parameter (base => { kind?: text = T } with fam => <T> base & { v: T }: fam’s kind is value: T, not value_param: T), so the shadowing rule classifies only what the template’s own body writes. !meta.record_template { … } written in source is refused as resolver vocabulary, as !meta.template { … } is.

A literal value needs a closed type. A field whose type is a parameter, or an application over one, takes value_param or no value: <T> { v?: T ~ 5 } and <M> { v?: capped<M> ~ 5 } are refused at the declaration, where judging the literal per application would make a template valid for some arguments and not others; the rule makes the template’s verdict its own, and the kernel states its whole-type half structurally, value being typed type! and having no sibling beside type_param, while the resolver refuses the application half (§5.2, §8.1). The same rule reaches a literal argument: with pin2 => <T, P> { a?: T = P }, <X> pin2<X, 3> & { … } and <X> { f: pin2<X, 3> } bind P, a value of T’s argument, to 3 while that argument is the open X, and both are refused at the application, naming 3, P and X; pin2<int8, 3> is a closed application and its member pins a to 3. A literal under a closed type is judged at the declaration, against the closed field’s type, as any field’s default or pin is (§5.2) — the type being closed, the verdict cannot vary by application, and deferring it would ship a template that fails at its first user’s application site.

Substitution is one rule at any depth. Binding a template’s parameters rewrites the body — the held text, or the record template’s fields and parents — by replacing every token whose text resolves into the entry’s parameter list with the corresponding argument — a reference argument as a reference (a bare name, or an application where the argument is itself one), a literal argument as written — and nothing else is classified. An argument is read as its parameter’s recorded type (below) and then substituted: where that type is meta.type_ref it must resolve as a type, where it is a value type it must be a value of it, and in an enum’s member list it is a member — e => <M> !enum [a b M] applied as e<c> admits c because M is recorded as a text value, not because of how c is spelled. When every parameter of an entry closes, a held body is read, once, against the constructor’s own vocabulary — materialisation binds against vocabulary exactly once, at the only moment binding is decidable — and a record template’s fields take their arguments in place, type_param becoming type and value_param becoming value, carrying the argument, with no other fact changed; the result is an ordinary closed body, a !meta.record for a record template (§8.2). A value where a type parameter stands (box<3>) and an application where a value parameter stands are refused at the application, by the label the argument lands at.

One spelling. A held body is carried as text and compared as the parsed form — never as the text, and never as bound values — and an open synthetic’s identity and content-derived name are computed over that parsed form with the parameters renamed positionally (§8.2), so the open form MUST have one spelling however many phases produce it: no-argument type names as bare tokens, applications as references with their argument lists, and parameter tokens unquoted — a producer that quotes every token has written a body that references no parameters at all. Whitespace is free: comparing the parsed form is what keeps “one spelling” from becoming a canonical-whitespace requirement the series has no emitter contract to support. Two of the template shapes cannot be normalised syntactically (composition and refinement need a namespace to flatten against), so “one phase” is not achievable and “one spelling” is the requirement.

A parameter’s type is derived from the positions it stands in, and recorded. Every parameter of an open entry carries the type an argument for it is read as — the kernel’s meta.template_param => { name: meta.param_name type: meta.type_ref bound?: meta.type_ref } (§8.1) — and the type is derived, never required of the author: it is the declared type of the slot the parameter stands in, in the vocabulary of the constructor the body applies. No parameter lacks one, since a parameter the body never references is refused (below) and every position has a declared type:

  • A type slot — a field declared meta.type_ref, such as meta.array.element_type or meta.map.key_type, or a record template field’s type_param — gives meta.type_ref.
  • A value slot gives the slot’s declared type: min_items: N gives numeric.count, and an enum’s member list gives text.
  • A routed default or fixed value (w?: int32 ~ N) gives the field’s declared type, int32 — §5.2’s value conformance, read off the field. Where the field is itself typed by a parameter, box => <T, N> { w?: T ~ N }, the derived type is that parameter: N’s type is T. That is the one scoping rule a parameter list needs: a type may name a parameter of the same template declared before it, and nothing else in the schema’s namespace under that name; at an application the argument for N is then read as the type T’s argument names.
  • An argument to another template gives that template’s recorded type for the position, so derivation is a fixed point across templates that apply each other. A parameter the fixed point leaves undetermined is given meta.type_ref: an alias template’s body is its application (meta.alias.target is a reference, §8.1), so loop => <T> loop<T> has no slot for a concrete use and its T is a type parameter — what is wrong with that declaration is that it applies itself forever and denotes no type, and that is what its diagnostic names.
  • A payload in §5.6’s positional form is walked as the one field it binds: names => <M> !enum [a b M] puts M in meta.enum_type.members exactly as !enum { members: [a b M] } does. The kernel’s own meta.ref => <T> !meta.reference_type { of: T allow_sibling: false } records T as meta.type_ref, of’s declared type, so it is a type parameter whose argument may name a constructor or a type (§4.2).

The kind follows from the type. A parameter whose type is meta.type_ref is a type parameter; any other is a value parameter. Nothing records the kind apart from the type, since a separate field would state one fact twice. Value parameters bind scalars only — numbers, booleans, identifiers, and quoted strings; type parameters bind references, simple or applied. There is no parameter arithmetic: every value argument is used as given.

Several uses must agree. A parameter’s type is the use type that IS-A every other; where the use types are not ordered by IS-A the declaration is a resolver error. <N> !meta.array { min_items: N max_items: N } derives numeric.count twice and is fine; both => <T> { a: T b?: int32 ~ T } derives meta.type_ref and int32, which no argument could satisfy, and is refused — a parameter standing in both kinds of position is this rule’s commonest case and needs none of its own. The rule follows IS-A edges only and proves nothing about value sets: two refinements of one source such as int8 and int32 are refused together though a small integer satisfies both, and the author declares the narrower type by name and uses it in both places. That is the price of a total, two-valued rule checked once at the declaration, where a list of use types checked one by one would move the verdict to every application. A declared parameter the body never references is a resolver error — <T> !meta.array { element_type: text } is a mistake, not a degenerate template, and the rule holds for every template form (box => <T> { v: text } likewise).

A declaration may narrow what it derives. A parameter may be written with a type after its name — <T: text, N: int8>, one production for every template form (§12.1) — and the written type is read by the kind the positions give, never the reverse, so an annotation cannot contradict the uses:

  • On a value parameter it narrows type. <N: int8> { w?: integer ~ N } records type: int8, and the written type MUST be IS-A the type the uses derive, or the declaration is a resolver error: <N: text> !meta.array { element_type: text min_items: N } is refused, text not being a numeric.count, and so is <N: int8> over a count-typed slot, int8 being IS-A integer and not numeric.count.
  • On a type parameter it is bound. boxed => <T: text> { a: T } records type: meta.type_ref bound: text, and an argument MUST name text or a type that IS-A it, judged after following its alias chain (§8.3); boxed<int32> is refused at the application. bound is present only where type is meta.type_ref, and is its own field because a type parameter carries two facts — its argument is a reference, and the reference is restricted — where narrowing type to text would lose the first and leave <T: text> { a: T } and <M: text> !enum [a b M] recording alike.
  • An unwritten parameter keeps what it derives, so a template that writes no types resolves as it always did.

A bound is inherited through another template’s argument list, as a type is: rebox => <T> boxed<T> records its T with bound: text, so rebox<int32> is refused at its own application rather than deep inside boxed. Several inherited bounds agree by the rule for several types, and a written bound MUST be IS-A every inherited one — <T: currency_code> boxed<T> narrows it where currency_code refines text, and <T: int32> boxed<T> is refused at the declaration. A bound follows IS-A edges only: date has a text form without being text, and a bound that followed a type’s form or its discrimination class would admit it. A bound names a type, and only a type: it resolves in the schema’s namespace (§3.3.2), and a constructor there — <T: unicode.text_type> — admits nothing and is refused at the declaration. Construction transfers kind and no supertypes (§5.5), so a nominal bound cannot say “any text-valued atom”: meta.identifier, a profile a schema constructs, and stock_code => !unicode.text_type {} are IS-A nothing and fail <T: text>. That separation is right for values and is a limit for bounds, recorded among the boundaries below.

A value parameter’s type is the slot’s declared type, in one namespace. A value parameter’s type is read where the slot it binds into is defined: a constructor’s own slot (meta.array.min_items, meta.reference_type.of) is typed as that constructor declares it, a field the schema types (w?: integer ~ N) as the schema declares it, and both resolve in the template’s one namespace — the names a constructor’s declaration carries being its declaring document’s, carried into the closure unchanged (§2.2.3). A written type resolves there too and MUST be IS-A the derived type (above), so <N: numeric.count> narrows an array bound and <N: int8> is refused on one; a schema’s own entry named count is a different type that never stands in for the kernel’s numeric.count, two same-named entries being §2.2.3’s collision where a closure holds both. An alias template is held to the same rule: rb => <N: int8> box<N> over box => <N> { w?: integer ~ N } reads int8 against the integer-typed default its parameter reaches through box, and is admitted. Two same-named entries with the same resolved body are one type wherever this section compares parameter types — a written type against a derived or inherited one, the several uses of one parameter against each other, and an argument against a parameter’s type or bound — and for nothing else: everywhere else a declared entry’s identity is its name (§8.2).

An application is checked at the call site. Each argument is read as its parameter’s type before anything is substituted, against the parameter list alone: vector<pixel, 1920> reads pixel as a type reference and 1920 as a numeric.count, and vector<pixel, "two"> is refused at the argument, not inside the body. A type argument must name a type that IS-A the parameter’s bound; a value argument must parse as the parameter’s type, or as the type an earlier parameter’s argument names. An argument the check cannot judge on its own — an application, a name that does not yet resolve — is left to the substituted body, whose verdict is the same one. The check is what lets a consumer of resolved output — an importing schema, a second resolver comparing output, a host binder generating a generic type — verify an application from the entry, without the held text (§1.3). An argument is a reference or a token by the grammar ([TSON-DATA] §7.4), and its channel is no part of the application: names<red> is one application whether red was read as a reference or filed as a value at its parameter.

Scoping and shadowing. Parameters are local to the declaring definition; they do not escape and do not compose across &, and two definitions can independently use the same parameter name. Two positions that must agree share a parameter (homogeneous_pair => <T> { first: T second: T }) — sharing the name is the link. Within a template’s body, held or a record template’s, parameter names take precedence over the schema’s namespace, and a parameter that shadows a schema entry — a type or a value entry — is a resolver error — renaming a parameter is free, and silent capture of an entry the body also means to use is the confusability hazard [TSON-DATA] §8.2 exists for. A second refusal keeps the text-recognition of a parameter among an application’s arguments unambiguous: a record template’s parameter MUST NOT be spelled like a name or an unquoted token written among the arguments of an application in a field type it absorbs — from a closed source, or from an open operand other than the operand’s own parameters, which the absorber’s arguments replace. With mode => !enum [T U], two => <A, M> { a: A m?: mode ~ M } and pair => <V> { z: two<V, T> }, t3 => <T> pair<T> & { x: T } is refused at t3, naming T and z: two<V, T>, while pair itself is valid: without the rule t3’s z would be two<T, T>, its first T the parameter and its second the member, which no meta.record_template_field can tell apart, and a closed base => { f: pick<T> } absorbed into <T> base & { … } would make every application’s f a different type from base’s, against IS-A. A literal value is not covered: the value/value_param labels tell it apart. The rule judges the source as it is when absorbed: a closed source of the same schema still writes its applications and is judged; an imported source is materialised, each closed application already the bare name of its entry, so there is nothing to collide with. A template’s parameter list is not one of §11.4’s hygiene scopes: one author writes it whole on one line, and a deliberate confusable pair there is not a population the list should grow for.

Declaration-time checking splits by what the parameter list obscures, and nothing more. Two questions are answered at the declaration from the held body’s own field names, needing no stand-in values and so unable to fabricate a verdict: that each binding key names a field the constructor declares (<T> !meta.array { elemen_type: T } is a typo, not a template), and that every unmarked-name field is bound. The typing of every concrete binding is checked there too (min_items: "two" is not a count whatever T becomes), as is the unreferenced-parameter rule, from the tokens the held body names, and so are the parameter types themselves: the derivation, the agreement of several uses, and a written type or bound against what the uses derive (above); a literal value’s need of a closed type is checked there (above). Everything value-shaped that a parameter obscures waits for materialisation, where the whole body binds through the constructor’s own reader (§8.2). An unapplied template is checked no further and receives no verdict: a resolver MUST NOT check it by substituting stand-in values, since that manufactures false errors on exactly the slots the mechanism exists for (<N> !numeric.integer_type { min: N max: 3 } is correct for every argument anyone passes and fails under a stand-in of 10). Deferring everything would make the template the one form validated at use rather than at declaration, shipping broken templates to fail at their first user’s application site; deferring only what the parameters obscure is the line.

Where a deferred diagnostic lands. A materialisation diagnostic MUST be located at the declaration whose text wrote the offending name, with the application as context — deferred checking is survivable only if the author is sent to the line they can edit. The rule is over the name, not over the template: m => <T> !meta.map { key_type: T value_type: no_such_type }, a held body whose names are read when it closes, applied by holder belongs to m, which wrote the name — a record template is read at its declaration, so a name it writes is reported there — while box => <T> { v: T } applied as box<3> belongs to holder, which wrote the 3 — and is found there by the call-site check, before substitution. One defect earns one diagnostic, however many declarations apply the template.

Sugar inside templates. A sugar form inside a template body desugars to the same construction it would outside one — §5.3’s table unchanged — and lifts to its own synthetic entry, open if the form mentions a parameter (§5.3). Nesting needs no special member: an inner form lifts to its own open synthetic, and the outer body holds an ordinary open application of it. At its smallest, box => <T> { a: [T] } desugars as if written

array_t => <T> !meta.array { element_type: T }
box     => !meta.record_template {
  fields: { a => !meta.record_template_field { type: array_t<T> } }
  parameters: [{ name: T  type: meta.type_ref }]
}

with array_t an internal name, held, and box a record template whose field holds an application still open, its argument a parameter. Applications inside a held body close before its entry is named: desugar lifts innermost-first, so a form it writes already names the entry its inner form became, and a form closed at materialisation MUST agree — [[pixel; 3]; 3] written out and grid<pixel, 3> closed land on one entry for one type. Because a closed lift hashes its binding record at desugar, before inner applications are rewritten, while an open lift hashes the closed record at materialisation, the two channels can produce two candidate entries for one type ([box<text>] written directly, and [box<T>] closed with T := text); §8.2’s merge pass, which re-derives every synthetic’s identity from its resolved record after Pass 2, is therefore required, not an optimisation.

A record template is a family base, and may be named at a type position. “A template is not a type” is right for vector<T, N> and wrong for a base, and the difference is elimination: a position typed vector would have to read a value against an element type nothing has fixed, leaving a reader to infer arguments from the payload — ill-defined, since [] determines nothing and two instantiations can accept one document. None of that reaches a base, because no value is ever read against it: a value there is a value of some member, selected by a tag or by reading the selector fields, and every member is a closed entry with every argument fixed. So a record template may stand at a field, element, variant or argument position as a base, and the record template entry is the base: a type, built with meta.record_template and so admitted by meta.ref<meta.record_template> and meta.type_ref and by no meta.ref<meta.record> (§4.2), what the position names, what subtypes indexes, what its own parents’ subtypes list, and what a host sealed type binds to, with no second entry minted beside it. Its members are the applications a declaration names — bt => box<int32>, or a composition dog => pet<"dog", dog_type> & { … } — and only those. An application written at a use site (k: box<text>) is a type, read structurally where it is written, and no member: no declaration names it, so it is never a candidate at a position typed by the template and never needs a name a document cannot write (§8.2). This is §5.2’s “a family member is declared”, kept by leaving the minted application out rather than by refusing the schema; an author who wants it in the family declares it. The base’s extension is ABSTRACT by construction — never OPEN, nothing being able to write a value whose type is the template rather than one of its applications, and never FINAL, its applications being subtypes by construction — and is not stated: the template’s own extension is each application’s, OPEN unless the template says otherwise, which is how a second-level base is spelled, result => abstract <T> { payload: T } making result<text> abstract over ok<text> and err<text>; final written on a template is a resolver error. Its discriminators are the base’s selector list: whatever selectors survive erasure of the parameters — a field written =?, or, in a fresh record template, a field pinned to a value parameter, whatever its name’s mark, one value per application being one pin per member (pet => <N, T> { type?: text = N pet: T }); the base reads such a selector required with the pin dropped, and each member is FIXED to its argument, optional as the name was marked (§5.7), exactly as a hand-written member restating pet_type?: = "dog" is. groups and name_type are each application’s, unchanged by substitution. A refinement or composition template derives no selector of its own: <T> meta.array ^ { element_type: = T } pins a constructor’s own facet and states nothing about a family. The one condition is on a selector: its declared type MUST mention none of the template’s parameters, at any depth — not as its head and not in an argument, a value parameter as much as a type one — because a decoder parses the selectors before it knows the member and their pins are exactly what the parameters supply; type: text = N satisfies it; over capped => <M> !numeric.integer_type { max: M }, kind: capped<9> = N satisfies it (a parameter-free application is the entry it expands to, in the base as in every member), and badge => <M, N> { kind: capped<M> = N name: text } is refused at the template, since badge<9, 3> and badge<5, 4> would give the selector two types. An alias template (id => <T> T, a partial application), a container template (arr => <T> [T]), a constructor-application template (vector) and an atom template (no form, §12.1) are not bases and are refused at a type position: an alias is indirection with no contract for a base to hold, abstractness has nowhere to live on a non-record body, and vector<float32, 3> and vector<float32, 4> erase to one host type no reader could place a value within. What a base carries is what survives erasure — a value parameter’s pin dropped and the field kept, a field whose type mentions a type parameter omitted — so a base’s field list may be smaller than its members’ and may be empty; member dispatch alone has a floor, at least one selector, and a base claiming dispatch with none is refused. The base’s facts do not reach a member: a declared member states no discriminators — closing pins each selector a parameter pins to its argument, and a =? selector is pinned by no argument, so closing does not pin it. A resolved-output consumer recognises the base by its constructor and dispatches on its discriminators, and never reads a field that carries a parameter (§1.3). A record template named bare at an operand of & or ^ is a resolver error: a position may name the base, but an operand is flattened, and a template has fields only once its parameters are bound (§5.8). A record template is no referent for a reference-typed slot of a value body (request: box over box => <T> { v: T } at request?: meta.ref<meta.record> is refused, a template denoting no value until applied, §5.12), and a document names there the application a declaration gives it.

Templates are not directly instantiable. An entry with a non-empty parameter list cannot validate data: a template with any parameter is a resolver error as a data annotation, without exception (§7.2), and a family base is reached by a position naming it, never by a tag naming it — the tag names a member.

Open bodies in output. While parameters remain unbound, an entry’s resolved form is one of two values (§8.1). A record template is !meta.record_template { fields: { … } parameters: [{ name: T type: meta.type_ref } …] } with its parameters in place — a field’s type_param or value_param holding a parameter name, or a type holding an application an argument of which names one — its discriminators stated as the family’s, its extension each application’s, and its chain.parents the parents as written. Every other open entry is an instance of the kernel’s meta.template constructor — !meta.template { parameters: [{ name: T type: meta.type_ref } { name: N type: numeric.count }] template: "!meta.array { element_type: T min_items: N max_items: N }" } — carrying the parameters, each with its type, and, as text, the held application under the one-spelling rule: a container as !meta.array/!meta.map/!meta.tuple/!meta.choice, an atom constructor as !numeric.integer_type, an alias as !meta.alias with its partial application in target, written as the reference it is (§8.1, §8.3). Text is what “held” means: a parameter stands wherever a token stands — min_items: N as readily as element_type: T — so a body carrying one is not a value of any constructor’s own record shape until it closes, and writing it as though it were leaves the two halves disagreeing, a value parameter refusing to read at all and a type parameter reading as a reference to a type nobody declared; a record body has slots a parameter can be labelled in once it stands in three places only, which is why it is not held. The meta.template constructor composes with meta.top directly and carries no chain, extension or discriminators: it is not a type, its open form denotes nothing, and an application of it is its expansion, with whatever chain the expansion’s own ^ or & gives it. Openness is one question with one answer — an entry is a !meta.record_template or a !meta.template, or it is closed. A parameter reference appears only inside an open entry’s own body: a held text, or a record template’s fields and parents. Closed bodies are binding records and never contain parameter references (§5.6).

Materialisation. When every parameter closes, the resolver materialises the result (§8.2): materialisation is total and closes innermost-out — each open synthetic the application reaches becomes closed as its bindings go concrete, a held body whose last parameter is substituted being read against its constructor’s vocabulary. Given grid => <T, N> { x: [[T; 1..N]; 2..N] }, closing grid<pixel, 3> yields

c1 => !meta.array { element_type: pixel  min_items: 1  max_items: 3 }
c2 => !meta.array { element_type: c1     min_items: 2  max_items: 3 }
c3 => !meta.record { fields: { x => !meta.record_field { type: c2 } } }

c1 and c2 are closed synthetics keyed on body structure — c1 is the same entry an independently written [pixel; 1..3] produces anywhere in the schema — and c3 is the expansion — a closed synthetic keyed on its content where a use site wrote the application; where a declaration did, the declaring entry itself, which carries @meta.application:grid<pixel 3> on its key (§8.2). Deferred value checks land here: substituting N := "two" yields min_items: "two", and that is the error, located per the rule above. A recursive reference inside a nested form denotes, at materialisation, the entry under construction — the open synthetic’s binding references that entry by its internal name before the entry is complete.

A declaration naming a fully-bound application is that application’s entry. string_triple => vector<text, 3> resolves to !meta.array { element_type: text min_items: 3 max_items: 3 } under the author’s name, with @meta.application:vector<text 3> on its key — the closed instantiation, in place, with no minted entry beside it and no alias hop: the axis is construction against alias, and an application is a parameterised reference syntactically and a construction semantically, filed by what it is (§8.2). A use-site application resolves to a declaration that owns it where one exists, and to its expansion otherwise — the closed synthetic the substituted form lands on, which any other spelling of the same form lands on too, minting no instantiation entry (§8.2). Minting is keyed on naming, not on the mark: an application at a composition operand is subsumed there and mints nothing (§5.8), and result<text> written at a position and ok<text> composing onto result<T> coexist under one rule; an application at a refinement source (x => box<text> ^ { n: integer }) is a trigger position — the application is materialised, or unified with the declaration that owns it in either declaration order, and the refinement IS-A the entry it denotes, as §5.7 promises (§8.2). A declaration whose body is a fully-bound application of a constructor resolves as a construction in place on the same footing (§5.6).

Partial application. When a reference to or refinement of a parameterized type leaves parameters open, it MUST re-declare the open parameters in its own <> slot — implicit parameter inheritance is not permitted; every parameter has a visible declaration site:

text_keyed_map => <V> {text => V}
uuid_pair      => <B> pair<uuid, B>

A partially-applied reference is itself a template, and — being an alias — never a family base (above). The sugar case is an open synthetic-backed template whose held body is !meta.map { key_type: text value_type: V }; the alias case is the held application !meta.alias { target: pair<uuid B> } under a one-entry parameter list for B — the application written as the reference it is, as every application inside a held body is written (§12.1); meta.alias.target is a reference, so an alias to an application states the arguments it already binds inside the held body (§8.1, §8.3), and the partial application is an ordinary open entry rather than the one shape that had to keep its arguments elsewhere. Alias templates compose during substitution — no intermediate entry per alias hop; a use site states the name it named (§8.3). meta.alias is applicable because it IS-A meta.top (§3.3.1): it describes no value, but it describes a hop, which is the question a construction site asks; its entries take the ALIAS kind from the alias form rather than from a base kind (§4.1). A resolver tells the open cases apart by the value — a !meta.record_template closes to a record, the declaration’s own where a declaration names the application and a content-keyed synthetic otherwise; a held !meta.alias composes and mints nothing; every other held constructor closes to its expansion, a synthetic (§8.2).

Heads are not parameters. Parameters bind arguments of a named head; the head itself — the type being refined, composed, or applied — is always a concrete name. <A, N> A ^ { max_items: = N }, abstracting over the refinement source, is not expressible, and a generic-application head that resolves to a parameter (weird => <map> map<text, text>) is a resolver error, diagnosed rather than applied (§3.3.1). Generic derivation over arbitrary heads is a deliberate v1 boundary, as is a parameterized value refinement: <N> !integer ^ { min: N } is no form at all (§12.1) — the constructor spelling <N> !numeric.integer_type { min: N } is the open route.

What a parameter does not carry — a second deliberate v1 boundary, narrower than it was. A parameter carries a type and, where it is a type parameter, a nominal bound (above). It carries no bound on the constructor an argument’s type was built with, by design. A type parameter names a type, so its bound is a type, and a constructor is a classifier of types: <T: unicode.text_type> set<T> would judge an argument by how it was built, which is what a reference over a constructor does and a bound does not (§4.2). “Any text-valued atom” is not a type, because construction creates peers, not subtypes (§5.5): stock_code => !unicode.text_type {} founds a family of its own so that a stock code never passes where text is declared, and an author who wants a type admitted by <T: text> refines into the family — stock_code => !text ^ { pattern: … } — rather than constructing beside it. “Any scalar” is likewise unneeded: a set of records is a set, and a record map key is a compound key (§7.7) that [TSON-JSON] writes in pairs form; a schema that wants scalars states the element or key type. A parameter carries no default argument either. A type slot cannot be bounded at a field by a nominal bound as it can at a template; what a slot can say of a name it holds is which constructor built the type (request?: meta.ref<meta.record>, §4.2), which type the value named is of (handler: meta.ref<method>, §5.12), or which sibling’s value names it (value?: kind!, §5.2). Each is additive: it can be introduced later without changing the template model, so its absence is a recorded deferral, not a design claim.

Closed entries are parameter-free, and the rule is structural rather than a MUST over two fields that have to agree: a closed entry is one whose value is a binding record, a !meta.record, a !meta.alias or a value, and contains no parameter reference at any depth, there being no parameter list anywhere for a token to resolve into — and an open entry is a !meta.record_template or a !meta.template, with its parameters declared once, in the value that holds them (§8.1). A resolved-form read verifies the one thing left to verify: that no token inside a closed body names a parameter of any enclosing template, which is a resolution failure like any other.

The _ token is not valid in field-type positions (§7.6); authors expressing “type to be filled later” use parameters.

5.10.1 Self-Referential Types

Types may reference themselves:

node => { value: text  children: [node] }
linked_list => <T> { value: T  next?: linked_list<T> }
tree => <T> { value: T  children?: [tree<T>] }

A name at a type position orders nothing (§3.4.1, rule 2), so a self-reference is sound: the cycle a resolver refuses is a wait cycle (§3.4.1, rule 4), never a reference cycle. In a template body, a recursive reference is an open application like any other — tree’s children carries a reference to the open synthetic for [tree<T>], whose binding applies tree<T> (§5.3, §5.10) — and needs no special representation.

Regular recursion. Within a template body, a recursive application — direct or mutual — MUST pass each parameter through unchanged: tree<T> inside tree’s own body is well-formed; weird => <T> { next: weird<[T]>? }, whose recursive application rebuilds the argument, is a resolver error at the declaration. This is the standard regularity restriction of recursive-type theory, and it belongs beside this section’s other v1 boundaries (no head abstraction, no constructor bounds): without it, one application could demand an unbounded family of distinct instantiations, and no portable depth limit distinguishes that from legitimate depth. The check is static, at declaration time.

Productivity. A type MUST admit at least one finite value; a definition with no finite member — a required direct or mutual self-reference with no terminating alternative — is a resolver error at schema load, not a warning: item => { inner: item } describes no data, and validation against it can only ever fail, so rejecting it at the schema is the earlier, better diagnostic. A recursive reference is guarded, and the definition productive, when every cycle through it passes at least one position that can terminate: a field whose key may be omitted or whose type admits _ (? on the name or on the type, §5.2), a voidable tuple position, an array or map position whose floor admits emptiness or whose elements may be void, or a choice with at least one non-recursive variant. a: void — a field that must be written and admits nothing — is the one declaration this rule refuses on its own, the record having no member at all (§5.2); a?: void is legal, emptying only the field. A required field typed by a reference that admits no name — meta.ref<meta.alias>, no entry past its hops being an alias (§4.2) — is refused here too. An ABSTRACT record with no subtype in the declaring schema’s closure is not a productivity error (§5.2): its members are its importers’ to supply. A group that must be chosen (§5.11) terminates when at least one of its options can be chosen without recursion — one option is chosen, so one terminating option suffices — and an option terminates when every member it requires does. Every entry is judged, synthetic entries included; a template is judged with its parameters assumed inhabited (a bound names a type that is itself judged, §5.10, so an argument can always be chosen productive), and its materialisations need no re-check the entry did not already pass. Productivity is structural: whether an atom’s constraints admit any value (a min > max interval) is a separate, value-level question (§5.3, §7.4).

Template materialisation. When a template application closes, it resolves to the declaration that names it where one exists, and to its expansion otherwise (§8.2). A self-reaching expansion is the one case an application nothing declares keeps an instantiation entry of its own: nested => <T> ( T | [nested<T>] ) closed at nested<text> names itself inside its own body, so its content cannot be written without a name, and the knot is tied on the application — a !meta.alias to the synthetic, keyed on the canonical application and carrying @meta.application (§8.2). A self-reaching application keeps one instantiation entry per canonical application — arguments compared after following alias chains (§8.2) — so tree<text> and tree<integer> are two entries and tree<user_id> over user_id => text is tree<text>’s, and every further occurrence of the same application anywhere in the schema reuses the existing one. The entry’s name is internal (§8.2). Recursive references within the body, once bound, denote the entry being materialised; the resolver ties the knot by resolving them to the entry’s own name (§5.10). A non-recursive template’s application is its expansion alone (§8.2).

5.11 Field Groups

A record body may declare a field group: a parenthesised presence rule over fields of the record. A group lists options separated by |, each holding one or more labelled members separated as fields are, and at most one option may be chosen in conforming data. The field names are the discriminator; the wire form of instances is unchanged by grouping.

integer_type => meta.atom & {
  size?: numeric.integer_size
  ( min: integer | exclusive_min: integer )?
  ( max: integer | exclusive_max: integer )?
  multiple_of?: integer
  members?:     numeric.integer_member_set
}

endpoint => { ( host: text  port: uint16 | socket: text ) }
contact  => { name: text  ( email: email | phone: text )+ }

Each member is a field-name/type-expr pair, the type optionally voidable (? on the type, §5.2) and the name optionally marked (? on the name, below); a member’s type may be a sibling reference, a member being a field (§5.2). Member labels share the enclosing record’s field namespace and its name type (§5.2): a label MUST be unique across the record’s plain fields and all groups’ members, including fields contributed by supertypes (§5.8’s disjointness rule extends to member labels) — so options are disjoint, and a field sits in one option of one group.

Three rules decide validity.

  • An option is chosen when any of its members is present.
  • A chosen option MUST contain every member not marked ?. A ? on a member’s name marks it optional within its option: it may be missing once its option is chosen.
  • A bare group admits exactly one chosen option. A ? after the closing parenthesis makes the group optional — it may be missing as a whole, as an optional field’s key may — and it then admits at most one. A group has no default or fixed form.

( include: I | name?: N type?: T ) therefore admits include, name, type, and name with type; it refuses an empty record, and include beside either of the others.

+ is sugar for “at least one of”. ( email: E | phone: P )+ desugars to ( email?: E phone?: P ) — a group of one option, not optional, whose members are all marked. That option must be chosen, and it is chosen when any of its members is present, so the group admits every non-empty subset of them. + is allowed only where every option is one unmarked member; the desugared form is the kernel’s, and + is the only way to write it. Like ?, it is adjacent to the ) it marks, and it is the one use the series has for a bare + ([TSON-DATA] §7.2.5).

RuleSpelling
exactly one of a and b( a: A | b: B )
at most one of a and b( a: A | b: B )?
at least one of email and phone( email: E | phone: P )+
include alone, or at least one of name and type( include: I | name?: N type?: T )
both a and b, or neither( a: A b: B )?
a requires b; b may appear alone( b: B a?: A )?
a and c each require b( b: B a?: A c?: C )?
either host and port, or socket( host: H port: P | socket: S )

Each presence rule has one spelling. A group is refused, at schema load, wherever it restates what plain fields or another group already state, and the diagnostic SHOULD name the spelling it restates:

  • The only member of an option takes no ?. It is present exactly when its option is chosen, so the mark changes nothing: ( a?: A | b: B ) is ( a: A | b: B ).
  • A written group of one option is optional, with at least two members and one of them unmarked — ( b: B a?: A )?. Every other group of one option restates something. Bare with an unmarked member, it is plain fields: ( b: B a?: A ) is b: B a?: A. Bare with every member marked, it is what + desugars to, and is written that way. Optional with every member marked, it is optional fields: ( a?: A b?: B )? is a?: A b?: B, and ( a: A )? is a?: A.
  • + takes options of one unmarked member each. ( a?: A | b: B )+ and ( host: H port: P | socket: S )+ are refused.

A declared group therefore has at least two members, and every option can be chosen on its own, so a declared group is never unsatisfiable and loading needs no search. There is no form for any number of options: options free to be chosen together are independent, and such a group is always its options written as separate groups or plain fields. Groups do not nest, and a field under two presence rules has no spelling — it sits in one option of one group.

Member positions are deliberately bare. A member takes a name, optionally marked, and a type expression, optionally voidable, and nothing else: the ~, = and =? modifiers are parse errors on a member — selection belongs to the label, presence belongs to the group, and a group never injects. A voidable member written as _ is present and chooses its option; at a member that is not voidable a written _ is the ordinary refusal (§7.6). A group is not a type expression: it cannot appear in field-type, element, argument, or variant positions, so multiplicity around a group ([( a: T | b: U )]) is not expressible; repetition of alternatives is written as an array of a named choice type (§5.4). A dependent field whose sibling is a group member is checked against the member’s presence as §5.2 states: absent because another option is chosen or none, the sibling leaves a dependent field written beside it a validation error.

Resolution. Groups flatten. Each member becomes an ordinary meta.record_field in the body’s fields map — in source order, contiguous with the group’s other members — with optional: true and role: FREE whatever the group says, and voidable as written. The grouping is recorded in the body’s groups list as a meta.field_group:

field_group => {
  members:           [[text; 1..]; 1..]
  optional_members?: [text; 1..]
  optional?:         boolean ~ false
}

members holds one list per option, options and their members in source order; each slot is text because a member names a declared field by its declared spelling, judged by the record’s name type and never a different one (§5.2). optional_members names the members marked ?, and is absent rather than empty where none is marked, so a group has one spelling. optional is the group’s own ?, omitted in output at its default (§8.1). So ( min: integer | exclusive_min: integer )? resolves to !meta.field_group { members: [[min] [exclusive_min]] optional: true }, and ( email: E | phone: P )+ to !meta.field_group { members: [[email phone]] optional_members: [email phone] }. The in-option mark is recorded on the group and not on the field, for three reasons: meta.record_field.optional keeps meaning “the key may be missing”, which stops being true of a member the moment its option is chosen; a consumer that ignores groups reads every member as optional, which is loose but never refuses a valid document, where the alternative would have it refuse one; and a member’s presence stays the group’s alone to state. The flattened fields-plus-groups form is canonical: group membership is fully derivable from groups in one local pass, and implementations SHOULD compile it into a per-record lookup at schema-load time, per the eager-resolution convention of §5.2 and §7.4 — the output form is canonical, not operational.

Validation. For each group of a record type, after ordinary field validation, the validator finds the chosen options, reports each chosen option’s missing members, and then counts: a group that is not optional errs with no option chosen or with two or more; an optional group errs only on two or more. A present member is validated as an ordinary field of its declared type. Diagnostics stay local, and SHOULD read in the group’s own terms: “port chose (host, port), which needs host”; “include and type choose two options; exactly one is allowed”; “none of (email, phone) is present; at least one is required”.

Refinement and composition. In a refinement or composition body, a restated member stays a member, in its own option. It may drop its ?, which narrows, and may not add one; a member of a + group is restated as it is written there, without a ?, and that changes nothing. Its type slot may move voidable true → false or narrow the type (§5.7), as at any field. A restated member narrowed to void and not voidable (a: void, which admits no value and refuses _) can no longer be present: an unmarked one makes its option unchoosable, and a marked one drops out of its option. A group left with no choosable option is unsatisfiable, as a group that must be chosen with every member so narrowed always was. Narrowed to void? a member can still be written, as _, and written so it is present and chooses its option — a member that carries presence and nothing else. A restated member may take = v, which is checked when written and never injected — the one pin that does not inject, because an injected member would be present, and presence is what chooses the group’s option. ~ v stays refused on a member: a default is a value only a missing key reaches, and whether a member’s key may be missing is the group’s to say. A member reachable by refinement may not acquire a selector (§5.2). A body entry may also restate a group: the restated group MUST have the same options, with the same member labels in the same order (member type expressions restated verbatim), and may tighten it from optional to not optional; the reverse is a resolver error, and changing membership is a resolver error. Supertypes contribute their groups whole; the composed entry’s groups lists inherited groups in supertype order followed by the body’s own. A field belongs to at most one group — guaranteed by label disjointness.

Removal. A removal clause naming a member (§5.9) removes it from fields and from its option; an emptied option leaves the group, and a group with no option left is removed outright, with nothing left to occupy its logical position. A group the declaration rules above would then refuse is rewritten as the spelling it equals — plain fields or an accepted group, one of which always exists. The simplest case is a group reduced to a single one-member option: it is dissolved, the surviving field taking the group’s optional as its own (an unmarked name where the group had to be chosen, ? on the name where it was optional; the field’s own voidability is unchanged). The declaration rules are thus an invariant of resolved output as well as of source: no resolved record carries a group they refuse.

The labelled-sum pattern (informative). A record whose entire body is a single group that must be chosen, each option one field, admits exactly one field — a labelled sum in record clothing:

timestamps => { ( created: timestamp | modified: timestamp | accessed: timestamp ) }

An instance is { modified: "2026-05-21T13:05:00Z" } — quoted, since a datetime contains colons ([TSON-DATA] §7.1); the label is both the semantic role and the discriminator. Where meta.choice (§5.4) discriminates by variant type name — via a !variant annotation, required except where the choice is disjoint under the encoding’s discrimination — and requires distinct variant types, a single-group record discriminates by label and permits variants of the same underlying type, needing no disjointness and no tag. The pattern’s kind is PRODUCT; host bindings MAY recognise the shape (one such group, no other fields) and lower it to a native sum, and where an option holds several fields the variant it lowers to is record-shaped. [TSON-GUIDE] discusses the pattern and the design history.

5.12 Values by Name

A schema may hold a value declared elsewhere and write it by its name. The slot that holds such a name is typed by a reference over a type that is no constructor, meta.ref<T> (§4.2), and the name is a qualified name: segments joined by ., the first naming an entry and each further one a selection inside what the name has reached so far. Every schema may declare and hold values (§4.1), so the need arises wherever one entry addresses another — an HTTP binding naming the method it serves, a configuration naming a profile:

!!id:"https://example.com/service.tn"
!!import:"https://tson.io/2026/39/m/meta.tn"
!schema {
  method_name => meta.identifier
  method      => { request?: meta.ref<meta.record>  response?: meta.type_ref  errors?: [meta.ref<meta.record>] }
  interface   => !meta.namespace { key_type: method_name  value_type: method }
  http_verb   => !enum [GET POST]
  binding     => { method: meta.ref<method>  verb: http_verb  route: text }
  bindings    => !meta.namespace { key_type: meta.identifier  value_type: binding }
}
orders => !interface { place_order => { request: place_order_request  response: order } }
routes => !bindings { post_orders => { method: orders.place_order  verb: POST  route: "/orders" } }

The qualified name. A qualified name is one unquoted token by [TSON-DATA] §7.1’s token profile — . is a token character and the lexer emits orders.place_order as one token — parsed into segments by the position that reads it, each segment a [TSON-DATA] §7.7 identifier. Every name resolves from the root (§3.3): the first segment names an entry of the document’s root namespace, locally or through imports, alias hops followed (§8.3); each further segment selects, in order, a member of a nested !schema map (geo.address, §4.1), a key of a namespace value (orders.place_order), or a field of a record value (geo.home.at, where geo.home is a value entry) — a value of a named product, the record being a namespace whose keys its type fixes. A map that is no namespace is not entered (field_map’s among them, so no qualified name selects a field declaration: foo.bar and foo.fields.bar for foo => { bar: text } are refused), and nothing else is. Every hop is written — nothing is skipped and nothing is looked up lexically, so a name means the same wherever it stands in a document — and the grammar is names only: no [n] (an element of a list is not a thing a schema declares, and [ is structural in the lexer), no quoted segment (api."/x".request is three tokens, and a text key is data, declared nowhere), and no empty segment (orders..place_order is three tokens under the range rule, and a leading or trailing . is refused by the segment grammar). A name that wants a position rather than a named thing is a JSON Pointer in a quoted string, not a widening of this grammar. A map is entered only where it is a value of a namespace type, whose key_type is an identifier family, so a key outside §7.7 under a wider key profile cannot be written as a segment. A qualified name carries no argument list: a value is named and never applied.

The judgement. The slot declares the type: a value of meta.ref<T>, T no constructor, names a value whose type is T or IS-A it, judged after following alias chains and compared by canonical identity — declaring schema and entry — each type named in the namespace of the schema that declares it. The referent is found first and judged after, never chosen among candidates, and its type is judged before its value, so a name wrong by type is refused for its type whatever the value holds. A type entry admits no further segment: projecting into a record type’s fields (order.sku) is a different capability and is not this one. An open entry is no referent, at a reference over a constructor as at one over values (request: box over box => <T> { v: T } is refused, a template denoting no value until applied); what a document names there is the application a declaration gives it, bt => box<int32> then request: bt. A framed name (@doc:"x" orders, !meta.record orders) is refused: a name’s type is its referent’s, and an annotation on it would have nowhere to go. A quoted token is text, never a name. A segment selecting a dependent field (§5.2) reaches a value of the type its sibling names in the same record value — the sibling’s pin, else what the value holds there, else its default — and the walk goes on with that type; a sibling naming no type a value can be read against is a resolver error at the slot holding the name, and so is a sibling the value holds nothing at, with no pin or default. Each refusal names its own step:

WrittenResult
method: orders.place_orderaccepted: orders is an interface entry, place_order a key of its map, the value a method
method: ordersrefused: an interface value, where a method is wanted
method: orders.cancel_orderrefused: orders has no key cancel_order; the keys are listed
method: orders.place_order.noperefused: a method value has no field nope
method: place_orderrefused: no entry place_order; a member is reached through its entry
method: place_order_requestrefused: a record type, where a method value is wanted
method: hs.h.headers.accept, headers: {text => method}refused: a map that is no namespace is not entered
method: shop.reads.fallback, service.fallback?: method absent in the value under readsrefused: a field the value does not state names nothing, as a key the map lacks does
response: ordersrefused: a value entry, where a type is expected (§4.1)
request: { … } at request?: meta.ref<meta.record>refused: a reference-typed slot of a value body holds one bare name (§5.6)
request: box<text> at the same slotrefused: an application in a value body lifts nothing; name the alias (§5.6)

A field of a record value reached past a key selects in that value as a key does in a map: the name reaches what the field holds, and is judged by the field’s declared type. shop.orders.methods.place_order, where service.methods is typed by interface, reaches the method the interface value holds under that key.

Where the rule stands. A value by name stands at a slot typed by a reference naming values and nowhere else: a slot typed by a record holds the record’s value inline (method: { response: order } is plain data of method), and a bare token at a record-typed slot in a schema document fills the record’s positional field where that field is atom-typed, and is refused as no record otherwise (§5.6). The rule reaches a data document. A data document has a namespace — the one its !!imports bind (§3.3.4) — so a name written at a meta.ref<T> slot in data is judged by the same walk over it: which: o2 at meta.ref<order> reads where o2 is a value entry of order or of a subtype in the closure, and a name reaching nothing, a type, or a value of another type is a validation error; a host receives the referent with the name, the referent being the schema’s own value, which the document only names. A field’s default or pin at such a slot is a qualified name judged at link by the same walk (§5.2). An imported entry’s names are judged once, when its own schema links, in its own namespace. Resolved output writes a name as written (§8.1): the referent is judged at link and delivered to a host with the name, and the two readings a later revision might give a name — substitution, where the value replaces the name and nothing downstream can tell the spellings apart, and identity, where the name’s referent is the value’s identity and what a coverage check reads — both stay available. Neither is decided here: this section is the lookup, the judgement and the grammar, which both share.

What it is, against what it replaces. A type’s name at a meta.type_ref slot, a name at a meta.ref<C> slot over a constructor, and a qualified name at a meta.ref<T> slot over values are one thing — a value of the reference atom, a name that must resolve (§4.2) — and differ only in the judgement: over a constructor the name must resolve to an entry whose value’s type is the constructor or IS-A it, and it may carry an argument list where an application is open; over any other type it may carry segments and must resolve to a value whose type is T or IS-A it. Types live only in schema maps, so a type’s name is a path of map members; a value may live in collections, so a value’s name may go on into them. The dotted-names family this spelling belongs to is the qualified name — Java’s, XML’s QName, Python’s dotted name, protobuf’s fully-qualified name, TOML’s dotted keys — where each segment is a name and nothing is a position; the address family (RFC 6901 JSON Pointer, RFC 9535 JSONPath) answers which position and is the one the series’ diagnostics already use.

6. Annotations as Types

[TSON-DATA] §3.1 defines annotation syntax and preservation. This section defines annotation semantics: an annotation is a typed metadata attachment, resolved and validated against a type of the annotating document’s namespace.

Annotation values are always data values — concrete values, not type definitions — both in data values and within the type-definition grammar. Placement follows [TSON-DATA] §3.1; the resolver preserves annotations in their authored positions. The type-definition grammar adds one position: in field-def (§12.1), annotations precede the field name and annotate the field itself, mapping to the meta.record_field in resolver output.

In schema declarations, an annotation may stand at two positions with two meanings, and the resolver does not hoist between them. An annotation immediately preceding the declared name binds to the name — the schema-map key — and is metadata about the declaration: documentation of the entry, provenance, lifecycle (@deprecated name => …), and the resolver’s own derived markers (@meta.synthetic and @meta.application, §8.1, §8.2) live here. An annotation after => binds to the entry’s value and is metadata inside the definition. Both positions are preserved in resolver output at their authored places (§8.1): a key annotation on the output schema-map key, a value annotation on the definition value. A checked annotation (below) is an assertion in either position and is honoured at both: a processor MUST consult both spellings, and MUST NOT ignore one with no diagnostic.

Annotations are types. An annotation @T (or @T:value) names a type T and attaches it as metadata to the surrounding value. T is a qualified name ([TSON-DATA] §3.1), resolved from the root in the annotating document’s namespace (§3.3.3): for a data document the closure its !!imports bind, for a schema document its own entries and its import closure — @doc the kernel’s root doc, @atom.ordering:TOTAL the kernel’s atom.ordering, @title:"…" core’s title, @expires:"…" a type the schema declares itself. The value is validated against T’s contract:

  • For void-targeted T (a type whose resolved body, after following its alias chain, is void — such as meta.annotation, atom.numeric or deprecated), the annotation form is @T with no colon and no value. Bare @T is shorthand for @T:_; the resolver fills the implicit _ and validates against void’s contract (§4.2) — presence is the information.
  • For any non-void T, the form is @T:value, where value is a single data-value conforming to T: @doc:"User's full name", @atom.ordering:TOTAL, @title:"Order".

Any type in the annotating document’s namespace can be used as an annotation — there is no separate annotation namespace. An annotation whose name does not resolve in that namespace is a resolver error, for the valueless form as much as the valued one: @doc in a document whose closure lacks the kernel fails exactly as @doc:"..." does — the bare form is shorthand for @T:_, and _ still needs a void-targeted T to validate against. Annotations are not free-form markers under a schema; the preserved-uninterpreted treatment belongs to schemaless processing alone ([TSON-DATA] §3.1). An annotation typed by an abstract entry — meta.top, meta.type or a base kind, or any other abstract record — is a schema error in either position, a value at an abstract position naming no subtype. A type annotated with itself on its value (note => @note:"x" text) is a wait cycle (§3.4.1); the kernel’s own meta.annotation is an alias of void and annotates nothing.

What an annotation may do. An annotation never changes a value, its type, or whether it is valid: validation is over values, and a schema with every annotation erased admits exactly the same values. That is the criterion for the home of a fact — an annotation is the right home exactly when the mark changes no value’s validity — and it is what keeps an encoding’s projection concern out of every processor’s type model. Within it an annotation may do two further things; the series has one of the first kind and none of the second:

  • It may carry load-time force: a checked annotation is verified against a fact the resolver derives, with two outcomes and no third — verified, or a resolver error at schema load — and the section defining it states the check. @meta.disjoint (§5.4) is checked against derived disjointness, and is the category’s one member.
  • It may be a representation directive: it directs how a class of encodings represents a value, and force is confined to the encodings that claim it — in every other encoding, and in the model, it changes nothing. A directive’s class is named, not inferred: it is the set of encodings whose encoding-rules documents declare, by the directive’s name, that they consume it, and each such document MUST say so where it states its rules. A class is never defined by the behaviour the directive directs — “the encodings that flatten” names no class — and no encoding is privileged, TSON text being one encoding beside [TSON-JSON]. An encoding-rules document MAY bind projection behaviour to a schema-side annotation declared for it on these terms; no such annotation is declared in this revision, and the category stands empty.

Two facts that are not annotations, and why. How a record may be realised (abstract, final) and which fields its members are selected by (=?) each change which values conform — erase the first and a direct instance becomes readable at every position typed by the record; erase the second and every untagged document at a member-dispatched position stops validating, in every encoding, so there is no other encoding for the mark to change nothing in. Both are therefore §12.1 syntax lowering into the kernel’s own meta.record.extension and meta.record.discriminators (§5.2), never annotations: the resolver consumes them into the body, they appear in no output annotation channel, and their names are reserved at exactly one position — a definition mark between => and the type definition — and are ordinary identifiers everywhere else ([TSON-DATA] §7.7). No bundled document declares abstract, final, sealed or discriminator as a type, so @abstract written as an annotation is the ordinary unresolved-name error above.

Everything else an annotation can be is advisory, and the bundled documents declare these (§9):

  • The kernel’s doc, whose text is CommonMark (version 0.31.2), with no extensions. CommonMark is versioned where “markdown” has no specification, so two renderers agree on what a doc says, and a doc that uses one vendor’s tables, task lists or autolinks reads as their literal text. Raw HTML in a doc is never executed, and a renderer need not render it: a doc is written by whoever published the schema, and rendering one fetched from elsewhere must not become a way to run its author’s code. The rule refuses nothing — every string is valid CommonMark, so no schema stops loading and no processor checks anything; it says how the text is displayed, which is all an advisory annotation may do.
  • The kernel’s classifying markers, under atom: @atom.ordering over !enum [NONE PARTIAL TOTAL], @atom.bounded and @atom.exact over boolean, and the void marker @atom.numeric — the ISO/IEC 11404 datatype properties the libraries state their atoms with.
  • The kernel’s meta.annotation, an alias of void, which signals “intended as annotation metadata” so tools can surface available annotations, carries no runtime force, and leaves a type without it no less usable as an annotation.
  • Core’s author annotations: @title, plain text, being a name; @comment, a note for a schema’s maintainers rather than its readers (JSON Schema’s $comment) — where @doc is what a documentation tool shows, @comment is what it leaves out, its format left unstated; @examples, a list of text, each an example as a reader is shown it, conventionally the value written in TSON notation (nothing parses an example or checks it against the type it illustrates — an annotation’s position hands its value no type to be read by); and the void presence markers @deprecated, @read_only and @write_only (a bare mark is the whole statement — absence is the default, so there is no false to write; @deprecated’s reason, where there is one, is @doc’s to give). A schema importing the libraries without core.tn may declare annotations of those names itself.

Resolver-attached annotations. Two annotations are attached by the resolver rather than written by the author, and an author’s spelling of either is a resolver error. Every synthetic entry’s key in resolver output carries the bare marker @meta.synthetic (§8.1, §8.2), so tooling can fold materialised entries back into nested display; it is derived, and when a resolved form is read it is discarded and recomputed (§8.1) — it carries no decode force and cannot be forged into a resolved document to change its meaning. An entry that closes a template application carries @meta.application:box<int32> on its key — the application the entry is the instantiation of, written as the reference it is, nested arguments unreduced (§8.2) — a fact about the entry and not about a type, so an entry of any kind can carry it: a record template’s declared member, a value entry closed from a templated instance, the alias a template’s application closes to, a self-reaching application’s instantiation entry. It is derived, as @meta.synthetic is, and never written by a declaration; unlike @meta.synthetic it is read as stated when a resolved form is read and not recomputed, being the one fact the entry’s value does not state, and what lets an imported schema’s instantiations unify with a later schema’s (§8.1, §10.1). Both are defined in the kernel and follow the same resolution rules as user-written annotations. No annotation records an alias: a use site names what the author named, and the chain is stated as written (§8.3).

7. Text Encoding Rules: Data Values Under a Schema

This section is the text format’s encoding rules: how the type system’s values are carried in TSON text. Every encoding of the type system owns a section shaped like this one — its atom lexical forms, its container syntax, its treatment of void, and its discrimination predicate over the derived disjointness fact (§5.4). This section states TSON text’s rules; every other encoding states its own against the same encoding-independent model in its own encoding-rules document — [TSON-JSON] is the first, and the two agree on every rule the model states, differing in spelling alone.

[TSON-DATA] defines the syntax of data values and their schemaless interpretation. This section defines how a data document’s !!imports bind it (§7.1) and how an active namespace changes interpretation: tag resolution (§7.2), atom parsing in place of base type resolution (§7.3, §7.4), sets (§7.5), the void sentinel (§7.6), resolver behaviours at typed positions (§7.7), and declared positions (§7.8).

7.1 Data Documents and !!import

A data document’s !!import directives name the schemas whose types are available for !name tags in it. Each directive value is an IRI-reference identifying a published schema document, resolved through the schema library (§10).

!!import:"https://example.com/people.tn?sha256=c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5"
!person { name: Alice age: 30 }

The referent is a schema document. An !!import URL resolves to a schema document — never to a resolved form, whose resolver-derived fields cannot be verified from the document alone. Resolved forms are read only as the caller’s explicit choice (§8.1, §10.1).

Several imports are one namespace. A data document imports as a schema does: its imports are read under one namespace over their closures, a name two of them declare differently being §2.2.3’s collision, and the document’s values are read in that one namespace, which no value opens, widens or replaces. A read under several imports is linked as a schema with no entries of its own importing each, and MAY be compiled once per set.

The directives name a namespace, not a root contract. !!import supplies the vocabulary against which tags resolve; it does not assert the document’s root type, and a schema has no privileged entry point. The value names its own type by annotation, and under !!import the root MUST name its type: an unannotated root under a bound namespace is a validation error in every mode, since under a schema every value is typed by its position or by its tag (§7.3) and a root has no position to be typed by. There is no “vocabulary-only” reading of a document under !!import — a value read by base type resolution is a schemaless value, and a document is one thing or the other from its header. The same holds at every position whose type is meta.declared: a value there names its type or is invalid (§7.8).

A document with no !!import directive has no type vocabulary: base type resolution ([TSON-DATA] §4) applies, type annotations are limited to the built-in vocabulary of [TSON-DATA] §5, and any other type annotation is a resolver error ([TSON-DATA] §3.2). The built-in names denote the types the kernel and the libraries declare — the kernel’s for !text, !integer, !bytes and the rest of its root, numeric.tn’s, temporal.tn’s and net.tn’s for their families ([TSON-DATA] §5.5, §5.6) — so a schema that wants them imports the document that declares them, or core.tn for all, and a schemaless !mac keeps its meaning when its document moves under a schema whose closure holds net.tn.

7.2 Tag Resolution

In data values, a type annotation — a tag, !name — marks instantiation: the value is concrete data conforming to the named type. The name is a qualified name ([TSON-DATA] §3.2), resolved from the root in the namespace the document’s !!imports bind — never against definitions within the same document (§3) — !order naming a root entry and !geo.address the member address of the nested map geo (§4.1). A tag carries no argument list; a generic type reaches a tag through a name some declaration gave it (§8.2).

The built-in vocabulary does not apply in schema scope. When a namespace is bound, all tags MUST resolve through it; a built-in annotation name no imported schema’s closure declares is an unresolved-type error. Schemas wanting uuid, bytes, datetime, and the other built-in names import the document that declares them — the kernel, a library, or core.tn ([TSON-DATA] §5.5, §5.6) — or define them locally. This is the normative statement of the scoping rule; [TSON-DATA] §5.1 restates it for schemaless processors. A document with no !!import falls back to schemaless processing (§7.1).

Records are closed under their type, with no exception. When a schema is in scope and a record’s type is known, the record MUST contain only fields defined by its type; fields not present in the type definition are validation errors — a field name that is no value of the record’s name type among them, since every declared name is one (§5.2), the diagnostic under a declared name type naming the type and its reason. A document’s field names are matched against the declared ones in the name type’s form (§5.5, §7.7), and the record that reads a value judges its names: a record nested in one with a relaxed name type judges its own. This applies to directly-typed records (!person { ... }) and structurally-positioned records (a record at a record-typed field position). Nothing in a schema makes a record absorb fields its type does not declare: open-ended data is carried by a declared map-typed field (extras: {text => text}), whose entries are keys and not names ([TSON-DATA] §2.5, §2.6) and are written the same way in every encoding. Schemaless records have no closure rule. Closure is what makes schema evolution a discrete operation: every published schema version is a precise, immutable contract about what fields exist (§3.5).

Type expression syntax is not available in data values ([TSON-DATA] §3.2). To annotate an array or map value with a named type, declare a named type in the schema (int_list => [integer]) and write !int_list [1 2 4 8 32]. A reference carrying arguments is a value, read at a reference-typed position (§4.2), never a tag.

Validation follows what the name is. !T value asserts that the value conforms to T, and conformance is determined by T’s own definition. An atom instance validates a single token by its parsing contract (§7.4): !age 42, never !age { ... }. A product validates a record against its field list; a choice validates any conforming variant. A constructor is a record-shaped type, so it validates a record against its constraint-field vocabulary: !numeric.integer_type { min: 0 max: 255 } is a record conforming to numeric.integer_type’s fields, receiving ordinary record validation; family coherence between bindings (e.g. min ≤ max) is a compilation concern (§8), not data validation. A tag naming a value entry (!orders) is refused: a value entry is no type, and a position cannot be typed by it (§4.1).

Subsumption at typed positions. At a position whose declared type is T, a value annotated !S is valid if and only if, after following both alias chains to their terminal entries (§8.3), S is T or T appears in S’s chain.supertypes (§4.1, §8.1). A subtracted type has no supertypes and does not stand where its source is expected. An admitted value validates against S in full: closure, constraints, and defaults are S’s; the position’s T contributed admission alone. What an unannotated value at such a position means is T’s own extension fact (§5.2), and there are three readings. Where T is OPEN or FINAL, the value is exactly T: there is no structural recovery of S, per the once-only reading of form ([TSON-DATA] §2.4), so the annotation carries information precisely when S is a proper subtype — a field typed person admits !employee where the schema declares employee => person & { ... }, and without the annotation the value is a person, full stop. Where T is ABSTRACT and names no discriminators, T has no direct instances, so the tag is REQUIRED: an untagged value is a validation error naming T, whose diagnostic SHOULD name the subtypes, and a tag naming T itself is refused, no value satisfying it. Where T is ABSTRACT and names discriminators, the value is placed by reading the marked fields at the types T declares them — the one set of types known before dispatch — and matching the value, or the tuple of values in declaration order, against the pins its members state; the matched member becomes the effective type and the whole value validates against it, which re-verifies each pin as an ordinary FIXED check and makes the dispatch read and the validation read agree by construction. A missing selector, or a value no member pins, is a validation error, the latter SHOULD naming the pinned alternatives; the tag is then optional and MUST name the dispatched member or a subtype of it where written — one selector per position, and the FIXED check is what forces the two to agree. This notation reads a member-dispatched position on those terms and so does every other encoding ([TSON-JSON] §6.1.5); the two differ in spelling alone, since the selectors are the model’s facts and not any encoding’s. A record’s fields have no significant order in this notation but for a dependent field’s free sibling ([TSON-DATA] §2.5), so a reader locates the selectors by lookahead over the record; an encoding may require them to lead. Choice-typed positions discriminate by variant membership (§5.4) and declared positions by the closure (§7.8) — the same surface gesture, an annotation selecting a more specific type than the position promises, under their own membership relations; this rule governs every other typed position. It is also what admits resolver output at the meta.declared-typed values of meta.schema: every entry names its type by its tag (§7.8, §8.1).

There is no construction in data. !C { bindings } produces a new type only in the schema grammar (§5.5); the same surface shape in a data document is a record that describes constraints — which is precisely what resolver output stores as an entry’s value (§8). The two category errors are symmetric, in data as in schema: a constructor never types its family’s atom values (!numeric.integer_type 42 is a type error — the value type is the instance, !integer 42), and an instance never types records (!age { min: 0 } is a type error — the constraint vocabulary belongs to the constructor).

No parameterized annotations. Resolver-output bodies (!meta.array { element_type: person }, !meta.map { key_type: … }, §8.1) are annotations by ordinary parameterless constructors (§4.2), validated by ordinary record validation: each field against its declared type, a type slot against meta.type_ref — a name judged a member in the reading closure (§4.2), or, for a value of meta.schema read as a resolved form, where that schema links (§8.1). No special rule remains — a template with any parameter is a resolver error as a data annotation, without exception (§5.10).

Schema values. The type annotation !schema marks a map as a schema value — the kernel’s root schema, an alias of meta.schema, the namespace from meta.type_name to meta.declared (§4.1), whose value is read as the map it describes and whose members declare (§4.1). It appears on schema documents (§2.1) and on documents that carry resolved schema structure, most notably resolver output (§8).

7.3 Atom Parsing Replaces Base Type Resolution

When a schema is in scope, base type resolution ([TSON-DATA] §4) does not apply at all: every value is typed by its position or by its tag, each declared atom type owns its own parsing contract (§7.4), and there is no third way to read a token. The tokens true and false have no special status when a schema is in scope — their meaning is determined entirely by the position’s type, and at the kernel’s boolean they are enum members like any other (§7.4). There is no null in the notation ([TSON-DATA] §4.4): the token null at a text position is the string null, at an int32 position a parse error, and at a void-typed position — any instance of meta.void_type: the kernel’s void, or one a schema constructs for itself (§4.2) — a validation error, since void admits the void sentinel _ and nothing else. A JSON document’s null reaches the model as void through the JSON reader ([TSON-DATA] §6), where the position decides whether a void value is admitted (§7.6); no keyword in this notation is needed to carry it.

7.4 Atom Token Parsing

Each atom type owns its parsing contract. When a token appears at a position whose type is an atom, the atom’s parser takes the token and produces either a typed host value or a parse error; the atom’s constraint record is applied as validation after parsing succeeds.

Parsing and validation are distinct. Parsing takes a token to a host value; validation checks the host value against the constraint record. twelve at an integer-typed field is a parse error; 300 at a field typed age (refining integer with {min: 0 max: 150}) parses as an integer, then fails validation. Implementations SHOULD distinguish these in error reporting ([TSON-DATA] §8.1).

Enum member semantics. An enum is a closed set of names, and the identifier family they are drawn from — the same two facts a record states of its field names through meta.record.name_type (§5.2), under the same name. The kernel’s constructor states both, and one refinement of it, the kernel’s root enum, pins the family for the common case:

enum_type => meta.atom & {
  name_type: meta.identifier_ref
  members:   meta.enum_set
}
enum      => meta.enum_type ^ { name_type?: = meta.identifier }

!enum [OPEN ACTIVE DONE] is a vocabulary: its members are names, written unquoted, and a named scope for name hygiene (§11.4). It is the positional form (§5.6) — name_type is pinned and injected, so members is the one unmarked field. !meta.enum_type { name_type: kebab members: [make-tea drink-tea] } is the general form, and any schema may write it: name_type is data, so a schema enumerates names of a naming vocabulary it declares itself, with no new constructor; !meta.enum_type { name_type: words members: ["lightly active" "sedentary"] } over words => !unicode.identifier_type { medial: " " } enumerates names with spaces in them, with full hygiene, medial placing a character only between two others and never at an edge or doubled. members is typed by the kernel’s meta.enum_set — !meta.set_type { element_type: text min_items: 1 } — so members are held as text whatever name_type is, every enum has meta.enum_type’s shape, and one reader and one host class serve them all: a processor binds a constructor that refines another as the one it refines. A closed set of text that is no name is no enum: it is the text family’s own members facet, !text ^ { members: ["80" "443"] } (below), string-class as every text atom is.

name_type names an identifier family — typed meta.identifier_ref (§4.2), the reference over the types built with unicode.identifier_type, so a value naming any other type is refused as admitted by no such reference, at the declaration. Everything that distinguishes one enum from another is a property of that type, not of enum:

derived from name_type
memberseach MUST be a value of the type, by the family’s own parsing and facets — so a name under its profile — and no two may be one value under its equality; a member the type refuses is a schema-load error (“is not an identifier” under the default, “is not a value of its name_type ‘words’” under an author’s)
hygiene§11.4’s scope and all three of [TSON-DATA] §8.2’s mechanisms, under the type’s own profile, whose added characters are its own
discrimination class (§5.4)the members’ shared class, read off each member’s own token
bindinga host enum by a mapping the binder owns (below)

The first row is a pair of rules and not structure: members are held as text and are unique as text by the set’s own contract, and a type whose equality is coarser than text — one that folds case (§5.5) — makes two of them one name, which is refused. The class row stays with the enum rather than following the type: it reads the members’ tokens (boolean => !enum [true false] is boolean-class, where the type meta.identifier is string-class). A name type’s normalization implies NFC, on an enum as on a record (§5.2): NONE is refused at load, and ASCII_CASEFOLD is admitted only for a type whose every character is ASCII, NFKC_CASEFOLD being the fold for any other, since a document’s name is matched against the members in the type’s form and no reader could match one under a form the type never stated.

Where a name_type resolves. A name_type an author writes names a type in the author’s own namespace. A value a constructor pinned was written in the constructor’s declaring document and resolves there: !enum [A B] names the kernel’s meta.identifier in any schema, however the enum is reached — directly, or through a template’s held body — and an identifier the schema declares for itself is a different type that does not displace it. This is §2.2.3’s rule that a merged entry’s own references stay in its defining schema’s namespace, stated for a pinned value; the same holds of a record’s name_type (§5.2). name_type is fixed at construction (§5.7): a refinement narrows the member set and never changes the type, since narrowing it would re-judge members already admitted and widening it would admit names the source’s type refuses. A ~/= value at the slot is judged admitted by meta.identifier_ref where it is written, so a constructor pinning a non-member fails at its own document’s link rather than at the first schema applying it; a name type written as a template application is admitted as the instance it expands to.

Numbers are never enums. An enum’s members are names, equal only to themselves under their type’s equality; an integer’s equality is a value equality (80 and 0x50 are one value, [TSON-DATA] §4.3), and a numeric value set is the numeric tier’s own facet — a contiguous range is !integer ^ { min: 1 max: 3 }, an arithmetic progression is multiple_of, a sparse set is members (!integer ^ { members: [80 443 8080] }, the numeric.integer_member_set-typed facet numeric.integer_type carries and the numeric.decimal_member_set-typed one numeric.decimal_type carries, §9) — and a choice is for alternatives that are not one family. A mixed source enum (["AUTO", "MANUAL", 80, 443]) becomes a choice over an identifier enum and a member-set integer, which is disjoint by class and tag-free. The split, in one sentence each: a vocabulary of names is !enum [OPEN ACTIVE DONE], or a meta.enum_type over the author’s own identifier family where the names need a wider profile; a value set on a type needed for its other facets is that family’s members — !text ^ { length: 2 members: ["AU" "NZ"] }, !int16 ^ { members: [80 443] } — where the family’s own parsing still applies. What an enum has that a member set has not is an unquoted spelling, a class of its own, name hygiene, and name equality in place of value equality.

An enum MUST have at least one member (!enum [] fails meta.enum_set’s min_items at schema load), and its members are unique (!enum [OPEN OPEN] is a schema-load error). There is no carve-out for true and false: the kernel defines boolean as !enum [true false], an ordinary enum of names whose class is read off its members’ tokens by [TSON-DATA] §4, so boolean stays boolean-class and [INDEX NAMED] string-class. Parsing at an enum-typed position puts the token’s decoded text into the label type’s normalization form (§5.5) and matches it against the members — a typed position never consults the form, so "true" and true are one value at boolean (as [TSON-DATA] §5.5’s !boolean states for the schemaless case). How an enum binds: to the host type of the natural parse of its members — a host boolean at boolean, host text everywhere else — with a host-language enum reached through a mapping the binder owns. Under an identifier family the members are names; that they are host-safe names is true of no host — [TSON-DATA] §7.7 admits in-progress, and the kernel’s own boolean has true and false for members — so binding is a mapping in every case (in-progress to IN_PROGRESS, "new york" to NEW_YORK), and the binder reports one that collides or yields no legal constant. A host language is what a schema maps to, never a constraint on it. members describes the permitted values, not the resolved representation.

Text member sets. unicode.text_type carries members?: unicode.text_member_set beside min_length, max_length, length, pattern and normalization, and unicode.identifier_type, net.uri_type, net.iri_type, unicode.regex_type and net.email_type inherit it through the composition they already use (§9): a value set on a constrained text family, where the family’s own parsing contract still applies — !uri ^ { members: ["https://a.example/" "https://b.example/"] } is two admitted URIs that are still parsed as URIs, exactly as !int16 ^ { members: [80 443] } is a sixteen-bit integer that happens to be sparse. It is a set, not an array, for three reasons — uniqueness comes from the set’s own contract, non-emptiness from its min_items, and member identity is the value’s, so two spellings of one member are a duplicate rather than two — and a plain field beside pattern rather than a group excluding it, because a pattern is two things nothing can tell apart — a|b|c is a value set and beside members is the same statement twice, [A-Z]{2} is a shape and beside members composes as a numeric range does. A member is a value: each is put into the type’s normalization form (§5.5), and two that are one value there are a duplicate and are refused, the set’s own uniqueness having compared only the text as written. The pattern MUST admit every member: the libraries’ uniform members rule, applied, and the one member-coherence check in the series that needs a regex match rather than a comparison. pattern and members are each settable once under refinement (§5.7).

Identifier families. unicode.identifier_type is the constraint vocabulary of names: a UAX #31 identifier profile, stated as data, with the text facets applied inside it.

identifier_type => unicode.text_type & atom.specification & {
  spec?:          = "https://www.unicode.org/reports/tr31/"
  start?:         unicode.identifier_base ~ XID
  continue?:      unicode.identifier_base ~ XID
  start_add?:     text
  continue_add?:  text
  medial?:        text
  exclude?:       text
  normalization?: unicode.normalization ~ NFC
}
identifier => !unicode.identifier_type { continue_add: "-" }     -- meta.identifier

The profile is UAX #31’s R1 default identifier syntax — Start Continue* (Medial Continue+)*, with Start = (start ∪ start_add) − exclude, Continue = (continue ∪ continue_add) − exclude and Medial = medial — where start and continue name the base each set is drawn from (unicode.identifier_base => !enum [XID ID ASCII NONE]: XID_Start/XID_Continue, ID_Start/ID_Continue, XID’s ASCII members — Start A–Z a–z, Continue adding 0–9 and _ — or the empty set) and each of the four text facets is read as the set of code points it holds. A medial character stands only between two others and never beside another medial. A floor no profile lifts: a unicode.identifier_type MUST NOT add, in start_add, continue_add or medial, a character that renders as nothing or as another character — a Default_Ignorable_Code_Point (so every Bidi_Control), a control, or a space other than U+0020 — and a profile that does fails to load ([TSON-DATA] §8.2): an added character meets no restricted-character rule, so without the floor a soft hyphen in continue_add would make two names no rule tells apart. U+0020 stays admissible, a medial space being how a name type holds words. Where a type is a name type — a record’s or an enum’s (§5.2) — its normalization MUST imply NFC: NONE is refused, and ASCII_CASEFOLD is admitted only over a profile whose every character is ASCII. The profile judges the value — the text put into the type’s normalization form, NFC unless the instance says otherwise (§5.5) — so a quoted decomposed spelling at an identifier-typed position reads as its composed value and is not refused for having been written decomposed, and a profile that folds case lists lowercase letters only and admits Content-Type. A join control (U+200C, U+200D) is admitted only in the contexts UTS #39 §3.1.1.1 permits, whatever the profile ([TSON-DATA] §7.7, rule 2). Because the family composes unicode.text_type, it takes lengths, a pattern — a naming convention such as snake_case, inside the profile — and members, a closed vocabulary of names; the member-coherence rule below counts the profile among the facets a member must satisfy, so members: [north "2nd"] under meta.identifier’s profile fails to load.

meta.identifier is [TSON-DATA] §7.7’s grammar exactly: XID_Start-initial, XID_Continue ∪ { - } thereafter, in NFC. Two identifiers are equal iff their NFC texts are byte-identical; identity is case-sensitive. It is the type of every naming position in the series — meta.type_name, meta.field_name and meta.param_name alias it, enum pins it as its name type, and a record whose chain states no other has meta.field_name as its (§5.2) — so a rule stated on it reaches all of them, and because every identifier lies inside the unquoted-token profile ([TSON-DATA] §7.1), the positions that admit no quoted form (! targets, @ names, and every schema-grammar name) lose nothing. A lexer and a schema parser hold that profile directly, the kernel’s own names being read before the kernel exists, and a kernel whose meta.identifier states any other is refused at bootstrap (§3.4). What follows from the family being a type:

  • A value typed by an identifier family is a name. [TSON-DATA] §8.2’s per-name mechanisms reach it under the identifier policy, at a map key and at a field’s value alike, and the keys of a map keyed by one and the elements of a set of one are look-alike scopes (§11.4). The line between a name and data runs through the type.
  • A schema declares its own. slug => !unicode.identifier_type { continue_add: "-" } is one line, after which a schema writes {slug => handler} and refines !slug ^ { … }. No bundled library declares identifier, which stays a schema’s to take.
  • Each profile is its own type. Every !unicode.identifier_type { … } is a distinct type entity: IS-A between two identifier types comes from refinement alone, never from one profile admitting a subset of another’s names. All are string-class (§5.4).
  • The profile facets and normalization are fixed where the profile is constructed (§5.7): a refinement narrows only the text facets, and a different profile is a fresh !unicode.identifier_type.
  • Only meta.identifier’s profile is known to lie inside the unquoted-token profile. A value under another profile may need quoting, which costs nothing: the positions that admit no quoted form are typed by the kernel’s roles, which stay on meta.identifier, and a field name or enum member under another profile is written quoted where it must be ("@id", "lightly active").
  • A profile’s own additions are exempt from the Identifier_Status rule, and a per-segment restriction level divides a name at its profile’s own separators ([TSON-DATA] §8.2).
  • A record’s field names and an enum’s members are values of an identifier family, their name_type (§5.2), so a schema translated from a JSON vocabulary declares the profile its names need — json_name => !unicode.identifier_type { start_add: "@$" } — and judges @context and $ref as names under it, with full hygiene.

The kernel’s second instance is net.scheme_name, a URI scheme (RFC 3986 §3.1) — a letter, then letters, digits, +, - and ., compared without case, written over the ASCII base: !unicode.identifier_type { start: ASCII continue: ASCII continue_add: "+-." exclude: "_" normalization: ASCII_CASEFOLD }. The ASCII base is what lets an ASCII protocol’s names be stated in one line — an HTTP field name is !unicode.identifier_type { start: ASCII continue: ASCII medial: "-" normalization: ASCII_CASEFOLD } — where a NONE base would list the alphabet twice.

Number-grammar reuse. Atoms that parse numeric values SHOULD use the number grammar of [TSON-DATA] §7.6 for the relevant numeric form; a single shared number parser dispatching on the atom’s declared form is the expected pattern.

A facet is read by its type, at load. Every facet of a bundled constructor holding a value of the constrained family is typed by that family’s own instance — a float’s bounds by number, a network list by [cidr4] or [cidr6] — declared in the same document (§5.5, §9): numeric.decimal_type’s bounds and multiple_of are number and its members a set of number; numeric.rational_type’s bounds and step are rational; temporal.date_type’s bounds are date, and so on through the temporal families; net.ipv4_type’s network lists are [cidr4]; a count — a length, an item count, a digit count, a prefix, a precision, a UUID version — is a numeric.count. The ordinary field reader does the rest: a facet is read at schema load by its type, in the wait order of §3.4.1, a value the type refuses being a resolver error with the family’s own diagnostic ('P1Y' is not a valid duration … a month is a period), a set’s members compared as values of the family ([1 1.0] on numeric.decimal_type.members is a duplicate), and a bound spelled as a value of its family (!number ^ { min: 0x10 } refused, the family admitting no based spelling). A refinement of a refinement reads its bound as the base instance: positive => !number ^ { min: 0 } then !positive ^ { min: -5 } reads -5 as a number, which it is, and §5.7’s tightening rule refuses the widening. Nothing in the series types a facet by an untyped escape hatch.

Coherence of a body’s facets. A body’s facets MUST between them admit a value: the lower bound not exceeding the upper (across whichever inclusive or exclusive forms are present), a multiple_of strictly positive, an integer_size.bits above zero, a precision at most 9, a network family’s within/excluding pair and prefix bounds admitting a value (§5.5), an identifier profile whose Start set is not empty and whose medial characters are disjoint from its Start and Continue sets, and — where members is present — every member satisfying the body’s other facets, the range a family derives rather than stores included ({ members: [443] size: { bits: 8 } } fails the way a stated bound outside the width does). Each is a resolver error at schema load; at materialisation the same rules run again over the operands that were parameters (§8.2).

A position typed by a reference over a constructor. In a data document a name at such a position — kind: stock_id at meta.ref<net.uuid_type>, type: int32 at a meta.record_field read as data — is judged by membership in the document’s closure, the one its imports bind (§4.2); an application there is resolved bottom-up to the entry the closure closed it to; and a sibling reference is refused by the facet where allow_sibling is false. A value of meta.schema read as a resolved form resolves its names in the schema it describes, and is judged where that schema links (§8.1).

Lexical classes vs type names. The categories of [TSON-DATA] §4 base type resolution (boolean, integer, float, string) are lexical classifications of tokens a schemaless read produces, not type names: the lexical class and the type number are distinct namespaces that share a word. A schema cannot reference a lexical class, and base resolution never produces a declared type; when a schema is in scope, the schemaless dispatch does not apply (§7.3).

7.5 Sets

A set is an unordered collection of unique values. Sets share array syntax [ ... ] at the data level ([TSON-DATA] §2.7); set-ness is a schema property declared via the kernel’s meta.set_type constructor, most often through the set<T> template (§4.2, §5.3). Without a schema, every [ ... ] is an array.

Duplicate handling. When source data contains a value more than once at a set-typed position, the duplicate is a validation error at the repeated occurrence — a set-typed position asserts uniqueness, and silently dropping a member the author wrote would decode the document to something other than what it says (the same posture as duplicate map keys, [TSON-DATA] §2.6). Two values are duplicates if the element type’s equality contract considers them equal, and for an atom that contract is the type’s value space (§5.5): name identity for a set of meta.identifier, value equality for a set of integer or number (1 and 1.0 are one member), octet equality for a set of bytes whatever the spelling, instant equality for a set of datetime, and equality in the type’s normalization form for a set of a text family (Content-Type and content-type are one member under a type that folds case). For an element type that is not an atom — a record, array, map or choice — two elements are duplicates if the processor’s host representation of them is equal, and which pairs that relates is implementation-defined: a processor detects at least the textual identity of [TSON-DATA] §2.6 in a tree it builds itself, and where elements are bound to host types, those types’ equality decides. The series can reduce to a value space only the atoms it implements on the host; past an atom, identity belongs to the host, and a schema that needs portable duplicate detection over a compound value keys it by an atom — a derived identifier or a canonical string — which is the only identity every encoding and every host can agree on.

Element order. Order is a facet a container states (§5.3): ordered says whether two values differing only in order are one value — true for an array unless it says otherwise, false for a set by meta.set_type’s pin, and false for a map unless it says otherwise. It never changes what a document may write. A set is thus unordered as a value: uniqueness and set equality are what the type states, and element order carries no meaning at a set-typed position. Output keeps the order written either way. In resolved output a set’s elements, like an unordered map’s entries, appear in source declaration order — for meta.enum_type.members, numeric.integer_type.members, numeric.decimal_type.members, unicode.text_type.members and net.uri_type.schemes alike — so that two resolved documents are compared as §8 writes them, with no table of unordered positions to consult and no canonicalisation step; a reader keeps the author’s order, which an ordinal-assigning encoding also needs. A refinement that shrinks a member set keeps the surviving members in their inherited order.

These rules apply uniformly to every set-typed position: the kernel’s meta.enum_set, the exact tiers’ members, user-defined set-typed fields, and set values produced by the positional fill rule (§5.6). A void element at a set-typed position is rejected — meta.set_type pins voidable: = false (§5.3) — and a set may be empty unless its type states min_items: 1, as set<T> does not and every member set of the kernel and the libraries does.

7.6 The Void Sentinel Under a Schema

[TSON-DATA] §2.9 defines the void sentinel and its data-value positions. _ is not itself a type; the type whose sole conforming value is _ is void (§4.2). It is the one spelling of void: the token null is a string, or a parse error, or a validation error, by the position’s type (§7.3).

This document extends the position rules of [TSON-DATA] §2.9:

Position_ permitted?Meaning
Array element (schema in scope)conditionalPermitted only when the array type is voidable, written [T?] (§5.3)
Tuple element (schema in scope)conditionalPermitted only when the position is voidable (§5.3); the void element occupies its slot
Record field value (schema in scope)conditionalPermitted when the field is voidable — its type carries ? (§5.2) — whatever its name’s mark; the field is then present with a void value. At a field that is not voidable, a validation error, whatever the modifier — at a defaulted field the fix is omission, which injects the default (§5.2). At a voidable group member, _ is present and chooses its option (§5.11). A dependent field and its sibling are never voidable (§5.2)
Map key (schema in scope)noResolver error — restates [TSON-DATA] §2.9’s resolver-layer rule; keys define membership and are always required
Map entry value (schema in scope)conditionalPermitted only when the map type is voidable, written {K => V?} (§5.3); the entry is then present with a void value ([TSON-DATA] §2.9) and counts toward the size bounds. A namespace value’s entries are never void (§4.1)
Field-type position (type-definition grammar)noParse error — use type parameters (§5.10)
Type expression position (type-definition grammar)noParse error
Type-def body (declaration right-hand side)noParse error — use {} for an empty record
Field modifier value (~/=)noResolver error — ~ _ and = _ are refused; “may be missing or written _, and nothing else” is the type slot’s to say, a?: void? (§5.2)

When a schema is in scope, a void element requires the governing container type to permit it. Void elements occupy positional slots, and size constraints count all slots. A tuple’s voidable position requires an explicit _ — the tuple’s length is fixed by its type, and short tuples are validation errors (§5.3).

7.7 Resolver Behaviours at Typed Positions

Field names in the name type’s form. A document’s field name is matched against the record’s declared names in the record’s name type’s normalization form (§5.2, §5.5): under a case-folding name type NAME is the field name, two spellings of one name in one record are a duplicate field, and a name that is no value of the name type can match no declared field and is unrecognised (§7.2). The schema’s own references to its fields match the declared spelling exactly; only a document’s names are matched in the type’s form.

Typed key equality. [TSON-DATA] §2.6 defines layered duplicate-key identity: textual identity as the parser’s minimum, decoded-value identity for any processor that decodes, and declared-type identity — which can only make more keys equal — on top. A schema-aware processor decodes keys by the declared key type’s contract (§7.4), so two keys equal under that contract are one key, and a duplicate that only the declared key type relates (1 and 1.0 under a number-keyed map, two spellings of one octet string under a bytes-keyed one, Content-Type and content-type under a text key type whose normalization folds case — the textual and decoded-value duplicates of [TSON-DATA] §2.6 are already resolver errors below this layer) is a Class 2 validation error at the repeated occurrence, consistent with the set rule (§7.5): identity under a declared atom key type is over the key type’s value space (§5.5), and under a compound key type — a record, array, map or choice — it is the processor’s host equality over what it built, at least the textual identity of [TSON-DATA] §2.6 in a tree the processor builds itself, on §7.5’s terms. Which host representation the decoded keys take afterwards is the implementation’s concern; the identity contract for an atom key is not. A key typed by an identifier family is a name as well as a key, and the keys of one such map are a look-alike scope (§11.4). A namespace value’s keys are names of the family its type’s key_type states, judged at link (§4.1).

Empty braces. [TSON-DATA] §2.8 defers empty-brace disambiguation to declared type information. Under a schema, the expected type at the position supplies it: the resolver transforms an empty-brace value into an empty record or an empty map per the expected type, defaulting to an empty record when the position is untyped. The rule covers exactly the two containers that share the brace form and cannot be told apart when empty; an empty map then faces the map’s size bounds like any other. At a position whose expected type is anything else — an array, a tuple, an atom, a choice with no brace-class variant — an empty brace is a validation error, reported as a value of the wrong form: an array or tuple has its own empty spelling ([]), and admitting {} there would let a brace-class value conform to a bracket-class type (§5.4).

7.8 Declared Positions: the Open Sum

Where meta.choice is the closed sum, enumerating its variants, the kernel’s meta.declared_type is the open sum: a value at a position typed by its one instance, meta.declared, names its own type by its tag, among the types of the governing closure — the schema’s own declarations and its imports (§2.2.3) — and is validated in full against it.

declared_type => meta.sum & {}
declared      => !meta.declared_type {}

The data rule. At a position whose type resolves, past its alias chain, to meta.declared:

  • a value carrying a tag names its type: the name resolves in the reading document’s namespace exactly as at any other position (§7.2), a tag naming no type of the closure is a resolver error, and the value validates in full against the type it names;
  • a value carrying no tag is a validation error — a value at a declared position must name its type — in every mode, since the position promises no type for an untagged value to be read as (§7.1).

meta.declared is SUM-kinded and has no discrimination class (§5.4): it is never a variant of a disjoint choice, and a value at a declared position inside a choice carries the choice’s tag and its own. It is what the schema map’s entries are: meta.schema’s value_type is meta.declared, so every entry of a schema map names its type by its tag — a constructor for a type, an ordinary type for a value — which is how a resolved form reads (§8.1), and how a data document of meta.schema is read at all.

A position admits a value of a foreign schema by importing it: a data document’s namespace is the union of its imports’ closures (§3.3.4), and no value opens a schema of its own — a directive at a value position is a parse error ([TSON-DATA] §3.3). A schema that means to admit a type from a document it does not import has no way to say so, by design: a foreign schema that kept its own names would put two namespaces in one document, so either a tag’s meaning would depend on its position or the data would carry a prefix, and neither is taken.

8. Resolver Output

The resolver’s output for a schema is a value of the kernel’s meta.schema — a map from each name to the value its entry resolves to (§4.1): the output records and their conventions (§8.1), template instantiation and identity (§8.2), and aliases (§8.3). Each construct’s mapping to its value is defined with the construct in §4–§5. Resolved output and source are one grammar: output is source with sugar expanded, bodies flattened and chains computed, and an entry written as a constructor application resolves to itself.

8.1 Output Records

An entry is its value. A schema map’s entry resolves to the value its declaration denotes, with no wrapper around it: person => !meta.record { fields: { … } }, integer => !numeric.integer_type {}, age => !numeric.integer_type { chain: { supertypes: [integer] } min: 0 }, meta.type_name => !meta.alias { target: meta.identifier }, vec => !meta.template { … }, a value entry orders => !api { … }, and a nested map geo => !meta.schema { … }. Values are annotated with the structurally appropriate constructor: !meta.record for vocabulary bodies (constructors, §4.2) and for every closed record; !meta.record_template for a record template; a binding record headed by the applied constructor for every closed non-alias definition (§5.6); !meta.alias { target: T } for alias-kind entries; !meta.template { … } for a held open entry (§5.10); an ordinary type’s tag for a value entry (§4.1); and !meta.schema for a nested map. meta.schema’s value_type is meta.declared, and every entry names its type by its tag (§7.8); a constructor’s path is written as the document that declared it declares it (!meta.record, !numeric.decimal_type, !net.uuid_type, !enum), and a name at every slot as the described schema writes it.

A resolved form’s header is its source’s, and its root is !schema: it carries the source’s !!id (under a fixture identity where one is published) and the source’s !!imports, so that the namespace in which its names resolve is the one the source resolved in; nothing in the document marks it resolved, and a processor reads it as a resolved form because its caller says so (§10.1). The kernel’s own resolved form carries its !!id alone and is read under the pre-loaded kernel. Nothing is supplied beside the document.

Four things a reader might expect are deliberately not on the entry. There is no constructor field: whether an entry is a constructor is whether it is meta.top or IS-A it, derivable from its chain (§4.2). There is no kind field: an entry’s kind is derived (below). There is no source field: a construction’s constructor is the value’s tag, a refinement’s origin is the chain’s nearest member, and the one fact a wrapper once held that nothing else states — the application an entry is the instantiation of — rides on the entry’s key as @meta.application (§8.2). And there is no members on a reference and no disjoint on the entry: the one derived index a body carries lives in the body it is derived over, and a reference’s admitted set is derived and never written (§4.2).

A type carries its chain. Every type body — every value whose constructor IS-A meta.type — carries meta.type’s one field, chain, a meta.type_chain (§4.1): supertypes, the closed IS-A chain, every element a path without arguments, complete to a fixed point through applied parents (§5.8) and sorted; subtypes, the inverse over the namespace of the schema being resolved (§3.3.4); and, on a record template alone, parents, the parents as written, one of which may be an application (§5.10). An empty chain is omitted. A composition’s fields are flattened and its chain stands among them first. meta.alias, meta.template and a value entry carry no chain — a hop, an open body and a value carry no IS-A, which is the rule. A declaration never writes chain and a constructor never declares a field of that name (§4.1).

Open entries. An open entry takes one of two forms (§5.10). A record template is !meta.record_template { fields: { … } parameters: [ … ] }: an ordinary record body read as the vocabulary, its fields meta.record_template_fields — ( type | type_param ) optional? voidable? role? ( value | value_param )? — a parameter standing as a field’s type_param or value_param or as an argument of an application in a field’s type or in a parent, its discriminators stated as the family’s, its extension each application’s, and chain.parents the parents as written. Every other open entry is an instance of the kernel’s meta.template constructor — template => meta.top & { parameters: [meta.template_param] template: text }, so set => !meta.template { parameters: [{ name: T type: meta.type_ref }] template: "!meta.set_type { element_type: T }" }. The parameters are a list of meta.template_param records, in declaration order — the arity, order and types an application binds against: template_param => { name: meta.param_name type: meta.type_ref bound?: meta.type_ref }, where type is the type an argument is read as, derived from the body and narrowed by a written type, and bound is present only where type is meta.type_ref (§5.10). It is a list of records rather than a map, which would lose the order arguments are positional in, or two parallel lists, which would be free to drift. type is a derived fact that may also carry what an author wrote — <N: int8> lives nowhere else — so a resolved-form read verifies it rather than recomputing it (below). A meta.template’s application is text, the held body as written under §5.10’s one-spelling rule, an application in it written as the reference it is (!meta.alias { target: pair<uuid B> }); what is compared — for identity, on reading a resolved form, and between two resolvers’ outputs — is the parsed form of the text, never the text, so whitespace is free (§5.10). A !meta.template carries no chain, extension or discriminators: it is not a type, and an application of it is its expansion (§8.2). A consumer of closed entries never meets either (§1.3), and dispatches on a record template’s stated facts without reading a field that carries a parameter; a consumer that reads a !meta.template’s held text needs a schema parser, which a resolved-form read already requires (below). Both constructors are resolver vocabulary: nothing is ever typed by them, and a source declaration applying either directly is a resolver error — the parameter list <…> is the authored spelling of an open entry (§5.10).

Kind is derived. A consumer that needs an entry’s kind (§4.1) derives it from the entry’s value, in this order, and every conforming resolver derives the same answer: (1) the value is a !meta.template → TEMPLATE, an open entry with no kind of its own; (2) else the value’s constructor head is meta.alias → ALIAS — the head lookup would say PRODUCT, since the kernel’s meta.alias is itself a product, and §4.1 makes ALIAS a kind the alias form confers rather than a base kind; (3) else the entry is meta.top or IS-A it — it is a constructor — → the base-kind name among its own chain.supertypes (meta.atom, meta.sum), each counted only as the kernel’s own entry, or PRODUCT if none: a constructor’s kind states what its instances are, not what its own body is (numeric.integer_type has a !meta.record body and is ATOM-kinded); (4) else the value’s constructor head is a constructor → the kind of that constructor, ATOM, PRODUCT or SUM, a !meta.record_template being PRODUCT; (5) else the head is an ordinary type → the entry is a value entry, and takes no kind of its own (§4.1). A kind is judged by origin, not spelling: a schema’s own record named product reaches none of the kernel’s entries, so a composition onto it is PRODUCT by structural default.

The meta.alias, meta.record_field, meta.record_template_field and meta.field_map records. An alias body is !meta.alias { target: T }, where target is a reference — a bare name for every closed alias, and an application with its arguments where the alias is still open and so inside a held body (!meta.template { … template: "!meta.alias { target: pair<uuid B> }" }, §5.10), so that a partial application states the arguments it binds. An alias is a hop: target names the immediate referent, and a use site elsewhere in the output that names the alias names the alias (§8.3). A record’s fields is a meta.field_map — an ordered meta.map from each field’s name, as text, to its meta.record_field (§4.1, §5.2): the name is the key, written once by its spelling, and each meta.record_field carries its type (a meta.field_type_ref value — a type’s name, or a sibling reference kind! written as written) and the four facts a reader consults — optional (the name’s ?, default false), voidable (the type’s ?, default false), role (FREE, DEFAULT or FIXED, default FREE) and value, typed type! and so read against the field’s own type — each default omitted in output, and value present exactly when role is not FREE. Three invariants hold over them: value is present exactly when role is not FREE; DEFAULT requires optional: true, a default on a key that is always written being a value nothing can reach; and FIXED requires voidable: false, since the _ decision is taken before the pin is consulted. A record field is closed: its type names a type or a sibling, and its value is a literal, never a parameter. A record template’s field is a meta.record_template_field, which labels a parameter standing for the whole type (type_param) or the whole value (value_param); its fields is a meta.record_template_field_map. There is no stored omission answer: a reader derives it — a field that is not optional is missing, an optional one with a value injects it, except at a field group’s member, which the group governs and never supplies (§5.2, §5.11). A meta.record body also carries extension (omitted at OPEN), discriminators (present where the record is a member-dispatched base), name_type (present exactly where it was stated, §5.2) and groups; a meta.record_template carries the same and parameters. The meta.field_map’s keys are text judged by the record’s name type — meta.field_name where neither the record nor a supertype states one — and two keys that are one value under it are a resolver error (§5.2).

optional and voidable in output. The series’ two kinds of nothing have one name each wherever a record states them (§5.2). optional — the slot may be missing — is a field of meta.record_field and of meta.field_group, where it is the group’s own ?, beside optional_members for the members that may be missing once their option is chosen (§5.11). voidable — the slot’s value may be void — is a field of meta.record_field, meta.tuple_element, meta.array and meta.map, where it speaks of the field’s value, the position, the element and the entry’s value. A container’s part is never missing, so a container has no optional; a map’s key is never void, so meta.map.voidable is its value’s alone; both are structural, and no rule has to refuse them. Each is a boolean defaulting to false and omitted there. There is no element_state and no state field: a binding record carrying one is refused as a member its vocabulary does not declare (§5.5), never read as the other fact.

Positional form and references. A slot typed by a reference holds a bare name — type: text, element_type: meta.record_field, target: meta.identifier — or, where an application is open, the reference with its arguments, written unquoted ([TSON-DATA] §7.4), and a sibling reference where allow_sibling admits it. Canonical output writes an application with no whitespace but one space between arguments and no comma; identity is the parsed form (§8.2). meta.record_field.type is an atom, so a meta.record_field has a positional form and a document may write fields: { x => text } for fields: { x => { type: text } }. Canonical output writes the explicit record, tagged: fields: { x => !meta.record_field { type: text } }. The untagged record and the positional form are read as the same value, meta.field_map’s value_type typing the position.

Reading parameter references. A parameter reference appears only inside an open entry’s own body — a held text, or a record template’s fields and parents — and every reference outside one names an entry and is read directly against the schema. Within a held body a parameter stands as its name, read against the entry’s parameters list first, then the schema’s namespace, the same precedence used during source-level parsing (§5.10); within a record template’s fields a parameter is labelled, type_param or value_param, and among an application’s arguments in a field’s type or in a parent it stands as its name, read by the same precedence. A constructor head in held text resolves in the schema’s namespace as it did at source level (§3.3.1). A token in a closed body that would resolve only as a parameter is an unresolved reference, the verdict every other reference form gives it.

@meta.application is the one fact the value does not state. An entry that closes a template application carries, on its schema-map key, @meta.application:box<int32> — the application as written, the reference with its argument list, nested arguments unreduced (@meta.application:pair<box<text> int32>), each bare-name argument past its alias chain and a value argument a token (§8.2, §8.3). It is the entry’s identity as an instantiation, and an entry of any kind can carry it: a record template’s declared member, a value entry closed from a templated instance, the alias a template’s application closes to, a self-reaching application’s instantiation entry (§8.2). It is a fact about the entry and not about a type, so it lives beside the entry as @meta.synthetic does, is derived — an author’s @meta.application is a resolver error, as a written chain is — and is read as stated when a resolved form is read, not recomputed (§6, §10.1), which is what lets an imported schema’s instantiations unify with a later schema’s. Nothing else records provenance: which operator a refinement used, and what a subtracted record was subtracted from, are what was written, nothing reads them after resolution, and the source document has them.

The derived indexes: subtypes and disjoint. Two indexes are computed by the resolver and never set by a declaration, each living in the body it is derived over. chain.subtypes is the transitive inverse of supertypes across the schema’s namespace — “what is in family X?” — and supertypes answers “is this in family X?” by name; a parent written as an application contributes, once the composing entry closes, the name of the entry its arguments name, and a declared member additionally the head (§5.8), and a record template is indexed by its own name, its members being the applications a declaration names (§5.10). supertypes’ standing differs from subtypes’: subtypes is a cache, fully derivable and never trusted, while supertypes is part of a type’s serialized meaning — the chain and a flat body cannot be recomputed from each other, and for the atom family the hop lives only there, since desugaring erases the surface distinction between refining an instance (age => !integer ^ { … }, IS-A integer) and constructing a fresh instance (port => !numeric.integer_type { … }, IS-A nothing), both serializing under the !numeric.integer_type tag with an identical body shape. Construction and expansion are not IS-A: a sugar construction (ids => [order]) carries no supertypes (§5.6), and a template’s expansion carries whatever chain the expansion’s own ^ or & gives it — a record template’s declared member IS-A the template (§5.10) — but a minted record takes no edge to a parent application nothing declares, and is a member of nothing (§8.2). disjoint is meta.choice’s: choice => meta.sum & { variants: [meta.type_ref] disjoint?: boolean }, and for every choice, declared or synthetic, the resolver writes true or false beside the variants it is derived over — discrimination-class distinctness, a total, two-valued derivation (§5.4). It sits there rather than on the entry because a variant list is the only thing it is a fact about, and §7.2’s closed-record rule refuses disjoint on any other body, so meta.declared_type needs no carve-out; the @meta.disjoint annotation (§5.4), when present, is the author’s assertion checked against this derived value at schema load, and does not appear in the value itself. A reference’s admitted set is derived at link and never written (§4.2). Like every name in resolver output, the names in these indexes are tokens in the described schema’s own namespace (§2.2.3): an index never reaches outside the resolution that wrote it, and a consumer follows the entries in the defining schema’s output.

Reading a resolved form. A resolved form is a schema document in canonical form, read as a value of meta.schema; reading one as the resolver’s output rather than as a source schema to resolve is the caller’s explicit choice (§10.1), as the form’s header carries no mark. The reader is told the root’s type and reads under the form’s own imports — the kernel’s own form, which imports nothing, under the pre-loaded kernel — and nothing is supplied beside the document. Then: chain.subtypes, disjoint in every choice body, and the derived annotation @meta.synthetic on synthetic entries’ keys (§8.2) MUST be discarded and recomputed, so none can be forged into a document to change how it reads; an entry’s kind is derived on the same terms and was never in the document to trust (above). chain.supertypes and chain.parents are read as stated, never recomputed, with integrity verified — every listed supertype must exist, the list must be closed to a fixed point, an atom’s supertypes must share the entry’s constructor tag, and a record’s must be consistent with the flattened fields — because a flat body and its chain cannot be recomputed from each other (§4.1). @meta.application is read as stated (above). Author-written annotations, key- and value-position alike (§6), are preserved as data, a restated field’s in the order §5.8 fixes. The closed-entry rule (§5.10) MUST be verified — no token inside a closed body that resolves only as a parameter (§5.6); a !meta.template’s held text is parsed and re-resolved as source against the form’s namespace, since a held body is never read against a vocabulary until it closes, and the parsed form MUST equal the one-spelling form §5.10 requires; the text is never compared as text. Each parameter’s recorded type and bound are verified, not recomputed: the reader derives them again and checks that a recorded type IS-A the derived one and a recorded bound IS-A every inherited one — the rule a written type already met at the declaration (§5.10) — so that a written narrowing survives and a stale or altered one is refused, where discarding and recomputing would silently drop every type an author wrote. Every entry carrying @meta.application MUST equal the recomputation by substitution from it, and every synthetic entry MUST carry the body its constructor’s vocabulary admits — the self-describing checks. A checked annotation (§6) is re-checked as it was at load: @meta.disjoint against the recomputed disjoint. Every record’s and enum’s names are re-judged against its name type, every reference-typed slot’s name against its reference (§4.2, §5.2), and a dependent field’s held value against the type its sibling names (§5.2). The family checks of §5.2 — every member of a member-dispatched base pinning each selector, pins distinct as values, nothing composed onto a FINAL record, no minted record a member of anything — are re-run over the form’s map and the closure that holds it (§3.3.4), the declaration rules of §5.11 are verified over every meta.field_group, and the three meta.record_field invariants above are verified per field. A nested map’s members are keyed under their paths, and a value entry’s payload is read against its type as data. Canonical-form violations (a positional meta.record_field, a chain written where none is admitted, a members list on a reference) are resolver errors. Within-family retargeting — a document claiming an entry refines a different instance of the same constructor — is internally consistent and undetectable from the document alone; this residual gap is one reason resolved forms are never schema sources (§10.1) and reading one is an explicit act.

Body patterns (informative — the construct sections govern):

Source formResolved valueExamples
Root record abstract {}PRODUCT; !meta.record { chain: { subtypes: [...] } fields: {} extension: ABSTRACT }; no supertypesmeta.top
meta.type, a base kindPRODUCT; !meta.record { chain: { supertypes: [meta.top …] … } … extension: ABSTRACT }meta.type, meta.atom, meta.sum
Fresh record { fields }PRODUCT; !meta.record { fields: { … } }; no supertypesperson, meta.record_field
Composition A & B & { ... }!meta.record { chain: { supertypes: [A B …transitive] } fields: { …flattened } }; kind per §5.5employee
Refinement T ^ { ... }chain.supertypes: [T …]; materialised bodyproduction
Subtraction T - { f }!meta.record { fields: { … } }; no supertypes (§5.9)account_public
Atom constructor meta.atom & { ... }ATOM; IS-A meta.top; chain.supertypes: [meta.atom meta.top meta.type]numeric.integer_type, meta.enum_type, meta.reference_type
Product constructor meta.product & { ... }PRODUCT; IS-A meta.top; chain.supertypes: [meta.product meta.top meta.type]meta.record, meta.array, meta.record_template
Sum constructor meta.sum & { ... }SUM; IS-A meta.top; chain.supertypes: [meta.sum meta.top meta.type]meta.choice, meta.declared_type
Constructor refinement T ^ { ... }IS-A meta.top through T; chain.supertypes: [T …]meta.set_type, meta.namespace, enum
Instance template <T, N> !C { ... }!meta.template { parameters: [{ name: T type: meta.type_ref } { name: N type: … }] template: "!C { ... }" } (§5.10)vector, set, meta.ref, meta.tuple1
Constructor instance !T {}kind from T’s family; !T {}; no supertypesinteger, meta.declared, uuid
Value refinement !I ^ { v }!ctor { chain: { supertypes: [I …] } v }age, numeric.count, meta.type_ref
Namespace type !meta.namespace { key_type: K value_type: V }PRODUCT; !meta.namespace { … }; no supertypesmeta.schema, api
Sugar or constructor-application bodyconstruction in place; binding-record value; no supertypes (§5.6)lookup => {text => integer}, meta.field_map
Record template <T> { ... }!meta.record_template { fields: { … } parameters: [{ name: T type: meta.type_ref }] }, a family base nameable at a type position, ABSTRACT by construction, chain.parents as written (§5.10)container, pair
Sugar form at a use sitelifts to a synthetic entry — closed (constructor body) or open (!meta.template) per §5.3; the use site holds a bare reference[text], {text => integer}, [T; N] in a template body
Template applicationthe declaration naming it, with @meta.application on its key; else its expansion, a closed synthetic, beside which a self-reaching application keeps an instantiation entry, a !meta.alias to the synthetic carrying @meta.application (§8.2)string_triple => vector<text, 3>, a use site’s set<text>
Value entry !T { … }, T an ordinary typethe value as read against T, T’s tag on it; no chain (§4.1)orders => !api { … }, create_order => !operation { … }
Nested map !schema { … }!meta.schema { … }, its members resolved and keyed by name, each an entry of the document by path (§4.1)geo => !schema { … }
Choice (A | B)SUM; !meta.choice { variants: [...] disjoint: … }, the fact always recorded in the body (§5.4)contact_method
Field group ( a: T | b: U )members flattened into fields as optional: true; grouping in groups, one list of members per option (§5.11)numeric.integer_type, meta.record_template_field
Abstract base with selectors abstract { a: T =? … }PRODUCT; !meta.record { … extension: ABSTRACT discriminators: [a] }; members in subtypes pin a (§5.2)pet
Alias nameALIAS; !meta.alias { target: name }; no chain — a bare name, never a fully-bound application, which is an instantiation in placeid => uuid (§8.3)

The meta.schema type is a map from type names to values; schema lookup is by path. Declared entries are keyed by their declared names, nested maps nested; synthetic entries and self-reaching instantiation entries are keyed by their internal names at the root, each synthetic entry’s key carrying the derived @meta.synthetic marker (§6, §8.2).

8.2 Instantiation and Synthetic Entries

Resolver output contains internally named entries beyond the declared ones, and one identity discipline covers them all. Every application materialises, as its expansion: a sugar form or constructor application at a use site lifts to a synthetic entry — closed or open per §5.3’s lift rule — and a fully-bound application of a template materialises its expansion, the closed synthetic entry the substituted form lands on, keyed on its content (§5.10). Nothing is carried structurally in place: a use site holds a bare reference to its entry, and an argument-bearing reference in output means an open application inside a template body or a record template’s fields and parents, an open alias’s target, or an @meta.application, nothing else (§8.1). A declaration’s own body resolves in place as a construction (§5.6) and is the one thing that never lifts.

Trigger positions. Lifting and materialisation fire wherever an application is named at a type position: record field types, group member types, tuple element types, array and map element positions, choice variants, arguments at any depth, and a refinement source — x => box<text> ^ { n: integer } materialises box<text>, or unifies it with the declaration that owns it in either declaration order, and x IS-A the entry it denotes, as §5.7 promises. A top-level type-def body that is a fully-bound template application makes the declaration the entry itself (below). Composition operands remain restricted to named type references, optionally with arguments; inline structural forms there are parse errors (§5.8, §12.1), and an application at a composition operand is subsumed as an operand and mints nothing, contributing its fields, an edge to its head and the head’s ancestry, and an edge to the entry its arguments name only where a declaration owns one (§5.8). An open application inside a template body does not materialise — it closes when its template does (§5.10).

An application nothing declares is its expansion. A use site naming set<text> names the synthetic !meta.set_type { element_type: text } lands on — the entry a [text]-style sugar or another template expanding to the same form also lands on; a use site naming box<int32> names a closed synthetic record, keyed on its content, which crate<int32> over a template with the same field list lands on too. No instantiation entry is minted and nothing records the application: the expansion records nothing a value can tell apart, since it admits exactly the synthetic’s values. The one exception is a self-reaching expansion: nested => <T> ( T | [nested<T>] ) closed at nested<text> names itself inside its own body, so its content cannot be written without a name, and the knot is tied on the application, which keeps an instantiation entry — a !meta.alias to the synthetic, keyed on the canonical application, carrying @meta.application (§5.10.1). The rule is total: whether closing reached the application again decides it. A minted record is a member of nothing. A minted record whose chain would name a declared type — k: dog_of<text> over dog_of => <T> pet & { … }, or a parent application a declaration owns — would be a family member with no name a read, a tag or a binding could use, and is a resolver error at the declaration that wrote the application (§5.2). A parent application nothing declares gives a minted record no chain edge: u: labelled<text> over labelled => <T> box<T> & { label: text }, beside w: box<text>, loads, the minted record IS-A nothing. A minted record gains no IS-A edge later; a declaration’s operand still reaches it — vip => customer & box<int32>, beside a use site’s box<int32>, is IS-A that synthetic and stands where crate<int32> is expected, the two applications being one type. A declaration names only itself. An unnamed form with a declaration’s content is not that declaration: beside text_list => [text], a field written t: [text] names the synthetic, and !text_list [a] there is refused by §7.2; collecting would need a new rule for every form kind and would make two declarations sharing a body ask whether they interchange, and they are two types. A diagnostic names what the author wrote, and nothing for a form with no spelling: a minted entry is named by its application as written ('b<10>': '5' is less than the minimum 10), a reference’s refusal lists meta.ref<net.uuid_type> where it would have listed an entry minted for it, and a minted record is named as the body a record is written with (unknown field 'nope' on '{ v: int32 }').

Two identities. Bytes identify a document, for distribution and pinning; names and parsed structure identify an entry, for typing. Neither stands in for the other: a whitespace edit changes a document’s digest (§10.2) and none of its entries, and two spellings of one form in a schema are one entry. A declared entry’s identity is its declaring document and its name, its path from that document’s root (§2.2.3, §4.1): two declarations are two types however alike their bodies — stock_id => !net.uuid_type {} and other_id => !net.uuid_type {} are two atoms, and a purely structural rule would collapse them and destroy a distinction §7.2 enforces (§5.5). A declaration whose body denotes a type is that type’s entry, in both lift channels: text_list => [text] is the construction in place (§5.6), and bx => box<text> is the instantiation in place — the substituted binding record its value, closed under the author’s name, @meta.application on its key, with nothing minted beside it and no alias hop. A use-site application resolves to a declaration that owns it where one exists, so “two fully-bound applications denote the same entry” holds within a schema. Two declarations naming one application are two entries, neither privileged and neither collapsed — what a => { v: text } beside b => { v: text } already is, and so what t => <T> { v: T } with a => t<text> and b => t<text> has to be; where the pair is a discriminated family §5.2’s pin-distinctness rule refuses it, exactly as it refuses the hand-written pair. What this gives up is stated plainly: a content-derived name is a function of the form alone, which is what lets two independently resolved namespaces agree on it across the import merge (below); a declared name cannot carry that, so a schema writing box<text> that has never seen the schema declaring bx lands on the content-keyed expansion while that schema calls it bx, and the merge sees two names for one form. That costs nothing a value can tell — both entries admit exactly the same values, and a use site in the importer resolves to the declaration owning the form — and it cannot reach a family, a family member being declared. A minted entry’s identity is its canonical content, since it has no declared name to be its identity; “identity is structural” is true of the minted half only, one rule per family:

  • Self-reaching instantiation entries — two fully-bound applications denote the same entry if and only if their canonical applications are structurally equal: the application with every argument’s alias chain followed to its terminal entry and every nested application compared as the entry it closed to (§8.3), compared member-by-member — heads, argument order, a reference argument against a reference argument, a token against a token under the rule below. Following the chain is what makes a rename transparent: box<user_id> over user_id => uuid denotes the same type as box<uuid>, because an alias is the same type under another name (§5.7’s table) — where box<!uuid ^ {}> and box<!net.uuid_type {}>, a refinement and a fresh instance, keep entries of their own. @meta.application records the application as written, nested arguments unreduced (§8.1), and identity compares each nested argument as the entry it closed to, and spelling variance never forks an entry; the name the author wrote is provenance, not identity, and survives at the use site that wrote it. A declared instantiation’s identity is its name (above); a use site reaches it by the canonical application, compared on these terms.
  • Identity is the parsed form. A reference is compared as its head and its arguments, each a reference or a token — never as text, and never by the channel a processor filed an argument in: box< text, > at an element type and box<text> at a type position close to one entry, and names<red> is one application whether red was read as a reference or filed as a value at its parameter.
  • Value arguments compare as values, and are recorded as written. A literal argument is recorded in @meta.application (and substituted into the body) as the token the author wrote, so resolver output round-trips the spelling; identity, however, compares the value the token denotes under [TSON-DATA] §4 — exactly the equivalence §4.3 states and no wider. Radix, digit separators, and a redundant sign fall away (255, 0xFF, 0b1111_1111, 0o377, and +255 are one argument), a float’s written scale falls away (.5/0.5, 1.0/1.00/1e0), and .inf/.infinity are one value; quoted strings compare by text. What does not fall away is the base type: §4 resolves 1 to an integer and 1.0 to a float, so those remain two arguments even though one magnitude covers both. vector<float32, 255> and vector<float32, 0xFF> therefore denote one entry, and a choice listing [float32; 255] and [float32; 0xFF] as two variants is the same-variant-twice error of §5.4, not an accepted non-disjoint choice.
  • Closed synthetic entries — structural equality of the resolved binding record: one entry per distinct concrete form schema-wide, whichever spelling or template produced it, and wherever in the document it was written — inside a nested map included, synthetics minting at the root (§4.1). A closed synthetic carries the constructor it builds as its tag, never the application that produced it, which is what lets every channel land on it.
  • Open synthetic entries — structural equality of the parsed held body up to consistent renaming of parameters (<T, N> and <A, B> over the same shape are the same template), normalised by renaming parameters positionally (p0, p1, …) before comparing; the text a !meta.template carries is never compared as text (§5.10). This is what “one spelling” exists for: <T> { a: [T] } written twice mints one entry, and an entry’s content-derived name is computed over the renamed parsed form.

The channels dedupe against each other’s products: [order; 1..] written directly in a plain declaration and the same form arising inside a materialised template land on one closed synthetic entry, because both comparisons occur after names have meaning, over resolved structure. The moment is normative: desugar-time lifting creates a synthetic entry, but its identity settles after Pass 2, when references have resolved — eagerly-lifted synthetics that become structurally identical under resolution merge into one entry, so the one-entry-per-form rule holds schema-wide regardless of which moment produced each candidate. The merge pass is required, not an optimisation: a closed lift hashes its binding record at desugar, before its inner applications are rewritten to entry names, while an open lift hashes the closed record at materialisation, so without a pass that re-derives every synthetic’s identity from its resolved record, [box<text>] written directly and [box<T>] closed with T := text land on two entries for one type (§5.10). The first occurrence materialises; later occurrences reuse; recursive references resolve to the entry’s own name (§5.10.1). Closure consults templates and never modifies them: closing grid<pixel, 4> after grid<pixel, 3> reuses the template entry and both open synthetics untouched while producing fresh closed entries. A refinement source’s application closes on the same terms in either declaration order: whether tb => box<text> is declared before or after x => box<text> ^ { … }, x IS-A one entry for box<text>, the declaration’s where there is one.

Synthetic entries are an implementation’s own. Their names are resolver-chosen and their shape is unconstrained beyond producing the required resolved output: a processor is free to mint one per schema, share them, alpha-normalise their parameters, or keep them however its own resolver requires, and the lift targets are never kernel-declared as such (§5.3). An author never writes one and never references one; the one thing an author does is name an application they want to reuse, and the spelling for that is an alias — order_page => page<order> — which is one line, gives the application an identity and a diagnostic located at the author’s own declaration, and is RECOMMENDED over repeating the application (§8.1).

Entry names are internal. Minted entries are named by the resolver, and the names are not part of this specification’s conformance surface: an implementation chooses them freely, subject to two rules and one recommendation. Freshness (MUST): an internal name is a valid identifier and collides with no declared entry and no other internal entry. Stability within a resolution (MUST): all references to one entry use one name. Determinism (SHOULD): names SHOULD be derived from the entry’s identity-bearing content — the canonical application for a self-reaching instantiation, the resolved body structure for a closed synthetic, the parameter-normalised body for an open one — a readable head plus a structural hash, for instance, a dotted name contributing its text with each dot written _ and a path under the kernel’s meta its member’s name alone. Content-derived names keep re-resolution output diff-stable, and they are what makes independently-resolved namespaces agree on their internal names wherever their structures agree — which is what lets the import merge (below) unify rather than collide. Internal names do not exist at source level (there is nothing to author), MUST NOT be relied on across resolutions or implementations, and resolved-form comparison between implementations is structural — equality up to renaming of internal entries, with set-typed fields compared as written, in source declaration order (§7.5). Two consequences for a consumer follow and are stated because a wrong pick is invisible until someone writes a configuration: no consumer contract may require a derived name — a binding map, a generated class table, a configuration file keys on a name some declaration gave the application, and a processor holding both SHOULD show and accept the declared name wherever it shows or accepts one, falling back to the entry’s own only for an entry no declaration names; and the declared applications of one template are recoverable from @meta.application — a declared instantiation records the application on its key, so the family a host language spells Result<T> is a walk over entries and needs no index. A derived name written in data (!box_text_04117bb4) is a valid identifier in the namespace and is not refused; such a document was written by reading one processor’s output and fails at the next, whose spelling differs.

Entry shape. A declared instantiation: @meta.application on its key, the canonical fully-bound application as written; its value the substituted binding record, headed by the applied constructor (§5.6) — closed, with the expansion’s kind by derivation and whatever chain the expansion’s own ^ or & gives it: for a record template’s member, chain.supertypes holding the template and everything it IS-A, plus the closed parent an open parents application named and that parent’s head (§5.8), its extension the record’s own (OPEN unless the template wrote abstract), never the base’s ABSTRACT, and no discriminators — closing pins each selector a parameter pins to its argument, and a =? selector is pinned by no argument, so closing does not pin it (§5.10). A synthetic entry resolves under the top-level-construction rule: a binding-record value headed by the constructor (a !meta.template while open), no supertypes, @meta.synthetic at its key (§6, §8.1). Materialisation also runs the value-level checks that the held body deferred: every family coherence rule that §5.3, §5.5 and §7.4 state for a literally written body applies again at materialisation, over the operands that were parameters — min_items ≤ max_items for the containers, min ≤ max and min_length ≤ max_length for the atom families, min_prefix ≤ max_prefix and the prefix range for the network families, and any rule a family states that is not a bound pair — together with the conformance of every substituted value to the slot it fills (§5.2, §5.10). A resolver needs no list: the rule an application closes onto is the rule the family already owns for a literal body, asked once more of the closed record. A violation is a resolver error located per §5.10’s rule — at the declaration whose text wrote the offending name, with the materialising application as context. For the §5.10 template vector => <T, N> !meta.array { element_type: T min_items: N max_items: N }, a declaration hd_row => vector<pixel, 1920> resolves to

@meta.application:vector<pixel 1920>
hd_row => !meta.array { element_type: pixel  min_items: 1920  max_items: 1920 }

while vector<pixel, 1920> written at a use site lands on hd_row, the declaration that owns it. Where no declaration names it, the use-site application and the structurally identical form written directly as [pixel; 1920] anywhere in the schema both land on one closed synthetic entry — the same body, @meta.synthetic at its key, no @meta.application — which is what §5.3’s “a use-site sugar form mints where a declared application does not” comes to: the sugar and the application are two spellings of one expansion. [pixel; 1920] lands on that synthetic beside hd_row too, a declaration naming only itself (above).

Non-exposure and the import merge. Minted entries are resolver-materialised, not declared: they cannot be named from any schema source, and authors reach a type by writing the application (reusing the entry) or by declaring a name for it, which is then the entry (above). Under the transitive import merge (§2.2.3) the internal entries travel with their namespace — an imported entry’s body references them, so they are part of what the import delivers — and they merge by the same structural identities as within a schema: a synthetic’s identity is its form and not its declaring document, so two documents that mint one form without importing each other declare one entry, which a closure holding both holds once, the first import’s, as it holds one schema reached by two routes; an importing schema whose own application derives an identical entry denotes the merged entry, never a duplicate; and internal names never collide with declared names by construction (§3.4.1). A document whose own sugar form derives a name an import already declares references the import’s entry, which must be that form: a declared entry of the name is the collision, and a synthetic of another form is an internal error, the naming function broken. A later schema’s application of a template an imported library declared and applied is unified with the library’s declared instantiation through the @meta.application the library’s output carries (§8.1).

Diagnostics and cross-schema identity. Internal names MUST NOT be the primary form shown to users: diagnostics surface the source application or form — recoverable from the originating source position, and from @meta.application where an entry carries one — located at the declaration whose text wrote the offending name (§5.10). Cross-schema identity of declared types is through named declarations or not at all; internal entries carry structural identity only.

8.3 Aliases

A type-def body that is purely a type reference — a bare name or a template application, with no body record, no &, and no construction — produces an ALIAS-kind entry whose value is !meta.alias { target: T }, where target names an entry (§8.1):

  • Simple leaf (id => uuid): id => !meta.alias { target: uuid }. The target is the immediate referent, not the ultimate type — alias chains (user_id → id → uuid, over id => uuid and user_id => id) appear as distinct entries, each pointing one hop forward. A target may be a qualified name, geo.address.
  • Fully-bound template application (string_triple => vector<text, 3>) is not an alias: the declaration is the instantiation entry itself, closed in place with the application as @meta.application on its key (§5.10, §8.2), exactly as a sugar-form body — string_triple => [text; 3] — is a construction in place (§5.3, §5.6). A !meta.alias body is a bare name or a still-open application, never a closed one — with the one exception of a self-reaching expansion’s instantiation entry, which is a !meta.alias to its own synthetic (§8.2).
  • Open template application (same_tree => <T> tree<T>, uuid_pair => <B> pair<uuid, B> — an application of a named template with at least one parameter still open): the declaration is itself a template, an alias template (§5.10); its held body is !meta.alias { target: pair<uuid B> }, the application written as the reference it is, so the alias states what it already binds. Alias templates compose during substitution without materialising intermediate entries.
  • An alias of a constructor (my_record => meta.record): target names a constructor, and the alias is applied as its target is, in a held body as anywhere (§3.3.1).

A top-level constructor application is not an alias — it resolves as a construction in place (§5.6). !meta.alias { target: X } is the explicit spelling of the alias X and denotes exactly that (§5.5).

The correction is worth its sentence: an earlier revision filed a declared application by its syntax — a parameterised reference — and made the declaration a hop to something else’s identity, so that pet.subtypes read a minted name where the author had written dogs, and every surface that showed a name had to follow the hop back. Filed by what it is — a construction — the declared name is the entry, which is what a family’s subtypes index, a tag, and a diagnostic all key on.

An alias is a hop, not a rewrite. Resolved output states an alias chain as the author wrote it: a use site — a field type, tuple element, choice variant, argument, or binding-record type slot — that names an ALIAS entry names that entry, and target names the next. Nothing is flattened, and no annotation records a source-level name, because the chain itself is the record: an earlier revision rewrote each use site to the chain’s terminal entry and attached an annotation naming the first hop, which was a second, lossy representation of a walk that happens anyway — a chain of two hops kept the name of the one that carried nothing — and every pass that needs the terminal entry (reader compilation, discrimination-class classification, inhabitance, variant distinctness, reference membership) walks the chain regardless. A processor MAY collapse a chain after linking, when it compiles for reading — an ALIAS entry’s reader is its target’s, named for the entry doing the referring, which is what gives a diagnostic the alias the author wrote for free — and MUST NOT collapse it in resolved output, which is what two conforming processors compare. The walk stops at an argument-bearing target — that is an application, not a hop to another entry, and there is no entry at the end of it until materialisation produces one. A chain with no terminal — a => b, b => a — is a resolver error (§3.4.1), and §11.5’s alias-chain limit bounds a long one.

Where a chain is followed, and where it is not. Identity of an application follows its arguments’ chains to their terminal entries, because an alias is the same type under another name (§8.2); subsumption at a typed position follows both chains (§7.2); the source of a refinement or composition, a choice variant judged for void, a discrimination class, a reference’s membership and a qualified name’s first segment are all read after following the chain (§4.2, §4.3, §5.4, §5.7, §5.12). What the chain never does is carry meaning of its own: nothing that changes how a value reads may live on an alias, and no bundled document declares an annotation that does — a fact that decides reading, such as an alphabet, lives in the type (§5.5), where every position already names it. That is what makes a rename safe to see through.

Aliases are not supertypes. ALIAS-kind entries carry no chain and do not contribute to the supertypes/subtypes graph: uuid.subtypes does not list id; id has no supertypes. The three ways to name a type after another, and what each buys, are tabled in §5.7.

Instantiation entries are not aliases. A declared instantiation (§8.2) carries its expansion’s kind and a binding-record value, not a !meta.alias record; an ALIAS entry may point at one by name (alias => string_triple), and the instantiation itself is a closed, concrete definition. The self-reaching instantiation entry of §8.2, and an alias a template’s application closes to (my => id<text> over id => <T> T), are the !meta.alias entries that carry @meta.application.

9. The Kernel and the Libraries

Six bundled documents carry the series’ vocabulary, and one of them is pre-loaded. Implementations MUST pre-load the kernel (§3.4, §10.1) and SHOULD pre-load the libraries. The normative source for each is the companion artifact (§1.5), pinned by content hash at publication; the kernel’s pre-loaded in-memory model is authoritative and its document a description of it.

The placement convention. A bundled document’s root holds the vocabulary an author writes, and its machinery sits under schema maps named for concepts, related concepts kept together — a library’s under one map of its own name, the kernel’s under six, two of which numeric.tn and net.tn add to (§2.2.3). The list is curated, entry by entry, so that an author writes text, !enum [A B], set<text>, @doc, bytes, int32, date and uuid bare, and meta.record, meta.identifier, meta.type_ref, meta.ref<meta.record>, meta.ref<method>, @atom.ordering:TOTAL, @meta.disjoint, !numeric.decimal_type {}, !temporal.date_type {} and !net.uuid_type {} by path:

DocumentImportsAt the rootUnder its maps
meta.tn, the kernelnothing (pre-loaded)void, boolean, integer, int64, text, bytes, iri, iri_reference, regex, doc, enum, set, schemameta, the type system: meta.top, meta.type, the base kinds meta.atom/meta.product/meta.sum, meta.alias, meta.template and meta.template_param, the constructors meta.record, meta.record_template, meta.array, meta.set_type, meta.map, meta.namespace, meta.tuple, meta.choice, meta.enum_type and meta.declared_type, the supporting records meta.type_chain, meta.record_field, meta.record_template_field, meta.field_group and meta.tuple_element, the maps meta.field_map, meta.record_template_field_map and meta.schema, the enums meta.product_access_type, meta.product_size_type, meta.field_role and meta.record_extension_type, the set meta.enum_set, the references meta.reference_type, meta.ref, meta.field_type_ref, meta.type_ref and meta.identifier_ref, meta.identifier and its aliases meta.type_name, meta.field_name and meta.param_name, meta.void_type, meta.declared, the annotation machinery meta.annotation, meta.synthetic and meta.application, the checked assertion meta.disjoint, and meta.tuple1 and meta.voidable_tuple1. atom: the ISO/IEC 11404 datatype properties atom.ordering, atom.bounded, atom.exact and atom.numeric, and the mixin atom.specification. numeric: numeric.integer_type, numeric.integer_size, numeric.integer_member_set and numeric.count. unicode: unicode.text_type, unicode.text_member_set, unicode.normalization, unicode.identifier_type, unicode.identifier_base and unicode.regex_type. net: net.iri_type, net.scheme_set and net.scheme_name. binary: binary.bytes_type and binary.bytes_encoding.
numeric.tnthe kernelnumber, rational, complex, int8, int16, int32, int128, int256, uint8–uint256, float32, float64numeric, beside the kernel’s members: numeric.decimal_type, numeric.float_type, numeric.rational_type, numeric.complex_type, numeric.ieee_format, numeric.complex_component, numeric.decimal_member_set
temporal.tnthe kerneldate, time, datetime, duration, periodtemporal: temporal.date_type, temporal.time_type, temporal.datetime_type, temporal.duration_type, temporal.period_type
net.tnthe kernelemail, ipv4, ipv6, cidr4, cidr6, mac, hostname, host, media_type, uri_reference, uri, uuidnet, beside the kernel’s members: net.email_type, net.ipv4_type, net.ipv6_type, net.cidr4_type, net.cidr6_type, net.mac_type, net.hostname_type, net.host_type, net.media_type_type, net.uri_type, net.uuid_type
core.tnnumeric.tn, temporal.tn, net.tnthe author annotations deprecated, title, comment, examples, read_only, write_onlynothing: it declares no map of its own
policy.tnthe kernelnothingpolicy: policy.processor and its parts policy.identifier_policy, policy.script_policy and policy.limits, policy.restriction_level and policy.limit

A library imports the kernel alone; core.tn is the bundle. A schema imports the libraries it uses: one importing temporal.tn alone has date and the kernel’s vocabulary, does not see int32, and may declare int32 as an entry of its own, though not numeric, which the kernel declares; one importing numeric.tn and net.tn has both, their closures meeting at the kernel by identity. Every schema whose closure holds the kernel reserves the kernel’s thirteen root names and the map names meta, atom, numeric, unicode, net and binary (§2.2.3). core.tn imports the three, so a schema wanting every bundled type imports core.tn and reserves those, temporal and every root above; record, identifier, type_ref, field_name, bounded, value, kind and core stay free. No two of the bundled documents declare one root name or one member path, and the maps two of them declare, numeric and net, are open and the union of their members, so no closure of them collides with itself. policy.tn imports the kernel alone; a closure holding it reserves policy.

The kernel, meta.tn. One document, header !!id alone and pre-loaded (§3.4): its one wait cycle — the entries around meta.record, meta.array, meta.set_type, enum, meta.map, meta.field_map, meta.reference_type, numeric.integer_type, unicode.text_type, boolean, integer, text, numeric.count, numeric.integer_size, meta.field_type_ref, meta.type_ref, meta.type_chain, meta.record_field, meta.field_group, meta.field_role, meta.enum_set and the arrays their declarations lift — every name its declarations reference, and the concepts that belong with them. The kernel’s tag !schema names its own root schema => meta.schema, an alias, and resolved output writes every nested map !meta.schema. A kind and a constructor are the kernel’s by declaring document, which is meta.tn alone; the libraries’ constructors are ordinary constructors of the documents that declare them, and a schema may contribute a member to any kernel map on §2.2.3’s terms (meta => !schema { code_type => unicode.text_type ^ { max_length?: = 4 } } beside code => !meta.code_type {}).

Every name resolves from the root, inside the bundled documents too. An entry names a member of a map by its path: integer => !numeric.integer_type {}, record => meta.product & { fields: meta.field_map … }, format: numeric.ieee_format, @atom.ordering:TOTAL. There is no lexical scope; the synthetics a document’s sugar lifts are its root’s.

Each atom family is a constructor/instance pair (§4.2): the constructor lists the family’s constraint fields, and the canonical instance lives in the same document — void, integer, int64, text, meta.identifier, iri_reference, iri, regex and bytes in the kernel; number, rational, complex, the sized integers and the floats in numeric.tn; date, time, datetime, duration and period in temporal.tn; email, ipv4, ipv6, cidr4, cidr6, mac, hostname, host, media_type, uri_reference, uri and uuid in net.tn. Every facet holding a value of the constrained family is typed by that instance — a float’s bounds by number, a network list by [cidr4] or [cidr6] (§5.5, §7.4), and every count by numeric.count (§9’s numeric map), the non-negative half of int64. Two rules hold across every family and are stated once in numeric.tn’s header: multiple_of, wherever it appears, is strictly positive, tests the value’s magnitude with the sign ignored, tightens only to an integer multiple, and composes with every other facet; and members, wherever it appears, has every member satisfying the body’s other facets — unicode.text_type’s pattern included — and a refinement shrinks the set on the numeric tiers and sets it once on unicode.text_type (§5.7, §7.4). Every set-typed field is emitted in source declaration order (§7.5). The classifying annotations the libraries state their atoms with are the kernel’s atom.ordering, atom.bounded, atom.exact and atom.numeric (§6).

What the kernel holds, and what the libraries do. The kernel holds what the type system needs to describe itself — its one wait cycle, every name the cycle’s declarations reference (integer, text, regex, iri, boolean, the references), and the concepts that belong with them (meta.tuple, meta.choice, meta.template, bytes, doc, set) — and the atoms every schema writes. A data-format family most schemas never use belongs in a library outside the kernel, where its names cost only the schemas that import it: the numeric tiers beyond integer and int64, the temporal families, the network and identifier families. core.tn holds the author annotations and nothing else, so that a schema importing a library alone may declare title or deprecated for itself.

JSON. A JSON document is read through the JSON encoding of [TSON-JSON], a second encoding of this model and not a mode of the TSON notation ([TSON-DATA] §6): it maps a JSON number to number, JSON null to void — admitted where the position is voidable (§7.6) — and a JSON object to a record where its keys are names its record admits, identifiers by default (§5.2). Every type doc in the artifacts describes its value space without reference to any encoding and names TSON text where it means it; the family facts of §5.2 and the disjoint fact of §5.4 are the model’s, and both encodings read them.

Guidance for schemas that extend the vocabulary. Any schema may declare a constructor (§4.2), and three rules keep an extended vocabulary coherent with the kernel’s. First, a slot that names a type is typed by a reference over a constructor, a slot that names a value by a reference over its type, and a slot that holds a sibling reference by meta.field_type_ref (§4.2, §5.2, §5.12): a field whose value is the name of a type a value can have is meta.type_ref, or a narrower reference — request?: meta.ref<meta.record> admits the names of the record types and refuses uuid’s — which is what makes the slot judged at link and visible to every reference mechanism (§8.1, §8.3); a field whose value is a value declared elsewhere, written by its name, is meta.ref<T>, which declares the spelling and the judgement at once; a field read against the type a sibling names is kind!. The kernel’s own meta.enum_type.name_type, meta.record.name_type, meta.record_template.name_type and meta.namespace.key_type are meta.identifier_ref, and meta.reference_type.of, meta.alias.target, meta.type_chain’s lists and meta.application are meta.type_ref. Second, a schema’s own vocabulary — the records its constructors’ instances hold, the namespace types it declares — is declared beside its constructors, under a map of its own where it is machinery (api => !meta.namespace { key_type: method_name value_type: method }, §4.1), and reserves what it declares in every importer. Third, the kernel’s regex atom is pinned to a specification (atom.specification, §4.2), and the pin is a strict gate: an implementation MUST implement the pinned dialect as specified — not a host library’s near-relative — and MUST document any divergence it cannot avoid; a pattern using a feature outside the pinned dialect is a resolver error at schema load, not a silently host-dependent behaviour. The same discipline applies to every spec-pinned atom an extension declares: the spec pin is the contract, and “whatever the platform regex engine does” is never a conforming reading. A constructor composing a base kind directly declares a new form, which a processor backs with readers and writers of its own; a schema that wants its own atom family composes a constructor the processor already backs, or declares a form and ships the processor that reads it.

10. Schema Resolution Model

Schema URLs are logical identifiers first: a URL names a schema, and resolving it does not by itself require a network request. Implementations resolve schema references through a schema library — a local store mapping canonical identities ([TSON-DATA] §2.2.1) to schema content. Fetching is a permitted but opt-in way to populate the library (§10.1, §11.2), not the meaning of a reference.

10.1 The Schema Library

When the resolver encounters an !!import directive, it looks the reference up in the library — the reference read to its canonical identity in any spelling ([TSON-DATA] §2.2.1); if not found, the resolver does not attempt to fetch unless fetching is enabled (§11.2), and reports one of two outcomes, neither a finding about validity ([TSON-DATA] §8.1). A schema this processor will not supply — one it does not hold and will not fetch, a host off its allowlist, an illegal identity, a missing required pin, or one past its size limit (§11.2) — is a refusal: a decision of this deployment’s configuration, as deployment-specific as an identifier policy, which a processor configured otherwise does not make and no rerun changes, so the document is rejected here and the sender’s fix is to name a schema this deployment will supply. A closed library’s miss is a refusal, never “not found”: the library is the deployment’s whole configuration, so a schema outside it is one this deployment will not supply. A schema this processor could not obtain — a source that looks beyond its configuration (an origin, a directory) and found nothing at the reference, a host that did not answer, a fetch that timed out — leaves the document undetermined: nothing has read the schema, so nothing is known about whether the document conforms, and whoever holds the fix, for nothing at the reference the sender most likely does, a reference with nothing behind it being most often a typo. Both are distinguishable from the four categories, in the same report, located at the reference, and name their reason (not held, not permitted, too large; not found, unreachable, timed out), since a sender acting on a report needs to tell “your document is wrong” from “I will not look here” from “I could not look”. A document with both an undetermined import and an ordinary error reports both, and a rejection settles the summary whatever else went unjudged ([TSON-DATA] §8.1). The library is populated through three mechanisms, in order of precedence:

  • Pre-loaded schemas. The implementation ships the kernel as a pre-loaded entry and SHOULD ship the bundled libraries. Pre-loaded schemas are authoritative — their in-memory representation takes precedence over any external source (§3.4).
  • Registered schemas. Applications register schemas before parsing documents that reference them, from local files, embedded resources, or any application-specific source.
  • Fetched schemas (optional). Implementations MAY support fetching by URL; fetching MUST be explicitly enabled by the application — never the default — and is subject to §11.2. Production systems SHOULD register all required schemas at startup rather than fetch at runtime.

Identity agreement. Registration whose target identity differs from the canonical identity of the content’s declared !!id is an error — the library MUST reject it as identity confusion. The comparison is over canonical identities, so scheme variance agrees; a differing host or path does not. Content with no !!id MAY be registered under an application-supplied identity (a development-mode convenience, §2.2.1); content with an !!id is registered under that identity and no other.

Schema sources are schema documents. Whenever the library is populated from a document — registered or fetched, for every target of !!import — that document MUST classify as a schema document ([TSON-DATA] §2.2): its root tagged !schema. A resolved form (resolver output, §8) is a schema document by that test and is nonetheless not a schema source: its derived fields cannot be verified from the document alone, it is non-canonical and not hash-pinnable under a source’s identity, and reading it as the resolver’s output is the caller’s explicit choice (§8.1), never the library’s inference. An implementation MUST NOT silently resolve an !!import to a document the caller has identified as a resolved form, and a resolved form read as output does not take derived indexes on trust: @meta.synthetic, subtypes and disjoint are discarded and recomputed; chain.supertypes, chain.parents and @meta.application are read as stated and verified; author-written annotations are preserved as data; and a value entry (§4.1) reads like any other entry — its value the data its type describes, validated against it, with the rule that no type position names it re-run on the map.

10.2 Hash-Pinned References

The hash-parameter URI convention — the algorithm-named query parameter, lowercase full-length hex, the query-is-hash-parameters-only rule, canonical identity, and the mismatch rule — is defined in [TSON-DATA] §2.2.1. This section defines how the library applies it. Bytes identify a document, for distribution and pinning; names and parsed structure identify an entry, for typing (§8.2): a whitespace edit to a schema changes its digest and every pin that names it, and none of its entries.

Library keys are canonical identities: a plain reference, its http/https variant, and its hash-pinned form all name the same library entry — the pinned form additionally demands verification. When a hashed reference resolves, the implementation MUST verify the library’s content against the declared hash before use; a mismatch is a resolver error — a verdict, unlike a refused or undetermined schema (§10.1), because the schema was obtained, its bytes were read, and they are not the bytes the document’s reference commits to, which is a finding about the reference the document carries — and the library MUST NOT silently substitute mismatched content. When registration supplies a document whose declared !!id itself carries a hash, the implementation SHOULD verify at registration time.

Hashes attach to the canonical entry. Verification is a property of the canonical identity, not of each reference. Within a single resolution’s reference closure (§3.4.1), the digests declared across all references to one identity form a set, and the set determines the identity’s treatment. An empty set resolves unverified. A single digest is verified once against the library entry’s content, before any reference to the identity resolves; every reference — pinned and plain alike — then resolves to the verified entry. A plain reference alongside a pinned one is therefore not a conflict but an inheritance: it rides the pin, and it inherits the pin’s failure — a verification failure applies to the identity for the whole resolution, and a plain reference MUST NOT proceed with content a declared digest has rejected. Two distinct digests for one identity is a resolver error: at most one describes the real bytes, and the library MUST NOT choose between them. A verified entry, once cached under its canonical identity, is immutable. A failed verification SHOULD NOT be cached under the canonical identity; an implementation MAY maintain a separate negative cache keyed on the full reference string.

Pins to the pre-loaded kernel. The kernel resolves to in-memory structures (§3.4, §10.1), but a pinned reference to its identity is an ordinary pinned reference and MUST be verifiable: an implementation MUST hold the published content digest (or the canonical document bytes) of the kernel its structures implement, and MUST verify declared pins against it — a mismatch is the ordinary verification error. The kernel’s header is its !!id alone and carries no reference of its own, so every reference to it may be pinned and the chain grounds in bytes the implementation ships; the libraries pin the kernel and core.tn pins the libraries (§13.2).

An implementation MAY offer a strict resolution mode that rejects any closure containing an identity whose digest set is empty (the pre-loaded kernel excepted), for deployments requiring a fully verified chain; the per-identity rule above is the default and the interchange baseline.

When no digest is declared for an identity anywhere in the closure, it resolves without integrity verification — appropriate for development, NOT RECOMMENDED for production interchange. References that cross a trust boundary SHOULD be hash-pinned: a schema’s !!import values pin its dependencies; a data document’s !!imports pin its vocabulary. Because pinning is per-identity, one pinned reference raises the guarantee for every reference to that identity in the closure, and a published plain reference never prevents downstream pinning. Pinning composes into the verification chain of [TSON-DATA] §2.2.1 — a consumer holding a single hashed reference can verify a document together with its schema, the libraries it imports, and the kernel, provided each rung’s own references are pinned; published schemas SHOULD pin every reference they carry.

!!import:"https://example.com/people.tn?sha256=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"

10.3 Canonical Identity

Reference identity is defined by [TSON-DATA] §2.2.1: two references name the same document if and only if their canonical identities — the host’s canonical text plus the path, scheme and query removed — are byte-for-byte identical. The library applies this as its key rule:

  • http://tson.io/2026/39/m/meta.tn and https://tson.io/2026/39/m/meta.tn — same entry; the scheme is a transport hint.
  • https://tson.io/2026/39/m/meta.tn and the same URL with ?sha256=… — same entry; the pinned form additionally requires verification (§10.2).
  • https://tson.io/2026/39/m/meta.tn and https://elsewhere.example/2026/39/m/meta.tn — different entries; the host is load-bearing, so a fetch-endpoint change cannot silently redirect a name.
  • https://xn--bcher-kva.example/sch%c3%a9mas/x.tn and https://bücher.example/schémas/x.tn — same entry; a reference in any spelling of an identity is read to the canonical form, the host as a host value and the path’s percent-encoded UTF-8 read back ([TSON-DATA] §2.2.1).

The identity profile holds a document’s own !!id to one spelling — the canonical form, with no userinfo, port, percent-encoded unreserved character, dot-segment or fragment — and reads a reference in any spelling of the same identity to that form before comparison; what has no IRI form is an error, never a candidate for normalization ([TSON-DATA] §2.2.1). A fetcher that reaches a host beyond US-ASCII does so in the URI spelling, and a report of such a fetch SHOULD name both the identity and the URI requested. The pin rides in the query so that it reaches the origin and the cache, and the fragment is reserved for a later intra-document reference ([TSON-DATA] §2.2.1). Content addressing is over bytes, and every identity rule above a digest is over value spaces (§5.5): two spellings of one octet string hash the same document to one identity and are one map key, and a content-addressed reference is never encoding-dependent.

Confusable identities. Because identity selects a document’s entire type vocabulary, a look-alike identity is a higher-value spoof than a confusable field name. Implementations processing untrusted references SHOULD surface, and MAY reject, two registered identities that differ only in ways the profile does not already forbid — e.g. visually confusable host labels (UTS #39; [TSON-DATA] §9.4).

11. Security Considerations

The security considerations of [TSON-DATA] §9 apply; this section adds the schema layer’s.

11.1 Schema Validation

Documents without !!import carry no type guarantees — only base type resolution applies. Applications processing untrusted TSON input SHOULD validate against a schema before use.

11.2 External References

Schema URLs are logical identifiers resolved through the schema library (§10), not fetch instructions; no network access occurs unless the application explicitly enables it. Implementations that support optional fetching MUST treat it as opt-in, disabled by default, and when enabled SHOULD enforce:

  • Transport security. Fetched references MUST use an authenticated, encrypted transport (in practice, https); a fetcher MUST NOT downgrade to a cleartext scheme for an unpinned reference.
  • Allowlists. Restrict fetchable references to approved hosts or path prefixes, matched on canonical identity (§10.3) so scheme variance cannot evade the list; a host is matched as a host value ([TSON-DATA] §5.5), so an allowlist entry in either label form admits an identity’s host in either, and a look-alike host (schemаs.example.com with a Cyrillic а) is refused unless the operator listed it, the comparison being by name. !!import references are the one channel through which a document selects its vocabulary, so a malicious reference could load an untrusted schema that redefines expected types. A reference the allowlist refuses is reported as a refusal ([TSON-DATA] §8.1; §10.1), and the report SHOULD NOT disclose the allowlist: telling an untrusted sender which hosts this processor will fetch from is reconnaissance for request forgery, where a name policy’s statement serves the sender and costs nothing.
  • Content hash verification. Require hashes on fetched references and reject mismatches — preventing tampering and silent schema drift; a reference whose pin is required and missing is a refusal.
  • Size limits on fetched content; a schema past the limit is a refusal.
  • No recursive fetching. A fetched schema’s own !!import directives MUST NOT trigger further fetches; transitive dependencies must be pre-registered or pre-fetched.
  • Caching. Cache verified schemas locally, keyed on canonical identity; a verified hash-matching schema is immutable. A failed verification MUST NOT overwrite or poison the canonical-identity entry (§10.2).

Production systems SHOULD pre-register all required schemas at startup and disable runtime fetching entirely.

11.3 Directive Security

Directives are a control channel that affects interpretation. !!import is the one directive that selects and extends a document’s vocabulary; applications processing untrusted input SHOULD restrict which import URLs are permitted, for data documents and schema documents alike. See [TSON-DATA] §9.3.

11.4 Name Hygiene at the Schema Layer

[TSON-DATA] §8.2 defines the name-hygiene mechanisms — skeleton distinctness within a scope, Identifier_Status, and a restriction level — as policy that every conforming processor implements and enforces by default, never as validity. This section supplies the schema layer’s scopes and the one rule the scope structure adds.

The named scopes at this layer are: the members of one enum (§7.4), under its name_type’s profile — all three mechanisms, every enum’s name type being an identifier family; the field names of one record definition (member labels of its groups included, §5.11), under the record’s name_type’s profile (§5.2), which is the same scope the data layer applies to a schemaless record ([TSON-DATA] §2.5, §8.2) — a profile’s own added characters (json_name’s @ and $, a medial space) are exempt from mechanism 2 and read by mechanisms 1 and 3, and the floor on what a profile may add is §7.4’s; the declared names of one schema map — the document’s root, and each nested !schema map as a scope of its own, the per-name rules judging each member’s own name (§4.1); and the merged namespace at !!import — the sharpest, because §2.2.3 decides collisions by exact identity, which a confusable pair passes by construction: two entries a reviewer reads as one name are, to the resolver, two names. Choice variants are not a scope: a variant is a reference to a declared name, so two confusable variants are two confusable entries in the namespace and are reported there. A template’s parameter list is not a scope either (§5.10): one author writes it whole on one line, and the list stays short so that it stays reviewable. Mechanisms 2 and 3 are per-name and reach every identifier position — each segment of a qualified name among them — without a scope ([TSON-DATA] §8.2); only mechanism 1 needs the list above.

Values that are names. A value whose declared type is an identifier family (§7.4) is a name wherever it stands, and the schema layer is what makes it one. Mechanisms 2 and 3 reach every such value, in every encoding, under the identifier policy and under the family’s own profile: a character the profile adds meets no Identifier_Status rule, and a per-segment level divides the value at the profile’s own separators ([TSON-DATA] §8.2). Mechanism 1 gains two data scopes, each a set the type itself delimits: the key set of one map whose key type is an identifier family — a namespace value’s keys among them (§4.1) — and the element set of one array whose elements are unique — a meta.set_type instance, or any array stating unique_items — and whose element type is one. An array that admits repetition is not a scope, since nothing says two of its elements name two things. A pair is refused at the second key or element, as a pair of field names is, and like every other scope these are skipped where the identifier policy switches skeleton distinctness off. A value a schema writes is judged the same way, under the same policy: a field’s default or fixed value of such a type when the schema loads, since a default reaches every document that omits the field, and a value in a payload the scopes above do not cover — the keys of a value entry’s map (orders => !api { place_order => … }, whose key_type is an identifier family), a qualified name’s segments (§5.12), or an annotation’s value. A reference’s value is a name of a declared entry and is judged where the entry is declared. A map keyed by text remains data: admin and аdmin may be two legitimately distinct keys, and nothing about a map position says they are names — the key’s type is the schema saying so.

Because identity selects a document’s entire type vocabulary, the import merge is exactly where the compositional hazard bites — two independently published, independently pinned schemas, each clean alone, can collide on a skeleton when one imports the other (list_item beside Iist_item), and the fix is a rename in a document the importing author may neither control nor republish. That is the strongest single reason the mechanism is policy rather than validity: as a validity rule it would make such an import impossible to write; as a policy rule the operator relaxes the check, visibly and in code, and proceeds. A refusal at the merge is reported at the second arrival with both declaring schemas named, in the manner of §2.2.3’s collision diagnostic, as [TSON-DATA] §8.1’s refused outcome — in the same report as the four categories, distinguishable from them — and the report carries the UCD version and the identifier and token policies it was judged under ([TSON-DATA] §8.2). The policy is not a property of a schema and no schema carries one: the merged namespace is what the check runs over, and only the importing site knows it.

A data document’s field names need no scope of their own under a schema: a data field name is valid only if it matches a declared one, matched in the record’s name type’s form (§5.2, §7.2), so it inherits the declaration’s verdict; a name the record does not declare is judged by the name type first: a value of it is a name, and meets the identifier policy under the type’s profile before closure reports it as unrecognised, and one that is not is unrecognised and meets no name rule. Its values typed by an identifier family are the data scopes above. The confusable-identity concern for schema references themselves is §10.3’s.

11.5 Resource Limits for Schemas

[TSON-DATA] §9.1 bounds what a processor will spend on a document under one limits policy — every limit with a default, a positive int64 (policy.limit), configurable or documented, its threshold reported on refusal, and a refusal that is not a verdict. A schema is untrusted input too wherever it is accepted over the wire or reached through !!import, and a schema document is a document, so §9.1’s counters apply to it as written. What §9.1 cannot see is the work resolution adds, and this section names those limits on the same terms, as part of the same policy and reported through the same surfaces; policy.limits carries each (§9):

LimitApplies toDefault
import closure (max_import_closure)schema documents reachable from one header through !!import (§3.4.1)64
entries (max_schema_entries)declarations in one schema map, nested maps’ members, synthetic and instantiation entries included65,536
alias chain (max_alias_chain)hops from a use site to a terminal entry (§8.3)64
supertype chain (max_supertype_chain)length of one type’s chain.supertypes (§8.1)64
materialisation depth (max_materialisation_depth)nested open synthetics closed for one application (§5.10, §8.2)64
wait depth (max_wait_depth)the longest chain of rule-1 waits in one document’s resolution, an entry waiting on the next being ready (§3.4.1)64

Exceeding one is a limit refusal, not a resolver error: the schema may be well-formed, load cleanly on the next processor along, and be refused here because this deployment declined to spend the resources, and the diagnostic names the limit and the configured threshold. The defaults are set at the tight end on [TSON-DATA] §9.1’s argument — a schema that fits them loads everywhere — and a deployment raises them; the bundled documents resolve under the defaults whatever a deployment sets. A processor holding a limit in a narrower type than int64 caps a larger configured value at the widest it holds ([TSON-DATA] §9.1).

12. ABNF: The Schema Grammar

This section defines the schema grammar — the schema document’s body grammar, the second of the two body grammars behind the shared document header ([TSON-DATA] §2.2, §7.4). This grammar adds no token of its own ([TSON-DATA] §7.3): the productions below consume the same token stream, and the reserved special tokens of [TSON-DATA] §7.2.5 receive their meaning here — +, which that section emits as a special token from Revision 37, among them. The reference production — a qualified name with an argument list, and the sibling reference — is [TSON-DATA] §7.4’s; this grammar’s sibling-ref and type-ref reuse it, type-ref with a wider argument.

12.1 The Schema Grammar

A schema document is the header [TSON-DATA]‘s grammar defines, followed by the root value: its annotations, the tag !schema, and the schema map, the annotated, braced declaration map. ws, ws1, separator, token, unquoted-token, annotation, field-name, record, empty-brace, identifier, qualified-name, sibling-ref, id-directive, import-directive and core-value are imported from [TSON-DATA] §7.3, §7.4, and §7.7; the data grammar’s value productions appear at exactly two points — the full core-value as the constructor-application payload (instance, §5.6), and its braced subset (record / empty-brace) as the value-refinement body (value-refinement, §5.5), the same text under two heads, which is the desugar §5.6 states. No production of this grammar uses the full data-value: a record-refinement body is a braced record-def (§5.7), and a field-modifier value is restricted to a bare token — or, for the selector =?, nothing at all — (§5.2), never the void sentinel, annotations, a tag, or a container.

A schema-map copies the shape of [TSON-DATA]‘s map production but requires at least one entry — {} at schema-body position is a parse error. An entry is called a declaration. Annotations before the tag bind to the schema; annotations at the head of an entry bind to the key; annotations after => bind to the type definition (§2.1, §6).

; ── Schema document ───────────────────────────────────────

schema-document  = [ id-directive ] ws *( import-directive ws )
                   *( annotation ws ) "!schema" ws schema-map ws
                 ; the root's tag, "!schema", names the pre-loaded kernel's
                 ; root `schema` (§2.1); it is what classifies the document
                 ; ([TSON-DATA] §2.2). A root with any other tag —
                 ; `!meta.schema` included — or none, is a data document.

; ── Schema Map (schema body) ──────────────────────────────

schema-map       = "{" ws schema-map-entry
                   *( separator schema-map-entry ) [ ws "," ] ws "}"
                 ; a comma may follow a value ([TSON-DATA] §2.4);
                 ; every list in this grammar admits one after its
                 ; last element

schema-map-entry = *( annotation ws ) type-name ws "=>" ws
                   *( annotation ws )
                   ( [ definition-mark ws ] type-def
                   / nested-map )

nested-map       = ( "!schema" / "!meta.schema" ) ws schema-map
                 ; a schema map nested as an entry (§4.1): its members
                 ; are declarations of the document, named by path

definition-mark  = "abstract" / "final"
                 ; how a record may be realised (§5.2): one word,
                 ; read unconditionally at this one position and an
                 ; ordinary identifier everywhere else; a bare mark
                 ; with no type-def is a declaration missing its
                 ; definition; a mark on a non-record definition is
                 ; a resolver error; written before the parameter
                 ; list of a template (§5.10)

; ── Type Definition (declaration right-hand side) ─────────

type-def = value-refinement              ; never parameterised
         / instance
         / [type-params] structural-def
         / [type-params] type-expr
         ; there is no constructor marker: an entry is a
         ; constructor by being IS-A `meta.top` (§4.2), and `~` is
         ; a special token with no role at type-def position

type-params = "<" ws type-param *( separator type-param ) [ ws "," ] ws ">"
type-param  = param-name [ ws ":" ws type-expr ]
            ; the written type narrows a value parameter's type
            ; or bounds a type parameter, by the kind the body's
            ; uses give (§5.10)
param-name  = type-name   ; same lexical class as type-name

structural-def = refined-def
               / construction-def
               / record-def

refined-def  = ( sibling-ref / type-ref ) ws "^" ws record-def
             ; record refinement, and constructor refinement (§5.7);
             ; the head's optional arguments serve user-template
             ; heads only (§5.7). No removal clause.

construction-def = supertype-ref 1*(ws "&" ws supertype-ref)
                   [ws record-def] [ws removal-set]
                 / supertype-ref ws "&" ws record-def [ws removal-set]
                 / supertype-ref ws removal-set

supertype-ref = sibling-ref / type-ref
              ; composition and subtraction operands are named
              ; type references, optionally with arguments —
              ; never paren, bracket or map forms (§4.3, §5.8); a
              ; sibling reference parses here, as at every reference
              ; position, and the resolver refuses it (§5.2)

removal-set  = "-" ws "{" ws field-name
               *( separator field-name ) [ ws "," ] ws "}"

record-def   = "{" ws [record-entry *(separator record-entry)
               [ ws "," ]] ws "}"
record-entry = field-def / group-def

value-refinement = "!" qualified-name ws "^" ws ( record / empty-brace )
                 ; value refinement (§5.5): the type's own field
                 ; bindings — the braced subset of the core-value
                 ; payload `instance` takes, read by the same data
                 ; grammar; the target MUST resolve to a value whose
                 ; type states refinement rules (§3.3.1, §5.7)

instance     = [type-params ws] "!" qualified-name ws core-value
             ; constructor application (§5.5): the target resolves
             ; in the document's namespace (§3.3.1) to a constructor,
             ; `meta.top` or IS-A it — `meta.alias` included (§5.10)
             ; — or to an ordinary type, which makes the entry a
             ; value entry (§4.1). The payload is a core value —
             ; braced bindings or the positional form (§5.6) — never
             ; annotations or directives; an application inside it
             ; is the reference production of [TSON-DATA] §7.4,
             ; written unquoted. With a parameter list the body is
             ; held unread until materialisation (§5.10), except a
             ; `!meta.record` body, which resolves to a
             ; `!meta.record_template`, read as vocabulary and
             ; labelled at its declaration; open and closed
             ; share one production and one payload grammar.

; ── Field Definitions ─────────────────────────────────────

field-def      = *annotation field-name ["?"] ws ":" ws
                 ( field-type field-modifier
                 / field-type
                 / field-modifier )
               ; the "?" on the name answers omission (§5.2);
               ; "a?:" lexes as name, "?", ":" with no lexer change

field-type     = type-expr ["?"]
               ; the "?" on the type answers a written "_" — the
               ; field is voidable (§5.2); a sibling-ref here is a
               ; dependent field (§5.2)

field-modifier = ws ("~" / "=") ws token
               / ws "=" ws "?"
               ; "~" default, "=" fixed: the value is a single
               ; scalar token, never a compound value, an
               ; application or the void sentinel (§5.2); "=?" is
               ; the selector a discriminated family's members pin
               ; (§5.2) — two special tokens, no value

; ── Field Groups (§5.11) ──────────────────────────────────

group-def    = *annotation "(" ws group-option
               *( ws "|" ws group-option ) ws ")" [ "?" / "+" ]
group-option = group-member *( separator group-member )
group-member = *annotation field-name ["?"] ws ":" ws type-expr ["?"]
               ; "|" separates options and a separator the members
               ; of one option. A "?" on a member's name marks it
               ; optional within its option; the "?" on its type
               ; makes it voidable; no modifier is admitted, a
               ; group never injecting. The group's own "?" makes
               ; it optional; "+" is sugar for at-least-one and is
               ; admitted only where every option is one unmarked
               ; member. Which groups restate plain fields or
               ; another group, and are refused for it, is the
               ; resolver's check (§5.11).

; ── Type Expressions (any type position) ──────────────────

type-expr = paren-type
          / bracket-type
          / map-type
          / sibling-ref
          / type-ref
          ; one token of lookahead after a name ("!", "<", or
          ; neither) picks sibling-ref, an application, or a name

type-ref  = qualified-name [ "<" ws type-args ws ">" ]
          ; a reference to a type: a name, or an application —
          ; [TSON-DATA] §7.4's production with the wider argument
          ; below.

; ── Compound Type Expressions ─────────────────────────────

paren-type = "(" type-expr "|" type-expr *("|" type-expr) ")"   ; choice, 2+ variants

bracket-type = "[" element-type [ ws ";" ws size-spec ] ws "]"           ; array
             / "[" element-type 1*(separator element-type) "]"           ; tuple

map-type     = "{" ws map-key ws "=>" ws element-type
               [ ws ";" ws size-spec ] ws "}"                            ; map sugar (§5.3)

map-key      = sibling-ref / type-ref
             ; a simple ref, optionally with arguments — never
             ; paren, bracket or map forms (§5.3); a sibling
             ; reference parses and the resolver refuses it (§5.2)

element-type = type-expr [ "?" ]
             ; nesting is this recursion: [[T; N]; N] and
             ; {text => [order; 1..]} work by the same rule
             ; that makes [[T]] work; a sibling-ref is refused
             ; here by the resolver, before any lift (§5.2)

size-spec    = size-bound [ ws ".." ws [ size-bound ] ]
             / ".." ws size-bound
             ; a size-bound is decimal-natural or, within a
             ; template body, a value-parameter name (§5.3)

; One production per container, at every position: an
; element's "?" belongs to element-type inside the brackets,
; and a field's own "?" to field-type, so "xs: [T?]?" is
; unambiguous — element voidable, field voidable.

; ── Terminals ─────────────────────────────────────────────

type-args  = type-arg *(separator type-arg) [ ws "," ]
           ; separator = ws "," ws / ws1; a trailing comma
           ; follows a value and changes nothing
type-arg   = type-expr / value-literal
           ; wider than the data grammar's: a schema argument
           ; may be sugar, which lifts to its own entry (§5.3);
           ; a sibling-ref is refused here by the resolver
value-literal = token
           ; a single scalar lexeme: a number, quoted string,
           ; or other non-name token. An unquoted token that
           ; satisfies qualified-name (`true` and `false`
           ; included) parses as a type-ref; it is read as its
           ; parameter's recorded type (§5.10), so a name bound
           ; to a parameter standing in an enum's member list
           ; is a member and one bound to a type parameter must
           ; resolve as a type.
size-bound = unquoted-token
           ; text MUST match the decimal-natural production
           ; of [TSON-DATA] §7.6 or, within a template body,
           ; be a value-parameter name (§5.3)
type-name  = identifier
           ; [TSON-DATA] §7.7: a declared key is one segment, an
           ; unquoted token whose decoded text matches the
           ; identifier grammar — XID_Start-initial, so no name
           ; begins with a digit, a sign, or a dot, which is what
           ; makes numbers undeclarable (param-name shares the
           ; rule). Every referenced name and every `!` head is a
           ; qualified-name, each segment held to the same grammar.
           ; A token in name position that fails the grammar is a
           ; parse error. field-name is any unquoted or single-line
           ; token ([TSON-DATA] §2.5, §7.4) — in field-def,
           ; group-member and removal-set as in data — and is
           ; judged not by this grammar but at link, by the
           ; record's name type (§5.2): a name that is no value of
           ; it is a resolver error, and under the default name
           ; type that is the identifier grammar. The schema
           ; grammar and the data grammar share the production.
           ; Resolver-materialised synthetic and instantiation
           ; entries (§8.2) carry internal names in this same
           ; class; the resolver keeps them disjoint from
           ; declared names by construction (freshness, §8.2),
           ; and they are unreachable from source because they
           ; do not exist at source level.

Notes:

  • The type-params slot declares type parameters (§5.10), each optionally followed by : and a type expression — a written type, decided on the one token after the name; parameters take precedence over namespace lookup, and references to a parameterized type MUST supply matching type arguments — except that a record template may be named bare at a type position as a family base (§5.10), never at an operand of & or ^ (§5.8).
  • definition-mark is read unconditionally: pet => abstract base & { … } is a marked composition, and a type named abstract cannot stand alone as a declaration’s whole body, which is the sole reservation the two words carry (§5.2, [TSON-DATA] §7.7). ABSTRACT’s rule — no direct instances, a value at a position typed by it a value of some subtype, a tag naming the base itself refused — is the rule the kernel’s own meta.top, meta.type, meta.atom, meta.product and meta.sum are read by, each marked abstract (§4.1), so a direct application of one is refused as any abstract record’s is.
  • =? is two tokens the lexer already emits — = then ? — admitted by field-modifier only with no value; a: T =? v is a parse error. It is refused by the resolver on a name carrying ?, on a voidable type, on a group member, on a dependent field, and on a type that is not an atom-family instance or an enum (§5.2).
  • sibling-ref is [TSON-DATA] §7.4’s — an unquoted token of one segment with an adjacent ! followed by whitespace — and is a type-expr alternative picked by one token of lookahead after the name; [ kind! ], box< kind! >, ( kind! | text ) and x => kind! parse, [kind!] does not, and every position but a record field’s type refuses one at the resolver (§5.2).
  • paren-type produces choice types; choices require at least two variants — (T) is a parse error.
  • group-def produces field groups (§5.11): one or more options separated by |, each one or more members. That a group has at least two members, and the rules giving each presence rule one spelling, are the resolver’s (§5.11). Inside a record body, ( at entry position (after any leading annotations) opens a group; ( after a field-name ":" opens a paren-type. The two never collide — a group is an entry, a choice is a type expression. A ? or a + after the closing ) marks the group; member positions reject modifiers by grammar.
  • A ? after a field’s name records optional: true on the meta.record_field; a ? after a type at a field, tuple position, array element, map value or group member records that the position admits _ (voidable: true on a meta.record_field, meta.tuple_element, meta.array or meta.map); a ? after a group member’s name lists it in the group’s optional_members; a ? after a group’s closing ) records optional: true on the meta.field_group, and a + there lowers to the one-option form (§5.11). Those are the only positions, and there is no generic “optional type” in TSON.
  • type-def reaches the bracket and map forms through type-expr like any other position; there is no separate declaration-level container production, and no positional restriction on size specifiers or element/position ? (§5.3).
  • instance is decidable on one token after the optional parameter list: ! opens an instance, with or without a preceding <…>; < only ever starts type-params, so consuming it first costs no lookahead. Inside the ! branch a following ^ separates value-refinement from instance; value-refinement admits no parameter list — a parameterised refinement of an instance is no form (§5.10), and <…> ! name ^ is a parse error.
  • A ! head naming an ordinary type declares a value entry (§4.1); one naming schema or meta.schema is nested-map, and the payload is the schema grammar.
  • The trailing record-def in construction-def is optional (customer => address & contact is valid). When a { follows a &-chain, it always belongs to the construction’s record-def.
  • The refined-def target and every supertype-ref operand are restricted to a named reference — a type-ref, a name optionally with type-args, or a sibling-ref; inline structural forms cannot precede ^, &, or - by grammar (§4.3); a sibling reference parses there, as at every reference position, and the resolver refuses it (§5.2). A ^ whose resolved target states no refinement rule for a field the body names (a container instance, for example) is a resolver error reported with the target’s type.
  • The removal clause attaches to construction heads only; a refinement head admits none — T ^ { ... } - { ... } is a parse error (§5.7, §5.9).
  • After a bare type-ref in type-def position, { is a parse error; the diagnostic SHOULD suggest ^ (refinement) or & (composition).
  • Parameters and type arguments inside <> alike separate by comma or whitespace — the general separator convention ([TSON-DATA] §7.4): <T, MIN> and <T MIN>, map<text, integer> and map<text integer> are all valid. Canonical resolved output writes one space and no comma.
  • _ is not valid in type-expr, type-def body or field-modifier positions (§5.2, §7.6); empty records use {}, and a field that may only be omitted or _ is spelled a?: void?.
  • A comma may follow the last element of every list in this grammar — declarations, fields, groups, removal sets, parameters, arguments, tuple elements ([TSON-DATA] §2.4); [text, int32, ] and pair<uuid, B, > are legal. A comma following nothing, or another comma, is a parse error, as in data.

12.2 Disambiguation Summary

This section is informative.

; document (after the header):
;   @              → annotation; before the tag it binds to the schema
;   !schema {      → schema document; the schema map opens
;   ! other        → data document; a Class 1 read, or a Class 2 read
;                    of a data value under the header's imports
;
; schema map:
;   @              → annotation; binds to the entry key (name) or,
;                    after "=>", to the type definition
;   name =>        → declaration (two-token lookahead)
;   }              → schema map closes
;   anything else  → parse error
;
; type-def position (after =>):
;   abstract / final → definition-mark (§5.2); consumed, then
;                    dispatch continues on what follows
;   <              → type-params (each name optionally followed
;                    by ":" and a type-expr); then dispatch continues:
;     !              → instance, held open (§5.10)
;     otherwise      → templated structural-def / type-expr
;   !schema {      → nested-map (§4.1); likewise !meta.schema
;   ! name ^       → value refinement (§5.5)
;   ! name         → constructor application, or a value entry (§5.5)
;   name ^         → refined-def (§5.7)
;   name &         → construction-def (composition, §5.8)
;   name -         → construction-def (subtraction, §5.9)
;   name {         → parse error (write ^ or &)
;   name!          → sibling-ref: refused at the resolver as a body
;   {              → brace form; consume "{" and dispatch on
;                    content (reusing [TSON-DATA] §2.8's
;                    consume-one-then-inspect machinery):
;     "}"            → empty record ({}, meta.top's shape)
;     "("            → record-def (leading field group)
;     "@"            → record-def (annotations precede field
;                      names, §6; the map sugar admits no
;                      interior annotations, §5.3, so "@"
;                      commits to a record)
;     name ":"       → record-def (field)
;     name "?"       → record-def (field with an omittable key)
;     name "=>"      → map-type
;     name "<"       → map-type (generic key; consume the
;                      arguments, expect "=>")
;     name (other)   → parse error
;   (              → paren-type (choice)
;   [              → bracket-type (array or tuple, full syntax)
;   name < / name  → type-ref
;
; type-expr position (field types, array elements, etc.):
;   (              → paren-type (choice)
;   [              → bracket-type (full syntax at every position)
;   {              → map-type: "{" name … "=>" required;
;                    "{" name ":" remains a parse error — bare
;                    records must be declared (§5.2); the
;                    diagnostic SHOULD say so and distinguish
;                    the two brace meanings
;   name <         → application ("<" adjacent, [TSON-DATA] §7.5)
;   name !         → sibling-ref ("!" adjacent, whitespace after)
;   name ? / name  → simple ref
;
; record-def entry position (after leading annotations):
;   (              → group-def (field group, §5.11)
;   name ":"       → field-def
;   name "?" ":"   → field-def, key omittable (§5.2)
;
; inside a group-def (§5.11):
;   name ":"       → member
;   name "?" ":"   → member, optional within its option
;   "|"            → next option
;   ")"            → group closes; an adjacent "?" or "+" marks it
;
; after a field's type-expr:
;   ?              → voidable (§5.2)
;   ~ token        → default
;   = token        → fixed
;   = ?            → selector (§5.2); no value follows
;   name "=>"      → parse error ("record body expected; =>
;                    begins a map type only at type positions")
;
; map-type internal rules:
;   exactly one key => value entry ("a map type is a single
;   key => value entry" — the data grammar's multi-entry habit
;   does not carry over, §5.3); a "?" on the key side of "=>"
;   is a parse error, and the value side admits it (§5.3); the
;   key "<" case is the one place the
;   generic-key rule is reachable — after the arguments close,
;   ":" here is a parse error whose diagnostic SHOULD read
;   "expected =>; if you meant a record field, a generic key
;   cannot name one"
;
; bracket-form internal disambiguation:
;   [type sep type  → tuple (whitespace or comma)
;   [type ; spec    → array with size constraint
;   [type ]         → unconstrained array
;   [type ? ...     → element "?"
;   [[ ...          → nested bracket-type (full syntax)
;   [{ ...          → nested map-type
;
; after a construction body "}":
;   -              → removal clause (§5.9)
;   otherwise      → declaration boundary rules below
;
; declaration boundary (resync): after a bare type-ref in
; type-def position, one/two-token lookahead decides:
;   ^              → refinement body of the current type-def
;   <              → type arguments of the current type-ref
;   &              → composition continues the current type-def
;   -              → removal clause of the current type-def
;   ","            → current declaration complete
;   name "=>"      → current declaration complete; a new one begins
;   "}"            → current declaration complete; map closes
;   "{"            → parse error (write ^ or &)
;   name (other)   → parse error

Each case in the type-def block is decided by at most two tokens of lookahead at the start of the production — the brace form by one consumed token plus one token of lookahead, the same budget in the same sense as the data grammar’s §2.8 dispatch; inside a bracket form, the choice between tuple, sized array, and unconstrained array is made by one-token lookahead after the complete preceding element type. A field-type can itself be nested ((email | [phone])?) and parses without backtracking via the disambiguation above; the outer ? there makes the field voidable (§5.2). The schema body requires at most two tokens of lookahead to detect a declaration boundary. The parser never backtracks at any level.

12.3 Adjacency Rules

The following rows extend the adjacency table of [TSON-DATA] §7.5 for the operators of the type-definition grammar; as there, the rules are enforced by the parser via source-position comparison.

OperatorTypeContextRule
!prefixtype-def body (constructor application, value refinement)MUST be adjacent to the following unquoted-token (constructor or instance name)
?suffixfield type, tuple position, array/map element (element-type, §5.3), field groupMUST be adjacent to the preceding token (type name or closing bracket/paren)
&binarycompositionwhitespace on either side optional
^binaryrefinement (§5.5, §5.7)whitespace on either side optional
-prefixremoval clause (§5.9)at least one whitespace character MUST separate the preceding token from -; whitespace optional before the following {
~modifierdefault valuewhitespace optional
=modifierfixed valuewhitespace optional
=?modifierselector (§5.2)whitespace optional before = and between = and ?
?suffixfield name (omittable key, §5.2); group-member name (optional within its option, §5.11)MUST be adjacent to the preceding field name
+suffixfield group (at least one of, §5.11)MUST be adjacent to the preceding )
:separatorparameter type (§5.10)whitespace optional on both sides
|separatorchoice variant; field-group optionwhitespace optional
;separatorarray size spec; map size spec (§5.3)whitespace optional
..binarysize-spec range (§5.3)whitespace on either side optional
=>separatorschema declaration; data map entry; map type sugar (§5.3)whitespace optional (compound token from lexer)

The whitespace requirement before removal - is a lexer fact restated as a rule: hyphen-minus continues an unquoted token ([TSON-DATA] §7.2.4 — hyphenated names are legal), so in account- { password } the hyphen is absorbed into account- and no removal clause exists — the same footgun class as [1-2] lexing as one token. When a token ending in - appears at a type-def position followed by {, the diagnostic SHOULD note the absorbed hyphen and suggest whitespace before -.

13. References

13.1 Normative References

ReferenceTitleURL
RFC 2119Key words for use in RFCs to Indicate Requirement Levelshttps://www.rfc-editor.org/rfc/rfc2119
RFC 5234Augmented BNF for Syntax Specifications (ABNF)https://www.rfc-editor.org/rfc/rfc5234
RFC 3339Date and Time on the Internet: Timestamps (Appendix A is the duration and period grammar, §5.5)https://www.rfc-editor.org/rfc/rfc3339
RFC 3986Uniform Resource Identifier (URI): Generic Syntaxhttps://www.rfc-editor.org/rfc/rfc3986
RFC 3987Internationalized Resource Identifiers (IRIs) (net.iri_type, net.host_type’s ihost, §5.5)https://www.rfc-editor.org/rfc/rfc3987
RFC 3492Punycode: A Bootstring encoding of Unicode for IDNA (the A-label ↔ U-label mapping, net.hostname_type, §5.5)https://www.rfc-editor.org/rfc/rfc3492
RFC 4291IP Version 6 Addressing Architecturehttps://www.rfc-editor.org/rfc/rfc4291
RFC 4632Classless Inter-domain Routing (CIDR)https://www.rfc-editor.org/rfc/rfc4632
RFC 4648The Base16, Base32, and Base64 Data Encodings (the text spellings of bytes, §5.5)https://www.rfc-editor.org/rfc/rfc4648
RFC 5322Internet Message Format (email address syntax)https://www.rfc-editor.org/rfc/rfc5322
RFC 5890IDNA: Definitions and Document Framework (net.hostname_type, §5.5)https://www.rfc-editor.org/rfc/rfc5890
RFC 5891IDNA: Protocolhttps://www.rfc-editor.org/rfc/rfc5891
RFC 5892The Unicode Code Points and IDNA (U-label validity)https://www.rfc-editor.org/rfc/rfc5892
RFC 5893Right-to-Left Scripts for IDNAhttps://www.rfc-editor.org/rfc/rfc5893
RFC 5952A Recommendation for IPv6 Address Text Representation (host’s canonical text)https://www.rfc-editor.org/rfc/rfc5952
RFC 6838Media Type Specifications and Registration Procedures (net.media_type_type, §5.5)https://www.rfc-editor.org/rfc/rfc6838
RFC 6839Additional Media Type Structured Syntax Suffixes (net.media_type_type.suffixes)https://www.rfc-editor.org/rfc/rfc6839
RFC 9110HTTP Semantics (§8.3.1, media-type parameters)https://www.rfc-editor.org/rfc/rfc9110
RFC 9485I-Regexp: An Interoperable Regular Expression Formathttps://www.rfc-editor.org/rfc/rfc9485
RFC 9542IANA Considerations and IETF Protocol and Documentation Usage for IEEE 802 Parameters (EUI-48)https://www.rfc-editor.org/rfc/rfc9542
RFC 9562Universally Unique IDentifiers (UUIDs)https://www.rfc-editor.org/rfc/rfc9562
RFC 9651Structured Field Values for HTTP (the TSON-Schema field, [TSON-JSON] §3.5)https://www.rfc-editor.org/rfc/rfc9651
UAX #15Unicode Normalization Forms (the normalization facet, §5.5)https://www.unicode.org/reports/tr15/
UAX #31Unicode Identifier and Pattern Syntax (unicode.identifier_type’s profile and toNFKC_Casefold, §5.5, §7.4)https://www.unicode.org/reports/tr31/
UAX #24Unicode Script Property (the script names a policy document writes, policy.tn)https://www.unicode.org/reports/tr24/
UTS #39Unicode Security Mechanisms (name hygiene, §11.4; [TSON-DATA] §7.7, §8.2)https://www.unicode.org/reports/tr39/
CommonMark 0.31.2CommonMark Spec (the text of @doc, §6)https://spec.commonmark.org/0.31.2/
UCDUnicode Character Database (the data files behind [TSON-DATA] §8.2, and the version a refusal reports)https://www.unicode.org/ucd/
IEEE 754-2019Standard for Floating-Point Arithmetichttps://ieeexplore.ieee.org/document/8766229

13.2 Series References

13.3 Informative References

ReferenceTitleURL
RFC 8820URI Design and Ownershiphttps://www.rfc-editor.org/rfc/rfc8820
ISO/IEC 11404:2007General Purpose Datatypeshttps://www.iso.org/standard/39479.html
ISO 8601-1:2019Date and time — Representations for information interchange (the informative source behind RFC 3339 Appendix A)https://www.iso.org/standard/70907.html
JSON Schema 2020-12JSON Schema: A Media Type for Describing JSON Documentshttps://json-schema.org/specification
W3C XSD Part 2XML Schema Part 2: Datatypes Second Editionhttps://www.w3.org/TR/xmlschema-2/
Resolver Output Fixturesmeta-resolved.tn, numeric-resolved.tn, temporal-resolved.tn, net-resolved.tn, core-resolved.tn (non-normative)<published alongside this document>