iter form-a.tidy: form_a.md class/instance/constraints + 3 documentary drift items

Six-task post-fieldtest documentary tidy. No production-code behaviour
changes; 559 tests green at every per-task gate.

T1-T3: form_a.md additions
- §Definitions intro "Three kinds" -> "Five kinds".
- New `### Class — (class ...)` subsection: EBNF carries optional
  superclass (0 or 1 per ClassDef.superclass schema), method
  signatures with optional defaults; anchored to
  examples/test_22c_user_class_e2e.ail (ok 24/2).
- New `### Instance — (instance ...)` subsection: EBNF carries the
  (method NAME (body LAM-TERM))* shape; canonical-form CLASS-REF
  rule explicitly stated (bare same-module / qualified cross-module);
  two examples — same-module abbreviated from
  mq3_class_eq_vs_fn_eq_classmod.ail and cross-module qualified
  from show_user_adt.ail; bare-cross-module-class-ref diagnostic
  named inline.
- §Types `(forall ...)` line extended with optional `(constraints
  (constraint CLASS-REF TYPE)+)?` clause; explanatory paragraph
  added with no-instance diagnostic anchor; fifth example added
  to the Examples block anchored to cmp_max_smoke.ail.

T4: docs/specs/2026-05-13-form-a-default-authoring.md "seven
carve-outs" → "eight carve-outs" at 6 sites contradicting the
§C4(b) compile-time-embed amendment (commit 9fcda8b). Sites:
preamble line 11, §C1 line 170, §C2 line 191, §C3 line 218,
§"Data flow" lines 363 + 374. Post-edit grep returns 4 surviving
"seven" lines (233, 238, 463, 469), all correctly §C4(a)-scope
or arithmetic/future-state.

T5: crates/ailang-core/src/hash.rs:50-57 — delete empty
`#[cfg(test)] mod tests {}` placeholder + 6-line relocation
comment. Tests live in crates/ailang-core/tests/hash_pin.rs
since form-a.1 T5; placeholder served no purpose. hash_pin.rs
still 10/10 passing.

T6: crates/ailang-surface/tests/round_trip.rs — module-level
`//!` and inner `///` docstrings rewritten to the post-T10
four-property framing (parse-determinism / idempotency /
CLI-pipeline-idempotency / carve-out-anchor) instead of the
retired Direction-1/Direction-2 language. Sibling-crate
breadcrumbs added pointing at the other two enforcement points
(roundtrip_cli.rs, carve_out_inventory.rs).

Drift item B2 (audit-form-a "plan file two sites") dropped per
recon verification: docs/plans/2026-05-13-iter-form-a.1.md
contains zero defective "seven" sites; all four hits are
internally scoped to §C4(a), arithmetic, or future-state.
Decision recorded in the journal.

Closes 2 of 3 fieldtest-form-a spec_gap findings (#2 form_a.md
typeclass surface + #3 form_a.md class-qualifier rule for
instance) and 3 of 4 audit-form-a drift items.
This commit is contained in:
2026-05-13 12:25:12 +02:00
parent 5e94204c21
commit e809f45e67
6 changed files with 252 additions and 31 deletions
+103 -2
View File
@@ -63,7 +63,7 @@ to the entry file's directory.
## Definitions
Three kinds, matched on the leading keyword:
Five kinds, matched on the leading keyword:
### Function — `(fn ...)`
@@ -112,6 +112,93 @@ The list length must match the number of params in `type`.
(body TERM))
```
### Class — `(class ...)`
```
(class NAME
(param TYVAR)
(doc STRING)?
(superclass (class CLASS-REF) (type TYVAR))?
(method NAME (type FN-TYPE) (default TERM)?)*)
```
A class declaration introduces a typeclass with one type parameter (`param`)
and a list of method signatures.
`CLASS-REF` in the optional `superclass` clause follows the canonical-form
rule (see *Types* below): bare for same-module, `MODULE.CLASS` for
cross-module. The `superclass` slot is at most one — multi-superclass
chains are not yet supported.
Each `method` carries a function-typed signature. The bound type variable
named in `param` is in scope throughout the method's `(type ...)`. A
`(default ...)` clause provides a fallback implementation; absent means
the method is abstract-required (every instance MUST implement it).
Example (`examples/test_22c_user_class_e2e.ail`):
```
(class Foo
(param a)
(method foo
(type (fn-type (params (borrow a)) (ret (con Int))))))
```
### Instance — `(instance ...)`
```
(instance
(class CLASS-REF)
(type TYPE)
(doc STRING)?
(method NAME (body LAM-TERM))*)
```
An instance declaration provides method implementations of `CLASS-REF` at
the concrete `TYPE`.
`CLASS-REF` follows the canonical-form rule: bare for same-module-to-
class (the instance and the class live in the same module), `MODULE.CLASS`
for cross-module (the class lives in another module — most commonly
`prelude.Show`, `prelude.Eq`, etc.).
Each `method` body is a `(lam ...)` term. The class's type parameter is
substituted for `TYPE` throughout the method body's parameter types and
return type; method bodies are type-checked under that substitution and
walk through the same identifier-resolution path as `(fn ...)` bodies,
so an unbound name inside a method body fires `[unbound-var]` at
`ail check`.
Two examples.
Same-module class + instance (`examples/mq3_class_eq_vs_fn_eq_classmod.ail`,
abbreviated):
```
(class MyEq (param a)
(method myeq (type (fn-type (params (borrow a) (borrow a)) (ret (con Bool))))))
(instance
(class MyEq)
(type (con Int))
(method myeq
(body (lam (params (typed x (con Int)) (typed y (con Int))) (ret (con Bool)) (body true)))))
```
Cross-module qualified class (`examples/show_user_adt.ail`, abbreviated):
```
(instance
(class prelude.Show)
(type (con IntBox))
(method show
(body (lam (params (typed x (con IntBox))) (ret (con Str))
(body (match x (case (pat-ctor MkIntBox n) (app int_to_str n))))))))
```
The `prelude.Show` qualifier is required here because `Show` is declared
in the `prelude` module, not the entry module. Writing `(class Show)`
bare would fail with `bare-cross-module-class-ref`.
## Types
Four shapes, all parenthesised except a bare type variable:
@@ -122,7 +209,9 @@ TYVAR-NAME ; type variable (e.g. `a`, `T`)
(fn-type (params PARAM*)
(ret RETURN-PARAM)
(effects EFFECT-NAME*)?) ; function type
(forall (vars TYVAR+) BODY-TYPE) ; polymorphic schema
(forall (vars TYVAR+)
(constraints (constraint CLASS-REF TYPE)+)?
BODY-TYPE) ; polymorphic schema with optional constraints
```
`PARAM` and `RETURN-PARAM` are types, optionally wrapped in a mode
@@ -140,6 +229,16 @@ Effects are a set; order is irrelevant.
Built-in type-constructors: `Int`, `Bool`, `Str`, `Unit`. User ADTs
use the name from the `(data ...)` def.
A `(forall ...)` may carry an optional `(constraints ...)` clause whose
inner items are `(constraint CLASS-REF TYPE)` pairs. Each constraint
requires the named class to have an instance at the given type; `TYPE`
is typically a type variable bound by the same `forall`. `CLASS-REF`
follows the canonical-form rule (bare for same-module, `MODULE.CLASS`
for cross-module). At a call site, every constraint must discharge — by
a matching instance in the workspace or by another constraint in the
caller's own schema. An undischargeable constraint fires `no-instance`
at `ail check`.
Examples:
```
@@ -147,6 +246,8 @@ Examples:
(con List (con Int))
(fn-type (params (own (con List)) (borrow (con Int))) (ret (con Int)))
(forall (vars a) (fn-type (params (con List a)) (ret (con Int))))
(forall (vars a) (constraints (constraint prelude.Ord a))
(fn-type (params a a) (ret a)))
```
## Terms
-9
View File
@@ -46,12 +46,3 @@ pub fn def_hash(def: &Def) -> String {
let hex = h.to_hex();
hex.as_str()[..16].to_string()
}
// `#[cfg(test)] mod tests` block relocated to
// `crates/ailang-core/tests/hash_pin.rs` in iter form-a.1 Task 5. The
// integration-test crate has `ailang-surface` as a dev-dependency so
// the schema-stability pins can load `.ail` fixtures via
// `ailang_surface::load_module`, eliminating the production-source
// dependency on `.ail.json` fixture reads.
#[cfg(test)]
mod tests {}
+21 -14
View File
@@ -1,24 +1,32 @@
//! Round-trip gate for the form-(A) projection.
//! Roundtrip Invariant gate (in-process) for the form-(A) projection.
//!
//! Two complementary checks over `examples/*.ail`, each gathering
//! fixtures dynamically via `read_dir` (no hardcoded lists). The
//! tests are pure readers — they do not write into the working
//! tree.
//!
//! 1. `parse_is_deterministic_over_every_ail_fixture`: for every
//! 1. `parse_is_deterministic_over_every_ail_fixture` — pins
//! **parse-determinism** (property 1 of the post-form-a Roundtrip
//! Invariant, DESIGN.md §"Roundtrip Invariant"). For every
//! well-formed `.ail` text `t`, `parse(t)` produces the same
//! canonical bytes every invocation. 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. `parse_then_print_then_parse_is_idempotent_on_every_ail_fixture`:
//! Direction 2 of the Roundtrip Invariant (DESIGN.md §"Roundtrip
//! Invariant"). For every well-formed `.ail` text `t`, asserts
//! 2. `parse_then_print_then_parse_is_idempotent_on_every_ail_fixture`
//! — pins **idempotency-under-print** (property 2). For every
//! well-formed `.ail` text `t`, asserts
//! `canonical_bytes(parse(t))` equals
//! `canonical_bytes(parse(print(parse(t))))`. The printer is a
//! left-inverse of the parser modulo canonical form.
//!
//! The other two properties of the post-form-a Roundtrip Invariant
//! live in sibling test crates: **CLI-pipeline-idempotency** is
//! pinned by `crates/ail/tests/roundtrip_cli.rs::cli_parse_then_render_then_parse_is_idempotent`,
//! and **carve-out-anchor** is pinned by
//! `crates/ailang-core/tests/carve_out_inventory.rs::examples_ail_json_inventory_matches_carve_outs`.
//!
//! Retired iter form-a.1 T9: `print_then_parse_round_trips_every_fixture`
//! (corpus shrunk to 8 carve-outs post-iter) and
//! `every_ail_fixture_matches_its_json_counterpart` (no longer
@@ -60,16 +68,15 @@ fn list_ail_fixtures() -> Vec<PathBuf> {
paths
}
/// Direction 2 of the Roundtrip Invariant (DESIGN.md §"Roundtrip
/// Invariant"): for every well-formed `.ail` text `t`, the
/// composition `parse → print → parse` is idempotent on the AST.
/// Pins **idempotency-under-print** (property 2 of the post-form-a
/// Roundtrip Invariant, DESIGN.md §"Roundtrip Invariant"): for every
/// well-formed `.ail` text `t`, the composition `parse → print → parse`
/// is idempotent on the AST.
///
/// For the 57 `.ail` fixtures that have a JSON counterpart this
/// follows logically from `print_then_parse_round_trips_every_fixture`
/// + `every_ail_fixture_matches_its_json_counterpart`. This test
/// asserts the property directly so it stays robust for future
/// `.ail` fixtures without a JSON counterpart, and so the spec's
/// Direction-2 claim has a dedicated enforcement point.
/// Direct enforcement complements the parse-determinism gate above
/// (which alone does not constrain the printer). Together they pin
/// `parse → print` as a left-inverse modulo canonical form for every
/// `.ail` fixture in the corpus.
#[test]
fn parse_then_print_then_parse_is_idempotent_on_every_ail_fixture() {
let fixtures = list_ail_fixtures();