prose: infix + paren elision + doc-wrap + nested-match (iter 20b)

Eleven binary builtins (+ - * / % == != < <= > >=) render as
lhs op rhs when called with two args (tail-flagged keeps prefix).
Standard Rust-aligned 4-level precedence table elides parens.
not(x) renders as !x. Long /// doc strings wrap at 80 cols on
word boundaries.

bench_list_sum: 'if ==(n, 0)' becomes 'if n == 0', '+(acc, h)'
becomes 'acc + h', tail keyword still visible on tail calls.

19 new unit tests (47 total). 4 snapshots (3 re-rendered + 1 new
bench fixture). Public API unchanged.
This commit is contained in:
2026-05-08 18:13:43 +02:00
parent a9d57c5c81
commit 9cf0e3e81c
5 changed files with 645 additions and 9 deletions
+93
View File
@@ -8964,3 +8964,96 @@ All explicitly out of 20a's scope; pinned for 20b.
- **Iter 20d** — the round-trip mediator (prose-edit → updated
AIL via LLM call). Specification + prompt template, not a
compiler pass.
## 2026-05-08 — Iter 20b: prose formatting polish shipped
The polish pass on top of 20a's renderer skeleton. Four polishes,
no public-API change.
### Polishes
1. **Infix binary operators.** Eleven canonical builtins
(`+ - * / % == != < <= > >=`) when called via
`Term::App { args.len() == 2, tail: false }` render as
`lhs op rhs`. Tail-flagged binary ops keep prefix form so the
`tail` keyword stays visible.
2. **Paren elision by precedence.** Standard 4-level Rust-aligned
table: `* / %` (mul) > `+ -` (add) > `< <= > >=` (cmp,
non-assoc) > `== !=` (eq, non-assoc). Atomic terms (Var,
Lit, Ctor, App-non-binary, etc.) bind tightest. Same-level
left-associative left side: no parens; same-level right
side: keeps parens. Non-assoc ops always keep parens at
same level.
3. **Unary `not`.** `App { callee: Var "not", args: [x] }`
renders as `!x`. Atomic operand binds at level 5 (tightest);
`!(a == b)` keeps parens because the operand is a level-1
binary op.
4. **Long doc-string wrap.** `///` lines exceeding 80 columns
wrap at word boundaries. Explicit newlines split first, then
each piece word-wraps independently.
5. **Nested-match formatting** (already structurally correct in
20a; locked in by a 3-deep test).
### Snapshot impressions
`bench_list_sum.prose.txt` after 20b:
```
fn cons_n_acc(n: Int, acc: IntList) -> IntList {
if n == 0 {
acc
} else {
tail cons_n_acc(n - 1, ICons(n - 1, acc))
}
}
```
vs. pre-20b `if ==(n, 0) { ... -(n, 1) ... ICons(-(n, 1), acc) }`.
The infix conversion is the headline win — arithmetic now reads
as arithmetic.
`rc_own_param_drop.prose.txt` doc string now wraps:
```
/// Take ownership of an IntList; return its head if Cons, else 0. The tail is
/// loaded as a pattern binder but never consumed — iter A dec's it at arm
/// close. The outer cell is dec'd at fn return via iter B's Own-param emission.
```
### Tests
Test count went from 28 → 47 (19 new unit tests in `lib.rs`),
plus a fourth snapshot (`examples/bench_list_sum.prose.txt`) that
exercises infix on a real benchmark fixture. Existing snapshots
re-rendered to incorporate the wrapping.
### Files
- `crates/ailang-prose/src/lib.rs``write_doc` rewrite,
`wrap_words` helper, `PREC_*` constants, `binop_info`,
`as_binop`, `as_unary_not` helpers, `write_term_prec`
threading `parent_prec` through term recursion.
- `crates/ailang-prose/tests/snapshot.rs` — new
`snapshot_bench_list_sum`.
- `examples/bench_list_sum.prose.txt` — new.
- `examples/rc_own_param_drop.prose.txt` — re-rendered.
### Build/test status
- `cargo build --workspace` — green, no warnings.
- `cargo test --workspace` — 47 prose-unit + 4 prose-snapshot
pass; everything else unchanged-green.
### What stays queued
- **Iter 20d** — round-trip mediator. Specification + prompt
template (the LLM bundles original .ail.json + edited prose
and emits the updated .ail.json). Likely a `ail merge-prose`
subcommand that prints a shaped prompt, the user mediates
through their LLM, then `ail parse` (or `ail check`) on the
re-emitted artefact. Possibly: skip the CLI shim, ship as a
documented prompt template under `docs/`.
- **Let-inlining** — questionable polish; deferred until the
corpus shows it's needed.
- **`do io/print_int(x)``print(x)`** — needs type context
to be safe; deferred.