Spec feedback
Issues, ambiguities, and inconsistencies found in the TSON spec while building this implementation. See CLAUDE.md
for why this file exists and when to add to it. Spec quotes below are from 2026 Revision 38 — Part 1 and Part 2,
the working copies in spec/, which publish at https://tson.io/raw/2026/38/tson-part1-data.md and
https://tson.io/raw/2026/38/tson-part2-schema.md — unless noted otherwise.
Format per entry: spec section, the problem, the interpretation this implementation chose, and a suggested resolution where there is one.
This register holds what is open against the current revision, and it renumbers from #1 each time a revision closes. It is an input to the next revision’s adjudication, so its numbering is the numbering that revision’s change log will answer against — a stable index of the open set, not an archive of everything ever raised.
The entries Revision 38 adjudicated are gone, and the spec carries their rules; the change log in spec/ keeps
them under their numbers. What is below was found on Revision 38.
This file is the as-built record, not a pointer to one: where an entry proposes a design this implementation
has built, the entry states the design, what is running, and what is not, so that a reviewer editing the spec needs
nothing beside it. Where the evidence is a consumer of this library rather than this library — the HTTP layer
in ltr8-io-tson-java-http is one, and this register is the collection point for all of it — the entry says so and
states what is running there on the same terms.
Part 3 is edited, not registered. [TSON-JSON] is an early draft this implementation exists to validate, and
spec/tson-part3-json.md is edited directly as findings arise — so a Part 3 finding becomes a spec change in
the same session, with git history as its record, rather than an entry waiting for adjudication. A Part 3 entry
appears here only where the finding is a choice between the text and the code that the author has to make, and it
closes by the edit or the code change. What stays here otherwise is Parts 1 and 2, whose current revision is
published and whose changes this implementation proposes rather than makes. An entry spanning both stays, and says
which half is which. Numbers are not reused and gaps are left while a
revision is open — the register renumbers only when one closes, so a citation written against the open set stays
valid until then.
Cite the spec, not the argument that got it there: design/ and the Javadoc name the section that requires a
behaviour, and an implementation-feedback.md #N citation is for an entry below, where there is no section to point at yet.
When an entry closes, its citations become spec citations and the entry is deleted — nothing here is an archive.
1. [TSON-SCHEMA] §5.7, §8.2 — a closed refinement source: “mints nothing” against “IS-A the source”
Problem. §8.2’s trigger positions say an application at a composition target or a refinement source “is subsumed
as an operand and mints nothing (§5.8)”. §5.7 says a refinement produces “a new definition with its own identity that
IS-A the source”. For x => box<text> ^ { n: integer } the two cannot both hold: an IS-A edge needs an entry at its
far end, and §8.2 says the source application has none. §5.8’s operand rule is written for composition, where the
operand contributes its fields and its template’s ancestors and no edge to itself is owed; a refinement owes exactly
that edge.
What is running. A composition operand is absorbed by value and mints nothing, as §8.2 says. A closed refinement
source is closed on demand through the same path a type-position application takes, so it reuses a declaration that
owns the application or mints the expansion — and which of the two happens depends on declaration order. Against
box => <T> { w: T n?: integer }:
tb => box<text>declared beforex:xhassupertypes: [box, tb]and nothing is minted.xdeclared beforetb: a syntheticrecord_w_text_n_integer_true_…entry is minted,xhassupertypes: [record_w_text_n_integer_true_…]—boxis not in its chain — andtbresolves to a second entry for the same application, withsupertypes: [box]and no edge to the minted one.
So the resolved output of one schema varies with the order of two unrelated declarations, and in one of the orders
§8.2’s “a declaration naming the application is that entry” does not hold either. design/template-materialisation.md
(“Operands and on-demand closing”) states the mechanism.
Resolution to decide. Either (a) §8.2 states that a refinement source is a trigger position — the application is
materialised (or unified with the declaration that owns it, in either order) and the refinement IS-A the entry it
denotes, which is what §5.7 already promises and what the declaration-first order already produces; or (b) §5.7 says
a refinement of an application IS-A the application’s template and nothing else, the source is absorbed as a
composition operand is, and no entry is minted. (a) keeps §5.7’s identity rule and asks the resolver to make the
closing order-independent, which §8.2’s content identity is for. (b) keeps §8.2’s sentence as written and loses the
edge from x to box<text>, so x would not be admissible where box<text> is expected. The implementation follows
(a) once this closes; until then the behaviour above is what runs.
2. [TSON-SCHEMA] §5.10 — an alias of a schema-namespace template: which namespace its value parameters read in
Problem. §5.10 says a template whose body applies a constructor of the governing meta other than record reads
its value parameters’ types in the structure namespace, a record template reads them in the schema’s own namespace,
and “a template whose body applies another template takes that template’s form”. An alias is a template whose body
applies another template — rb => <N: int8> box<N> against box => <N> { w?: integer ~ N } — so by the third
sentence it takes box’s form and reads int8 in the schema’s namespace. But an alias’s resolved entry is an
alias, a kernel constructor (#10), which by the first sentence makes it a structure-namespace template. The section
does not say which sentence wins for an alias.
What is running. The resolved entry decides: rb’s held head is alias, not record and not an open
template, so ParameterTypes.readsInStructure answers true and the written type is checked in the structure
namespace. The schema above is refused at rb: “parameter ‘N’ is declared ‘N: int8’, and this template applies a
constructor of the governing meta, so its value parameters are the meta’s types and a written type names one there —
‘int8’ is not in the structure namespace (§5.10)”. The same alias with no written type (rb => <N> box<N>) resolves,
N’s type being derived from box’s use rather than written. design/held-template-bodies.md (“The template’s form
decides where value types are read”) states the rule as built.
Resolution to decide. The third sentence is the one an author relies on — an alias of a record template is used
exactly like the record template — so the proposal is that §5.10 says an alias takes the form of the template its
target applies, following aliases through to the first non-alias head, and that alias is never itself a form.
The implementation then follows the target through alias.target in readsInStructure. The alternative, that
the resolved entry’s constructor is the form in every case, is what runs and should then be stated in §5.10 with the
alias named, since the third sentence reads against it.
3. [TSON-DATA] §2.4, [TSON-SCHEMA] §5.4 — at a disjoint choice the form selects the variant, with no emitter rule
Problem. [TSON-DATA] §2.4 says a token’s form is consulted exactly once, by base type resolution, that
“quoting is a lexical necessity, not a semantic signal”, and that content inside the profile “may be written in
any form”; §5.2 makes !date 2025-03-13 and !date "2025-03-13" one value. [TSON-SCHEMA] §5.4 makes disjoint
mean that the same single pass recovers an untagged value’s variant, so at a disjoint choice the one consultation
§2.4 allows is the one that picks the type. There, form is meaning, and §2.4’s freedom changes the value:
- the text
42written bare at( int8 | text )reads as the number 42, with no diagnostic, since the reader cannot know; - the text
42written bare at( small | text ),smallanint8capped at 10, is refused bysmall— the variant its class selected — with no second attempt attext; - the text
truewritten bare at( boolean | abc )dispatches toboolean; - the number 42 written
"42"at( int8 | text )reads as text, where §2.4 says!number "10.2"is a number.
§5.4’s emitter sentence — “if two variants share a class, tag every value of the choice; a tag is never wrong” — covers the non-disjoint case only. Nothing tells an emitter what form to give an untagged value at a disjoint choice, and nothing in §2.4 warns that there is a position where its freedom does not hold. [TSON-JSON] has no counterpart problem: kind is meaning at every typed position there (§5.3, §5.6), so its encoder sentence in §8.2 is complete.
What is running. The readers do exactly what §5.4 says: an untagged value at a disjoint choice goes to the
variant of its token’s class and is validated as that variant, with no fallback. The schemaless writers comply
without knowing the schema — AtomWriter quotes every host String and writes every number and boolean bare —
so a value written by this library round-trips through a disjoint choice to the variant it came from. The misread
is a hand-written document’s, or a producer’s that quotes everything or nothing, which is where the
structured-output case lives.
Resolution, a proposal. One sentence in §5.4’s tagging paragraph, the twin of the one it has: 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 is quoted, and a value of a boolean or number variant is written bare; a tag is never wrong. And one clause in [TSON-DATA] §2.4 after “consulted exactly once”: at a disjoint choice that consultation selects the variant, so the form an emitter chooses there is its tag. No reader rule changes.
4. [TSON-SCHEMA] §3.4, §3.4.1, §5.10.1, §11.5 — resolution order within a schema is unstated
Problem. §3.4.1 orders resolution between documents (the closure, dependencies first) but not within one: Pass 2 “resolves each entry’s body against the populated namespace”, forward references are permitted, and §5.10.1 says the resolver “MUST detect and handle cycles” without saying which dependencies form one. The kernel’s own order is left to pre-loading (§3.4). Under the ladder every constructor an ordinary schema applies is in another, already resolved document, so the gap shows only in the kernel and in composition chains; once a constructor and its instances share a document, an order has to be computed and its cycles defined.
What is running. Rules 1 to 7 run for every schema, by one algorithm; what differs is only what is ready before
a document’s resolution starts: the pre-loaded set for the kernel, the import closure for every other schema. An
entry is made ready the first time anything asks for it, and source order is not consulted (rule 5). A declaration
waits on its head (C in !C { … }, the source in !I ^ { … }), on its refinement source and & operands, on the
type of each annotation it carries, and on every type a read of its value reaches; a value read against an entry of
the same document is read through readers compiled from the entries ready so far, and a read that reaches one not
yet ready makes it ready first, then starts over. A name at a type position, and a held value, wait on nothing
(rules 2, 3). So a constructor and its instances, a type and a value of it, and an annotation type and its uses share
a document in either order, and a test resolves each bundled schema, and a schema whose entries read each other, with
its declarations reversed and shuffled and gets the same output entry for entry.
Entries waiting on each other through rule 1 are refused naming the cycle in wait order ('a' waits on 'b' waits on 'a', 'step' waits on 'step'); a composition cycle (a => b & {}, b => a & {}) is the same rule, and a type
annotated with itself on its value (note => @note:"x" text) is a cycle of one. An alias loop is refused by §8.3, not
as a wait. A judgement that walks a name to what it names waits for every entry to be ready (rule 6): §5.8’s one name
type over a composition’s supertypes is judged then, so walking it never makes the composition wait on an entry that
waits on it.
Rule 7 is counted: 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, so a chain is counted
within one document’s resolution. A chain past the limit (default 64) refuses its head with LIMIT_EXCEEDED, a
refusal and not a verdict, whichever order the chain is reached in; the bundled schemas resolve under the default
whatever a deployment sets.
The kernel, meta.tn (#15), measured from meta-resolved.tn’s own declarations, no compiled reader consulted, has one
wait cycle, holding meta.record and 24 more of its 88 entries (boolean, integer, text, numeric.count and
numeric.integer_size (#16), the enum and set machinery, meta.field_group, meta.record_field, meta.type_chain,
meta.map and its instance meta.field_map, meta.type_ref, and the synthetics they name), every other entry ordered
after it, the longest chain 4; the kernel holds that cycle’s closure and the concepts that go with it (#15). The kernel
declares meta.annotation => void: annotating itself with @meta.annotation would be a second cycle, and the marker
changes no value (§6). The bootstrap builds by hand exactly the instances and the one refinement on that cycle (a test
holds the set to the measurement), and makes every other entry ready by the algorithm above. A pin reached through an
entry not yet ready binds as written (rule 3), which is what lets iri be read against iri_type, whose spec pin is
typed by iri. The output equals the kernel’s ordinary resolution against it, entry for entry. Compilation follows rule
3: a record’s reader holds its pins and defaults as written while it is built and decodes them through their types once
every reader of the schema exists, before the schema is handed out. Link remains the one place a pin is judged, and a
bad pin or default still fails the load (rule 6).
What is not running: a resolved form is still read under its source’s imports rather than by this algorithm over its own
entries (§8.1, “Resolved forms” in the proposal), so a fixture holding an instance of a constructor it declares reads back
only beside its source. And rule 2’s example pair, a constructor whose facet is typed by its own instance (decimal_type
beside number), runs only as numeric.tn declares it. decimal_type composes a base kind directly, and a constructor that
does makes a new form, which a processor backs with readers and writers of its own: an extension point, not schema
vocabulary. This library backs the forms the kernel and the data-format libraries declare and no other, so it refuses an
instance of any other such constructor as NOT_IMPLEMENTED. Refining an existing constructor with a field of its own (c => record & { extra: text }) is no new form and should follow record composition; it is refused the same way today.
Resolution, a proposal. Terms:
- Form. A structure serialized data takes, read by an algorithm the processor owns: an atom (one token), a record or tuple (fixed length, named or positional), an array or map (variable length, positional or keyed), a choice. Sets, enums and namespaces are refinements of these.
- Constructor. An entry that links a name to the data a form needs to be read:
array’selement_typeandmin_items,integer_type’s facets. - Declaration. An entry’s value, the data its constructor describes. An entry is ready when its declaration has been read; a type’s reader is its form over its declaration, so a type can be read against exactly when it is ready.
The rule is 1; the rest are its consequences.
- The rule. Reading a value against a type
Twaits onTbeing 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 (Cin!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. - A reference orders nothing. A name at a slot typed
meta.type_reformeta.ref<T>, an application among them (#12), is read by its atom’s grammar alone; what it names is resolved and judged once every entry is ready (§4.2, §8.3). Sotree => { t: tree }, mutual recursion, and a constructor whose facet is typed by its own instance (decimal_type => atom & { min?: number }besidenumber => !decimal_type {}) are sound. - A held value orders nothing. A field’s default or pin, a dependent field (#21), 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
field_mapreachesmeta.record_field, whichfield_mappins as itsvalue_type, and a value of a family base (§5.2) reaches each subtype, whose selector pins §5.2 confines to an atom or an enum. - A cycle is refused. Entries waiting on each other through rule 1 are a resolver error naming the entries on the
cycle. A loop of aliases is not a cycle under this rule; §8.3 refuses an alias chain with no terminal. The
pre-loaded entries (§3.4) are ready before any declaration is read, and are exactly the kernel’s one cycle, the one
holding
record, whose declaration is a!record. Every other kernel entry is read by these rules, and the kernel’s resolved form must come out of them unchanged. - 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. This replaces §3.4.1’s forward-reference sentence.
- 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.
- Limits. §11.5 gains a row, wait depth: the longest chain of rule 1 waits in one closure, default 64. Template expansion needs no new bound; §5.10.1’s regular recursion and §11.5’s materialisation depth bound it.
With nni => !integer ^ { min: 0 }:
| declaration | waits | verdict |
|---|---|---|
tree => { t: tree } | record; t’s type is a reference | sound |
uint8 => !integer ^ { min: 0 max: 255 } | integer (source); min and max on integer | sound |
point => { x: int32 next?: point ~ { x: 1 } } | the default is held | sound |
integer_type.multiple_of?: nni | a multiple_of value on nni, on integer | sound: the kernel |
integer_type.min?: nni | nni’s min: 0 on nni | cycle: refused |
a => b & {}, b => a & {} | a on b on a | cycle: refused |
c1 => !c2 {}, c2 => !c1 {} | c1 on c2 on c1 | cycle: refused |
a => b, b => a | none | no cycle; refused by §8.3 |
Sections amended: §3.4 (the pre-loaded set is rule 4’s), §6 (its “one exception is structural” sentence goes, the
kernel’s annotation no longer annotating itself), §3.4.1 (Pass 2 runs in rule 1’s order; rule 5), §5.10.1
(its cycle sentence cites rule 4), §11.5 (rule 7’s row).
5. [TSON-SCHEMA] §5.5, §5.7, §12.1 — ! marks an instance, and ^ refines any value under its type’s rules
Problem. §5.5 and §5.7 state two refinements under one operator, each with its own rules: record refinement,
T ^ { … } over field declarations, and atom refinement, !I ^ { … } over data merged into an atom instance’s
bindings. No other value can be refined. A container instance (names => [text]) has no refinement, and two
instances of one constructor have no IS-A edge between them even where one admits a subset of the other.
Declared as two instances of meta.reference, meta.type_ref (!meta.reference { of: meta.type }) and
meta.identifier_ref (!meta.reference { of: unicode.identifier_type }) are siblings, so a record refinement
narrowing a meta.type_ref field to meta.identifier_ref is not a narrowing §5.7 admits, although every identifier
family is a type.
Under one level a ! head looks as though it plays two roles, a type from !decimal_type { … } and a value from
!operation { … }. It plays one: !C { … } is a value of C, and whether the entry is a type follows from C’s
chain (§3.3.1), as constructorness already does. Resolved output writes every type this way, so the source spelling
and the resolved form are one.
What is running. !C { … } construction, atom refinement !I ^ { … } with each atom family’s tightening rules,
and record refinement T ^ { … }. Nothing else is refined. Item 5’s worked example runs: meta.reference states its
rule for of (a refinement’s of is the source’s or IS-A it, judged at link, where both names resolve, and a widening
or an unrelated of is refused), identifier_ref => !meta.type_ref ^ { of: unicode.identifier_type } is IS-A
meta.type_ref, and namespace => meta.map ^ { key_type: meta.identifier_ref voidable?: = false } is IS-A
meta.map, and passes §5.7 by that edge: the link judges every restated field’s type against its source’s by IS-A
(#18), and refuses a restatement whose type is no subtype.
Resolution, a proposal.
!is an instance of the named type in a schema as in data and resolved output.!integer_type {}is a value ofinteger_type, and a type becauseinteger_typeIS-Ameta.type. The grammar does not classify a line; its head’s chain does.^refines a value. Givenfoo => !bar { … },foox => !foo ^ { … }isfoo’s value with the body merged over it, under the refinement rulesbarstates for its fields. The result is an instance ofbar, and IS-Afoowherefoois a type. Atom refinement is this rule with an atom constructor asbar.- Record refinement is the same rule with
recordasbar. A record type is a value ofrecord, its field-declaration body is sugar for that value, and §5.7’s rules arerecord’s: a field may only narrow, to a type IS-A the original. The two forms differ only in the body’s grammar:!before the operand means the body is data, and its absence means field declarations. - A type states its refinement rules, field by field: a bound moves inward, a member set shrinks, a type slot
narrows to a type IS-A the original. A value whose type states none is refused at
^, naming the type, so the rule is total. A namespace type states none, so!orders ^ { … }, withorders => !api { … }, is refused. - Worked example. With
meta.referencestating thatofnarrows to a constructor IS-A the original,identifier_ref => !meta.type_ref ^ { of: unicode.identifier_type }is IS-Ameta.type_ref. A record refinement narrowing ameta.type_reffield tometa.identifier_ref, such asnamespace => meta.map ^ { key_type: meta.identifier_ref … }, then passes §5.7 by that edge.
Grammar (§12.1). Unchanged. atom-refinement’s head generalises from an atom instance to any value whose type
states refinement rules; no new token, and no lexer change.
Kept out. A type as the right operand, its bindings merged over the left’s (pos_int32 => int32 ^ positive), and
a refinement carrying type parameters (<N> !integer ^ { min: N }, refused as today; a template over the constructor
is the route). The tightening rules of the container constructors (array, set, map) are each that constructor’s
to state before its instances can be refined.
6. [TSON-DATA] §2.3, §3.3, §7.4, §9.1; [TSON-SCHEMA] §7.1, §7.8 — the scope push names a foreign type in data
Problem. §7.8’s EXTERN cell lets a data value name a schema its governing schema never imported, by a
!!schema directive on the value. The foreign schema is then found mid-document: a schema the processor will not
supply surfaces as a refusal halfway through a read, the pushed closure’s pins need not agree with the governing one’s,
and [TSON-DATA] §9.1 has to count the schemas a document can make a processor load. It is also a second directive
position, and the only one inside a value, for what is a fact about the schema: which foreign types a position admits.
The LOCAL cell needs none of this — a value naming, by its tag, a type of the governing closure.
What is running. The push is gone and declared is its own constructor:
- The kernel declares
declared_type => sum & {}anddeclared => !declared_type {}(#9). Adeclaredposition reads exactly as!scoped { scope: [LOCAL] }did: the value names its type by its tag, the name resolves in the governing schema’s closure (§2.2.3), and the value is checked in full against it; an untagged value is a validation error in every mode, and a tag naming no type of the closure a resolver error.declaredis SUM-kinded and has no discrimination class (§5.4), as thescopedinstances were. scoped,scope_kind,scope_setandschema_identity, andextern,dynamic,extern_ofandextern_type, are in no bundled schema.- [TSON-DATA]‘s
scoped-valueproduction is gone: a record field value, a map entry value and an array element are each adata-value, so a directive there is outside its placement and a parse error (§3.3’s closed positional set). Directives stand in the header alone, and the header’s one binding directive is!!import(#8). - [TSON-DATA] §9.1’s foreign-schemas row is gone, from the limits policy and from
policy.tn. - [TSON-JSON] (edited in place) admits its binding member,
$importsince #8, at the top of a document alone; anywhere else it is a resolver error, and §8.5 is “Declared Positions”.
Resolution, a proposal. Sections amended:
- [TSON-SCHEMA] §7.8 reduces to
declared: thescopedconstructor, its table, the two templates, the cell rule, the scope push, the typed-position restriction and the schemaless-document paragraph go, and what stays is the LOCAL rule above, stated ofdeclared_type. §7.1 loses its scoped-value paragraphs (the binding of one value, the typed-position sentence) and keeps the header binding. §4.1’s sum bullet nameschoiceanddeclared_type; §5.10’sscoped.schemasexample, §7.5’sscoped.scope, §8.1’s table rows namingscoped,extern_ofanddynamic, and §9’s meta and core rows follow. - [TSON-DATA] §2.3 has one value rule,
data-value; §2.5, §2.6 and §2.7 lose their directive sentences, §2.1’s example its field-level!!schema, §3’s augmentation intro and §3.3 place directives in the header alone, §7.4’s grammar dropsscoped-value, and §1.5’s Class 1 obligation to preserveschemadirectives concerns the header’s. - [TSON-DATA] §9.1 drops the foreign-schemas row.
A foreign type is used by importing its schema. No schema-driven external form is proposed for this revision: a foreign schema that keeps its own names puts two namespaces in one document, so either a tag’s meaning depends on its position or the data carries a prefix, and neither is taken.
7. [TSON-SCHEMA] §2.2.2, §2.2.3, §3.1, §3.3, §3.4, §6, §9 — one namespace per schema: the ladder’s rules go
Problem. A schema document resolves in two stores: ! targets, kind/ref of and annotations in the
structure namespace of its !!meta target (§3.3.1, §3.3.3), every other name in its type-name namespace (§3.3.2).
Four rules hold the two apart and keep each document on one rung: placement (§2.2.2, only a kernel-governed
document declares a constructor, a namespace type or a reference), direction of service (§3.3.1, a schema’s !
targets come from its meta, never its own entries), the level of an import (§2.2.3) and a !!meta target’s
eligibility (only a kernel-governed schema governs). They cost core.tn nine sibling declarations of the meta-kernel’s and
meta.tn’s entries (§3.3.5), and make a constructor declared beside its instance, or in an ordinary library its
importer applies, unexpressible. Each is a rule about levels, which the dependency model of #4 replaces.
What is running. One namespace per schema:
- A schema’s namespace is its own entries and every entry of its import closure, transitively.
!!metacontributed exactly as!!importdoes, and is retired by #8:!!importis the one directive, and the root’s!schematag classifies the document. - Every name resolves there: a
!head, an atom refinement’s source, a type-ref, a kind’s or a reference’sof, a template bound, and an annotation (§3.3.3: in the annotating schema’s namespace, for a schema document as for a data document). - Deleted: placement (§2.2.2) — any schema declares a constructor, a namespace type or a reference, with no extension guard; direction of service (§3.3.1); the import-level rule (§2.2.3); the governing-target eligibility check; the structure/type-name split (§3.3.1, §3.3.2, §3.3.4’s invisibility of structural vocabulary); and §3.3.5’s sibling allowance. Two same-named entries in one closure are §2.2.3’s collision, decided by identity.
- core.tn imports numeric.tn, temporal.tn and net.tn, each importing the kernel alone, and declares none of
void,boolean,integer,text,bytes,regex,iri,iri_reference,docorset(the kernel’s root vocabulary), each where the kernel places it (#15), nor akind, the kernel’smeta.refbeing the one spelling (#11). The kernel’sirirefines itsiri_reference(iri => !iri_reference ^ { allow_relative: false }), soiriIS-Airi_referenceasuriIS-Auri_reference. The descriptive annotations Revision 38’s core puts on its copies (@ordering,@bounded, …) go with them. - A same-document read waits: applying a constructor declared in the same document (
!C { … }withClocal), reading a value of a type declared there, and reading an annotation value against one each wait on that declaration being ready, in #4’s wait order, whichever is written first. A type used by name waits on nothing. - The cost: every name in the closure is reserved. The bundled documents keep their roots to the vocabulary an
author writes and their machinery under
meta,atom,numeric,unicode,net,binaryandtemporal(#15), so a schema importing core reserves those seven and the roots’ names —text,enum,set,doc,uuidandtitleamong them;record,map,namespace,template,identifier,field_nameandboundedare free.
Resolution, a proposal. Sections amended:
- §2.2.2: goes with
!!meta(#8); its two bullets, the ladder paragraph, “Constructor declarations are a meta-schema privilege” and the namespace-type paragraph’s level sentences go with it. - §2.2.3: “An import stands at the importer’s governing level” and “exactly as
!!metacontributes” go; the sentence placing imports in the type-name namespace only goes. - §3 intro, §3.1, §3.3, §3.3.1, §3.3.2, §3.3.4 restate one namespace: §3.3.1 keeps the constructor-role
applicability rule (IS-A
top) and the desugar targets, now resolved in that namespace, and its direction-of-service, one-hop and “import what you expose” paragraphs go; §3.3.2’s lookup order gains the closure’s imports alone; §3.3.5 goes. - §3.3.3: an annotation resolves in the annotating document’s namespace; core declares no
docof its own. - §3.4: the kernel’s entries reach a schema through its import closure; “NOT automatically available as type-refs” goes.
- §4.2 (core’s
kind,voidandsetsiblings), §4.x’s “Core declares no sibling ofmeta.identifier”, §1’s worked example (“reached through the structure namespace”), §6, §9 and §13.2: core imports numeric.tn, temporal.tn and net.tn; core’s sibling rows go.
8. [TSON-DATA] §2.2, §7.1; [TSON-SCHEMA] §2.2.2, §2.2.3, §3.4, §7.1, §8.1 — one directive, the root says the kind
Problem. Three directives state one relation. !!import brings a schema’s namespace into a schema; since #7
!!meta does the same and also classifies the document as a schema; and !!schema on a data document names “a
namespace, not a root contract” (§7.1), which is what an import is, but admits one. The kernel names itself with
!!meta, a cycle closed only by pre-loading, and a resolved form cannot carry its source’s imports, so they are
supplied at ingest (§8.1).
What is running.
- The header is an optional
!!id, then any number of!!imports, for every document.!!metaand!!schemaare parse errors naming their replacement; an unknown directive, and an!!idafter an!!import, are refused by the same grammar. A directive past the header is the parse error #6 left. - A document is a schema exactly when its root is tagged
!schema. The tag always names the pre-loaded kernel’s rootschema,schema => meta.schema, an alias, whatever the document imports, and is read as a spelling; a schema parser refuses an untagged root. Classification reads the header, the root’s leading annotations and its tag, and never the root’s value, so a schema map’s sugar is never parsed to classify it. That is not §7.1’s “at most two directives of lookahead and no value parsing”: annotations stand before the tag (§7.4’s*annotation [type-ref] core-value), so a schema carrying a document-level@docis classified past that annotation’s value. This is the reading running; the alternative, a schema’s annotations after its tag, would make the root the one value whose annotations follow its type-ref. - A Class 1 processor still refuses a schema document with §1.5’s categorized diagnostic, now at the root’s tag. Reading one as a map with a preserved tag would surface its sugar as a misleading parse error.
- Data documents import as schemas do. Several imports are read under one namespace over their closures, a name two of them declare differently being §2.2.3’s collision; under imports the root names its type (§7.1). A read under several imports is linked as a schema with no entries of its own importing each, and compiled once per set.
- Every import target is a schema document: a target whose root is not tagged
!schemafails to parse as one, and the importer fails to load. Two pins of one identity in one closure are refused, since at most one matches the content (§2.2.1). Two revisions of one base are refused where they declare a common name, as §2.2.3’s collision; nothing defines a revision for an arbitrary identity, so no rule refuses them otherwise. - The kernel’s header is its
!!idalone, its root!schema. It imports nothing, is pre-loaded (§3.4) and is never resolved from a document, and a document claiming its identity is checked against the bytes the processor ships. Its names reach a schema only by import: an import-free!schema { a => { b: b } b => {} }is valid and sees its own names. numeric.tn, temporal.tn and net.tn each import the kernel alone, core.tn imports the three, and policy.tn imports the kernel alone. - A resolved form’s header is its source’s, and its root
!schema; nothing in it marks it resolved. Reading one is the caller’s choice, as §10.1’s ingest is: 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. Nothing is supplied at ingest. - No reading rule is left of the ladder. A kind-typed position is judged by membership in every data document, and read by its grammar alone in a schema document, whichever schema it is read under (#9).
- [TSON-JSON] (edited in place): the binding member is
$import, a list of references, at the top of a document only.
Resolution, a proposal. Sections amended:
- [TSON-DATA] §2.2: a header is
[id-directive] *import-directive; the directive table losesmetaandschema; a document is a schema when its root is tagged!schema. §3.3’s positional set isidthenimport. §7.1’s classification reads the header, the root’s annotations and its tag. §7.4:document = [id-directive ws] *(import-directive ws) data-value ws. §1.5 and §8.1 identify a schema document by its root’s tag. - [TSON-SCHEMA] §2.2.2 goes. §2.2.3 states imports for every document, every target a schema document, and the pin
rule above. §3.4: the kernel’s header is
!!idalone, and it is pre-loaded, never resolved. §7.1: a data document’s!!imports bind it, several in one namespace, and the root names its type. §8.1: a resolved form carries its source’s header, and nothing is supplied at ingest. §12.1:schema-document = [id-directive ws] *(import-directive ws) *(annotation ws) "!schema" ws schema-map.
9. [TSON-SCHEMA] §4.1, §4.2, §5.12, §7.4, §8.1 — value entries are ordinary values; namespace builds map types
Problem. §4.1 makes namespace a fourth kind beside type: a constructor IS-A top alone, whose instances are
maps, so a namespace type is the one constructor whose instances are not types and the only way a schema map holds a
value. Every rule that tells types from values carries an exception for it — meta.type_kind holds the namespace
constructors, meta.kind<namespace> holds both namespace types and their values, meta.ref<T>’s “of is no constructor”
reads api as one — and a schema cannot hold a value of an ordinary record type at all
(create_order => !operation { … } is refused as “not applicable”). The schema map’s own value_type: top is false
once any entry holds a value. And with one namespace per schema (#7), a meta layer’s vocabulary is ordinary types, so
“governed by a meta layer” no longer says which documents describe schemas.
What is running.
namespaceis a refinement ofmap,namespace => map ^ { key_type: meta.identifier_ref voidable?: = false }, IS-Amapand through itproduct, and a namespace type is an instance of it, as a map type is one ofmap:schema => !namespace { key_type: meta.type_name value_type: meta.declared },api => !namespace { key_type: meta.identifier value_type: method }. A value of one is the map its facets describe, every key a name of the familykey_typestates, a member ofmeta.identifier_refjudged at link, and no value void.key_typeis required: no host supplies a namespace’s key family.orderedand the size bounds aremap’s, and a value of a namespace type is read as that map, itsmin_itemsandmax_itemsjudged as a map’s are.meta.ref<map>holds every namespace type beside the plain map types, and a qualified name still enters a value of a namespace type alone: a plain map such asfield_map’s is never entered.schemais the kernel’s one.- A record’s fields are no namespace.
field_map => !map { key_type: text value_type: meta.record_field ordered: true }. A field name is text within its own record, judged by the record’sname_typeasdiscriminatorsand a group’smembersare, and asenum_setis for an enum’s members: the map’s own key uniqueness compares the text as written, and two keys that are one value under the name type’s equality are a resolver error. §5.12’s further segments select in a value and a type entry admits none, so no qualified name selects a field declaration (foo.barandfoo.fields.barforfoo => { bar: text }are refused); and a record value whose field is typedfield_mapholds a map that is no namespace, which no segment enters (point.p.fields.xis refused atfields). - An entry is a type exactly when its value is built by a constructor IS-A
type.name => !T { … }withTa constructor builds a type; withTan ordinary type — a record, a namespace type, any type of the closure — the entry is a value ofT, read againstTas a data value is, and marked by nothing but its tag. A value entry is a member of no reference over a constructor (meta.type_refexcludes it unmarked), nothing is typed by it, and a reference overTnames it where its type IS-AT:meta.ref<api>(#11) namesorders => !api { … }. A value entry’s payload lifts and mints nothing (a value ofschemais no value entry: it declares, #14); its names at kind- and reference-typed slots are judged at link, and a qualified name walks into it by key and by field. A template over an ordinary type closes to a value entry (fetch => <T> !operations { … },get_order => fetch<order>). A type declared in the same document is applied in wait order (#4). - The schema map’s values are
declared. The kernel declaresdeclared_typeanddeclared(Revision 38 declares them in meta.tn and core.tn), andschema’svalue_typeisdeclared: every entry’s value names its type by its tag — a constructor for a type, an ordinary type for a value.topis the root of the constructors only.declaredis the kernel’smeta.declared(#15), and the descriptive annotations Revision 38’s core puts on its copy (@ordering,@bounded) go with it. - A kind-typed position is judged by membership in every data document, over the reading document’s closure. A
name in a schema document — a construction’s or a value entry’s payload, or a value of the kernel’s
schemaread as data, a resolved form — resolves in that schema, which the reading closure does not hold: its grammar is read and its membership judged where that schema links, or at ingest. That replaces the stand-in that read grammar alone under the kernel and any schema importing it directly: a data document importing temporal.tn that writes!meta.record_field { type: int32 }is refused,int32being no member ofmeta.type_refin temporal.tn’s closure. - The kernel’s wait cycle (#4) holds
map,field_mapbeing its instance, and notnamespaceormeta.field_name,field_map’s keys beingtext: 25 of the kernel’s 88 entries, longest chain 4 (#4). The bootstrap buildsfield_mapby hand with the rest of the pre-loaded set. - Hosts. One bind context serves schemas and data; the names the kernel declares and the libraries’ members of
numeric,temporalandnetalways bind to the schema model (#15), and every other name binds through the consumer’s binder. A value entry whose type no class binds is held as a tree, so a schema loads and validates without host classes; a schema obtained to bind refuses such an entry withBIND_MISMATCHat the entry, anywhere in the closure it compiles, since a bound reference can reach a value of any schema there. An instance of a constructor whose instances are no types (one composingtopand nottype,s => !step { … }overstep => top & { … }) is held the same way, as the value it is. These are a processor’s choices, not the spec’s.
Resolution, a proposal. Sections amended:
- §4.1: the kinds are
atom,productandsumundertype, besidealias(#10) andtemplateundertop;namespaceis a refinement ofmapwhose instances are namespace types, and the paragraph making a namespace type the one constructor whose instances are not types goes. An entry is a type exactly when its value is built by a constructor IS-Atype, and any other entry is a value, its tag naming its type.namespace’skey_typeis required.schema’svalue_typeisdeclared, which withdeclared_typeis the kernel’s.field_mapis an orderedmapkeyed bytext, each key judged by the record’sname_type. - §4.2: a namespace type is a member of
meta.type_ref,meta.ref<product>,meta.ref<map>andmeta.ref<namespace>, and a value entry of no reference over a constructor; the rest of §4.2 is #11’s. - §5.7: “Constructor refinement in a meta-schema is the meta-level exception and MAY replace fixed values (§4.2)” is ladder wording; with no meta-schema it is restated for a constructor refinement wherever it is written, or the exception goes. The omission order stands.
- §5.12: a qualified name’s first segment names an entry holding a value; a further segment selects a key of a value of a namespace type or a field of a record value.
- §7.4, §8.1: a kind-typed position in a data document is judged by membership in the document’s closure; in a schema document, and a resolved form read as one, its grammar is read and membership is the schema’s link’s or ingest’s.
10. [TSON-SCHEMA] §4.1, §7.2, §8, §11.5, §12.1 — the alias body is alias; a reference is a name in a slot
Problem. “Reference” names two things. §4.1’s entry kind and §8.3’s body, !reference { target: T }, are an
alias: §8.3 is headed “References” and says a reference is a hop, §7.2 compares “after reference flattening”, and
§11.5 bounds the “reference chain”. §4.2 and §5.12 use the same word for a name held in a slot: meta.ref<T>, a type
reference, a qualified name. The kernel’s reference holds the word an atom of names would take, and every rule that
says “reference” leaves the reader to work out which of the two it means.
What is running.
- The kernel declares
alias => top & { target: meta.type_ref }(#12). An alias body is!alias { target: T }, written explicitly or derived fromname => T, and its kind is ALIAS. The kernel’s and core.tn’s resolved forms state!meta.aliasformeta.type_name,meta.field_name,meta.param_name,annotation,docand the annotation markers. - §8.3’s walk is the alias chain. Policy’s limit is
max_alias_chain, default 64, and diagnostics that walk it say “resolves through its alias chain to”. - Nothing else moves.
aliasandreferenceare constructors,meta.aliasandmeta.reference, members ofmeta(#15), so neither reserves its bare name andalias => portin a schema importing core collides with nothing.
Resolution, a proposal. §4.1’s fourth entry kind is Alias: “an entry whose value is a pointer to another
entry: !alias { target: T }”. §8.1’s record paragraph and table name alias and ALIAS. §8.3 is headed “Aliases”:
“an alias is a hop, not a rewrite”, “aliases are not supertypes”, “instantiation entries are not aliases”. Every
“reference chain” becomes “alias chain”: §4.3, §5.2, §5.4, §5.7, §5.10, §5.12, §6, §7.2, §8.1 to §8.3, and §11.5’s
row. §7.2’s phrase becomes “after alias flattening of both”, §5.10’s “reference template” (id => <T> T, a partial
application) becomes “alias template”, and §12.1’s instance comment names the kernel’s alias.
“Reference” keeps one meaning: a name held in a slot, as in §4.2’s meta.ref<T> and §5.12’s qualified name, an
application being one with arguments (#12).
11. [TSON-SCHEMA] §4.1, §4.2, §5.12, §7.4, §8.1 — one reference atom, and a constructor is a record type
Problem. Every entry of a schema map is a name and a value, and the value has a type, its tag: order => { … }
holds a value of record, int32 => !integer ^ { … } a value of integer_type, orders => !api { … } a value of
api. A field’s type position (meta.type_kind), a meta.kind<C> slot and a meta.ref<T> slot all ask one
question of a name: does the named entry’s value, past its alias hops, have a type that IS-A the slot’s T? §4.2
asks it through two atoms, kind_type and ref_type, and partitions by T: meta.kind<T> requires a constructor
and meta.ref<T> refuses one, so meta.kind<api> and meta.ref<record> are each refused pointing at the other
spelling. kind_type also writes a derived members index into resolved output. And a kind excludes an entry that
is itself a constructor, and a field’s type position refuses one (“a constructor, not a type”), although a
constructor’s declaration is a record (uuid_type => atom & { … }) and a record type’s values are records.
What is running.
- One atom. The kernel declares
reference => atom & { of: meta.type_ref }(ofa qualified name, #14).kind_typeandref_typeare gone, and so, with #12, are the recordstype_refandtype_argument. - One spelling.
ref => <T> !reference { of: T }is the kernel’s one template over it, and nokindstands beside it: whatofnames decides what a slot holds, so a second spelling could say nothing the first does not.meta.ref<record>reads as a reference to a record type andmeta.ref<method>as one to a method value. The two named instances read as instances of it,<target>_ref:type_ref => !reference { of: meta.type }, a reference to a type, typing every slot that names the type of a value, andidentifier_ref => !meta.type_ref ^ { of: unicode.identifier_type }, a reference to an identifier family, typing bothname_typeslots. Revision 38’stype_kindandidentifier_kindare these two renamed, and itstype_refrecord is #12’s. “Kind” stays the concept, §4.2’s set of the types a constructor builds, which a reference over a constructor holds. - The slot’s meaning comes from
of, never from the spelling. Whereofis a constructor,topor IS-A it, the values it admits are types: the slot holds a type’s name, judged a member over the governing closure and delivered to a host as aTypeRef. Whereofis any other type, the values it admits are values: the slot holds a value’s qualified name, judged at link and delivered as the referent (Ref<T>). Someta.ref<bar>over a record type names values ofbar,meta.ref<record>names types, andmeta.ref<api>namesmyapi. ofnames any type or constructor. Refused are a name that resolves to nothing, an open entry and a value entry.meta.ref<top>holds every type;meta.ref<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.membersis no part of the body. A resolved form statesofalone, and an author writingmembersis refused as an unknown field. The admitted set is derived at link and kept by the processor for its own judgements.- Where a name is judged. In a schema document the grammar where it is written and the judgement when the schema
links. In a data document a type’s name by membership in the reading closure, and a value’s qualified name by what
it reaches there: §5.12’s walk, run over the governing schema’s namespace (the closure of the document’s imports),
the referent’s type IS-A
of, sowhich: o2atmeta.ref<order>reads whereo2is a value oforderor of a subtype, 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 (aRefdelivered at the read, a reference whose referent is the value); JSON reads the same name from a string (Part 3 §5.6). This departs from §5.12’s “the rule does not reach a data document … a host component bound to a reference there receives a name, unresolved”, which was written when a reference was kernel vocabulary and no data document’s schema could declare one; with any schema declaring a reference (#7), a data document holds values at reference-typed slots routinely, and an unjudged name there would be the one value a schema types that its data is never checked against. The store is not the problem §5.12 names: a data document has a namespace, the one its!!imports bind, as a kind-typed slot in it already relies on. - A field’s default or pin at a reference naming values is a value’s qualified name, judged at link by the same
walk, and a refusal is the declaring entry’s resolver error (§5.2: the value must be the field’s declared type).
§5.12 states the walk for a value body’s names alone; nothing states that a
~/=token at aref<T>field is a name and not a token of an atom’s grammar. - A qualified name selects a key of a value of a named product: a namespace value’s key, or a record value’s field, the record being a namespace whose keys its type fixes. That is §5.12’s walk restated, and nothing it admits moves.
- A constructor is an ordinary record type. Nothing in the predicate sets a constructor apart.
uuid_type’s value is ofrecord, someta.type_ref,meta.ref<meta.record>andmeta.ref<product>hold it, andrecord,top,aliasandtemplatewith it. A field typeduuid_typeholds a declaration’s body as data,!holder { spec: { version: 4 } }, read in a TSON tree, bound to the constructor’s own model class, and read from JSON;boxed<uuid_type>and[uuid_type]close as any application does. A declaration read as data is judged by its record shape alone: it is not resolved, so a derived field such aschainis admitted as written and no coherence rule runs. - Unchanged: a template parameter’s
boundstill refuses a constructor. A bound is IS-A, not membership, and construction transfers kind and not IS-A, so only another constructor (set_typeIS-Aarray) could satisfy one.
Resolution, a proposal.
- §4.2: one atom,
reference => atom & { of: meta.type_ref }, admitting a name exactly when the type of the named entry’s value, past its alias hops, IS-Aof;meta.ref<T>its one spelling;ofany type or constructor of the writer’s namespace, never a value or an open entry. A slot over a constructor holds a type’s name and takes part in structural identity; any other holds a value’s name.kind_type,ref_type, thekindtemplate (the kernel’s and core’s) and both refusals go.type_kindandidentifier_kindare renamedtype_refandidentifier_ref, in §4.2, §5.10 and §8.1 alike. - §4.1: a constructor is a type like any other at a name slot and a type position: its declaration is a record, whose instances are declarations. The sentence making a constructor “never itself the type of a value” goes.
- §5.12: a further segment selects a key of a value of a named product, a namespace value’s key or a record value’s
field, the record being a namespace whose keys its type fixes; a map that is no namespace is not entered. “The rule
does not reach a data document” goes: a name a data document writes at a
ref<T>slot is judged by the same walk over the namespace its imports bind, and delivered with its referent, and a field’s default or pin at one is a name judged so when the schema links. - §7.4 and §8.1: “a kind-typed position” reads “a position typed by a reference over a constructor”;
membersis no longer a derived index written in resolved output.
12. [TSON-SCHEMA] §4.2, §5.10, §8.1, §8.2 — an application is a reference value
Problem. A type is named in two value forms. A bare name is a value of the reference atom (#11); an application
still open in resolved output, an alias’s target, a record template’s parents, the application annotation, is the
§8.1 record type_ref { name arguments?: [type_argument] }, with
type_argument { ( name: type_ref | value: meta.value ) }. The record exists only so a value argument has somewhere
to sit, and that somewhere is the kernel’s last value-typed slot outside facets and defaults. It also says nothing
the token does not: §12.1 already reads an argument by its token and leaves the head’s parameters to judge which it
binds.
What is running.
- One value. The
referenceatom’s values are the reference #13 makes data grammar: a qualified name with an optional argument list,geo.address<T>, each argument a reference or a token.type_refandtype_argumentare gone;alias.target,meta.type_chain.parentsand theapplicationannotation are typedmeta.type_ref. - Written unquoted, canonically. Resolved output and held text write an open application as source does, one
space between arguments and no comma:
!alias { target: pair<uuid B> },chain: { parents: [result<T>] }. - The value is an object, in every mode. A reference read at a reference-typed position, in a tree read and a bound one alike, is one object: its qualified name’s segments, its arguments (each a nested reference or a value token), its canonical text, and its referent, the value of the entry it names in the closure it was read under, each nested argument resolving to its own. A schemaless read keeps an application as a structured value with no referent (#13).
- Identity is the parsed form. Whitespace inside an argument list is free:
box< text, >at an element type andbox<text>at a type position close to one entry. An argument’s channel is no part of the reference:names<red>is one application whetherredwas read as a reference or filed as a value at its parameter. - Arguments are typed where they land. A type argument is judged against the parameter’s
bound; a value argument is a token of the parameter’s type, substituted into the body, which is read as the constructor’s instance (maxint<10>closes to!integer_type { max: 10 }). The linker judges as type names only the arguments at the head’s type parameters. - Nested applications close innermost first (§5.3), and a closed one is named bare where it stands. The
instantiation entry’s
@meta.applicationstates the application as written, its nested arguments unreduced, each bare name argument past its alias chain and a value argument a token:@meta.application:pair<box<text> int32>, where the innerbox<text>closed toinnerandinnerstates@meta.application:box<text>. An application nothing declares is its expansion and still states no@meta.application. - In a value body an application at a kind-typed slot is refused and the alias spelling named: nothing lifts or mints there.
- In a data document a kind-typed slot may name an applied type,
t: set<text>, resolved bottom-up, level by level, by structural identity against every application the closure closed, an expansion’s among them (the resolver keeps them beside the entries; resolved output does not carry them). Each argument is compared past its alias chain, as §8.2 compares a schema’s own: overuser_id => uuid,box<user_id>names whatbox<uuid>closed to, and an argument refininguuidnames nothing the schema closed overuuid. A schema loads only when every application in it closes, so data never creates one: an application nothing in the closure closed names no type the schema closes and is refused, as a name no member is. - A message names a minted entry by its application, as written:
'b<10>': '5' is less than the minimum 10, and a kind’s refusal listsmeta.ref<uuid_type>where it would have listed the entry minted for it. - This retires
value’s argument slot. #17 retires its facets and #21 its last two slots, andvalue_typewith them.
Resolution, a proposal.
- §4.2: the
referenceatom’s values are #13’s reference, compared as their parsed form; a value carrying arguments is an application, stands where one is still open, and in data names the entry the schema closed it to. - §8.1:
type_refandtype_argumentgo from the kernel table;alias.target,meta.type_chain.parentsandapplicationare typedmeta.type_ref;@meta.applicationstates the application as written, nested arguments unreduced. - §8.2: identity compares a reference’s parsed form, its head, and its arguments as references or tokens, never the text or the channel a processor filed an argument in; a nested argument is compared as the entry it closed to, and a bare-name argument past its alias chain, in data as in a schema.
- §5.10: an argument is a reference or a token by the grammar, and typed by the head’s parameter; a parameter standing in an argument is its name, in held text as in a record template.
Open. Whether an expansion, an application nothing declares, should state its @meta.application too, so that every
entry an application closed to names the reference that closed it in resolved output; today the resolver keeps that
in memory only. An expansion is keyed on its content, so two applications closing to one record (box<int32> and
crate<int32>) would give one entry two applications.
13. [TSON-DATA] §4, §7.2.5, §7.4, §7.5, §7.7; [TSON-SCHEMA] §8.1, §12.1, §12.3 — a reference in data is grammar
Problem. A reference that carries arguments (#12) has no spelling in data: < and > are reserved there
(§7.2.5), so target: result<T> is a parse error, and the one spelling left, a quoted token ("result<T>"), makes a
quoted token text or a reference by its shape, so a held body’s text literal that parses as an application would be
read as one. The production a data value
needs already exists: [TSON-SCHEMA] §12.1’s type-ref has type-name "<" type-args ">". Under one level a !T { … }
body is data read against T — a value entry, a constructor body, a resolved form, the schema document itself — so
whatever can stand in one is data grammar. The line between the two grammars is values against sugar: [TSON-DATA] is
every value, a reference with its arguments included, and [TSON-SCHEMA]‘s grammar is the sugar at type positions that
desugars into values ({ a: text }, [T], {K => V}, (A | B), ^, &, -, ~, =, ?).
What is running. The grammar below, in both encodings.
- The stream assembles a reference into one event, delivered as one value; a quoted token is never a reference, so a
held body’s
pattern: "box<T>"is text and a quoted application at a reference-typed position is a type mismatch.<with whitespace before it, after a quoted token, or after a token that is no qualified name is a parse error, as is an unclosed list. - A schemaless read keeps an application as a structured value: the qualified name’s segments and the arguments, each read as a schemaless read reads a token, a nested application another such value, with its text recreated and no referent, there being no closure to resolve in. A Class 1 processor no longer refuses one.
- A qualified head parses at every position, data and schema, and resolves from the root as any name does (#14).
- The schema grammar keeps its own production, whose
type-argadmits sugar, and produces the same tree; it applies the adjacency rule and admits a qualified head. - JSON carries a reference as a string holding the same text, read by the same production.
Resolution, a proposal. Grammar only.
[TSON-DATA] §7.2.5: < and > join ! and @ as the special characters with a grammar role in data values; the
other eleven stay reserved by [TSON-SCHEMA]. The lexer is unchanged: each is a single-character special token, so
map<text list<int32>> lexes as map < text list < int32 > >.
[TSON-DATA] §7.4: the reference production, [TSON-SCHEMA] §12.1’s type-name "<" type-args ">" with a qualified
head, moves into the data grammar, with arguments limited to what data can spell.
core-value = record / map / array
/ empty-brace / void / reference / token
reference = qualified-name "<" ws type-args ws ">"
; "<" adjacent to the qualified-name (§7.5)
type-args = type-arg *(separator type-arg) [ ws "," ]
; separator = ws "," ws / ws1, as [TSON-SCHEMA] §12.1
type-arg = reference / token
; a token that is a qualified-name is a reference with no
; arguments; any other token (a number, a quoted token)
; is a value argument. A quoted token is never a reference.
qualified-name = unquoted-token
; segments joined by "." (§7.7), each an identifier:
; `order`, `geo.address`. One token, since "." is a
; token character.
- One value.
geo.address<T>is data syntax delivered as onereferenceatom value, the arguments part of the reference. The parser builds the head and its arguments, nested to any depth, and a position typed by thereferenceatom rolls the tree up into one value; a qualified name without arguments is one token, rolled up the same way. An atom says what a value is, not how many tokens spell it. At any other typed position a reference with arguments is a type mismatch. - Where it stands. Anywhere a core value does, a map key included. A
!tag is"!" qualified-name(#14), with no arguments. - No sugar in data.
[T],{K => V}and(A | B)are [TSON-SCHEMA]‘s, and[and{are structure in data, so a data argument is a reference or a token. An applied sugar form is named by the entry that declares it.
[TSON-DATA] §7.5: one row, < (reference arguments) MUST be adjacent to the preceding unquoted token; whitespace
inside the argument list is free. >> is two tokens.
[TSON-DATA] §4: a schemaless read keeps a reference with arguments as a structured value, its head and arguments, resolving nothing; a Class 1 processor no longer refuses one.
[TSON-SCHEMA] §12.1: type-ref’s type-name "<" type-args ">" is the data production with the schema’s wider
type-arg = type-ref / value-literal, so a schema argument may be sugar; type-name becomes qualified-name there,
in supertype-ref and in map-key. §12.3 gains the adjacency row for <.
[TSON-SCHEMA] §8.1: an open application in resolved output is written as it is in source, unquoted:
!alias { target: result<T> }, @meta.application:response<order 201>, parents: [result<T>]. Identity is the parsed
tree (§8.2), so whitespace inside the list is free; canonical output has one space between arguments and no comma.
[TSON-JSON]: a JSON string holding the same text, read by the same production; JSON has no unquoted form.
Open. Whether a value argument may be any token, or only those §12.1’s value-literal admits.
14. [TSON-DATA] §4, §7.4; [TSON-SCHEMA] §2.2.3, §4.1, §5.12, §12.1 — every !schema value declares, from the root
Problem. One namespace per schema (#7) reserves every name of an import closure, and the answer one level gives,
a library grouping its declarations under one entry of its own (meta => !schema { … }, geo => !schema { … }),
has no reading: §4.1 makes a value of a namespace type a value entry, “nothing in it lifting or minting”, so geo => !schema { address => { … } } is read as data and refused (declared takes a value naming its own type). Nor could a
member be named: type-name = meta.identifier (§12.1) and [TSON-DATA] §7.4’s type-ref = "!" identifier admit one segment,
and the kernel types meta.reference.of and meta.type_chain.supertypes/subtypes by meta.type_name, one identifier.
What is running.
- Every value of
schemadeclares.geo => !schema { … }is a schema map nested as an entry: its tag,!schemaor its target’s path!meta.schema, names what the root’s does and is read as a spelling, its payload is the schema grammar, and its members are declarations that lift, mint, link and compile exactly as root entries do — types, constructors, values, aliases, templates. It declares at least one member and takes no parameters. A value of any other namespace type (!api { … }) stays a value map. The document root is the outermost!schema. - A member’s identity is its path from the root.
geo.addressis an entry of the document: diagnostics, a kind’s member list, a chain’ssupertypesandsubtypes, and@meta.applicationname it so, and two maps may each declareaddresswithout colliding. The map’s own entry,geo, is a value ofschemakeyed by its members’ own names; resolved output writes it nested, its members in resolved form, and a resolved form read back keys each member under its path. - Open maps. A schema map of one path declared by several documents of a closure is one map, the union of their
members, as the root namespace is across imports: two imports each declaring
geocontribute their members to onegeo, and a member path both declare is §2.2.3’s collision, by identity. A member’s identity is its declaring document and its path. The annotations written on a map’s entry are its declaring document’s, and the map an importer sees carries none of an import’s. A document contributes by declaring the map itself (geo => !schema { … }); no grammar declares a dotted name. - No lexical scope: every name resolves from the root. A name’s first segment resolves in the document’s root
namespace, its root entries and its import closure; each further segment selects a member of the
!schemamap reached so far. Insidegeo, a sibling is writtengeo.address, and a bareaddressnames the root’saddressor fails to resolve. There is no relative lookup, so there is no shadowing rule. - A qualified name stands at every type position: a field’s type, an element, key or value type, a choice
variant, a supertype and an
&operand, a refinement or subtraction source, a!head, a template bound, a reference’sof(meta.ref<geo.address>), an alias target, and an application’s head (geo.box<text>). A slot over a constructor reads a qualified name in data as it does in a schema (#11):t: geo.locationat ameta.ref<meta.record>slot is a member. - The value walk passes through nested maps. A value’s qualified name enters a
!schemamap by member name, then a namespace value by key and a record value by field:geo.homenames the value entryhomeofgeo, andgeo.home.atits field. - Synthetics mint at the root, one entry per form across the document (§8.2), sugar written inside a nested map
included:
[uuid]ingeo.taggedand in a root record is onearray_uuid_…. A minted name is an identifier; a dotted name contributes its text with each dot written_(array_geo_inner_tag_…), a path under the kernel’smetaits member’s name (array_record_…for[meta.record], #15); a path under another of the kernel’s maps keeps its first segment (numeric_integer_type_…for an application ofnumeric.integer_type). A synthetic’s identity is its form, not its declaring document: 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. A name only one of them mints is §2.2.3’s collision. 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 an internal error, the naming function broken. - Qualified tags (Part 1). A tag is
"!" qualified-namewith no arguments:!geo.address { … }in data resolves by the walk, and one naming no member isUNKNOWN_TYPE. A schemaless read keeps a dotted tag as written, as it keeps any tag. [TSON-JSON]‘s$typetakes the same text (edited in place). - Name hygiene is per map. Each schema map is a §8.2 scope of its own, and the per-name rules judge each member’s own name.
- The kernel.
meta.reference.of,meta.type_chain.supertypesandmeta.type_chain.subtypesare typedmeta.type_ref;meta.type_namestays the schema map’s key type, one identifier.ref’s parameter is then a type parameter, the[meta.type_name]synthetic is gone, and the kernel’s measured wait cycle gains[meta.type_ref]and losesmeta.identifier(#4). - Hosts. The default name binder binds a dotted type name to the class of its last segment in one sub-package per
earlier segment:
geo.routetogeo.Route. A processor’s choice, not the spec’s.
Resolution, a proposal.
- §4.1: “nothing in it lifting or minting” holds for a value of a namespace type other than
schema. Added: “The rules for a schema document’s root map apply to every value ofschema: its members are declarations, which lift and mint, may be types, constructors or values, and are named by their path from the root.” - §2.2.3, §10: an entry’s identity is its declaring document’s
!!idplus its path, and a synthetic’s is its form (§8.2), so one form minted by two documents is one entry of a closure holding both. A schema map of one path declared by several documents of a closure is one map, the union of their members; a member path two of them declare collides as any entry’s name does, and a schema map’s name collides with an entry of that name that is no schema map. - §3.3.2, §5.12: every name resolves from the root. The first segment names an entry of the root namespace; a further
segment selects a member of a
!schemamap, a key of a namespace value or a field of a record value. - §4.2, §8.1:
meta.reference.of,meta.type_chain.supertypesandmeta.type_chain.subtypesare typedmeta.type_ref; a type’s name is its path from the root. - §12.1:
schema-map-entry’s value admits"!schema" ws schema-map; a declared key staystype-name = meta.identifier, and every reference position readsqualified-name(#13’s grammar). - [TSON-DATA] §7.4:
type-ref = "!" qualified-name. §4: a schemaless read keeps a dotted tag as written.
Open.
- Lexical scope. A bare name inside a map resolving in that map first, then outward, needs a way to say “from the root” once relative lookup exists, and with it a shadowing rule, silent or diagnosed.
- Synthetics homed by the innermost map, identity by path, two maps minting one form being two entries. It waits
on lexical scope: a name minted inside
geomust resolve where it is written. - Who may contribute to a map. Any document may: the open rule protects a member already declared by collision,
and nothing reserves a map’s unclaimed paths for the document that declared the map first. A library that wants
its members its own (
geo.for geo.tn alone) has no way to say so; a closure where two libraries contribute to one map is the flat namespace’s collision risk inside the map, found only when both are imported.
15. [TSON-DATA] §3.1, §7.4; [TSON-SCHEMA] §2.2.3, §3.4, §4.1, §5.3, §6, §8–§9, §13.2 — the roots hold the common
vocabulary
Problem. One namespace per schema (#7) reserves every name of an import closure, and every bundled library
imports the kernel, so a schema importing one reserved every kernel and meta-schema name: record, map,
template, namespace, alias, type_kind, field_name, identifier, bounded, and every *_type
constructor. Those are plausible names for an author’s own entries, and almost none is a name a data schema writes:
it writes text, integer, uuid and enum on most lines, and the rest rarely. And one library holding every
data format made a schema that wants dates reserve every network and numeric name too.
What is running.
-
The placement rule. 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. The list is curated, entry by entry, not derived from what an entry is:
Document Imports At the root Under its maps meta.tn nothing (pre-loaded) void,boolean,integer,int64,text,bytes,iri,iri_reference,regex,doc,enum,set,schemathe other 68. meta, the type system (48): the kinds and constructors,meta.schemaandmeta.field_map, the map, tuple and choice constructors,meta.template,meta.identifierand its aliases (meta.type_name,meta.field_name,meta.param_name), the supporting records, sets and enums (meta.type_chain,meta.record_field,meta.field_role, …), the references (meta.reference_type,meta.ref,meta.field_type_ref,meta.type_ref,meta.identifier_ref),meta.declared, the annotation machinery (meta.annotation,meta.synthetic,meta.application), the checked assertionmeta.disjointand the one-element tuple templates.atom(5): the ISO/IEC 11404 datatype properties atom.ordering,atom.bounded,atom.exact,atom.numericand the mixinatom.specification.numeric(4):numeric.integer_type,numeric.integer_size,numeric.integer_member_set,numeric.count(#16).unicode(6):unicode.text_type,unicode.text_member_set,unicode.normalization,unicode.identifier_type,unicode.identifier_base,unicode.regex_type.net(3):net.iri_type,net.scheme_set,net.scheme_name.binary(2):binary.bytes_type,binary.bytes_encodingnumeric.tn meta.tn number,rational,complex,int8…int256butint64,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_settemporal.tn meta.tn date,time,datetime,duration,periodtemporal: their five*_typeconstructorsnet.tn meta.tn email,ipv4,ipv6,cidr4,cidr6,mac,hostname,host,media_type,uri_reference,uri,uuidnet, beside the kernel’s members: their eleven*_typeconstructorscore.tn numeric.tn, temporal.tn, net.tn the author annotations deprecated,title,comment,examples,read_only,write_onlynothing: it declares no map of its own policy.tn meta.tn nothing policy: the processor policy’s records (policy.processorand its parts),policy.restriction_levelandpolicy.limit(#16)So an author writes
text,!enum [A B],set<text>,@doc,bytes,int32,dateanduuidbare, andmeta.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. -
A library imports the kernel alone; core.tn is the bundle. numeric.tn, temporal.tn and net.tn each import meta.tn and nothing else, so a schema imports the libraries it uses: one importing temporal.tn alone has
dateand the kernel’s vocabulary, does not seeint32, and may declareint32as an entry of its own, though notnumeric, which the kernel declares; one importing numeric.tn and net.tn has both, their closures meeting at the kernel by identity. Every schema reserves the kernel’s map namesmeta,atom,numeric,unicode,netandbinary. core.tn imports the three, so a schema wanting every bundled type imports core.tn and reserves those,temporaland every root above;record,identifier,type_ref,field_name,bounded,value,kindandcorestay free. No two of the bundled documents declare one root name or one member path, and the maps two of them declare,numericandnet, are open and the union of their members, so no closure of them collides with itself. The corpus pins each case (a-schema-importing-temporal-alone-*,a-schema-importing-numeric-and-net-has-both-libraries,a-schema-importing-a-library-declares-its-map-name). -
enumis declared at the kernel’s root,enum => meta.enum_type ^ { name_type?: = meta.identifier }. It is on the kernel’s wait cycle (its own enumerations,booleanandmeta.field_role, apply it), the kernel writes them!enum [...], and resolved output tags an enumeration!enum. There is nometa.enum. -
The author annotations are core.tn’s:
deprecated,title,comment,examples,read_onlyandwrite_only, each@meta.annotation, written bare by a schema importing core.tn (@title:"…",@deprecated). A schema importing the libraries without core.tn may declare annotations of those names itself. Nothing checks one against another:@read_onlyand@write_onlyon one field load. The reading form (tson strip) drops@docalone. -
An alias of a constructor is applied as its target (§3.3.1), a template’s held body included: the resolver’s read of a declaration follows its head’s alias chain and reads the value against the constructor at the end, so
<T> !my_enum [a T]overmy_enum => enumbuilds an enumeration. The bundled documents do not rely on it; it stands as the behaviour §3.3.1 states. -
The kernel is one document, meta.tn, header
!!idalone and pre-loaded (§3.4): its one wait cycle (#4), every name its declarations reference, and the concepts that belong with them. A kind and a constructor are the kernel’s by declaring document (§4.1), which is meta.tn alone, so the libraries’ constructors are ordinary constructors of the documents that declare them. -
The kernel, measured. Over
meta-resolved.tn’s own declarations (no compiled reader consulted), the cycle is 25 entries: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(count’s resolvedsizeis read against it, #16),meta.field_type_ref,meta.type_ref,meta.type_chain,meta.record_field,meta.field_group,meta.field_role,meta.enum_setand four lifted arrays. Closing it under every name its declarations reference — every rule-1 wait, every field type andof, every annotation type its entries carry, and what reading the document itself waits on, its root tag’sschema, an alias ofmeta.schema, with that map’s key and value types and the annotations on its keys — gives 66 entries, addingschema,meta.namespace, the base kinds,meta.alias,meta.declared_type, the identifier, iri and regex families withatom.specification,meta.template_param,meta.declared,meta.type_name,doc,meta.annotation,meta.synthetic,[meta.template_param], the classifying annotationsatom.ordering,atom.bounded,atom.exactandatom.numeric, which the root atoms carry on their keys,iri_reference, the sourceirirefines, andint64, the sourcenumeric.countrefines. The iri and regex families are there becausetext_type.patternis typedregexandidentifier_type,iri_typeandregex_typecomposeatom.specification, whosespecis aniri. The kernel is that closure and the concepts that belong with it, which nothing in the closure names:meta.tuple,meta.choice,meta.tuple_element(with its lifted array),meta.template,meta.application,meta.ref,meta.field_name,meta.disjoint,meta.tuple1,meta.voidable_tuple1,set, the octet sequence (binary.bytes_type,binary.bytes_encoding,bytes) — 88 entries, 68 under its six maps, 13 at the root and seven synthetics, with the six maps themselves the 89th to 94th. A test holds the kernel to containing the closure, pins the cycle at 25 and the closure at 66, and pins the related set, so an entry that drifts in either direction fails it. The longest wait chain is 4 (doc→meta.annotation→void→meta.void_type→[meta.type_ref]). A schema importing meta.tn alone has every elided construction andmeta.ref<T>. -
The bootstrap builds by hand 13 of the cycle’s 25 entries — its instances and its refinements (
boolean,meta.field_role,meta.enum_set,integer,numeric.count,text,meta.field_type_ref,meta.type_ref,meta.field_map) and the four lifted arrays — resolves the other twelve, records and compositions that read nothing, from their grammar, and reads the 63 entries beyond the cycle in wait order (#4). -
Each library declares
<name> => !schema { … }and its instances at its root (uuid => !net.uuid_type {},int32 => !integer ^ { size: { bits: 32 signed: true } }). The schema model binds the kernel’s entries and each library’s members under its own map, by declaring document, each by its last segment (meta.recordtoRecordBody,unicode.text_typetoTextType,numeric.decimal_typetoDecimalType,net.uuid_typetoUuidType,atom.specificationby alias toAtomSpecification); no two of them share a last segment, and the binder refuses a vocabulary where two do. A member a consumer contributes to an open map binds through the consumer’s binder, and a consumer’s dotted name keeps #14’s sub-package rule. A processor’s choice, not the spec’s. -
Open namespaces (#14).
metais open: any schema may contribute a member,meta => !schema { code_type => unicode.text_type ^ { max_length?: = 4 } }besidecode => !meta.code_type {}loads and reads. A member path already declared is the ordinary collision (meta => !schema { record => … }collides with the kernel’s), and a member’s identity is its declaring document and its path, so a user’smeta.code_typeis no kernel entry: an instance of a refinement of a backed constructor reads through it, and one composing a base kind directly is a new form this library does not back (#4).atom,numeric,unicode,net,binaryandtemporalare open on the same terms, and the bundled documents use it: numeric.tn adds its members to the kernel’snumericand net.tn to the kernel’snet, and an importer of either sees one map holding both documents’ members. -
Every name resolves from the root (#14), 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,@doc:"…". There is no lexical scope; synthetics a document’s sugar lifts are its root’s. -
An annotation’s name is a qualified name ([TSON-DATA] §3.1, §7.4:
"@" qualified-name), resolved from the root as every name is:@docand@titlebare,@atom.ordering:TOTAL,@atom.numericand@meta.disjointby path. A schemaless read keeps a dotted name as written, and an empty segment (@x..y) is a parse error. -
The elided constructions target the kernel’s entries, whatever a document imports:
{ … }ismeta.record,[T]meta.array,{K => V}meta.map,[A, B]meta.tuple,(A | B)meta.choiceand a partial applicationmeta.alias. The root tag stays!schema, the pre-loaded kernel’s rootschema,schema => meta.schema, an alias (#8), so a position typedmeta.schemaadmits it as it admits any alias of its type; a nested map is tagged!schemain source, or!meta.schema, as resolved output writes it. -
schemais a kernel root name like the others. A schema whose closure holds meta.tn writes it bare at a type position (types: schema, kept as written in resolved output) and may not declare it, the collision any root name is (a-schema-importing-the-kernel-declares-schema). A schema importing nothing may declareschema, as it may declareint64; a tag still names the pre-loaded kernel’s, the root’s and every nested map’s, sogeo => !schema { … }beside a localschema => { … }is a nested map and the local name is the schema’s own everywhere else (an-import-free-schema-declares-schema-and-nests-a-map). The tag is matched as a spelling because 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. -
Resolved output tags a body by its constructor’s name (
!meta.record,!enum,!numeric.decimal_type,!net.uuid_type) and names each entry as its document does (!meta.record_field { type: text },@meta.synthetic,@meta.application). The kernel’s form is!schema { numeric => !meta.schema { … } … meta => !meta.schema { … } … }, its 69 members in its six maps and its thirteen root entries and seven synthetics at the root; each library’s is!schema { numeric => !meta.schema { … } number => !numeric.decimal_type {} … }, its map holding its own members and none of the kernel’s; core’s holds its six annotations. The five fixtures this implementation holds itself to aremeta-resolved.tn,numeric-resolved.tn,temporal-resolved.tn,net-resolved.tnandcore-resolved.tn(§13.3); policy.tn has none. -
Hash pins run bottom-up in the import order: each library and policy.tn pin meta.tn, core.tn pins the three libraries, so an edit to the kernel moves every pin above it.
-
Measured:
Tson.standard()loads six documents; in a fresh JVM, the first call about 440 ms and a warmed call about 65 ms. -
What is not running: a library’s resolved form holds instances of constructors its own source declares (
!numeric.decimal_type {},!net.ipv4_type {}), or the kernel’s form!meta.template { … }, so each reads back only beside its source, the gap #4 states for any such form. An annotation inside a value read through an imported type’s reader resolves in the namespace of the schema declaring that type, not the annotating document’s (§3.3.3): a value entry’s payload annotated@atom.numericis refused where the value’s type is declared by a schema whose closure lacks it. This library’s gap.
Resolution, a proposal.
- §3.4, §9, §13.2: the kernel, meta.tn,
!!idalone and pre-loaded, holds its wait cycle, what it names and the concepts that go with them; numeric.tn, temporal.tn and net.tn each import it alone and declare a data-format family; core.tn imports the three and declares the author annotations; policy.tn imports the kernel alone. §2.2.3’s reservation of a closure’s names covers each root and the map namesmeta,atom,numeric,unicode,net,binaryandtemporal, andpolicyfor a closure holding policy.tn. - §4.1, §9: the bundled documents’ convention, stated once with the table above: the root holds the vocabulary an
author writes, the machinery sits under maps named for concepts: the kernel’s
meta(the type system),atom(datatype properties),numeric,unicode,netandbinary(the atom constructors by family), and a library’s map of its own name, which may be one of the kernel’s. A kind and a constructor are the kernel’s by declaring document. - §2.2.3 (#14): a schema map of one path declared by several documents of a closure is one map, the union of their members; a member path two declare collides.
- §4.1, §5.3, §5.4, §5.10 and every bundled name in the spec’s text: written as the table places it; the desugar
targets are
meta.record,meta.array,meta.map,meta.tuple,meta.choice,meta.alias. - §6: the documentary annotation is
@doc, the kernel’s; the author annotations are core.tn’s. - §3.3.1: an alias of a constructor is applied as its target is, in a held body as anywhere.
- §3.4, §4.1, §12.1 ([TSON-DATA] §2.2): the kernel’s root declares
schema => meta.schema, and the!schematag of a schema document’s root and of a nested map names it, resolved against the pre-loaded kernel whatever the document imports or declares. - §8.1 to §8.3: resolved output tags a body by its constructor’s name; an alias of a template applies it.
- [TSON-DATA] §3.1, §7.4:
annotation = "@" qualified-name [ ":" data-value ].
Open.
- The list is a judgement per entry. It puts
iri,regexandbytesat the kernel’s root andmeta.identifierinmeta,uuidanduriin net.tn,docat the kernel’s root, the classifying annotations underatomand the kernel’s atom constructors undernumeric,unicode,netandbinary. A revision that wants a rule rather than a list needs a test an entry can be put to; “what an author writes” is the intent, not yet a test. - The tag beside a local
schema. §2.2 and §3 say the root tag classifies the document and §3.3.1 that no local declaration captures a!target, but no section says whichschemaa nested map’s tag names in a schema that imports nothing and declares its own. The running answer is the pre-loaded kernel’s, as for the root, so the one spelling means two things in that document, the tag and every other position. The alternative, refusing a localschemain every schema, would be a reservation of a name no import brings, which no other root name has. - Where the author annotations live. core.tn holds them for now; whether they belong in the kernel, in a library of their own, or in core.tn is open with the user.
- Who may contribute to
meta(#14’s open item): any schema may, the collision rule guarding the kernel’s own members only by their being declared first. The same holds for every bundled map; numeric.tn and net.tn contributing to the kernel’snumericandnetis that rule used by the bundled documents themselves.
16. [TSON-DATA] §9.1; [TSON-SCHEMA] §5.3, §5.10, §5.11, §9, §11.5, §13.2 — a count is a fixed 64-bit range
Problem. Every facet that counts something — min_items/max_items, the text and bytes lengths,
total_digits/fraction_digits, a temporal precision, a network’s min_prefix/max_prefix, a UUID’s version —
is typed non_negative_integer, !integer ^ { min: 0 }, which is unbounded. So a conforming processor must accept
max_length: 10^40 and must hold it, which forces an arbitrary-precision integer into every implementation’s schema
model for a value no string, array or digit count reaches. And §5.11’s integer_type types multiple_of the same
way, though a step is not a count but a value of the constrained integer’s domain: a uint128 stepping by 2^64 is
refused. The resource limits of §9.1 and §11.5 carry the same type and the same problem, and the spec states no
range for any of them.
What is running.
- The count type has a fixed range.
int64is declared at the kernel’s root, besideinteger, and numeric.tn declares the rest of the sized ladder. The kernel’snumeric.count => !int64 ^ { min: 0 }isint64’s non-negative half, 0 to 2^63-1, a refinement rather than an alias so that it keeps its minimum and is anint64by its chain, asiriis aniri_reference: every implementation holds a count in a native type (JavaScript in a BigInt). It stands where Revision 38 writesnon_negative_integer, which the bundled documents do not declare, and types every count facet of the six.max_length: 9223372036854775807resolves;max_length: 9223372036854775808is outside the facet’s type, a resolver error. - A facet holding a value of the constrained type is typed by that type’s family.
integer_type’s bounds andmemberswereintegeralready;multiple_ofis nowintegertoo, its strictly-positive rule the load-time coherence check it was, so auint128steps by 2^64. integer_size.bitsisinteger, not a count.int64’ssizeis read againstinteger_size, andcountrefinesint64, sobits: numeric.countmakescountwait on itself and the kernel never becomes ready (§3.4.1). Thatbitsis above zero is a schema-load coherence check, asmultiple_of’s sign is:bits: 0is a resolver error.- A count beyond what a host holds is not an error. A
max_*past the most a host’s strings or collections hold is never reached and admits every value; amin_*past it is met by no value, so every value fails validation. Neither is a schema error or a refusal to bind. - policy.tn’s limits are
policy.limit,!int64 ^ { min: 1 }, and its entries are members of apolicyschema map: the record a deployment writes ispolicy.processor, besidepolicy.identifier_policy,policy.script_policy,policy.restriction_levelandpolicy.limits. A processor holding a limit in a narrower type than the range caps a larger value at the widest it holds (this library holds a count or depth in a 32-bitint). - The corpus pins the edges (
a-count-facet-holds-the-largest-signed-64-bit-value,a-count-facet-past-the-signed-64-bit-range,a-multiple-of-above-the-count-range,an-integer-width-of-zero,a-minimum-length-no-value-reaches,a-maximum-item-count-every-value-meets,a-schema-importing-the-kernel-declares-int64,a-schema-importing-policy-declares-the-policy-map-name,a-deployments-policy-document-is-a-policy-processor,a-policy-limit-of-zero).
Resolution, a proposal.
- §9, §13.2: the kernel declares
int64andcount => !int64 ^ { min: 0 }, and every count facet of the bundled vocabulary is typed bycount; §5.10’s derived parameter type for a count slot (min_items: N) iscount. - §5.11:
integer_type.multiple_ofisinteger, strictly positive by coherence;integer_size.bitsisinteger, above zero by coherence. - §5.3 and the facet descriptions: a count facet admits any value of
count; a bound past what a processor’s values can reach is met or never reached, never a schema error. - [TSON-DATA] §9.1, §11.5: a limit is a positive
int64, and a processor holding one in a narrower type caps it.
17. [TSON-SCHEMA] §4.2, §5.2, §5.5, §7.4, §13.2 — a facet is typed by its family’s instance
Problem. §7.4’s “Constraint fields typed as value” types every bound, step, member set and network list of the
non-kernel atom constructors value, because “the constrained atom cannot be referenced at the point of declaration”
(bootstrap ordering, §3.4), and then needs a rule of its own to make those slots mean anything: “the resolver reads
it under the atom it stands for, once that atom is in scope”, a set’s members read before the set is formed, and an
eager-conversion paragraph whose accepted types are “an implementation choice”. Once a constructor and its canonical
instance sit in one document (#7’s one namespace, #15’s roots), that reason is gone: number => !numeric.decimal_type {} is declared beside numeric.decimal_type, so decimal_type.min can be typed number, which is the kernel’s own
pattern — integer_type’s bounds are integer, “the exception that proves the rule”. With the facet typed, the
ordinary field reader does what the bespoke rule described, and the rule, its eager-conversion paragraph and the
implementation choice inside it have nothing left to govern.
What is running.
- Every facet of a bundled constructor holding a value of the constrained family is typed by that family’s
instance, declared at the root of the same document: numeric.tn’s
decimal_typebounds andmultiple_ofarenumberandmembersa set ofnumber;rational_type’s bounds andmultiple_ofarerational; temporal.tn’sdate_type,time_typeanddatetime_typebounds aredate,timeanddatetime,duration_type’s bounds andmultiple_ofduration,period_type’speriod. Nothing in numeric.tn, temporal.tn or net.tn namesmeta.value. - A float’s bounds are
number, notfloat64. float_type’s own@docvalidates them “against the value as written, before rounding”, so they are exact; typing them by the grid would round a bound the type says is unrounded. - net.tn’s network lists are
[cidr4]and[cidr6]:ipv4_typeandcidr4_typelistcidr4s,ipv6_typeandcidr6_typelistcidr6s. A malformed entry, or one with nonzero host bits, is refused at schema load by that reader, as any value of it would be, so the coherence rule of §5.5 is left only the pair’s emptiness. - No wait cycle. Each instance is
!C {}with no facets, so a facet read against it waits on nothing further; the resolver’s wait order (#4) reports none over the five documents. - A refinement of a refinement reads its bound as the base instance.
positive => !number ^ { min: 0 }then!positive ^ { min: -5 }reads-5as anumber, which it is; its widening is §5.7’s tightening rule to refuse, not the read’s. The verdict is the one Revision 38’s text gives, by the rule that owns it. - A bound is spelled as a value of its family.
!number ^ { min: 0x10 }is refused, as!number 0x10in data is (§5.6 admits no based-integernumber), where avalue-typed slot decoded0x10by §4 and narrowed it. A refused bound carries its family reader’s verdict ('P1Y' is not a valid duration … a month is a period), as the kernel’sinteger-typed bounds already do. valuekeeps no slot here. What remained of it was a field’s default or pin (meta.record_field.value, and the same slot on #19’smeta.record_template_field), whose type is the field’s declared type: #21 types bothtype!, andvalueandvalue_typeare gone from the kernel.- The resolved fixtures (
spec/fixtures/numeric-resolved.tn,temporal-resolved.tn,net-resolved.tn) carry the typed facets; net.tn’s lifted[meta.value]becomes two synthetics,[cidr4]and[cidr6].
Resolution, a proposal.
- §13.2 and the constructor listings: type each facet as above.
- §7.4: delete “Constraint fields typed as
value” and the eager-conversion paragraph after it; what remains true is the ordinary rule that a field is read by its type at schema load, a value the type refuses being a resolver error. - §4.2’s
valueentry goes, with #21. §5.2’s eager-resolution paragraph drops its reference to thevalue-typed default, and “this matches §7.4’s eager-conversion rule for constraint-field values”. - §5.5: a network list is a list of the family’s network instance, and its coherence rule is the pair’s emptiness.
18. [TSON-SCHEMA] §5.7, §5.8, §7.2 — a restated field’s type narrows by IS-A, which no section states
Problem. §5.7 says a refinement “can only restrict, never expand”, and that “restating the type-ref remains
necessary when the tightening also narrows the field’s type”, so a restated type is meant to narrow. It never says by
which relation. Its one worked case argues by value sets — void “refines every type at a voidable position, the
value set {_} being a subset of every voidable position’s” — while the only relation §7.2 defines between two
types is the nominal one, T in S’s chain.supertypes. The two readings part on ordinary schemas: [int8]
restating [int32], a variant restating its choice (x: (int32 | text) restated x: int32), and a fresh
!integer_type { size: { bits: 8 } } restating int32 each narrow the value set and are no IS-A. §5.8 applies
§5.7’s rules to a composition’s tightening entries, so the two operators agree, and the gap is §5.7’s alone.
What is running. The nominal reading, with §5.7’s void case beside it. A restated field’s type is the inherited
type, or a type IS-A it by §7.2’s chain, alias hops followed on both sides, or void at a voidable position; anything
else is a resolver error against the restating declaration, in refinement and composition alike. It is judged at link,
where every name both types hold resolves, against the nearest closed records on the declaration’s chain. The kernel’s
meta.namespace => meta.map ^ { key_type: meta.identifier_ref … } passes by meta.identifier_ref’s refinement of
meta.type_ref; [int8] for [int32] and a variant for its choice are refused, no IS-A edge joining either pair
(#5: a container instance has no refinement, and two instances of one constructor have no edge). A restatement over an
applied operand that denotes no entry, dog => pet<"dog", dog_type> & { pet: cat_type }, reaches only the open
family base, and a subtraction’s tightening entry (§5.9’s rule 4) has an empty chain; neither is judged yet.
Nominal, because it is the relation every typed position already admits by (§7.2’s subsumption), 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 needs a containment judgement for every pair of families, containers and choices included, that the series states nowhere.
Resolution, a proposal. §5.7, beside the elided type-ref paragraph: “A restated field’s type MUST be the inherited
type or a type IS-A it (§7.2), or void at a voidable position; any other restatement is a resolver error. §5.8’s
tightening entries follow the same rule.” If a structural narrowing between containers or a variant restating its
choice is wanted, it is a rule of its own that each constructor states, as #5’s last paragraph leaves the container
constructors’ tightening rules theirs to state.
19. [TSON-SCHEMA] §1.3, §4.2, §5.7, §5.10, §8.1, §13.2 — a record template is its own constructor, its fields labelled
Problem. §5.10 and §8.1 make a record template “a !record with parameters”, and §1.3’s resolved-output
consumer recognises one “by its parameters”. So one constructor holds two things with opposite contracts: a closed
record, which is what readers and writers are built for, and a family base, which nothing is ever read or written
against — a position it types dispatches to a member. Every consumer that builds a reader or writer from a !record
has to ask the optional list first, and a consumer that forgets builds a reader for a template; nothing in the
vocabulary stops it. Inside the template, a parameter rides a field’s type or value unlabelled, told from a type
name or a literal by §5.10’s shadowing rule “in resolved output as in source”, so a resolved-output consumer that wants
to know whether value: S is a pin or a parameter needs the template’s parameter list and the shadowing rule, and a
literal that happens to spell a parameter has no resolved form at all.
What is running (on the Revision 39 proposal branch; spec/m/meta.tn and the Part 2 draft carry it).
- The kernel splits the constructor.
meta.recordlosesparametersand is closed: no field carries a parameter.meta.record_template => meta.product & { access_pattern? size_type? fields: meta.record_template_field_map groups? extension? discriminators? name_type? parameters: [meta.template_param; 1..] }is the family base: a type a position may name, ABSTRACT by construction, IS-A its parents and parent of its declared applications, and never read or written against.extensionis each application’s,discriminatorsthe family’s,groupsandname_typeeach application’s, unchanged by substitution. - A template’s field labels a whole parameter.
meta.record_template_field => { ( type: meta.field_type_ref | type_param: meta.param_name ) optional? voidable? role? ( value: type! | value_param: meta.param_name )? }:type_paramnames a parameter standing for the whole type,value_paramone standing for the whole value, andtypeandvalueare as on a record field,valuea dependent field (#21). A parameter as an argument of an application intype(items: box<T>) or in a parent stays where it is, recognised by its text resolving intoparameters— the one place resolved output still applies the shadowing rule.<T, S> { status?: int32 = S body: T }resolves to!meta.record_template { fields: { status => !meta.record_template_field { type: int32 optional: true role: FIXED value_param: S } body => !meta.record_template_field { type_param: T } } … }, and its applicationcreated => response<order, 201>to an ordinary!meta.recordwhosestatusisvalue: 201: substitution turnstype_paramintotypeandvalue_paramintovalue, and changes no other fact. - The source spelling is unchanged, and the shadowing rule is spent where it is written. The labels are the
processor’s: a token the template’s own body writes is a parameter by §5.10’s 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 }withfam => <T> base & { v: T }:fam’skindisvalue: T, notvalue_param: T). §5.10’s “a literal spelled like a live parameter is unreachable” therefore reaches only what the template writes.!meta.record_template { … }written in source is refused as resolver vocabulary, as!meta.templatealready is. - Recognising a parameter by its text needs two refusals, both running. A type’s arguments carry no label, so a
parameter there is recognised by its text, and two spellings would make that ambiguous.
- §5.10’s own rule, enforced: a parameter spelled like an entry the schema’s namespace holds, a type or a
value entry, is a resolver error at the declaration, a held template and a record template alike (
T => { … }beside<T> [T]). The draft’s sentence reads “within a template’s body, held or a record template’s” and “a parameter that shadows a schema entry, a type or a value entry”; §5.8’s<T> T & { … }sentence leaves a same-named entry to that rule. - New: 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 }andpair => <V> { z: two<V, T> },t3 => <T> pair<T> & { x: T }is refused att3, namingTandz: two<V, T>;pairitself is valid. Without it,t3’szwould betwo<T, T>, its firstTthe parameter and its second the member, which norecord_template_fieldcan tell apart; and a closedbase => { f: pick<T> }absorbed into<T> base & { … }would make every application’sfa different type frombase’s, against IS-A. A literalvalueis not covered: thevalue/value_paramlabels 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 no argument to collide with and nothing is refused.
- §5.10’s own rule, enforced: a parameter spelled like an entry the schema’s namespace holds, a type or a
value entry, is a resolver error at the declaration, a held template and a record template alike (
- Two declaration-time rules, one of them new. A selector’s type names no parameter — §5.10’s existing condition,
unchanged and already refused (
a-discriminator-typed-by-a-parameter). New: a literalvalueneeds a closed type, so a field typed by a parameter, whole or as an argument, takesvalue_paramor no value:<T> { v?: T ~ 5 }and<M> { v?: capped<M> ~ 5 }are refused at the declaration. Revision 38 accepts both and judges the literal per application (box<int32>andbox<text>pass;capped<3>refuses5), so a template could be valid for some arguments and not others; the rule makes the template’s verdict its own. The same rule reaches a literal argument: withpin2 => <T, P> { a?: T = P },<X> pin2<X, 3> & { … }and<X> { f: pin2<X, 3> }bindP, a value ofT’s argument, to3while that argument is the openX, and both are refused at the application, naming3,PandX;pin2<int8, 3>is a closed application and its member pinsato3. - Kinds follow the constructor. A record template is a member of
meta.ref<meta.record_template>and of nometa.ref<meta.record>, where §5.10 calls it “a member ofkind<record>”; its declared applications are records. The kernel’smeta.ref<meta.map>gainsmeta.record_template_field_map(the-kind-of-map-holds-the-namespace-types). - In this implementation the split is a type-level fact, end to end. The record template is its own Java type
beside the record’s, so a reader, a writer or a member dispatcher built from a record cannot be handed a template,
and the “a template IS-A the base is no member” rule both encodings’ dispatchers carried is the record type test
they already made. No record value holds a parameter, inside the resolver as in output: a template is built as a
template where it is declared, and an application substitutes on that value into a record —
type_paramandvalue_paramtake their arguments, a parameter among an application’s arguments is replaced where it stands — with nothing written out as text and read back. 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. The record template’s fields are a second field type, and the two record shapes share no Java parent, asrecord_templateandrecordshare no kernel parent beyondproduct: the five record-level facts both state (name type, field names, discriminators, groups, extension) are a value each produces, read in one place by the checks that need only them (field-name hygiene, the name type). Resolution builds a record over a field type of its own, which becomes arecord_fieldfor a closed declaration and arecord_template_fieldfor a template, and the body written for a record template is the labelled!record_template { … }from the start. spec/fixtures/meta-resolved.tncarries the three new entries andrecordwithoutparameters; the corpus’s record-template outputs move to the new form, with vectors for the labels, an absorbed literal left a literal, both literal refusals and the refused construction.
Open edge. The kernel duplicates record’s record-level fields on record_template rather than composing a
shared parent; if one is wanted, it is a meta.product refinement holding chain, groups, extension,
discriminators and name_type, with fields left to each.
Resolution, a proposal.
- §5.10, “Template forms”: a record template is a
record_template, not arecordwithparameters; a record is closed. “Held bodies”:<T> { x: T }is!record_template { fields: { x => !record_template_field { type_param: T } } parameters: [ … ] }; the three places a parameter stands are a field’stype_param, itsvalue_param, and an argument of an application; add “A literal value needs a closed type: a field whose type is a parameter, or an application over one, takes a value parameter or no value, and a literal there is a resolver error at the declaration”, and that the shadowing rule classifies what the template’s own body writes, a field it absorbs keeping its literal; add “A record template’s parameter MUST NOT be spelled like a name or an unquoted token written among an application’s arguments in a field type it absorbs, an open operand’s own parameters aside; the resolver refuses one at the declaration”. “Substitution”: atype_paramand avalue_parambecometypeandvalue. “Scoping and shadowing”: “within a held body” becomes “within a template’s body”, since a record template is no longer held and the refusal is every template’s, and “a schema type” becomes “a schema entry, a type or a value entry”. §5.8, “Parameterized references”:<T> T & { … }is refused as an operand, a same-named entry being the shadowing rule’s. The family-base paragraph: a member ofkind<record_template>, recognised by its constructor; no reader or writer is built for it. - §8.1, “Open entries” and the
record_fieldparagraph: staterecord_templateandrecord_template_fieldas above, and drop “in a record template it may hold a parameter, unlabelled”;templateandrecord_templateare both resolver vocabulary. §8.1’s table row and “Closed entries are parameter-free”: an open entry is a!record_templateor a!template. - §1.3: a consumer recognises a record template by its constructor; §4.2’s application-form list names
record_template_field.type; §5.7’s open modifiers: the parameter is the field’svalue_paramwhile open. - §13.2: the kernel listing gains the three entries and
recordlosesparameters.
20. [TSON-SCHEMA] §5.8, §5.10, §8.1 — an applied parent reaches the head at a closed operand, not through a template
Problem. §5.8 gives two answers to one question: does a type whose parent is an application of a record template
take an IS-A edge to that template? For a closed declaration it does: an application at a composition operand “is
subsumed where it stands”, so dog => pet<"dog", dog_type> & { … } is the hand-written
dog => pet & { pet_type?: = "dog" pet: dog_type } “with one IS-A edge to pet”, the head. For a template it does
not: with ok => <T> result<T> & { note: text }, “ok<text> reaches an IS-A edge to result<text>”, and §8.1’s
derived indexes add “ok<text> indexes result<text>, never a head name”. So ok_of => ok<text> is IS-A ok and,
only where a declaration names result<text>, result<text> — never result. Yet §8.1 also says “a template’s
expansion carries whatever chain the expansion’s own ^ or & gives it”, and ok<text> expands to
result<text> & { note: text }, whose operand by §5.8’s first answer earns the edge to result. The application and
its hand-written expansion land in different families, and a position typed result admits the second and refuses
the first.
What is running. Both texts as written. A closed declaration composing an application is a declared member of the
template and IS-A its head; a template’s applied parent is substituted and closed, and the closed entry’s chain
reaches the entry its arguments name, so the template is IS-A nothing of result’s and ok_of’s supertypes are
[ok] unless some declaration names result<text> (SubtypeTemplateFamilyTest).
Resolution, a proposal. Make the expansion rule govern: a declared application of a template whose parent is an
application of another record template is IS-A that template too, as its hand-written expansion is, and joins its
family. §5.8’s “never asked to carry the distinction” survives — the edge to result<text> still distinguishes
instantiations, and the head edge says only “a member of the result family”, as dog’s does. Two consequences to
state with it: the member must pin the outer family’s discriminators, which ok’s own pins (or its parameters’
arguments) supply; and §8.1’s “never a head name” narrows to “a parent application contributes the entry its
arguments name, and a declared member additionally the head”. The alternative, keeping the asymmetry, needs §8.1’s
expansion sentence to except applied parents, and the hand-written-expansion equivalence §5.8 states for dog to be
said to hold only for closed declarations.
21. [TSON-DATA] §2.5, §4, §7.2.5, §7.4, §7.5; [TSON-SCHEMA] §4.2, §5.2–§5.12, §7.4, §8.1, §8.2, §9, §12 — dependent fields
Problem. A record field whose type is chosen per instance by another field of the same record has no spelling.
The kernel’s own record_field.value is one: its type is whatever record_field.type names, so §8.1 types it
meta.value and states the real type in prose. Spelling the dependency needs a reference that names a sibling field
rather than a schema entry, which neither grammar has, a rule saying where such a reference may stand, the rules a
sibling and its dependent field answer to, and how one is read.
What is running. All of it, end to end, in both encodings and every read mode; nothing about a sibling at a field’s type is a gap.
-
Data grammar.
kind!is one reference value, delivered whole as an application is (#13): an unquoted token of one segment, an identifier, with an adjacent!that is followed by whitespace.a!b,kind!},kind!],kind!,,kind!?,a.b!and"kind"!are parse errors; so is!int32!, a tag naming a type. The end of the document counts as following whitespace, so a document that is onlykind!parses (the choice is this implementation’s; the rule as drafted says “followed by whitespace”). -
Schema grammar.
sibling-refis atype-refalternative, picked by one token of lookahead after the name, with the same whitespace rule, so[ kind! ],box< kind! >,( kind! | text )andx => kind!parse and[kind!]does not. -
The reference atom and its facet. The kernel’s reference constructor is
meta.reference_typeand gains a permission facet, asfloat_typehasallow_nan:reference_type => meta.atom & { of: meta.type_ref allow_sibling?: boolean ~ true } ref => <T> !meta.reference_type { of: T allow_sibling: false } field_type_ref => !meta.reference_type { of: meta.type } type_ref => !meta.field_type_ref ^ { allow_sibling: false } record_field => { type: meta.field_type_ref … value?: type! } record_template_field => { ( type: meta.field_type_ref | type_param: meta.param_name ) … ( value: type! | value_param: meta.param_name )? }A refinement may withdraw
allow_siblingand never grant it back (!meta.type_ref ^ { allow_sibling: true }is a resolver error); resolved output writes the facet where it is false and leaves it out at its default, someta.ref<C>resolves to!meta.reference_type { of: C allow_sibling: false }.meta.identifier_ref, a refinement ofmeta.type_ref, is IS-Ameta.field_type_reftoo. -
The position rule. A sibling reference stands only at a record field’s type. Elsewhere 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 schema error namingelement_type); inside a data argument list as a parse error; and in data at a field typedmeta.type_ref, where the facet refuses it as a validation error (ATOM_CONSTRAINT_VIOLATION: the value is well formed and breaks the facet, as NaN breaks afloat_typewithdrawingallow_nan). A schema read as data against the kernel admitsrecord_field.type: kind!, and refusesarray.element_type: kind!, in tree mode as in bind mode. A reference over a type that is no constructor names values and never holds a sibling reference, whatever itsallow_siblingsays (the choice is this implementation’s; a sibling names a field, which is no value). -
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, so a base’s sibling precedes a subtype’s dependent field);
- it is typed by a reference whose
ofis a constructor ortop, past its alias hops:meta.type_ref,meta.field_type_ref, ameta.ref<C>over a constructor, a refinement of one;int32, a value reference (meta.ref<method>) and a dependent field are refused. A value names no type, so a reference over values cannot type a dependent field. Reaching a type through a value is deliberately absent, in any spelling: in{ exchange: meta.ref<exchange> body: exchange!.request },bodywould be read against therequestfield of the exchange the packet addresses. That evaluates a path through a value at read time, so 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 (place_order_envelope => envelope<new_order> & { exchange?: = orders.place_order }). §5.2’s text states the refusal with this reason. (The evidence is a consumer’s: the rpc wire form inltr8-io-tson-java-http.) - it is there to read whenever the dependent field is: required, defaulted, pinned, or a field group’s member with
the dependent field optional. It is never voidable, and optional with no default it is refused. A member beside a
required dependent field is refused, as is a refinement making such a field required, the rule being asked of the
flattened record. A group’s members are declared
optional, so a dependent field that is itself a member counts as optional, asvaluedoes onrecord_template_field; - 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 (§5.2’s modifier grammar admits a bare token), so the admission of an application the closure holds is never reached.
- 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?: int32overv?: 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 keepstype: kind!beside the pin; - a sibling may be a discriminator, the family rules asking of it what they ask of any required atom-typed field.
-
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. 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.
-
Schema documents. A schema document’s 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
record_field.valueandrecord_template_field.value, bothtype!, wait on no entry. A dependent field written without its group-member sibling is refused in a schema document as in data, a holding read refusing it all the same. The type a sibling names is one of the reading schema’s namespace, which the governing layer’s readers do not hold, 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, by the reading rule above), each refusal a resolver error naming the entry, the slot, the dependent field and its sibling:d => !settings { s => { kind: int32 value: abc } }is refused, andvalue: { port: 8080 }underkind: serveris read byserver’s reader. A default or pin is judged as any field value is. A resolved form read as data, whose names resolve in the document read, holds the value as written too (the choice is this implementation’s: its types are the document’s own, and a resolved form is judged at ingest). The kernel’s wait cycle holdsmeta.field_type_ref, pre-loaded besidemeta.type_ref(reading itsofwaits onmeta.type_ref, its own refinement). Read as data, a record field’svalueis read against its owntype:!meta.record_field { type: boolean optional: true role: DEFAULT value: 5 }is a validation error at/value. -
valueandvalue_typeare gone from the kernel. Their last user wasrecord_template_field.value. A literal besidetype_paramhas no sibling to be read against, which is §5.10’s rule that a literal needs a closed type, read off the kernel:!meta.record_template_field { type_param: T value: 5 }is a validation error at/value, andbox => <T> { v?: T ~ 5 }keeps §5.10’s own refusal, stated by the resolver before the body is read. A literal under an application over a parameter (w?: box<T> = x) has its sibling, and is refused by §5.10 as before. A literal under a closed type is held as written and judged when the template closes, against the closed field’s type. [TSON-DATA] §4’s base type resolution stays, for a position nothing types; it no longer backs a typed position. -
Binding. A user schema’s dependent field binds to an
Objectcomponent, which receives what the named type binds to; any narrower class is a bind mismatch at compile. The binding library takesObjectonly where the context registers it as an atom, as the context this library builds for the schema model does, and a meta layer’s host class with anObjectcomponent then receives the value a schema document holds as written. The kernel’sRecordField.valuestays a heldToken. -
Class 1. A schemaless read keeps a sibling reference as a structured value, the field name and the mark, resolving nothing; the writers write it back as
kind!followed by whitespace. -
JSON. A sibling reference is a string holding the same text,
"kind!", read by the same production ([TSON-JSON] §5.6), and a dependent member is read with the text encoding’s rules, the field-order exception included ([TSON-JSON] §6.1.6, edited in place). -
Values by name. §5.12 judges a name reaching a record value’s field “by the field’s declared type”, which a dependent field has none of. Running: a segment selecting a dependent field 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, by the reading rule above, is a resolver error at the slot holding the name, and so is one the value does not hold. With
setting => { kind: meta.type_ref value?: kind! },d => !settings { s => { kind: meta.identifier value: hello } }makesd.s.valueameta.identifiervalue, andd.s.value.portwherekind: serveris judged byserver’s fields and reaches what the held record holds there.
Resolution, a proposal. The design above, as written in the spec:
[TSON-DATA] §7.4, extending #13’s production:
reference = sibling-ref
/ qualified-name ["<" ws type-args ws ">"]
sibling-ref = unquoted-token "!"
; "!" adjacent to the token before it, and followed by
; whitespace or the end of the document (§7.5). The
; token is one segment, an identifier: it contains no
; ".". A quoted token is never a reference. It is never
; a type-arg, and a tag ("!" qualified-name) is never one.
; A reference atom reads it unless its allow_sibling is
; false ([TSON-SCHEMA] §8.1).
[TSON-DATA] §7.2.5: ! keeps its prefix role and gains a postfix one; the lexer is unchanged, kind! lexing as an
unquoted token and an adjacent special token, and adjacency telling the two apart (a prefix ! is adjacent to the
token after it).
[TSON-DATA] §7.5 and [TSON-SCHEMA] §12.3, one row: ! (suffix, sibling reference) MUST be adjacent to the preceding
token and MUST be followed by whitespace.
[TSON-DATA] §4: a schemaless read keeps a sibling reference as a structured value, its field name and the mark.
[TSON-DATA] §2.5: a record’s field order is insignificant except that a dependent field’s free sibling — neither pinned nor a discriminator — is written before the dependent field.
[TSON-SCHEMA] §12.1: type-ref gains / sibling-ref, one token of lookahead after the name (!, <, or neither)
choosing the alternative.
[TSON-SCHEMA] §5.2: the dependent field, the position rule stated once (a sibling reference stands only at a record field’s type, a resolver error naming the slot elsewhere in a schema and a validation error in data), the sibling’s rules and the declaration-order rule, all as listed above, and the reading. The rule a sibling answers to reads:
A sibling is there to read whenever its 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 is declared 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. A group member is absent when another option is chosen: a dependent field written without it is a validation error at the dependent field.
[TSON-SCHEMA] §5.11: a member may be a sibling, as above.
[TSON-SCHEMA] §5.7, §5.8: restating a dependent field only as itself, pinning a sibling in a refinement, flattening’s order, and a sibling as a discriminator.
[TSON-SCHEMA] §5.12, after “is judged by the field’s declared type”: a dependent field’s is the type its sibling names in the same record value, its pin or default where the value states none (§5.2), and a sibling naming no type a value can be read against is refused at the name.
[TSON-SCHEMA] §8.1: meta.reference renamed meta.reference_type with allow_sibling; meta.field_type_ref;
meta.type_ref and meta.ref<T> setting allow_sibling: false; record_field.type and
record_template_field.type typed meta.field_type_ref; record_field.value?: type!; record_template_field’s
( value: type! | value_param: meta.param_name )?; resolved output writing kind! as is.
[TSON-SCHEMA] §4.2: “Atoms with no constraint vocabulary” loses value_type and value, leaving void_type and
void; §5.2’s eager resolution and §5.10’s routed value lose their mention of a value-typed field. §7.4: delete
“Constraint fields typed as value” and the paragraph after it (#17). §8.2’s table and §9’s kernel listing and family
pairs drop value, and the §5.4 discrimination text that excepts it follows.
Open. Whether the end of the document counts as following whitespace (running: it does). Whether a field group
member’s type takes a sibling reference as a plain field’s does (running: it does, a member being a field). Whether a
sibling and its dependent field in two options of one group is refused (( kind: meta.type_ref | v: kind! ): running,
it is admitted, the rule asking only that v is optional, and v can then be written in no document). Whether a
literal under a closed type is judged on the template, where no application closes it (box => <T> { n?: int32 ~ nope w: T }: running, only its §8.2 hygiene is, and the value is judged once an application closes the field, so a template
nothing applies loads). The quoted-name gap: a field whose name needs quoting cannot be a sibling, and "name"! is
refused. Which host component a bound dependent field takes (running: Object, registered as an atom; a tree value or a
type the binding names are the alternatives). A sibling in a template field typed by a parameter (kind: meta.type_ref = T) waits on whether a pin on a meta.type_ref slot may carry a type parameter.
22. [TSON-SCHEMA] §1.2, §3.3.5, §5.1, §5.3, §5.7, §5.11, §8.1, §12.1 — “type-ref”, “type reference”, “sibling”
Problem. §12.1’s type-ref covers the structural forms as well as references: type-ref = paren-type / bracket-type / map-type / type-name "<" type-args ">" / type-name (and / sibling-ref under #21). A choice, an
array, a tuple and a map are not references to a type but expressions that denote one; the spec says as much
elsewhere, §5.3 being titled “Type Expressions” and §12.1’s compound forms sitting under “Compound Type Expressions”.
The kernel’s meta.type_ref (§8.1) is the reference alone — the name of a type, with any arguments — and a structural
form is never its value: resolution lifts every one to an entry and leaves a reference to it behind (§5.3). So one
name means two things across the grammar and the kernel, and a reader of §12.1 who knows meta.type_ref takes
type-ref for the narrower thing. The narrower production has no name of its own: map-key, supertype-ref and
refined-def’s head each spell type-name ["<" type-args ">"] out in place.
The prose phrase “type reference” is wider still. It names a value’s !name tag (“Every type reference (!name)”,
§1.2), an alias declaration (id => uuid ; type reference (§8.3), §5.1), the grammar’s type-ref with its structural
forms (§12.1), and the kernel’s meta.type_ref, the names of types with the sibling reference withdrawn (#21). A tag
is no reference to a type in the kernel’s sense: it is resolved, but it is not a value of meta.type_ref.
“Sibling” has the same problem in the other direction. #21 makes it a technical term, the field whose value types a
dependent field: it is in a facet’s name (allow_sibling), a grammar production (sibling-ref) and the diagnostics.
Part 2 also uses the word informally, for five other things:
- a same-named entry core declares beside the kernel’s, “a fresh sibling” (§3.3.5, and so §3.3.3, §4.2, §5.10 and §9; this sense goes with §3.3.5 under #7);
- a family founded by construction, “Construction creates siblings, not subtypes” (§5.5, §8.1);
- two refinements of one source, “two sibling refinements such as
int8andint32” (§5.10); - a facet’s neighbours, “
float_type’sallow_nanand its three siblings” (§5.7); - a field group’s other members, “contiguous with its sibling members” (§5.11).
The evidence is partly a consumer’s. The HTTP layer in ltr8-io-tson-java-http met both words while citing the
draft, and proposed “selector” for the typing field. That is not adopted: Part 2 already uses “selector” for a family’s
discriminating field (§5.1, §5.2) and for a facet kind (§5.7), and a sibling need not be a discriminator.
What is running. The grammar text unchanged, and the AST named by what each form is, mirroring the resolved model
and the kernel: TypeExpr is what type-ref parses to; TypeRef is the reference, a name or an application (as
meta.type_ref is), and with SiblingRef makes up Reference; the structural forms are ChoiceExpr, ArrayExpr,
TupleExpr and MapExpr. A slot the grammar holds to a reference (a map’s key, a refinement’s source) is typed by
the reference in the AST.
Resolution, a proposal. Rename the production type-expr at every use (field-type, group-member,
element-type, paren-type, type-arg, type-param, the reference-bodied type-def alternative), and keep
type-ref for type-name ["<" type-args ">"], which map-key, supertype-ref and refined-def then use instead of
restating it. type-expr’s alternatives become the structural forms, sibling-ref (#21) and type-ref. No
document’s meaning changes; prose that says “type-ref” meaning any type position (§5.3’s “every type-ref position”,
§5.7’s “elided type-refs”) reads “type expression”.
“Type reference” is kept for type-ref and meta.type_ref, a name with any arguments. §1.2 calls a !name a tag, and
§5.1’s comment calls an alias’s body a reference. “Sibling” is kept for #21’s typing field alone, and the five informal
uses are reworded: “a same-named entry” (gone with §3.3.5 regardless), “peers” or “fresh constructions” for a
construction’s family, “two refinements of one source”, “its three companions”, and “the group’s other members”.
23. [TSON-SCHEMA] §5.5, §5.7 — a float carries bounds, though §5.7 asks a bound’s family to be totally ordered
Problem. §5.7’s “Value tightening is per facet kind” says an ordered bound “requires its family’s value space to be
totally ordered, which every family carrying one now is”. float_type carries both bound groups, ( min: number | exclusive_min: number )? and ( max: number | exclusive_max: number )?, and float32 and float64 are
@atom.ordering:PARTIAL: NaN is unordered. So either the claim is wrong or float’s bounds are. Nor does any section say
how a bound treats the values a float adds to the reals: NaN, which compares with nothing, and ±infinity, which
compare with everything.
What is running. A float’s bounds are judged only on finite values. NaN, where allow_nan admits it, and ±infinity,
where allow_infinity admits it (both default true), skip the bound checks, so !float64 ^ { max: 100 } admits +.inf
and .nan. The infinity half is an implementation defect and is in BACKLOG.md: an infinity is ordered, and max: 100
refuses +.inf under any reading.
Resolution, a proposal. A bound needs an order on the values it can admit, not a total order on the family:
- §5.7 reads “requires its family’s value space to be ordered” in place of “totally ordered”. Float stays bounded.
- A bound excludes NaN. A bound admits a value inside its interval, and NaN is inside none. So a float body with
any bound admits no NaN, and
allow_nandecides NaN only where no bound is written. The alternative, NaN passing every bound asallow_nanalone decides, makesmin: 0admit a value that is no number. That reads a bound as “refuses what compares below”, which is a weaker facet than the one an author writes. - ±infinity are ordered values, and bounds judge them as any value:
max: 100refuses+.inf, andmin: 0admits it whereallow_infinitydoes. - §5.5’s coherence paragraph gains nothing: a bounded body with
allow_nan: truestill admits a value, and the permission is simply unreachable while a bound stands.
24. [TSON-SCHEMA] §8.2, §10.2 — a document is identified by its bytes, an entry by its structure; no section says both
Problem. Two identity rules sit far apart and read as contradictory until both are in view. §10.2 pins a document by a digest of its content, so a whitespace edit to a schema changes its digest, and every pin that names it has to move. §8.2’s “Two identities” makes an entry a declared name or, for an application or a synthetic, a parsed structure compared after following alias chains, so two spellings of one form are one entry. An external reviewer of the kernel read “a whitespace edit moves every pin” against “two spellings of one form reduce to one entry” as a conflict.
What is running. Both, as written: digests over a document’s bytes, verified on load against
TsonBundledSchemas’s pins and every pinned reference, and structural identity for applications and synthetics.
Resolution, a proposal. One sentence, in §8.2’s “Two identities” and referenced from §10.2: “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 and none of its entries, and two spellings of one form in a schema are one entry.”