prose: human-readable projection renderer (iter 20a)

New crate ailang-prose with one public fn module_to_prose(&Module)
-> String. Rust-flavour with braces, =>-match-arms, mode keywords
(own/borrow), effects as trailing 'with IO', Cons(1, Nil) ctor
form, /// doc strings.

Lossy projection where the LLM can re-derive: (con T) wrap,
(fn-type ...) wrap, (term-ctor ...) wrap. Load-bearing semantic
detail stays visible (modes, effects, clone, reuse-as, doc strings,
type annotations, tail flag).

CLI: 'ail prose <file.ail.json>' prints the projection.

3 snapshot fixtures + 28 unit tests. 215-line .ail.json reduces
to ~18 lines of legible source.

20b queued: infix arithmetic, paren elision, let-inlining, do
prettify.
This commit is contained in:
2026-05-08 18:06:16 +02:00
parent d8e80cb9c5
commit a9d57c5c81
11 changed files with 1171 additions and 0 deletions
+85
View File
@@ -8879,3 +8879,88 @@ is the form-B *projection*).
- Whether snapshot tests live in `crates/ailang-prose/tests/`
or `examples/` (as `.prose.txt` siblings to the .ail.json).
## 2026-05-08 — Iter 20a: prose renderer skeleton shipped
The renderer-side of the family-20 design pinning. New crate
`ailang-prose` with one public fn `module_to_prose(&Module) -> String`,
plus a `ail prose <file.ail.json>` CLI subcommand that prints the
projection to stdout.
### Style commitments (orchestrator-fixed for 20a, not up to renderer)
- Rust-flavour with braces and `=>` for match arms.
- Mode keywords `own T` / `borrow T` (not sigils — Decision-10 consistency).
- Effects trailing the return type: `with IO`.
- Constructor application as `Cons(1, Nil)` (no `(term-ctor ...)` wrap).
- Types as bare names: `Int`, `List<Int>` (no `(con ...)` wrap).
- Doc strings as `///` lines above the def.
- `tail` flag on calls renders as a `tail ` prefix.
- All other load-bearing semantic detail stays visible: `clone`,
`reuse-as`, effects, doc strings, type annotations on signatures
and lambdas.
- No infix yet; arithmetic primitives stay as `i64-add(a, b)`.
That's 20b.
### Snapshot coverage
Three fixtures got `.prose.txt` siblings, asserted by
`crates/ailang-prose/tests/snapshot.rs`:
- `examples/rc_own_param_drop.prose.txt`
- `examples/rc_match_arm_partial_drop_leak.prose.txt`
- `examples/rc_app_let_partial_drop_leak.prose.txt`
Sample (`rc_own_param_drop.prose.txt`):
```
fn head_or_zero(xs: own IntList) -> Int {
match xs {
Nil => 0,
Cons(h, t) => h
}
}
```
vs. the 215-line `.ail.json` it projects from.
The `head_or_zero` body is exactly the kind of thing the user
named: dramatically less syntactic noise, immediately legible to
a human, with `own` mode and `Cons(h, t)` as conventional source
forms.
### Visible nits queued for 20b
- Long doc strings stay on one wrap-less line.
- `do io/print_int(x)` could be `print(x)` for the IO/Int-print
default.
- Nested match arms could break across lines.
- Arithmetic primitives are still prefix.
- No precedence-aware paren elision yet.
All explicitly out of 20a's scope; pinned for 20b.
### Files
- New crate `crates/ailang-prose/` (`Cargo.toml`, `src/lib.rs`
~924 lines, `tests/snapshot.rs` 64 lines).
- `Cargo.toml` (root) + `crates/ail/Cargo.toml`: workspace + dep wiring.
- `crates/ail/src/main.rs`: `Cmd::Prose { path }` variant + dispatch.
- Three new `examples/*.prose.txt` snapshots.
### Build/test status
- `cargo build --workspace` — green, no warnings.
- `cargo test --workspace` — 28 prose-unit + 3 prose-snapshot pass;
existing 69 e2e + 53 ailang-check + others all unchanged-green.
### What stays queued
- **Iter 20b** — formatting polish: infix for arithmetic, paren
elision by precedence, let-inlining, prettier `do`, line-wrap
for long doc strings.
- **Iter 20c** — the CLI subcommand was bundled into 20a (one
natural unit). Originally pinned as a separate iter; collapsed.
- **Iter 20d** — the round-trip mediator (prose-edit → updated
AIL via LLM call). Specification + prompt template, not a
compiler pass.