Files
AILang/design/contracts/roundtrip-invariant.md
T
Brummel bcd41810f4 design/ + source rustdoc: replace opaque shorthand with content phrases + links
Reader-facing prose and rustdoc carried opaque shorthand like
"Decision 10", "clause-5", "mq.1", "ct.1", "eob.1", "rpe.1",
"post-mq.3", and "Iter 22b.1:" with no in-repo definition the reader
could follow. This commit replaces every such occurrence in the
durable tier the reader is most likely to land on (design/ ledger +
source //! module headers + the central /// public-item rustdoc) with
an inline content phrase plus, where applicable, a Markdown link to
the file that defines the referenced concept.

design/ ledger — 16 files:
  Definition-site headings demoted from "Decision N: <title>" to
  "<title>": authoring-surface, tail-calls, memory-model section in
  rc-uniqueness.md, dual-allocator section, typeclass design,
  effects "pure core + algebraic effects".
  Cross-reference sites: "Decision 1" -> canonical-schema principle
  (data-model); "Decision 3/4" -> effects + scope-boundaries; "Decision
  6" -> authoring-surface; "Decision 8" -> tail-calls; "Decision 9" ->
  rc-uniqueness (dual-allocator); "Decision 10" -> memory-model;
  "Decision 11" -> typeclasses (model). "clause-5" -> body-link
  durability gate. "clause-3" (in language-constraints) ->
  bug-class-reintroduction discriminator. "mq.1/2/3", "ct.1/4",
  "eob.1", "rpe.1" -> the canonical-form rule / the type-driven
  dispatch / the Str carve-out / etc. "post-mq.3" -> "type-driven".

design/contracts/feature-acceptance.md: file-local "clauses 1/2/3"
-> "criteria 1/2/3" (sprachliche Kohärenz mit der File-Überschrift
"Feature-acceptance criterion"); "the clause-3 mechanism" -> "the
bug-class-reintroduction discriminator".

Source //! module headers — 24 files:
  Stripped "Iter X.Y:" prefixes and "(Decision N)" / "(mq.X)" tags
  from spec_drift, uniqueness, reuse_shape, migrate_canonical_types,
  typeclass_22b{2,3,c}, suppress_filter, lift, mono, linearity,
  diagnostic, method_dispatch_pin, method_collision_pin,
  no_per_type_print_ops, mq3_multi_class_e2e, print_mono_body_shape,
  print_no_leak_pin, cli_diag_human_workspace_load_error,
  ct1_check_cli, prose snapshot, unbound_in_instance_method_pin,
  mono_xmod_ctor_pattern, desugar.

Central /// public-item rustdoc:
  ast.rs (full sweep — every "Iter X" + "Decision N" prefix
  reformulated; mode/Type::Fn rustdoc now points at memory-model.md;
  Constraint / SuperclassRef / InstanceDef / ClassDef rustdoc points
  at typeclasses contract).
  diagnostic.rs (all "(Iter X)" / "(mq.X)" tags on diagnostic codes
  removed).
  lib.rs (FORM_A_SPEC rustdoc points at authoring-surface.md
  instead of "Decision 6").
  canonical.rs (type_hash + Float-literal rustdoc).

Still outstanding (for a follow-up commit): ~500 inline `//`
code-body comments with `Iter X.Y` markers across the workspace, and
a handful of `///` rustdoc items in hash_pin / workspace_pin / lift /
mono / suppress_filter test-pin and internal-function bodies. Code
identifiers (test filenames like `mq3_multi_class_e2e.rs`, function
names like `iter18e_drop_iterative_default_preserves_hashes`) stay
verbatim per the user's "code identifiers stay verbatim" rule.

Tests: design_index_pin 5/5 + docs_honesty_pin 5/5; workspace builds
clean; full `cargo test --workspace` previously green (every
`test result: ok` line, no FAILED line).
2026-05-20 09:47:33 +02:00

4.8 KiB

Roundtrip Invariant

Roundtrip Invariant

Every well-formed AILang module has a canonical .ail.json representation (the hashable, content-addressed JSON-AST) and a hand-authored .ail representation (Form A, the authoring projection). 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, "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 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.