Iter 15e: render is the exact inverse of parse — CLI hygiene

The CLI help text claimed `render` was symmetric to `parse`, but
`render` was wired to ailang_core::pretty::module (an older
human-pretty form) while `parse` consumed form (A). Two different
"text projections" coexisted under one name.

Fix: Cmd::Render and Cmd::Describe (both branches) now call
ailang_surface::print, the actual inverse of `parse`. Round-trip
gate via `ail render | ail parse` is now an e2e test in
crates/ail/tests/e2e.rs in addition to the existing unit-level
round-trip across all 25 fixtures in ailang-surface/tests.

Cleanup: `ailang_core::pretty::module` and its three private
helpers (`def_block`, `term_block`, `term_inline`) deleted —
~260 LOC of duplicate-purpose code removed. pretty.rs goes
from 457 to 196 LOC. Surviving surface is intentionally narrow:
manifest, type_to_string, pattern_to_string — the diagnostic
stringification used in error messages, where one-line ML
notation reads better than form (A)'s nested S-expressions.
Module-level doc rewritten to nail down the single-purpose
framing.

Tests: 89/89 (e2e +1, ailang-core unit -1 since the deleted
test exercised the deleted fn).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 18:54:07 +02:00
parent df5b76bd65
commit 0b1b39f829
5 changed files with 140 additions and 286 deletions
+11 -7
View File
@@ -13,8 +13,9 @@
//!
//! - `manifest` — compact symbol table of a module (or workspace);
//! one line per def with type, effects, hash.
//! - `render` — pretty-print a `.ail.json` module as the textual
//! `.ail` projection.
//! - `render` — print a `.ail.json` module as form-(A) text. Exact
//! inverse of `parse`; piping `render | parse` round-trips to the
//! same canonical bytes.
//! - `describe` — full detail of a single definition, JSON or text.
//! - `deps` — static call edges per def (or for one named def);
//! workspace mode emits cross-module edges.
@@ -70,7 +71,10 @@ enum Cmd {
#[arg(long)]
workspace: bool,
},
/// Prints the module in text form (pretty-printer).
/// Prints the module as form (A) text — the canonical authoring
/// surface and the exact inverse of `parse`. Round-tripping
/// `render | parse` reproduces the input's canonical bytes
/// (gated by `crates/ailang-surface/tests/round_trip.rs`).
Render { path: PathBuf },
/// Prints a single definition as JSON or pretty text.
Describe {
@@ -278,7 +282,7 @@ fn main() -> Result<()> {
}
Cmd::Render { path } => {
let m = ailang_core::load_module(&path)?;
print!("{}", ailang_core::pretty::module(&m));
print!("{}", ailang_surface::print(&m));
}
Cmd::Describe { path, name, json, workspace } => {
if workspace {
@@ -310,7 +314,7 @@ fn main() -> Result<()> {
let h = ailang_core::def_hash(def);
println!("module: {}", mod_name);
println!("hash: {h}");
print!("{}", ailang_core::pretty::module(&one));
print!("{}", ailang_surface::print(&one));
}
} else {
let m = ailang_core::load_module(&path)?;
@@ -323,7 +327,7 @@ fn main() -> Result<()> {
let s = serde_json::to_string_pretty(def)?;
println!("{s}");
} else {
// Pretty form: render module with only this def.
// Form-(A) projection of a one-def module.
let one = ailang_core::Module {
schema: m.schema.clone(),
name: m.name.clone(),
@@ -332,7 +336,7 @@ fn main() -> Result<()> {
};
let h = ailang_core::def_hash(def);
println!("hash: {h}");
print!("{}", ailang_core::pretty::module(&one));
print!("{}", ailang_surface::print(&one));
}
}
}