Iter 10: Term::Seq sequencing operator

Adds `Term::Seq { lhs, rhs }` (serde tag "seq") as a first-class
AST node for sequencing effectful expressions. Equivalent in
behaviour to `let _ = lhs in rhs`, but the dedicated node gives the
pretty-printer and diagnostics a cleaner shape and surfaces the
intent ("run for effect, then yield rhs") to future tooling.

Typecheck: lhs must be Unit; rhs's type is the result; effects
accumulate.
Codegen: lower lhs (drop SSA), lower rhs (return).
Capture / deps walkers: recurse into both sides.

Refactored examples/list_map.ail.json's print_list to use seq
instead of `let _ = ...`. Output unchanged (2/4/6).

Hash stability: existing examples without Term::Seq serialise
bit-identical; only list_map.ail.json's hashes changed (deliberate
refactor).

Tests: 51 green (was 50). New unit test
`ailang_check::tests::seq_lhs_must_be_unit` covers the type-error
path; existing list_map e2e covers the happy path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 13:14:28 +02:00
parent 4852df51fc
commit c75517ac79
8 changed files with 131 additions and 5 deletions
+4 -1
View File
@@ -186,12 +186,15 @@ hashes stay bit-identical.
"retType": Type,
"effects": ["<id>"...],
"body": Term }
{ "t": "seq", "lhs": Term, "rhs": Term }
```
In the MVP, `do` is only a direct call to a built-in effect op (no handler).
A `lam` term constructs an anonymous function value; free variables of
its body are captured from the enclosing scope (see Iter 8 closure
conversion in JOURNAL).
conversion in JOURNAL). A `seq` term evaluates `lhs` for its effects
(its result must be Unit) and yields `rhs`'s value — equivalent to
`let _ = lhs in rhs`.
### Type
+51
View File
@@ -759,3 +759,54 @@ big architectural gap. Candidates, ranked:
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.
## 2026-05-07 — Iter 10 done: Term::Seq sequencing
Followed the Iter 9 plan and shipped (2). New AST node
`Term::Seq { lhs, rhs }` with serde tag "seq". Semantics: evaluate
lhs (which must be Unit), discard the value, return rhs. Effects
from both sides accumulate.
This is sugar for `let _ = lhs in rhs`, but it's a first-class node
because:
- The pretty-print renders cleanly (`(seq lhs rhs)` instead of
borrowing the `let` form with a discard binding).
- Diagnostics are sharper: a non-Unit lhs gets a "type mismatch"
error pointing at the seq site, not "binding `_` had type X" at
a let site.
- Future tooling (effect inference visualisation, dataflow) can
treat sequencing as a structural concept instead of a special-
cased let.
Codegen is trivial: lower lhs (drop SSA), lower rhs (return).
Refactored `examples/list_map.ail.json`'s `print_list` to use seq
instead of `let _ = ...`. Output unchanged (`2\\n4\\n6\\n`); the
JSON shed a few lines and reads more honestly.
**Architecture self-check:**
- *Would I use this language now?* Same answer as Iter 9 (yes for
small but real programs), but the seq node makes IO-heavy
recursion read better — closer to "call this effect, then this
one" instead of "bind this effect to nothing, then this one".
- *Did I break anything?* Hash stability check: existing examples
without `Term::Seq` serialise identically; their fn hashes are
unchanged. `list_map.ail.json`'s hashes shifted as expected since
its body changed.
- *KISS:* +30 LOC across AST/pretty/check/codegen/walker. One
unit test for the lhs-must-be-Unit rule. The dogfood example
proves the e2e path.
**Tests:** 51 green (was 50). New `seq_lhs_must_be_unit` unit test
in ailang-check. Existing list_map e2e still passes after the
refactor.
**Plan iteration 11:**
Polymorphism, as queued in the Iter 9 plan. Concretely: HM-style
unification + let-generalisation in the typechecker, monomorph-
isation at codegen time. Touches the typechecker→codegen pipeline.
Bigger commit than the recent stretch, will probably need to be
phased (typechecker substitution machinery, then codegen
specialisation, then docs).