Iter 9c: docs (DESIGN.md + JOURNAL.md)

DESIGN.md:
- CLI block: add `ail run`.
- Smoke-test list: add list_map.ail.json (the dogfood example).

JOURNAL: append Iter 9 entry covering the dogfood result, two
friction points surfaced (single-arg fn-type pretty-print parens;
absence of a sequencing operator), the `ail run` CLI helper, and a
ranked Iter 10 plan with three candidates (polymorphism, `;`, GC).
Tentative pick is (2) `;` next, (1) polymorphism for Iter 11.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 13:10:28 +02:00
parent 849eca4fcf
commit 4852df51fc
2 changed files with 105 additions and 0 deletions
+3
View File
@@ -226,6 +226,7 @@ ail render <module> — JSON → pretty-print
ail parse <module.ail> — pretty-print → JSON (for bootstrapping)
ail emit-ir <module> — writes .ll
ail build <module> — full pipeline → binary
ail run <module> — build + execute (tempdir), passthrough exit code
```
## Verification and correctness (across cycles)
@@ -282,3 +283,5 @@ Pipeline regression smoke tests:
- `examples/list.ail.json` → prints 42 (ADTs + match).
- `examples/hof.ail.json` → prints 42 (first-class fn-refs, indirect call).
- `examples/closure.ail.json` → prints 42 (lambda capturing a let-bound var).
- `examples/list_map.ail.json` → prints 2/4/6 (ADTs + closure + recursive
HOF + IO; the dogfood smoke test).
+102
View File
@@ -657,3 +657,105 @@ Leaning toward (1) for the next iteration: it's the smaller bite
*and* the bigger expressivity unlock. (2) becomes acute only when
someone tries to run an unbounded loop, which the current examples
don't.
## 2026-05-07 — Iter 9 done: dogfood + `ail run`
Course-corrected from the Iter-8 plan. Polymorphism is the bigger
expressivity unlock on paper, but I hadn't actually proved that the
language was sufficient for "small but real" programs without it. So
Iter 9 became a dogfood iteration: write a non-trivial program
that exercises everything Iter 1-8 shipped, and use `ail run` /
errors / type-checker output as the user would. If something broke,
fix it. If nothing broke, document the boundary moved.
**`examples/list_map.ail.json`**:
```jsonc
type IntList = Nil | Cons Int IntList
map_int :: ((Int) -> Int, IntList) -> IntList
map_int(f, xs) = match xs {
Nil -> Nil
Cons(h, t) -> Cons(f(h), map_int(f, t))
}
print_list :: (IntList) -> Unit !IO
print_list(xs) = match xs {
Nil -> ()
Cons(h, t) -> let _ = do io/print_int(h) in print_list(t)
}
main = let xs = Cons 1 (Cons 2 (Cons 3 Nil)) in
print_list(map_int(\\x. x * 2, xs))
```
**Result:** nothing broke. Output `2\\n4\\n6\\n`, exit 0. The full
pipeline (`ail run`) covers: ADTs with two ctors of different
arity; pattern matching with nested `Var` fields; recursion over
ADT; closures (with no captures here, so env is null but the
closure-pair plumbing still gets exercised); fn-typed parameters in
a top-level def; `do io/...` inside a match arm body, with `let _`
to sequence two effectful operations; effect propagation through
the call chain. This validates Iter 1-8 as a self-contained
foundation.
**Friction surfaced:** writing the AST by hand is tedious — the
JSON for this 4-def module is 200+ lines. That's not surprising
(the format is for LLMs, not humans), but it suggests an Iter 10
priority: a richer pretty-print form, or an `ail snippet` helper
for common boilerplate (`mk_list_int`, etc.). Not blocking; noted.
**`ail run` (Iter 9b):** Builds into a tempdir + execs the binary,
exit code passthrough. Saves a `cd && ./bin` step in the dogfood
loop. Tiny addition — `Cmd::Build`'s body factored into a shared
`build_to` helper.
**Architecture self-check:**
- *Would I use this language now?* For self-contained Int-typed
programs over recursive ADTs: yes. The list_map example is what
I would have wanted to write since Iter 6 and bounced off
repeatedly. It now compiles and runs without me adapting the
source — the language is what its authors said it was, end to
end.
- *Did I think of everything?* Two cracks observed during the
dogfood:
- `(Int)` parens around single-param fn-types in pretty-print are
visual noise. Cosmetic, can wait.
- `let _ = do <effect> in <body>` is the only way to sequence
effects today. Working as intended given KISS, but a `;`
operator (sequencing) would be cheap polish.
- *Consistency:* DESIGN.md CLI block + smoke-test list updated.
Iter 8c invariant — "What is not (yet) supported" ≡ truth at end
of latest iteration — held; no new pending items.
- *KISS:* Iter 9 added 0 LOC of language semantics. All gain came
from validating the existing surface and a small CLI helper.
**Tests:** 50 green (was 49). New e2e
`list_map_doubles_then_prints`. No test for `ail run` itself —
`build_and_run` already exercises the equivalent path.
**Plan iteration 10:**
The dogfood revealed two real-but-not-blocking pain points and one
big architectural gap. Candidates, ranked:
1. **Polymorphic let-bindings with monomorphisation at codegen.**
Allows `let id = \\x. x in (id 1, id true)` and ultimately
`map :: (a -> b) -> List a -> List b`. The ground truth-ier
answer for the "would I use it for X?" question, but a
non-trivial pipeline change (typechecker→codegen needs to thread
instantiation info to the call site).
2. **Sequencing operator `;` and richer effect ergonomics.** A
`Term::Seq { lhs, rhs }` (or compile sugar to `Let { name: "_",
value: lhs, body: rhs }`) plus a small pretty-print update.
Cheap, satisfying.
3. **GC.** Heap reclamation for ADT boxes, lambda envs, closure
pairs. Real architecture step. Becomes acute the moment someone
writes a long-running loop; the current examples don't.
Tentative pick: (2) for the next sprint as a satisfying small
polish, then (1) as Iter 11. (3) bides its time until a real
program needs it.