e809f45e67
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.
167 lines
6.8 KiB
Rust
167 lines
6.8 KiB
Rust
//! 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` — 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`
|
|
//! — 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
|
|
//! expressible: only one form is hand-authored post-iter).
|
|
|
|
use std::path::PathBuf;
|
|
|
|
fn examples_dir() -> PathBuf {
|
|
// `CARGO_MANIFEST_DIR` is the surface crate root; examples live two
|
|
// levels up.
|
|
let crate_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
crate_dir.parent().unwrap().parent().unwrap().join("examples")
|
|
}
|
|
|
|
// 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();
|
|
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| n.ends_with(".ail"))
|
|
.unwrap_or(false)
|
|
})
|
|
.collect();
|
|
paths.sort();
|
|
paths
|
|
}
|
|
|
|
/// 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.
|
|
///
|
|
/// 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();
|
|
assert!(
|
|
!fixtures.is_empty(),
|
|
"no .ail fixtures found under {}",
|
|
examples_dir().display()
|
|
);
|
|
|
|
let mut failures = Vec::<String>::new();
|
|
let mut passed = 0usize;
|
|
for path in &fixtures {
|
|
let text = match std::fs::read_to_string(path) {
|
|
Ok(t) => t,
|
|
Err(e) => {
|
|
failures.push(format!("{}: read failed: {e}", path.display()));
|
|
continue;
|
|
}
|
|
};
|
|
let parsed_once = match ailang_surface::parse(&text) {
|
|
Ok(m) => m,
|
|
Err(e) => {
|
|
failures.push(format!("{}: parse(t) failed: {e}", path.display()));
|
|
continue;
|
|
}
|
|
};
|
|
let printed = ailang_surface::print(&parsed_once);
|
|
let parsed_twice = match ailang_surface::parse(&printed) {
|
|
Ok(m) => m,
|
|
Err(e) => {
|
|
failures.push(format!(
|
|
"{}: parse(print(parse(t))) failed: {e}\n--- printed ---\n{printed}",
|
|
path.display()
|
|
));
|
|
continue;
|
|
}
|
|
};
|
|
let bytes_a = ailang_core::canonical::to_bytes(&parsed_once);
|
|
let bytes_b = ailang_core::canonical::to_bytes(&parsed_twice);
|
|
if bytes_a != bytes_b {
|
|
let s_a = String::from_utf8_lossy(&bytes_a).into_owned();
|
|
let s_b = String::from_utf8_lossy(&bytes_b).into_owned();
|
|
failures.push(format!(
|
|
"{}: parse → print → parse is NOT idempotent.\nparse(t): {s_a}\nparse(print(parse(t))): {s_b}",
|
|
path.display()
|
|
));
|
|
continue;
|
|
}
|
|
passed += 1;
|
|
}
|
|
|
|
if !failures.is_empty() {
|
|
panic!(
|
|
"{} of {} .ail fixture(s) failed idempotency check (passed: {}):\n{}",
|
|
failures.len(),
|
|
fixtures.len(),
|
|
passed,
|
|
failures.join("\n\n")
|
|
);
|
|
}
|
|
eprintln!("parse→print→parse idempotency ok for {passed} .ail fixtures");
|
|
}
|
|
|
|
/// Parse-determinism (post-form-a-default-authoring §C3): for every
|
|
/// well-formed `.ail` text `t`, `parse(t)` produces the same canonical
|
|
/// bytes every invocation. Loader is a pure function of input — no
|
|
/// randomness, no time dependence, no environment leak.
|
|
#[test]
|
|
fn parse_is_deterministic_over_every_ail_fixture() {
|
|
let mut checked = 0usize;
|
|
for ail_path in list_ail_fixtures() {
|
|
let text = std::fs::read_to_string(&ail_path)
|
|
.unwrap_or_else(|e| panic!("read {ail_path:?}: {e}"));
|
|
let m1 = ailang_surface::parse(&text)
|
|
.unwrap_or_else(|e| panic!("parse {ail_path:?}: {e:?}"));
|
|
let m2 = ailang_surface::parse(&text)
|
|
.unwrap_or_else(|e| panic!("parse {ail_path:?} (2nd): {e:?}"));
|
|
let b1 = ailang_core::canonical::to_bytes(&m1);
|
|
let b2 = ailang_core::canonical::to_bytes(&m2);
|
|
assert_eq!(
|
|
b1, b2,
|
|
"parse non-determinism on {ail_path:?}",
|
|
);
|
|
checked += 1;
|
|
}
|
|
assert!(checked > 0, "no .ail fixtures found");
|
|
}
|