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:
@@ -1,16 +1,24 @@
|
||||
//! CLI roundtrip: every `examples/*.ail.json` survives
|
||||
//! `ail render` → tempfile → `ail parse` with BLAKE3 hash
|
||||
//! identity on canonical bytes.
|
||||
//! CLI-pipeline idempotency gate: every `examples/*.ail` survives
|
||||
//! `ail parse | ail render | ail parse` with byte identity on the
|
||||
//! canonical JSON output.
|
||||
//!
|
||||
//! This is the user-facing-surface defence line. Crate-internal
|
||||
//! roundtrip tests in `ailang-surface` cover the same property
|
||||
//! at the library level; this test additionally protects the
|
||||
//! `ail render` and `ail parse` CLI wrappers from drift (output
|
||||
//! `ail parse` and `ail render` CLI wrappers from drift (output
|
||||
//! formatting, exit codes, stdout framing).
|
||||
//!
|
||||
//! Pure reader: the test writes only to a `tempfile::TempDir`
|
||||
//! that lives outside the repo and is cleaned up by Drop. No
|
||||
//! committed content is mutated.
|
||||
//!
|
||||
//! Retired iter form-a.1 T9:
|
||||
//! `cli_render_then_parse_preserves_canonical_bytes_on_every_fixture`
|
||||
//! (walked `.ail.json` corpus; replaced by the corpus-flipped
|
||||
//! `cli_parse_then_render_then_parse_is_idempotent` below, added in
|
||||
//! T1). The `list_json_fixtures` helper and `roundtrip_one` helper
|
||||
//! were the only consumers of `.ail.json` walks and are retired
|
||||
//! alongside.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
@@ -28,23 +36,6 @@ fn examples_dir() -> PathBuf {
|
||||
workspace_root().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| n.ends_with(".ail.json"))
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.collect();
|
||||
paths.sort();
|
||||
paths
|
||||
}
|
||||
|
||||
fn list_ail_fixtures() -> Vec<PathBuf> {
|
||||
let dir = examples_dir();
|
||||
let mut paths: Vec<PathBuf> = std::fs::read_dir(&dir)
|
||||
@@ -62,106 +53,6 @@ fn list_ail_fixtures() -> Vec<PathBuf> {
|
||||
paths
|
||||
}
|
||||
|
||||
fn strip_trailing_newlines(mut v: Vec<u8>) -> Vec<u8> {
|
||||
while v.last() == Some(&b'\n') {
|
||||
v.pop();
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
/// Run the full render → parse pipeline for one fixture. Returns
|
||||
/// Ok(()) on hash-identity, Err(reason) otherwise.
|
||||
fn roundtrip_one(fixture: &Path, tmpdir: &Path) -> Result<(), String> {
|
||||
// Step A: load the original fixture and compute canonical bytes
|
||||
// (independent of how the JSON file is formatted on disk).
|
||||
let original_module = ailang_core::load_module(fixture)
|
||||
.map_err(|e| format!("load_module: {e}"))?;
|
||||
let bytes_orig = ailang_core::canonical::to_bytes(&original_module);
|
||||
let h_orig = blake3::hash(&bytes_orig);
|
||||
|
||||
// Step B: `ail render <fixture>` → captured stdout = ail text.
|
||||
let render_out = Command::new(ail_bin())
|
||||
.args(["render", fixture.to_str().unwrap()])
|
||||
.output()
|
||||
.map_err(|e| format!("spawn ail render: {e}"))?;
|
||||
if !render_out.status.success() {
|
||||
return Err(format!(
|
||||
"ail render exit={:?}\nstderr: {}",
|
||||
render_out.status.code(),
|
||||
String::from_utf8_lossy(&render_out.stderr),
|
||||
));
|
||||
}
|
||||
|
||||
// Step C: write the rendered .ail into the tempdir.
|
||||
let stem = fixture
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.and_then(|n| n.strip_suffix(".ail.json"))
|
||||
.unwrap_or("fixture");
|
||||
let tmp_ail = tmpdir.join(format!("{stem}.round.ail"));
|
||||
std::fs::write(&tmp_ail, &render_out.stdout)
|
||||
.map_err(|e| format!("write tempfile {}: {e}", tmp_ail.display()))?;
|
||||
|
||||
// Step D: `ail parse <tmp_ail>` → captured stdout = canonical
|
||||
// bytes + trailing newline.
|
||||
let parse_out = Command::new(ail_bin())
|
||||
.args(["parse", tmp_ail.to_str().unwrap()])
|
||||
.output()
|
||||
.map_err(|e| format!("spawn ail parse: {e}"))?;
|
||||
if !parse_out.status.success() {
|
||||
return Err(format!(
|
||||
"ail parse exit={:?}\nstderr: {}",
|
||||
parse_out.status.code(),
|
||||
String::from_utf8_lossy(&parse_out.stderr),
|
||||
));
|
||||
}
|
||||
let bytes_round = strip_trailing_newlines(parse_out.stdout);
|
||||
let h_round = blake3::hash(&bytes_round);
|
||||
|
||||
if h_orig != h_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!(
|
||||
"BLAKE3 mismatch:\n orig: {}\n round: {}\noriginal bytes: {s_orig}\nround bytes: {s_round}",
|
||||
h_orig.to_hex(),
|
||||
h_round.to_hex(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_render_then_parse_preserves_canonical_bytes_on_every_fixture() {
|
||||
let fixtures = list_json_fixtures();
|
||||
assert!(
|
||||
!fixtures.is_empty(),
|
||||
"no .ail.json fixtures found under {}",
|
||||
examples_dir().display()
|
||||
);
|
||||
|
||||
let tmpdir = tempfile::TempDir::new().expect("create tempdir");
|
||||
let mut failures = Vec::<String>::new();
|
||||
let mut passed = 0usize;
|
||||
|
||||
for fixture in &fixtures {
|
||||
match roundtrip_one(fixture, tmpdir.path()) {
|
||||
Ok(()) => passed += 1,
|
||||
Err(msg) => failures.push(format!("{}: {msg}", fixture.display())),
|
||||
}
|
||||
}
|
||||
|
||||
if !failures.is_empty() {
|
||||
panic!(
|
||||
"CLI roundtrip failed for {} of {} fixtures (passed: {}):\n{}",
|
||||
failures.len(),
|
||||
fixtures.len(),
|
||||
passed,
|
||||
failures.join("\n\n")
|
||||
);
|
||||
}
|
||||
eprintln!("CLI roundtrip ok for {passed} fixtures");
|
||||
}
|
||||
|
||||
/// CLI-pipeline idempotency (post-form-a-default-authoring §C3): for
|
||||
/// every `.ail` fixture, the user-facing pipeline
|
||||
/// `ail parse <ail> | ail render | ail parse` is byte-identical
|
||||
|
||||
Reference in New Issue
Block a user