Normative text
The normative specification at the version VERSION names. Ten files and
one rule set.
At a fixed build-parameter binding folding a file and running it are
observationally equivalent. That is the objective every rule here serves.
objective.md states it with the two profiles an
implementation may claim and the notation the judgments are written in. Start
there.
The rules in reading order:
grammar.md(S-*) the grammar of the fold subset.evaluation.md(F-Eval-*) J1 expression evaluation.verdict.md(bareF-*) J2 the per-file verdict.taint.md(bareF-*) J3 the identity-taint fixpoint.observables.md(F-Obs-*) J4 properties and observables.values.md(F-Val-*) the value domain.divergence.md(F-Direction,F-Div-*,F-Exc-*) divergence between the shape classifier and the folder.hosts.md(F-Host-*) the host interface.rules.md(F-Rule-*) rules over values.
judgments.md indexes J1 through J4.
Nothing in the three appendices is normative.
rationale.md carries the reasoning that motivated each
rule. inventory.md is the coverage ledger. Every decision
point in chant core cites the rule that governs it and CI gates on it.
prior-art.md surveys the neighboring systems and says
where each one is nearest.
Process rules for the specification itself
These govern the document set rather than the mechanism. CI structure enforces them and no fixture cites them.
- Every normative rule carries a stable identifier.
- Every identifier is exercised by at least one fixture and every fixture cites a real identifier. Both directions in CI.
- Rejections are located. Node and rule. Message stability is not normative.
- The subset is versioned. The policy is the Versioning section below; the current version is
VERSIONand the history isCHANGELOG.md.
A disagreement between two implementations is triaged here first and reclassified as an implementation bug only once this specification is shown to be unambiguous on the point. The order is the rule because the incentives run against it: amending an implementation takes an afternoon and amending a specification takes a decision so the cheap label is the one most likely to be wrong. A disagreement that survives triage is recorded with the issue that will settle it and the record may only shrink. This has been the working practice since the first cross-check and was written down nowhere.
A difference a missing capability explains is not a disagreement. Where one implementation has no answer to give, for want of a host or of a form it does not implement, the difference is counted under a named limit and reported apart from the agreement figure. Folding it into drift overstates what the comparison established. The corpus cross-check names two such limits and counts each of them.
A generator’s output is in the subset and folds to its input.
A generator is any function from the value domain to source: a template
importer, a live import, a carve-out. Its obligation is the fidelity half of
the round trip, fold(generate(v)) = v in data-host and
fold(generate(v)) = revive(v) in full, where revive is F-Val-Fate. The
completeness half is F-Val-Source (values.md), which says such a form
exists for every value; the generator itself is not specified, and how it
factors its output is quality and not correctness. The conformance suite
holds a generator to it through the roundtrip fixture kind: the input is
a namespace as data, the implementation’s generator writes the source, and
the fold of that source is compared with the input.
Ownership
The subset is defined here. chant’s packages/core/src/fold/subset.ts is an
implementation of this specification and cites the rule identifiers it
implements.
A subset change goes spec-first. The rule is proposed and landed here (grammar, judgment, or value-domain text with an identifier and a fixture), then implemented in chant citing that identifier, then released. The specification version chant declares moves with it.
The provisional path, for a change chant needs before the spec can be
written properly: chant may ship it with the affected rule marked
provisional in subset.ts’s module doc, naming the issue here that will
specify it. A provisional marker may survive at most one chant release; the
docs-parity gate on chant’s side fails on one older than that. A
provisional change is not conformance-tested until the rule exists here and
chant’s documentation may not describe it as supported until then.
What this costs chant. The subset can no longer change by editing code and a comment. That is the price of the paper being able to call this a specification and of the conformance suite testing chant against a document chant cannot invalidate by itself.
What this specification decided
The ordering above is a policy. What follows is the record of where it was exercised. It exists so that “extracted from chant” can be checked rather than assumed.
S-ExportDefault. A default export is the declarator named default.
The profile table admits it in data-host. In full it is permitted rather
than required, and chant does not yet admit it. forgejo-warden’s loader asks
for that form. Its error message tells a user to export the policy as
export default. It accepts export const policy as a fallback. That form
folds as an ordinary S-TopConst export. So the form warden instructs its
users to write is one chant cannot fold.
F-Profile-DataHost. chant has no equivalent mode. The profile exists for
an evaluator with no JavaScript runtime. warden names it explicitly and folds
with an empty host.
Those two answer the transcription charge. A document transcribed from chant
would carry neither. warden would still run because its fallback form folds
without S-ExportDefault. The form it teaches would not fold and the profile
it names would not exist.
The rest are smaller and none has a consumer yet.
F-Eval-New,F-PrebuildandF-Countare permitted rather than required, so an implementation may rejectnewunderF-Eval-Rejectinstead.ι ∈ {open, isolated, executing}is a parameter of J2 rather than a flag of one implementation. Conformance requires neitherisolatednorexecuting.F-Rule-Supplyadmits a rule written in the evaluator’s own language. For a data-host evaluator that is not JavaScript.F-Val-Sourceis stated per profile. chant’s single profile does not need that split.F-Host-Interfaceitems 1, 2 and 6 live in revival and interpretation, which the reference implementation does not yet port.
Versioning
A version names a set of rules. It is the S-* and F-* identifiers
and their normative text at a point in time recorded in
VERSION as major.minor and tagged spec-<major>.<minor>
on the commit that set it. It is not a chant release: chant declares which
specification version it implements and the two move separately. It is not
a revision of the rationale, the inventory, a note or a fixture, none of
which is normative; a change confined to those does not move the version
and is not entered in CHANGELOG.md.
Four kinds of change. A rule is added or widened when source that did not fold now does; the minor version moves, existing source is unaffected, and an implementation claiming the new version must implement the rule. A rule is narrowed when source that folded no longer does, or a verdict changes; the major version moves, because a project that folded under the old version may not under the new. A rule is retired when it is struck through in place with a note naming its successor (the identifier rules above); that is also major, and the fixtures citing it move to the successor in the same change, which the coverage gate enforces, since a struck identifier is no longer defined and a citation of it fails.
A host obligation moves the minor. The major exists for the reason narrowed gives. That reason is about a project’s source. An obligation on the host is not about source at all. It constrains the implementation. Every file that folded still folds under any host that still conforms. What narrows is the set of conforming hosts. That is the added case. Claiming the new version requires meeting the obligation. Implementing a new rule requires the same. Withdrawal is the reverse and is also minor.
F-Host-Interface item 1 is the case that settled it. Bounding what an entity
constructor may do at 2.1 stopped a validating host from conforming. No source
changed verdict. The everyday reading of “a host that conformed no longer
does” says major. The four kinds do not say it. 2.1 landed minor on the
reading above and this paragraph is what records it (#204).
Two profiles. objective.md
defines full and data-host (F-Profile, F-Profile-DataHost); the second is
the first with nothing that needs a JavaScript runtime for an evaluator
embedded in a platform written in another language. Fixtures carry profile
tags and the suite judges an implementation on its own profile’s fixtures
only.
An implementation declares the version it implements. The conformance
adapter carries specVersion and the suite compares the reference
implementation’s declaration against VERSION so the two cannot drift
apart unnoticed. chant declares its version in its own tree (its docs gate is
where a stale declaration is caught); an implementation that declares none
is reported as undeclared.
A major opens a bounded window. Spec-first means the rule lands here before any implementation carries it so a major necessarily leaves chant declaring the previous one for a time. The suite allows a declaration at most one major behind while that is true and names the issue that closes it. It never allows two and it never allows undeclared. The assertion tightens back on its own once chant declares the current major.
What the paper cites is one version, by tag, and the artifact is this
repository at that tag (paper/README.md). Numbers in the paper that
depend on the rule set say which version produced them.
How a change is proposed. As an issue here labeled area:spec that
carries the rule text with its identifier and the fixture that would pin it.
It lands spec-first and then in implementations as the ownership section
says. The one exception is chant’s provisional path above. Anyone may propose. The decision is recorded on the issue and dated
in the text as the decisions above are.
Scope
The name overclaims
This section is the correction. “typescript-as-data” names a general idea; what is specified here is one statically evaluable subset of TypeScript, the one chant’s fold mechanism implements, together with what happens at its edge. It is not a claim that this is the way to read TypeScript as data.
Fixed to TypeScript
The syntax is the TypeScript AST as the typescript
compiler package parses it; the module system is ES modules; and the semantics
of every admitted operator are ECMAScript’s, stated per operator, because a
second implementation in another language must reproduce ECMAScript coercion
rather than its host’s. Two places depart from
ECMAScript on purpose and are stated as departures.
Varies by host
A host supplies seven things listed under F-Host-Interface in
hosts.md.
- the classes whose instances are entities
- how an entity exposes its attributes as symbolic references
- an intrinsic registry
- an allowlist of authoring helpers
- a trust set of packages
- the registration form that makes a project-defined factory interpretable
- rules over the folded values
chant’s lexicons are one instantiation of it. The generality this buys covers host vocabularies and stops there. “Parameterized by a host” reaches no further.
What another language would reuse
Reusable as stated.
- the value-domain shape
- the per-file decision and identity-taint fixpoint
- the two-layer admissibility structure
- the three evaluation modes
- the conformance obligations
To be reproduced rather than reused: the AST classification, which is TypeScript’s node kinds, and ECMAScript coercion.
What the reference implementation covers
The reference implementation covers all three judgments, with revival through
a host’s real constructors, and declares the full profile at the version in
VERSION. With an empty host it is also an implementation of data-host
without new, and the suite judges it on that profile’s fixtures too.
What it does not cover is packages/reference/CAVEATS.md. Spec 2.0 closed
the gap that used to sit there. F-Call step 6 for a project module belongs to
executing alone. Never importing project code is conformant rather than
partial.
Identifiers
Two families of identifier and the family is part of the meaning:
S-*is a shape rule decidable from syntax alone by a classifier with no binding resolver and no registry. Lives ingrammar.md. AnS-rule may accept what anF-rule later rejects; never the reverse (F-Direction).F-*is a fold rule. It cannot be decided from syntax alone. Its sub-prefix names the file that owns it.
| Prefix | Owner |
|---|---|
F-Eval- | evaluation.md (J1) |
bare F- | verdict.md (J2), taint.md (J3) |
F-Obs-, F-NoOwnExecution, F-Depth | observables.md (J4) |
F-Val- | values.md |
F-Div-, F-Exc-, F-Direction | divergence.md |
F-Host- | hosts.md |
F-Rule- | rules.md |
Names are Prefix-CamelWords, no digits in the name part, specific enough to
read alone, like F-Eval-Member. A rule with numbered steps is
cited as F-Eval-Member step 4; the step number is not part of the
identifier.
Stability. An identifier names one rule for the life of the spec. A rule
that is split keeps its identifier on the part closest to its original
meaning and the new part gets a new one. A rule that is removed or renamed
is struck through in place with a note naming its successor; the identifier
is never reused. Inventory row identifiers (L3.10) follow the same rule.
Enforcement. spec/coverage.test.ts asserts every inventory row
cites a defined rule. The reverse, every defined rule has a conformance
fixture, is the other. Together they make an identifier that nothing
exercises, or a citation of nothing, a CI failure rather than a drift.
One vocabulary. S-* and F-* are the only normative identifiers, each
rule’s reasoning is a non-normative note under it, and inventory.md cites
rules only.
- The objective At a fixed build-parameter binding, folding a file and running it are
- Grammar of the fold subset Normative draft. Identifiers are S-. Every production here is
- Judgments Normative draft. The three judgments are expression evaluation, the per-file
- J1. Expression evaluation `Γ, H ⊢ e ⇓ v` Derived from fold() (fold.ts), foldResource,
- J2. Per-file verdict `B, ι ⊢ f ⇓ fold(X, L) | run(reason)` Derived from
- J3. The identity-taint fixpoint Derived from planFoldTaint (fold-import.ts),
- J4. Properties and observables Rules that are about the whole mechanism rather than one judgment.
- The value domain Normative draft. What a fold produces. Rules are F-Val-: the value
- Divergence between the shape classifier and the folder Normative draft. One subset and two consumers with unequal information.
- The host interface Normative draft. What a host supplies to make the subset mean
- Rules over values Normative. The contract a semantic check runs under. It names the input a
- Rationale Non-normative. The reasoning that motivated each rule carried over from the retired requirements.md. Keyed by the rule(s) each note supports.
- Fold mechanism inventory Every decision point in chant core's fold path with the requirement that covers
- Changelog The versioned history of the rule set. The policy is in [README.md](/typescript-as-data/spec/normative/) under "Versioning". A version names a set of rules, S-
- Prior art Not normative. A survey of the neighboring systems and where each one is