diff --git a/crates/ail/tests/e2e.rs b/crates/ail/tests/e2e.rs index 053d478..477e406 100644 --- a/crates/ail/tests/e2e.rs +++ b/crates/ail/tests/e2e.rs @@ -93,6 +93,21 @@ fn list_map_doubles_then_prints() { assert_eq!(lines, vec!["2", "4", "6"]); } +/// Iter 11 dogfood: insertion sort over an 11-element IntList. +/// Exercises `<=`, `if`, mutual-leaf recursion (`insert` and `sort`), +/// nested ctor construction, and the Iter 10 seq operator inside +/// print_list. Validates that the language handles deeper ADT +/// recursion + branching without surprises. +#[test] +fn insertion_sort_orders_list() { + let stdout = build_and_run("sort.ail.json"); + let lines: Vec<&str> = stdout.lines().collect(); + assert_eq!( + lines, + vec!["1", "1", "2", "3", "3", "4", "5", "5", "5", "6", "9"], + ); +} + /// Guards `ail diff`: a modified body changes the hash of `sum`, while /// `main` stays unchanged. Expects exit code 1, `changed` contains exactly /// `sum`, `unchanged` contains `main`, `added`/`removed` empty. diff --git a/docs/DESIGN.md b/docs/DESIGN.md index bc3a115..58ab4f3 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -288,3 +288,5 @@ Pipeline regression smoke tests: - `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). +- `examples/sort.ail.json` → prints sorted [3,1,4,1,5,9,2,6,5,3,5] + one-per-line (insertion sort over an 11-element list). diff --git a/docs/JOURNAL.md b/docs/JOURNAL.md index abe504d..57c1f03 100644 --- a/docs/JOURNAL.md +++ b/docs/JOURNAL.md @@ -810,3 +810,85 @@ 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). + +## 2026-05-07 — Iter 11 done: deeper dogfood (insertion sort) + +Pulled back from polymorphism for one more validation cycle before +the architectural step. Polymorphism is a substantial pipeline +change (typechecker substitution + codegen monomorphisation) and I +wanted one more "small but real" program to confirm the existing +foundation holds before disturbing it. + +`examples/sort.ail.json` — insertion sort over `IntList`: + +```jsonc +insert :: Int -> IntList -> IntList +insert(y, xs) = match xs { + Nil -> [y] + Cons(h, t) -> if y <= h then Cons(y, Cons(h, t)) + else Cons(h, insert(y, t)) +} + +sort :: IntList -> IntList +sort(xs) = match xs { + Nil -> Nil + Cons(h, t) -> insert(h, sort(t)) +} + +print_list :: IntList -> Unit !IO // uses Iter 10 seq + +main = print_list(sort([3,1,4,1,5,9,2,6,5,3,5])) +``` + +**Result:** typechecks first try, runs first try, prints +`1 1 2 3 3 4 5 5 5 6 9` (each on its own line). 11-element input, +correct sorted output. The combination of recursive ADT pattern +match + comparison ops + branching + leaf recursion + IO +sequencing all worked end to end without the language tripping me +up. Iter 10's seq made `print_list` notably cleaner than the +`let _ = ...` form would have been. + +**Architecture self-check:** + +- *Would I use this language now?* For "small but real" + monomorphic programs over Int, Bool, Unit, Str, and ADTs of + those: confidently yes. Insertion sort writes out as the + textbook recursion, no bookkeeping that the language couldn't + do for me. +- *Did I think of everything?* The remaining wall is still + polymorphism. Sort over `IntList` needs hand-monomorphisation; + a generic `sort :: (a -> a -> Bool) -> List a -> List a` is + what the language eventually wants. No new architectural cracks + surfaced from this dogfood. +- *Visualisation:* `ail describe sort.ail.json sort` reads the + way I'd expect a sort definition to read, with `IntList` types + inline and the recursive call rendered cleanly. +- *KISS:* Iter 11 added 0 LOC of language semantics and 1 e2e + test. The 250-line JSON for the example is verbose but + mechanical — no friction once you accept that the JSON is the + surface for LLM authors. + +**Tests:** 52 green (was 51). New e2e +`insertion_sort_orders_list`. Pure addition; existing tests +untouched. + +**Plan iteration 12:** + +Now polymorphism. Two more dogfood programs would just keep +producing the "the language is fine for monomorphic programs" +result, which is already established. The real expressivity +unlock — and the answer to "would I use it for X?" for X that +actually needs generic data — is HM inference + let-generalisation ++ monomorphisation. Phased plan: + +12a. Typechecker: introduce a `Subst` (type variable substitution) + and unification. Thread through `synth`. At `let`, generalise + syntactic values (lambdas) — no value-restriction subtlety + needed yet, the MVP has no mutable refs. +12b. Codegen: at each polymorphic call site, the typechecker + records the instantiation. Codegen walks the AST a second + time per (def, instantiation) pair and emits a specialised + version with the type variables substituted by concrete + types in fn signatures. +12c. Docs + a polymorphic `id` test + a generic `map :: (a -> b) + -> List a -> List b` rewrite of `list_map.ail.json`. diff --git a/examples/sort.ail.json b/examples/sort.ail.json new file mode 100644 index 0000000..2158122 --- /dev/null +++ b/examples/sort.ail.json @@ -0,0 +1,328 @@ +{ + "schema": "ailang/v0", + "name": "sort", + "imports": [], + "defs": [ + { + "kind": "type", + "name": "IntList", + "doc": "Singly-linked Int list, boxed.", + "ctors": [ + { "name": "Nil", "fields": [] }, + { + "name": "Cons", + "fields": [ + { "k": "con", "name": "Int" }, + { "k": "con", "name": "IntList" } + ] + } + ] + }, + { + "kind": "fn", + "name": "insert", + "type": { + "k": "fn", + "params": [ + { "k": "con", "name": "Int" }, + { "k": "con", "name": "IntList" } + ], + "ret": { "k": "con", "name": "IntList" }, + "effects": [] + }, + "params": ["y", "xs"], + "doc": "Insert y into a sorted list xs, preserving order.", + "body": { + "t": "match", + "scrutinee": { "t": "var", "name": "xs" }, + "arms": [ + { + "pat": { "p": "ctor", "ctor": "Nil", "fields": [] }, + "body": { + "t": "ctor", + "type": "IntList", + "ctor": "Cons", + "args": [ + { "t": "var", "name": "y" }, + { + "t": "ctor", + "type": "IntList", + "ctor": "Nil", + "args": [] + } + ] + } + }, + { + "pat": { + "p": "ctor", + "ctor": "Cons", + "fields": [ + { "p": "var", "name": "h" }, + { "p": "var", "name": "t" } + ] + }, + "body": { + "t": "if", + "cond": { + "t": "app", + "fn": { "t": "var", "name": "<=" }, + "args": [ + { "t": "var", "name": "y" }, + { "t": "var", "name": "h" } + ] + }, + "then": { + "t": "ctor", + "type": "IntList", + "ctor": "Cons", + "args": [ + { "t": "var", "name": "y" }, + { + "t": "ctor", + "type": "IntList", + "ctor": "Cons", + "args": [ + { "t": "var", "name": "h" }, + { "t": "var", "name": "t" } + ] + } + ] + }, + "else": { + "t": "ctor", + "type": "IntList", + "ctor": "Cons", + "args": [ + { "t": "var", "name": "h" }, + { + "t": "app", + "fn": { "t": "var", "name": "insert" }, + "args": [ + { "t": "var", "name": "y" }, + { "t": "var", "name": "t" } + ] + } + ] + } + } + } + ] + } + }, + { + "kind": "fn", + "name": "sort", + "type": { + "k": "fn", + "params": [{ "k": "con", "name": "IntList" }], + "ret": { "k": "con", "name": "IntList" }, + "effects": [] + }, + "params": ["xs"], + "doc": "Insertion sort: build the result by inserting each head into the sorted tail.", + "body": { + "t": "match", + "scrutinee": { "t": "var", "name": "xs" }, + "arms": [ + { + "pat": { "p": "ctor", "ctor": "Nil", "fields": [] }, + "body": { + "t": "ctor", + "type": "IntList", + "ctor": "Nil", + "args": [] + } + }, + { + "pat": { + "p": "ctor", + "ctor": "Cons", + "fields": [ + { "p": "var", "name": "h" }, + { "p": "var", "name": "t" } + ] + }, + "body": { + "t": "app", + "fn": { "t": "var", "name": "insert" }, + "args": [ + { "t": "var", "name": "h" }, + { + "t": "app", + "fn": { "t": "var", "name": "sort" }, + "args": [{ "t": "var", "name": "t" }] + } + ] + } + } + ] + } + }, + { + "kind": "fn", + "name": "print_list", + "type": { + "k": "fn", + "params": [{ "k": "con", "name": "IntList" }], + "ret": { "k": "con", "name": "Unit" }, + "effects": ["IO"] + }, + "params": ["xs"], + "body": { + "t": "match", + "scrutinee": { "t": "var", "name": "xs" }, + "arms": [ + { + "pat": { "p": "ctor", "ctor": "Nil", "fields": [] }, + "body": { "t": "lit", "lit": { "kind": "unit" } } + }, + { + "pat": { + "p": "ctor", + "ctor": "Cons", + "fields": [ + { "p": "var", "name": "h" }, + { "p": "var", "name": "t" } + ] + }, + "body": { + "t": "seq", + "lhs": { + "t": "do", + "op": "io/print_int", + "args": [{ "t": "var", "name": "h" }] + }, + "rhs": { + "t": "app", + "fn": { "t": "var", "name": "print_list" }, + "args": [{ "t": "var", "name": "t" }] + } + } + } + ] + } + }, + { + "kind": "fn", + "name": "main", + "type": { + "k": "fn", + "params": [], + "ret": { "k": "con", "name": "Unit" }, + "effects": ["IO"] + }, + "params": [], + "doc": "Sort and print [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5].", + "body": { + "t": "let", + "name": "xs", + "value": { + "t": "ctor", + "type": "IntList", + "ctor": "Cons", + "args": [ + { "t": "lit", "lit": { "kind": "int", "value": 3 } }, + { + "t": "ctor", + "type": "IntList", + "ctor": "Cons", + "args": [ + { "t": "lit", "lit": { "kind": "int", "value": 1 } }, + { + "t": "ctor", + "type": "IntList", + "ctor": "Cons", + "args": [ + { "t": "lit", "lit": { "kind": "int", "value": 4 } }, + { + "t": "ctor", + "type": "IntList", + "ctor": "Cons", + "args": [ + { "t": "lit", "lit": { "kind": "int", "value": 1 } }, + { + "t": "ctor", + "type": "IntList", + "ctor": "Cons", + "args": [ + { "t": "lit", "lit": { "kind": "int", "value": 5 } }, + { + "t": "ctor", + "type": "IntList", + "ctor": "Cons", + "args": [ + { "t": "lit", "lit": { "kind": "int", "value": 9 } }, + { + "t": "ctor", + "type": "IntList", + "ctor": "Cons", + "args": [ + { "t": "lit", "lit": { "kind": "int", "value": 2 } }, + { + "t": "ctor", + "type": "IntList", + "ctor": "Cons", + "args": [ + { "t": "lit", "lit": { "kind": "int", "value": 6 } }, + { + "t": "ctor", + "type": "IntList", + "ctor": "Cons", + "args": [ + { "t": "lit", "lit": { "kind": "int", "value": 5 } }, + { + "t": "ctor", + "type": "IntList", + "ctor": "Cons", + "args": [ + { "t": "lit", "lit": { "kind": "int", "value": 3 } }, + { + "t": "ctor", + "type": "IntList", + "ctor": "Cons", + "args": [ + { "t": "lit", "lit": { "kind": "int", "value": 5 } }, + { + "t": "ctor", + "type": "IntList", + "ctor": "Nil", + "args": [] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + }, + "body": { + "t": "app", + "fn": { "t": "var", "name": "print_list" }, + "args": [ + { + "t": "app", + "fn": { "t": "var", "name": "sort" }, + "args": [{ "t": "var", "name": "xs" }] + } + ] + } + } + } + ] +}