# Roundtrip Invariant

## Roundtrip Invariant

Every well-formed AILang module has a canonical `.ail.json`
representation (the hashable, content-addressed
[JSON-AST](0002-data-model.md)) and a hand-authored `.ail` representation
(Form A, the [authoring projection](0001-authoring-surface.md)). The
JSON-AST is derived from `.ail` in-process by consumers that need
it; the round-trip is now the *property* of that derivation, not
the byte-level agreement of two on-disk forms.

Concretely:

1. **Parse-determinism.** For every well-formed `.ail` text `t`,
   `ailang_surface::parse(t)` produces a unique AST. The parser is
   a pure function of input — no randomness, no time dependence,
   no environment leak. Hashing `canonical::to_bytes(parse(t))` is
   therefore well-defined for any `.ail` source.

2. **Idempotency under print.** For every well-formed `.ail` text
   `t`, `canonical(parse(t)) == canonical(parse(print(parse(t))))`.
   The printer is a left-inverse of the parser modulo canonical
   form: print-then-parse is a no-op on the canonical bytes.

3. **CLI-pipeline idempotency.** For every `.ail` fixture, the
   public CLI pipeline `ail parse | ail render | ail parse` is
   byte-identical to direct `ail parse` of the source `.ail`.
   Pins drift that crate-internal tests cannot see.

4. **Carve-out anchor.** Seven `.ail.json`-only fixtures
   (subject-matter rejection tests) survive in the corpus by
   structural necessity. They participate in their own dedicated
   rejection-shape tests, not in the round-trip gate. (The prelude is
   embedded as `examples/prelude.ail` in `ailang-surface` and parsed
   at compile time via `ailang_surface::parse_prelude`.)

Hashing is the consequence the language depends on: BLAKE3 of the
canonical bytes is well-defined for any `.ail` source via parse-
determinism. An LLM author writes `.ail`; the build derives the
canonical hash without ambiguity.

### Float literals are inside the invariant

Float literals carry an IEEE-754 bit pattern, not a decimal
approximation. The canonical encoding is
`{"kind":"float","bits":"<16-lowercase-hex>"}` and the surface
emits the same bits-hex string. NaN, ±Inf, signed zero, and subnormals all
round-trip exactly because the JSON-number path is bypassed (see
[Float semantics](0005-float-semantics.md), "Form-A serialisation").
Floats are not an exception to the invariant — the bits-hex
encoding is the mechanism that keeps them *inside* it.

### Enforcement

The invariant is workspace-CI-enforced by five tests, each
operating on the `examples/` corpus via dynamic `read_dir`
collection (no hardcoded fixture list, so newly added fixtures
inherit the gate automatically):

- `crates/ailang-surface/tests/round_trip.rs::parse_is_deterministic_over_every_ail_fixture`
  — for every `.ail`, parse twice and assert canonical-byte
  equality between the two ASTs. Direction 1 above.
- `crates/ailang-surface/tests/round_trip.rs::parse_then_print_then_parse_is_idempotent_on_every_ail_fixture`
  — for every `.ail` text `t`, `parse(t)` and `parse(print(parse(t)))`
  produce canonical-byte-equal AST. Direction 2 above.
- `crates/ail/tests/roundtrip_cli.rs::cli_parse_then_render_then_parse_is_idempotent`
  — for every `.ail`, the public CLI pipeline `ail parse | ail render | ail parse`
  reproduces canonical bytes byte-identical to direct `ail parse`.
  Pins drift internal tests cannot see.
- `crates/ailang-core/tests/schema_coverage.rs::every_ast_variant_is_observed_in_the_fixture_corpus`
  — every variant of `Def`, `Term`, `Pattern`, `Literal`, `Type`,
  and `ParamMode` appears in at least one `.ail` fixture. New AST
  variants fail compile until the visitor and corpus are extended
  in lockstep.
- `crates/ailang-core/tests/carve_out_inventory.rs::examples_ail_json_inventory_matches_carve_outs`
  — exactly the seven named carve-out files exist under
  `examples/*.ail.json` at any commit. A new `.ail.json` or a
  missing carve-out fails the test.

A new fixture or a new AST variant that violates the invariant
fails one of these tests; the fix is in render or parse code,
never by relaxing the test.

### Why this is anchored at top level

The invariant is a property of the language identity, not of any
one surface-design choice. The [authoring surface](0001-authoring-surface.md)
introduces the `.ail` surface and lists round-trip-as-property as
one of its constraints, but the property is load-bearing for every
downstream concern that treats the two forms as exchangeable:
content-addressed hashing, the LLM-author's choice of authoring
form, the integrity of fixture cross-references, and the
prerequisite for empirical cross-model authoring-form studies.
Lifting it out of the authoring-surface contract makes the property
quotable on its own and reviewable by the architect agent at every
milestone close.

Ratified by: `crates/ailang-surface/tests/round_trip.rs`.
