Files
AILang/docs/PROSE_ROUNDTRIP.md
T
Brummel 9c09dfbc8d ail: merge-prose subcommand + PROSE_ROUNDTRIP.md (iter 20d)
Closes family 20: prose-edit cycle is end-to-end. ail merge-prose
<original.ail.json> <edited.prose.txt> composes a shaped LLM prompt
to stdout. User pipes to their LLM (Claude/etc.), gets new .ail.json,
runs ail check, saves.

No built-in API client — AILang stays a compiler + tooling, the LLM
mediator is supplied externally. PROSE_ROUNDTRIP.md documents the
6-step cycle, the prompt contract (preserve modes/effects/tail
flags from original), failure modes, and the design choice to omit
an API client.

Family 20 now closes: 20a renderer + 20b polish + 20d mediator
shipped; 20c rolled into 20a. Three deferred polishes (let-inlining,
print sugar, doc-wrap widow control) wait for corpus signal.
2026-05-08 18:22:46 +02:00

6.4 KiB
Raw Blame History

Prose round-trip cycle

Why

The prose surface (ail prose, Iter 20a/20b) is a deliberately lossy projection of a .ail.json module: it strips (con T) wrappings, drops redundant parentheses, infixes arithmetic, suppresses schema rigor that the LLM can re-derive. That makes prose pleasant to read and edit — but it also means there is no syntactic parser that turns edited prose back into a .ail.json.

Re-integration of free-text edits therefore requires an LLM mediator: a model that understands both the prose intent and the load-bearing detail in the original .ail.json, and emits a new .ail.json that respects both. This document describes the workflow, the prompt template ail merge-prose composes for that mediator, and the failure modes to watch for.

The cycle

1.  ail prose foo.ail.json > foo.prose.txt
2.  $EDITOR foo.prose.txt                     # human edits freely
3.  ail merge-prose foo.ail.json foo.prose.txt > prompt.txt
4.  cat prompt.txt | <your-llm-cli> > foo.new.ail.json
5.  ail check foo.new.ail.json
6.  mv foo.new.ail.json foo.ail.json          # if check is clean

Step 1 is the deterministic projection (Iter 20a/20b). Step 3 is the prompt composer this iter ships. Step 4 is the user's external LLM client — Claude Code, the Anthropic API, OpenAI's CLI, anything that takes a prompt on stdin and emits text on stdout. AILang ships no client of its own; see Why no built-in API client below. Steps 5 and 6 reuse existing tooling: ail check validates schema + typechecks, then the user accepts the new module.

The prompt template

ail merge-prose <original.ail.json> <edited.prose.txt> reads both files and prints the following text to stdout. Nothing more, nothing less — no fences, no JSON envelope. The shape is exactly what a human would compose by hand if they wanted to drive the same cycle without the CLI helper.

You are integrating prose edits back into an AILang module.

ROLE
Your job is to produce an updated AILang JSON-AST (.ail.json) that
reflects the human's prose edits while preserving the load-bearing
semantic detail from the original .ail.json.

CONTRACT
The prose is the source of intent: the human edited it to express
what the program should now do. The original .ail.json carries
load-bearing detail that the prose surface elides — preserve those
details unless the prose explicitly contradicts them. Specifically:

  - Mode annotations on fn parameters (`own T`, `borrow T`).
    These are hard contracts (memory model). The prose shows them,
    but if the prose is ambiguous, default to the original.
  - Effect annotations on return types (`with IO`, `with Diverge`).
    Prose shows these too, but again: if uncertain, keep what the
    original had.
  - `tail` flags on calls. The prose prints `tail f(x)`; if the
    edit moved a call, decide whether the new position is still in
    tail position and flag accordingly.
  - Doc strings (`///` lines).
  - Type annotations on signatures and lambdas.
  - Constructor names and arities (the prose `Cons(h, t)` must
    round-trip to a `Term::Ctor` with the right `type` field).

OUTPUT
Output ONLY the new .ail.json bytes. No commentary, no markdown
fences, no preamble or postscript. The output must be valid JSON
parsable by `ail parse` (i.e. by `serde_json` against the
`ailang/v0` schema). The first byte should be `{` and the last
non-whitespace byte should be `}`.

SCHEMA ESSENTIALS
A full reference lives in docs/DESIGN.md ("Data model (MVP)"). The
shape is:

  - Module: { "schema": "ailang/v0", "name": ..., "imports": [...],
    "defs": [...] }
  - Defs are tagged via `kind` ("fn" | "type" | "const").
  - Types are tagged via `k` ("con" | "fn" | "var" | "forall").
  - Terms are tagged via `t` ("lit" | "var" | "app" | "let" | "if"
    | "do" | "ctor" | "match" | "lam" | "seq").
  - Patterns are tagged via `p` ("var" | "lit" | "ctor" | "wild").
  - On `fn-type`: `param_modes` (one of "own"/"borrow" per
    parameter) and `effects` are mandatory. `ret_mode` is mandatory
    when the return type carries a mode.
  - Constructor application uses `Term::Ctor` (`"t": "ctor"`,
    fields `type` + `ctor` + `args`), NOT `Term::App`. The prose
    surface elides the `type` tag (`Cons(h, t)` instead of
    `(term-ctor IntList Cons h t)`) — re-introduce it.
  - Base type names that appear bare in prose (`Int`, `Bool`,
    `Unit`, user types like `IntList`) must be wrapped as
    `{"k": "con", "name": "Int"}` etc. in the JSON.

ORIGINAL .ail.json
<<<ORIGINAL_AIL_JSON
{original_ail_json_here}
ORIGINAL_AIL_JSON

EDITED PROSE
<<<EDITED_PROSE
{edited_prose_here}
EDITED_PROSE

Emit the updated .ail.json now.

The two payloads (original JSON, edited prose) are inserted verbatim between the heredoc-style markers. merge-prose does not strip, re-encode, or otherwise transform either input — both go in byte-for-byte.

Failure modes

The mediator is an LLM, so the cycle is intentionally iterative. Common failure modes and their fix:

  • Output wrapped in markdown fences (```json ... ```). Strip the fences (sed -n '/^{/,/^}/p') and re-run ail check. If the LLM does this consistently, paste a corrective note ("emit raw JSON, no fences") at the top of the next prompt.
  • Output contains commentary before/after the JSON. Same treatment. Most modern LLMs respect "OUTPUT ONLY" but not all.
  • Invalid JSON / serde_json rejects. Re-run with the parse error pasted into the prompt as a corrective note.
  • Schema-valid but ail check fails. Re-run with the diagnostic messages pasted in. Common cases: missing param_modes, Term::App instead of Term::Ctor, bare type names instead of {"k":"con","name":"…"}.
  • Preserved detail dropped (mode flipped, doc string lost). Re-run with a corrective note naming the missing detail.

The cycle converges in 13 rounds for typical edits.

Why no built-in API client

AILang stays a compiler + tooling. Shipping an HTTP client to one particular LLM vendor would mean an API key story, rate limiting, streaming semantics, error mapping, version tracking — all carrying zero language-level value. The user already has Claude Code, the Anthropic SDK, the OpenAI CLI, curl, or any other client they prefer; ail merge-prose composes the prompt and gets out of the way. The Unix-pipe shape (ail merge-prose ... | client | ail check) also makes the cycle scriptable without lock-in to one vendor.