iter form-a.1 (Tasks 6-12): milestone close

Second half of the form-a-default-authoring milestone-close iter
(Boss-decided strategy C, big-bang). All seven tasks DONE; cargo
test --workspace green at every per-task boundary.

T6 — Bench-driver suffix flip from .ail.json to .ail across 4
Python scripts + run.sh. compile_check.py + cross_lang.py exit 0.

T7 — Re-author e2e.rs raw-JSON-inspect tests:
- diff_detects_changed_def — derive sum.ail.json on-the-fly via
  `ail parse examples/sum.ail` into tempdir, then mutate + diff.
- borrow_own_demo_modes_are_metadata_only — same pattern.
- reuse_as_demo_under_rc_uses_inplace_rewrite — same pattern.
- render_parse_round_trip_canonical — RETIRED (subsumed by T1's
  cli_parse_then_render_then_parse_is_idempotent over whole corpus).
- ail_run_accepts_ail_source_with_same_stdout_as_ail_json —
  re-authored to derive hello.ail.json in a per-process tempdir
  from hello.ail via `ail parse`, then assert dual-form stdout.

T8 — Bulk-delete 156 non-carve-out .ail.json. Inventory:
8 .ail.json (carve-outs, alphabetical: broken_unbound + prelude
+ 3× test_22b2_* + 3× test_ct1_*) + 157 .ail. carve_out_inventory
test un-#[ignore]'d and green. Forward-pulled 20 repairs that the
T1-5 dispatch's recon missed (12 Group-B suffix + 5 Group-A
load_workspace + 3 ail_run sites). Also forward-pulled T9 Step 5
(schema_coverage corpus flip from .ail.json to .ail) to satisfy
T8's green-gate.

T9 — Retire obsolete roundtrip tests:
- print_then_parse_round_trips_every_fixture (round_trip.rs)
- every_ail_fixture_matches_its_json_counterpart (round_trip.rs)
- cli_render_then_parse_preserves_canonical_bytes_on_every_fixture
- Dead helpers: list_json_fixtures ×2, round_trip_one,
  strip_trailing_newlines.
Schema-coverage corpus already flipped in T8 (forward-pull).

T10 — DESIGN.md §"Roundtrip Invariant" (lines 2027-2109) restated
with parse-determinism + idempotency + CLI-pipeline-idempotency +
carve-out-anchor framing. Five surviving enforcement tests named.
§"Float literals" and §"Why anchored at top level" preserved.

T11 — §A4 doctrine edits: CLAUDE.md:5-6 + DESIGN.md:465-466.
Canonical form remains JSON-AST; authoring projection is .ail;
build derives JSON-AST in-process via ailang_surface::parse.

T12 — Milestone close:
- WhatsNew entry: user-facing language, lead with the change.
- Roadmap: [milestone] form-a struck [x] with closing note.
- Final inventory verified: 8 .ail.json + 157 .ail.
- Final cargo test --workspace: 557 passed, 0 failed, 3 ignored.
- bench/compile_check.py + bench/cross_lang.py: exit 0.

Test math: pre-iter 558 baseline + 3 new T1 tests = 561, − 1
(T7 retire) − 3 (T9 retire) = 557 final.

INDEX.md appended with the full iter summary covering T1-T12 (the
T1-5 commit at 77b28ad deferred the INDEX line to full-iter close).

Milestone [Form-A as the default authoring surface] structurally
closed. The compile-time-embed carve-out (prelude.ail.json) is
the subject of the queued follow-up milestone [Prelude embed:
Form-A as compile-time source]. audit-form-a runs as the next
dispatch.
This commit is contained in:
2026-05-13 11:31:39 +02:00
parent 9332d1e155
commit 9fdc4cacff
173 changed files with 551 additions and 6645 deletions
+27 -170
View File
@@ -1,29 +1,30 @@
//! Round-trip gate for the form-(A) projection.
//!
//! Three complementary checks over `examples/`, each gathering
//! 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. `print_then_parse_round_trips_every_fixture`: Direction 1 of
//! the Roundtrip Invariant (DESIGN.md §"Roundtrip Invariant").
//! For every `examples/*.ail.json` fixture, load → `print` →
//! `parse` → canonical bytes; assert byte-equal to original.
//! 1. `parse_is_deterministic_over_every_ail_fixture`: 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. `every_ail_fixture_matches_its_json_counterpart`: hand-
//! authored ground-truth check. For every `examples/*.ail`
//! fixture, parse → canonical bytes; if a same-stem `.ail.json`
//! counterpart exists, assert canonical-byte equality against
//! it. Pins the `.ail` corpus against semantic drift between
//! the two forms at the fixture level.
//! 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
//! `canonical_bytes(parse(t))` equals
//! `canonical_bytes(parse(print(parse(t))))`. The printer is a
//! left-inverse of the parser modulo canonical form.
//!
//! 3. `parse_then_print_then_parse_is_idempotent_on_every_ail_fixture`:
//! Direction 2 of the Roundtrip Invariant. For every well-formed
//! `.ail` text `t`, asserts `canonical_bytes(parse(t))` equals
//! `canonical_bytes(parse(print(parse(t))))`. Robust against
//! future `.ail` fixtures without a JSON counterpart.
//! 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
//! expressible: only one form is hand-authored post-iter).
use std::path::{Path, PathBuf};
use std::path::PathBuf;
fn examples_dir() -> PathBuf {
// `CARGO_MANIFEST_DIR` is the surface crate root; examples live two
@@ -32,73 +33,15 @@ fn examples_dir() -> PathBuf {
crate_dir.parent().unwrap().parent().unwrap().join("examples")
}
fn list_json_fixtures() -> Vec<PathBuf> {
let dir = examples_dir();
let mut paths: Vec<PathBuf> = std::fs::read_dir(&dir)
.unwrap_or_else(|e| panic!("read_dir({}): {e}", dir.display()))
.filter_map(|entry| entry.ok())
.map(|e| e.path())
.filter(|p| {
p.file_name()
.and_then(|n| n.to_str())
.map(|n| {
// Round-trip every fixture. The 22b.1/22b.2/22b.3 filter
// was retired in 22b.4a once the Form-A parser+printer
// arms for ClassDef / InstanceDef landed.
n.ends_with(".ail.json")
})
.unwrap_or(false)
})
.collect();
paths.sort();
paths
}
#[test]
fn print_then_parse_round_trips_every_fixture() {
let fixtures = list_json_fixtures();
assert!(
!fixtures.is_empty(),
"no .ail.json fixtures found under {}",
examples_dir().display()
);
let mut failures = Vec::<String>::new();
let mut passed = 0usize;
for path in &fixtures {
match round_trip_one(path) {
Ok(()) => passed += 1,
Err(msg) => failures.push(format!("{}: {msg}", path.display())),
}
}
if !failures.is_empty() {
panic!(
"round-trip failed for {} of {} fixtures (passed: {}):\n{}",
failures.len(),
fixtures.len(),
passed,
failures.join("\n")
);
}
eprintln!("round-trip ok for {passed} fixtures");
}
fn round_trip_one(path: &Path) -> Result<(), String> {
let original = ailang_core::load_module(path).map_err(|e| format!("load: {e}"))?;
let text = ailang_surface::print(&original);
let parsed = ailang_surface::parse(&text)
.map_err(|e| format!("re-parse failed: {e}\n--- printed text ---\n{text}"))?;
let bytes_orig = ailang_core::canonical::to_bytes(&original);
let bytes_round = ailang_core::canonical::to_bytes(&parsed);
if bytes_orig != bytes_round {
let s_orig = String::from_utf8_lossy(&bytes_orig).into_owned();
let s_round = String::from_utf8_lossy(&bytes_round).into_owned();
return Err(format!(
"canonical bytes differ.\noriginal: {s_orig}\nround: {s_round}"
));
}
Ok(())
}
// Retired iter form-a.1 T9:
// - `list_json_fixtures` helper (walked `.ail.json` for the two retired tests below).
// - `print_then_parse_round_trips_every_fixture`: corpus shrunk to 8 carve-outs
// post-iter; property subsumed by `parse_is_deterministic_over_every_ail_fixture`
// plus the surviving `parse_then_print_then_parse_is_idempotent_on_every_ail_fixture`.
// - `round_trip_one` helper (only consumer was the retired test above).
// - `every_ail_fixture_matches_its_json_counterpart`: no longer expressible —
// only one form is hand-authored post-iter; counterparts are derived in-process
// via `ail parse`.
fn list_ail_fixtures() -> Vec<PathBuf> {
let dir = examples_dir();
@@ -117,92 +60,6 @@ fn list_ail_fixtures() -> Vec<PathBuf> {
paths
}
#[test]
fn every_ail_fixture_matches_its_json_counterpart() {
let fixtures = list_ail_fixtures();
assert!(
!fixtures.is_empty(),
"no .ail fixtures found under {}",
examples_dir().display()
);
let mut failures = Vec::<String>::new();
let mut paired = 0usize;
let mut parse_only = 0usize;
for ail_path in &fixtures {
let text = match std::fs::read_to_string(ail_path) {
Ok(t) => t,
Err(e) => {
failures.push(format!("{}: read failed: {e}", ail_path.display()));
continue;
}
};
let parsed = match ailang_surface::parse(&text) {
Ok(m) => m,
Err(e) => {
failures.push(format!("{}: parse failed: {e}", ail_path.display()));
continue;
}
};
// Same-stem counterpart lookup. `<stem>.ail` → `<stem>.ail.json`.
let stem = ail_path
.file_name()
.and_then(|n| n.to_str())
.and_then(|n| n.strip_suffix(".ail"))
.unwrap_or("");
let json_path = ail_path.with_file_name(format!("{stem}.ail.json"));
if !json_path.exists() {
// Spec: parse success alone is sufficient for `.ail` files
// without a JSON counterpart. (Today: none expected.)
parse_only += 1;
continue;
}
let original = match ailang_core::load_module(&json_path) {
Ok(m) => m,
Err(e) => {
failures.push(format!(
"{}: load_module({}) failed: {e}",
ail_path.display(),
json_path.display()
));
continue;
}
};
let bytes_orig = ailang_core::canonical::to_bytes(&original);
let bytes_parsed = ailang_core::canonical::to_bytes(&parsed);
if bytes_orig != bytes_parsed {
let s_orig = String::from_utf8_lossy(&bytes_orig).into_owned();
let s_round = String::from_utf8_lossy(&bytes_parsed).into_owned();
failures.push(format!(
"{} vs {}: canonical bytes differ.\noriginal: {s_orig}\nparsed: {s_round}",
ail_path.display(),
json_path.display()
));
continue;
}
paired += 1;
}
if !failures.is_empty() {
panic!(
"{} of {} .ail fixture(s) failed cross-check (paired ok: {}, parse-only: {}):\n{}",
failures.len(),
fixtures.len(),
paired,
parse_only,
failures.join("\n\n")
);
}
eprintln!(
".ail cross-check ok ({} paired, {} parse-only)",
paired, parse_only
);
}
/// 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.