Files
AILang/crates/ailang-surface/tests/round_trip.rs
T
Brummel 176821c2e7 iter design-md-rolesplit.1 (DONE 9/9): DESIGN.md -> design/ ledger role-split
The 3020-line docs/DESIGN.md is replaced by the design/ ledger:
design/INDEX.md (sole addressable spine, typed Contracts+Models tables,
polymorphic links — prose file OR authoritative source //!), 14
design/contracts/*.md test-linked invariants + 3 source-link-only
contracts (mangling/env-construction/qualified-xref, no prose file —
code is SoT), 5 design/models/*.md whitepapers, and
docs/journals/2026-05-19-design-decision-records.md (the
relitigation-guard archive — every why/rejected/does-not-do/rollback/
empirical ### moved out at ###-granularity). Clean cut: git rm
docs/DESIGN.md, no stub.

RED-first crates/ailang-core/tests/design_index_pin.rs — the 4-clause
anti-regrowth spine (DESIGN.md-gone / every-INDEX-link-resolves /
every-contract-names-a-resolvable-ratifier /
contracts-carry-no-decision-record-prose) — demonstrably RED before,
GREEN after. Build-atomic by task ordering: design_schema_drift.rs's
include_str! (the only compile-time consumer) retargeted to
design/contracts/data-model.md BEFORE the deletion; its
## Data model/## Pipeline slicer dropped (a simplification the split
enables). 2 NoInstance diagnostics + 2 lockstep E2Es retargeted to
design/contracts/{float-semantics,typeclasses}.md. ~12 agent reading
lists + 5 SKILL bodies + CLAUDE.md + skills/README.md + ~25
code/C/.ail/spec comment xrefs retargeted; OQ7 dangling 'Iter 13b'
cite deleted (no forward target — a pointer would be fiction).
honesty-rule.md rewritten so the rule names the new home
(rationale->journals), resolving the recon-found internal
contradiction; the two docs_honesty_pin.rs:70,72 pinned phrases kept
verbatim+contiguous.

Boss-verified independently: cargo test --workspace 646 passed /
0 failed; design_index_pin 4/4; acceptance grep CLEAN of live
DESIGN.md refs (residuals = only the spec-mandated clause-4
deletion-enforcer). 2 DONE_WITH_CONCERNS routed to the mandatory
milestone-close audit: (a) str-abi.md:23 '(iter str-concat,
2026-05-13)' provenance stamp trips advisory architect_sweeps Sweep-1
— Boss-confirmed byte-identical to DESIGN.md@deeffb1:2062-2065, a
faithfully-migrated PRE-EXISTING anchor (regexes verbatim, only path
retargeted), NOT split-introduced — RATIFY-or-tidy at audit; (b) a
now stale-direction intra-prose 'see Str ABI below' cross-ref in
float-semantics.md — audit-adjudication candidate. Plan defect noted:
Task 9 Step 4's verbatim acceptance grep used a ^./ anchor not
matching the system's grep -rIn output; substance re-verified CLEAN.

Spec grounding-check PASS x2. Journals INDEX + decision-records
pointer appended (Boss-only).
2026-05-19 13:04:22 +02:00

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/contracts/roundtrip-invariant.md). 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/contracts/roundtrip-invariant.md): 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");
}