diff --git a/plans/agent-briefings/haskell-loop.md b/plans/agent-briefings/haskell-loop.md index c4901bd5..79d1413a 100644 --- a/plans/agent-briefings/haskell-loop.md +++ b/plans/agent-briefings/haskell-loop.md @@ -1,6 +1,8 @@ # haskell-on-sx loop agent (single agent, queue-driven) -Role: iterates `plans/haskell-on-sx.md` forever. Mini-Haskell 98 with real laziness (SX thunks are first-class). Phases 1-3 are untyped — laziness + ADTs first; HM inference is phase 4. +Role: iterates `plans/haskell-completeness.md` forever. Mini-Haskell 98 with +real laziness (SX thunks are first-class). Phases 1–6 are complete; this loop +works Phases 7–16. ``` description: haskell-on-sx queue loop @@ -11,66 +13,141 @@ isolation: worktree ## Prompt -You are the sole background agent working `/root/rose-ash/plans/haskell-on-sx.md`. Isolated worktree, forever, one commit per feature. Push to `origin/loops/haskell` after every commit. - -**Note:** there's an existing `/root/rose-ash/sx-haskell/` directory (~25 M). Check whether it has prior work you should fold into `lib/haskell/` rather than starting from scratch. Summarise what you find in the first iteration's Progress log entry; do not edit `sx-haskell/` itself. +You are the sole background agent working +`/root/rose-ash-loops/haskell/plans/haskell-completeness.md`. Isolated worktree, +forever, one commit per feature. Push to `origin/loops/haskell` after every commit. ## Restart baseline — check before iterating -1. Read `plans/haskell-on-sx.md` — roadmap + Progress log. -2. First-run only: peek at `/root/rose-ash/sx-haskell/` — does any of it belong in `lib/haskell/`? Report in Progress log. Don't edit sx-haskell/. -3. `ls lib/haskell/` — pick up from the most advanced file. -4. Run `lib/haskell/tests/*.sx` if they exist. Green before new work. -5. If `lib/haskell/scoreboard.md` exists, that's your baseline. +1. Read `plans/haskell-completeness.md` — roadmap + Progress log. +2. `ls lib/haskell/` — orient on current state. +3. Run `bash lib/haskell/test.sh`. All 775 tests must be green before new work. +4. Check `lib/haskell/scoreboard.md` — baseline is 156/156 (18 programs). ## The queue -Phase order per `plans/haskell-on-sx.md`: +Phase order per `plans/haskell-completeness.md`: -- **Phase 1** — tokenizer + parser + **layout rule** (indentation-sensitive, painful but required per Haskell 98 §10.3) -- **Phase 2** — desugar + eager eval + ADTs (`data` declarations, constructor tagging, pattern matching). Still untyped. -- **Phase 3** — **laziness**: thunk-wrap every application arg, `force` = WHNF, pattern match forces scrutinee. Classic programs (infinite Fibonacci, sieve of Eratosthenes, quicksort, n-queens, expression calculator) green. -- **Phase 4** — Hindley-Milner type inference (Algorithm W, let-polymorphism, type-sig checking) -- **Phase 5** — typeclasses (dictionary passing, Eq/Ord/Show/Num/Functor/Monad/Applicative, `deriving`) -- **Phase 6** — real `IO` monad backed by `perform`/`resume`, full Prelude, drive corpus to 150+ +- **Phase 7** — String = [Char] via O(1) string-view dicts. No OCaml changes. + Read the "String-view design" section below before touching anything. +- **Phase 8** — `show` for arbitrary types; `deriving Show` generates proper + instances; `print x = putStrLn (show x)`. +- **Phase 9** — `error` / `undefined`; partial functions raise; top-level runner + catches and a new `hk-test-error` helper checks error messages. +- **Phase 10** — Numeric tower: `fromIntegral`, Float/Double literals, + `sqrt`/`floor`/`ceiling`/`round`/`truncate`, `Fractional`/`Floating` stubs. +- **Phase 11** — `Data.Map` — weight-balanced BST in pure SX in `map.sx`. +- **Phase 12** — `Data.Set` — BST in pure SX in `set.sx`. +- **Phase 13** — `where` in typeclass instances + default methods. +- **Phase 14** — Record syntax: `data Foo = Foo { bar :: Int }`, accessors, + update `r { field = v }`, record patterns. +- **Phase 15** — `IORef` — mutable cells via existing `perform`/`resume` IO. +- **Phase 16** — Exception handling: `catch`, `try`, `throwIO`, `evaluate`. Within a phase, pick the checkbox with the best tests-per-effort ratio. -Every iteration: implement → test → commit → tick `[ ]` → Progress log → next. +Every iteration: implement → test → commit → tick `[ ]` → Progress log → push. + +## String-view design (Phase 7 — read before touching strings) + +A string view is a pure-SX dict `{:hk-str buf :hk-off n}`. Native SX strings +also satisfy `hk-str?` (offset = 0 implicitly). No OCaml changes needed. + +- `hk-str?` covers both native strings and view dicts. +- `hk-str-head v` returns the character at offset `n` as an **integer** (ord + value). Char = integer throughout. +- `hk-str-tail v` returns a new view dict with offset `n+1`; **O(1)**. +- `hk-str-null? v` is true when offset ≥ string length. +- In `match.sx`, the `":"` cons-pattern branch checks `hk-str?` on the scrutinee + **before** the normal tagged-list path. On a string: head = char-int, tail = + shifted view (or `(list "[]")` if exhausted). +- `chr n` converts an integer back to a single-character SX string for display + and for `++`. +- `++` between two strings concatenates natively via `str`; no cons-spine built. +- The natural hazard: any code that checks `(list? v)` or `(= (first v) ":")` on + a value must be audited — string views are dicts, not lists. Check `hk-str?` + first in every dispatch chain. + +## Conformance test programs + +For each phase's conformance programs: + +1. **WebFetch the source** from one of: + - 99 Haskell Problems: https://wiki.haskell.org/H-99:_Ninety-Nine_Haskell_Problems + - Rosetta Code Haskell: https://rosettacode.org/wiki/Category:Haskell + - Self-contained snippets from Real World Haskell / Learn You a Haskell +2. **Adapt minimally** — no GHC extensions, no external packages beyond + `Data.Map`/`Data.Set`/`Data.IORef` (once those phases are done). +3. **Cite the source** as a comment at the top of the `.sx` test file. +4. Add the program name (without `.sx`) to `PROGRAMS` in `lib/haskell/conformance.sh`. +5. Run `bash lib/haskell/conformance.sh` and verify green before committing. + +Target: scoreboard grows from 156 → 300+ as phases complete. ## Ground rules (hard) -- **Scope:** only `lib/haskell/**` and `plans/haskell-on-sx.md`. Do **not** edit `spec/`, `hosts/`, `shared/`, other `lib//` dirs, `lib/stdlib.sx`, `lib/` root, or `sx-haskell/`. Haskell primitives go in `lib/haskell/runtime.sx`. -- **NEVER call `sx_build`.** 600s watchdog. If sx_server binary broken → Blockers entry, stop. -- **Shared-file issues** → plan's Blockers with minimal repro. -- **SX thunks** (`make-thunk`, force on use) are already in the trampolining evaluator — reuse. Don't invent your own thunk type. -- **SX files:** `sx-tree` MCP tools ONLY. `sx_validate` after edits. -- **Worktree:** commit, then push to `origin/loops/haskell`. Never touch `main`. +- **Scope:** only `lib/haskell/**` and `plans/haskell-completeness.md`. Do + **not** edit `spec/`, `hosts/`, `shared/`, other `lib//` dirs, + `lib/stdlib.sx`, `lib/` root. +- **NEVER call `sx_build`.** 600s watchdog. If sx_server binary broken → + Blockers entry in the plan, stop. +- **Shared-file issues** → plan's Blockers section with minimal repro. +- **SX thunks** (`make-thunk`, force on use) already in the trampolining + evaluator — reuse. String views are SX dicts, not thunks. +- **SX files:** `sx-tree` MCP tools ONLY (`sx_read_subtree`, `sx_find_all`, + `sx_replace_node`, `sx_insert_child`, `sx_insert_near`, + `sx_replace_by_pattern`, `sx_rename_symbol`, `sx_validate`, `sx_write_file`). + `sx_validate` after every edit. Never `Edit`/`Read`/`Write` on `.sx` files. +- **Shell, Markdown, JSON:** edit with normal tools. +- **Worktree:** commit then push to `origin/loops/haskell`. Never touch `main`. - **Commit granularity:** one feature per commit. - **Plan file:** update Progress log + tick boxes every commit. +- **Tests:** `bash lib/haskell/test.sh` must stay green. Never regress existing + 775 tests. After new programs, run `bash lib/haskell/conformance.sh`. ## Haskell-specific gotchas -- **Layout rule is the hard bit of parsing** — you need a lexer-parser feedback loop that inserts virtual `{`, `;`, `}` based on indentation. Budget proportionally. -- **Every application arg is a thunk** — compiling `f x y` to `(f (thunk x) (thunk y))` not `(f x y)`. Pattern-match forces. -- **ADT representation:** tagged list, e.g. `data Maybe a = Nothing | Just a` → constructors are `(:Nothing)` (0-ary) and `(:Just )` (1-ary). Pattern match on the head symbol. -- **Let-polymorphism** (phase 4): generalise at let-binding boundaries only, not at lambda. -- **Typeclass dictionaries** (phase 5): each class is a record type; each instance builds the record; method call = project + apply. -- **`IO`** (phase 6): internally `World -> (a, World)` but in practice backed by `perform`/`resume` for real side effects. Desugar `do`-notation to `>>=`. -- **Out of scope:** GHC extensions. No `DataKinds`, `GADTs`, `TypeFamilies`, `TemplateHaskell`. Stick to Haskell 98. +- **String views are dicts** — `(list? v)` returns false for a string view. + Audit every value-dispatch chain in `match.sx` and `eval.sx` for this. +- **Char = integer** — `'a'` parses to int 97. `chr 97 = "a"` (1-char string). + Do not represent Char as a 1-char SX string internally. +- **`deriving Show`** (Phase 8): nested constructor args need parens if their + show string contains a space. Rule: `if string-contains (show arg) " " then + "(" ++ show arg ++ ")" else show arg`. +- **`error` tag** (Phase 9): use `(raise (list "hk-error" msg))`. The top-level + `hk-run-io` guard must catch this tag; do not let `hk-error` leak as an + uncaught SX exception into the test runner's output. +- **`Data.Map` module resolution** (Phase 11): qualified imports `import + qualified Data.Map as Map` need the eval import handler to resolve the dotted + module name to the `map.sx` namespace dict. Check `hk-bind-decls!` import arm. +- **Record update field index** (Phase 14): `r { field = v }` needs the field → + positional-index mapping at runtime. Store it in `hk-constructors` when + registering `:con-rec`. +- **IORef mutation** (Phase 15): `dict-set!` is the SX in-place mutator. The + `IORef` dict is heap-allocated and passed by reference — mutation is safe. +- **Every application arg is a thunk** — `f x y` → `(f (thunk x) (thunk y))`. + Pattern-match forces before matching. Builtins force their args. +- **ADT representation:** `("Just" thunk)`, `("Nothing")`, `(":" h t)`, `("[]")`. +- **Let-polymorphism:** generalise at let-binding boundaries only, not lambda. +- **Typeclass dictionaries:** class = record; instance = record value; method + call = project + apply. Defaults stored under `"__default__ClassName_method"`, + used as fallback when the instance dict lacks the key. +- **Out of scope:** GHC extensions. No `DataKinds`, `GADTs`, `TypeFamilies`, + `TemplateHaskell`. Haskell 98 only. ## General gotchas (all loops) -- SX `do` = R7RS iteration. Use `begin` for multi-expr sequences. -- `cond`/`when`/`let` clauses evaluate only the last expr. +- SX `do` = R7RS iteration. Use `begin` for multi-expression sequences. +- `cond`/`when`/`let` clauses evaluate only the last expression. - `type-of` on user fn returns `"lambda"`. -- Shell heredoc `||` gets eaten — escape or use `case`. +- Shell heredoc `||` gets eaten by bash — escape or use `case`. +- `keys` on an SX dict returns keys in implementation-defined order. ## Style - No comments in `.sx` unless non-obvious. -- No new planning docs — update `plans/haskell-on-sx.md` inline. -- Short, factual commit messages (`haskell: layout rule + first parse (+10)`). +- No new planning docs — update `plans/haskell-completeness.md` inline. +- Short, factual commit messages (`haskell: string-view O(1) head/tail (+15)`). - One feature per iteration. Commit. Log. Next. -Go. Read the plan; (first run only) peek at sx-haskell/ and report; find first `[ ]`; implement. +Go. Read `plans/haskell-completeness.md`; find the first `[ ]`; implement. diff --git a/plans/haskell-completeness.md b/plans/haskell-completeness.md new file mode 100644 index 00000000..138a09ca --- /dev/null +++ b/plans/haskell-completeness.md @@ -0,0 +1,285 @@ +# Haskell-on-SX: completeness roadmap (Phases 7–16) + +Continuation of `plans/haskell-on-sx.md`. Phases 1–6 are complete (156/156 +conformance tests, 18 programs, 775 total hk-on-sx tests). This document covers +the next ten features toward a more complete Haskell 98 subset. + +## Scope decisions (unchanged from haskell-on-sx.md) + +- Haskell 98 subset only. No GHC extensions. +- All work lives in `lib/haskell/**` and this file. Nothing else. +- SX files: `sx-tree` MCP tools only. +- One feature per commit. Keep `## Progress log` updated. + +## String-view design note + +Haskell defines `type String = [Char]`. Representing that naively as a linked +cons-spine makes `length`, `++`, and `take` O(n) in allocation — unacceptable +for string-processing programs. The design uses **string views** implemented as +pure-SX dicts, requiring no OCaml changes. + +### Representation + +A string view is a dict `{:hk-str buf :hk-off n}` where `buf` is a native SX +string and `n` is the current offset (zero-based code-unit index). Native SX +strings also satisfy the predicate (offset = 0 implicitly). + +- `hk-str?` returns true for both native strings and string-view dicts. +- `hk-str-head v` extracts the character at offset `n` as an integer (ord value). +- `hk-str-tail v` returns a new view with offset `n+1`; O(1). +- `hk-str-null? v` is true when offset equals the string's length. + +### Char = integer + +`Char` is represented as a plain integer (its Unicode code point / ord value). +`chr n` converts back to a single-character string for display and `++`. `ord c` +is the identity (the integer itself). `toUpper`/`toLower` operate on the integer, +looking up ASCII ranges. This is already consistent with the existing `ord 'A' = +65` tests. + +### Pattern matching + +In `match.sx`, the cons-pattern branch (`":"` constructor) checks `hk-str?` on +the scrutinee **before** the normal tagged-list path. When the scrutinee is a +string view (or native string), decompose as: +- head → `hk-str-head` (an integer char-code) +- tail → `hk-str-tail` (a new string view, or `(list "[]")` if exhausted) + +The nil-pattern `"[]"` matches when `hk-str-null?` is true. + +### Complexity + +- `head s` / `tail s` — O(1) via view shift +- `s !! n` — O(n) (n tail calls) +- `(c:s)` construction — O(n) for full `[Char]` construction (same as real Haskell) +- `++` on two strings — native `str` concat, O(length left) +- `length` — O(n); `words`/`lines` — O(n) + +No OCaml changes are needed. The view type is fully representable as an SX dict. + +## Ground rules + +- **Scope:** only `lib/haskell/**` and `plans/haskell-completeness.md`. No edits + to `spec/`, `hosts/`, `shared/`, other `lib//` dirs, or `lib/` root. +- **SX files:** `sx-tree` MCP tools only. `sx_validate` after every edit. +- **Commits:** one feature per commit. Keep `## Progress log` updated. +- **Tests:** `bash lib/haskell/test.sh` must be green before any commit. After + adding new programs, run `bash lib/haskell/conformance.sh` and commit the + updated `scoreboard.md`. +- **Conformance programs:** WebFetch from 99 Haskell Problems or Rosetta Code. + Adapt minimally (no GHC extensions). Cite the source URL in the file header. + Add to `conformance.sh` PROGRAMS array. +- **NEVER call `sx_build`.** If sx_server binary broken → Blockers entry, stop. + +## Roadmap + +### Phase 7 — String = [Char] (performant string views) + +- [ ] Add `hk-str?` predicate to `runtime.sx` covering both native SX strings + and `{:hk-str buf :hk-off n}` view dicts. +- [ ] Implement `hk-str-head`, `hk-str-tail`, `hk-str-null?` helpers in + `runtime.sx`. +- [ ] In `match.sx`, intercept cons-pattern `":"` when scrutinee satisfies + `hk-str?`; decompose to (char-int, view) instead of the tagged-list path. + Nil-pattern `"[]"` matches `hk-str-null?`. +- [ ] Add builtins: `chr` (int → single-char string), verify `ord` returns int, + `toUpper`, `toLower` (ASCII range arithmetic on ints). +- [ ] Ensure `++` between two strings concatenates natively via `str` rather + than building a cons spine. +- [ ] Tests in `lib/haskell/tests/string-char.sx` (≥ 15 tests: head/tail on + string literal, map over string, filter chars, chr/ord roundtrip, toUpper, + toLower, null/empty string view). +- [ ] Conformance programs (WebFetch + adapt): + - `caesar.hs` — Caesar cipher. Exercises `map`, `chr`, `ord`, `toUpper`, + `toLower` on characters. + - `runlength-str.hs` — run-length encoding on a String. Exercises string + pattern matching, `span`, character comparison. + +### Phase 8 — `show` for arbitrary types + +- [ ] Audit `hk-show-val` in `runtime.sx` — ensure output format matches + Haskell 98: `"Just 3"`, `"[1,2,3]"`, `"(True,False)"`, `"'a'"` (Char shows + with single-quotes), `"\"hello\""` (String shows with escaped double-quotes). +- [ ] `show` Prelude binding calls `hk-show-val`; `print x = putStrLn (show x)`. +- [ ] `deriving Show` auto-generates proper show for record-style and + multi-constructor ADTs. Nested application arguments wrapped in parens: + if `show arg` contains a space, emit `"(" ++ show arg ++ ")"`. +- [ ] `showsPrec` / `showParen` stubs so hand-written Show instances compile. +- [ ] `Read` class stub — just enough for `reads :: String -> [(a,String)]` to + type-check; no real parser needed yet. +- [ ] Tests in `lib/haskell/tests/show.sx` (≥ 12 tests: show Int, show Bool, + show Char, show String, show list, show tuple, show Maybe, show custom ADT, + deriving Show on multi-constructor type, nested constructor parens). +- [ ] Conformance programs: + - `showadt.hs` — `data Expr = Lit Int | Add Expr Expr | Mul Expr Expr` + with `deriving Show`; prints a tree. + - `showio.hs` — `print` on various types in a `do` block. + +### Phase 9 — `error` / `undefined` + +- [ ] `error :: String -> a` — raises `(raise (list "hk-error" msg))` in SX. +- [ ] `undefined :: a` = `error "Prelude.undefined"`. +- [ ] Partial functions emit proper error messages: `head []` → + `"Prelude.head: empty list"`, `tail []` → `"Prelude.tail: empty list"`, + `fromJust Nothing` → `"Maybe.fromJust: Nothing"`. +- [ ] Top-level `hk-run-io` catches `hk-error` tag and returns it as a tagged + error result so test suites can inspect it without crashing. +- [ ] `hk-test-error` helper in `testlib.sx`: + `(hk-test-error "desc" thunk expected-substring)` — asserts the thunk raises + an `hk-error` whose message contains the given substring. +- [ ] Tests in `lib/haskell/tests/errors.sx` (≥ 10 tests: error message + content, undefined, head/tail/fromJust on bad input, `hk-test-error` helper). +- [ ] Conformance programs: + - `partial.hs` — exercises `head []`, `tail []`, `fromJust Nothing` caught + at the top level; shows error messages. + +### Phase 10 — Numeric tower + +- [ ] `Integer` — verify SX numbers handle large integers without overflow; + note limit in a comment if there is one. +- [ ] `fromIntegral :: (Integral a, Num b) => a -> b` — identity in our runtime + (all numbers share one SX type); register as a builtin no-op with the correct + typeclass signature. +- [ ] `toInteger`, `fromInteger` — same treatment. +- [ ] Float/Double literals round-trip through `hk-show-val`: + `show 3.14 = "3.14"`, `show 1.0e10 = "1.0e10"`. +- [ ] Math builtins: `sqrt`, `floor`, `ceiling`, `round`, `truncate` — call + the corresponding SX numeric primitives. +- [ ] `Fractional` typeclass stub: `(/)`, `recip`, `fromRational`. +- [ ] `Floating` typeclass stub: `pi`, `exp`, `log`, `sin`, `cos`, `(**)` + (power operator, maps to SX exponentiation). +- [ ] Tests in `lib/haskell/tests/numeric.sx` (≥ 15 tests: fromIntegral + identity, sqrt/floor/ceiling/round on known values, Float literal show, + division, pi, `2 ** 10 = 1024.0`). +- [ ] Conformance programs: + - `statistics.hs` — mean, variance, std-dev on a `[Double]`. Exercises + `fromIntegral`, `sqrt`, `/`. + - `newton.hs` — Newton's method for square root. Exercises `Float`, `abs`, + iteration. + +### Phase 11 — Data.Map + +- [ ] Implement a weight-balanced BST in pure SX in `lib/haskell/map.sx`. + Internal node representation: `("Map-Node" key val left right size)`. + Leaf: `("Map-Empty")`. +- [ ] Core operations: `empty`, `singleton`, `insert`, `lookup`, `delete`, + `member`, `size`, `null`. +- [ ] Bulk operations: `fromList`, `toList`, `toAscList`, `keys`, `elems`. +- [ ] Combining: `unionWith`, `intersectionWith`, `difference`. +- [ ] Transforming: `foldlWithKey`, `foldrWithKey`, `mapWithKey`, `filterWithKey`. +- [ ] Updating: `adjust`, `insertWith`, `insertWithKey`, `alter`. +- [ ] Module wiring: `import Data.Map` and `import qualified Data.Map as Map` + resolve to the `map.sx` namespace dict in the eval import handler. +- [ ] Unit tests in `lib/haskell/tests/map.sx` (≥ 20 tests: empty, singleton, + insert + lookup hit/miss, delete root, fromList with duplicates, + toAscList ordering, unionWith, foldlWithKey). +- [ ] Conformance programs: + - `wordfreq.hs` — word-frequency histogram using `Data.Map`. Source from + Rosetta Code "Word frequency" Haskell entry. + - `mapgraph.hs` — adjacency-list BFS using `Data.Map`. + +### Phase 12 — Data.Set + +- [ ] Implement `Data.Set` in `lib/haskell/set.sx`. Use a standalone + weight-balanced BST (same structure as Map but no value field) or wrap + `Data.Map` with unit values. +- [ ] API: `empty`, `singleton`, `insert`, `delete`, `member`, `fromList`, + `toList`, `toAscList`, `size`, `null`, `union`, `intersection`, `difference`, + `isSubsetOf`, `filter`, `map`, `foldr`, `foldl'`. +- [ ] Module wiring: `import Data.Set` / `import qualified Data.Set as Set`. +- [ ] Unit tests in `lib/haskell/tests/set.sx` (≥ 15 tests: empty, insert, + member hit/miss, delete, fromList deduplication, union, intersection, + difference, isSubsetOf). +- [ ] Conformance programs: + - `uniquewords.hs` — unique words in a string using `Data.Set`. + - `setops.hs` — set union/intersection/difference on integer sets; + exercises all three combining operations. + +### Phase 13 — `where` in typeclass instances + default methods + +- [ ] Verify `where`-clauses in `instance` bodies desugar correctly. The + `hk-bind-decls!` instance arm must call the same where-lifting logic as + top-level function clauses. Write a targeted test to confirm. +- [ ] Class declarations may include default method implementations. Parser: + `hk-parse-class` collects method decls; eval registers defaults under + `"__default__ClassName_method"` in the class dict. +- [ ] Instance method lookup: when the instance dict lacks a method, fall back + to the default. Wire this into the dictionary-passing dispatch. +- [ ] `Eq` default: `(/=) x y = not (x == y)`. Verify it works without an + explicit `/=` in every Eq instance. +- [ ] `Ord` defaults: `max a b = if a >= b then a else b`, `min a b = if a <= + b then a else b`. Verify. +- [ ] `Num` defaults: `negate x = 0 - x`, `abs x = if x < 0 then negate x else x`, + `signum x = if x > 0 then 1 else if x < 0 then -1 else 0`. Verify. +- [ ] Tests in `lib/haskell/tests/class-defaults.sx` (≥ 10 tests). +- [ ] Conformance programs: + - `shapes.hs` — `class Area a` with a default `perimeter`; two instances + using `where`-local helpers. + +### Phase 14 — Record syntax + +- [ ] Parser: extend `hk-parse-data` to recognise `{ field :: Type, … }` + constructor bodies. AST node: `(:con-rec CNAME [(FNAME TYPE) …])`. +- [ ] Desugar: `:con-rec` → positional `:con-def` plus generated accessor + functions `(\rec -> case rec of …)` for each field name. +- [ ] Record creation `Foo { bar = 1, baz = "x" }` parsed as + `(:rec-create CON [(FNAME EXPR) …])`. Eval builds the same tagged list as + positional construction (field order from the data decl). +- [ ] Record update `r { field = v }` parsed as `(:rec-update EXPR [(FNAME EXPR)])`. + Eval forces the record, replaces the relevant positional slot, returns a new + tagged list. Field → index mapping stored in `hk-constructors` at registration. +- [ ] Exhaustive record patterns: `Foo { bar = b }` in case binds `b`, + wildcards remaining fields. +- [ ] Tests in `lib/haskell/tests/records.sx` (≥ 12 tests: creation, accessor, + update one field, update two fields, record pattern, `deriving Show` on + record type). +- [ ] Conformance programs: + - `person.hs` — `data Person = Person { name :: String, age :: Int }` with + accessors, update, `deriving Show`. + - `config.hs` — multi-field config record; partial update; defaultConfig + constant. + +### Phase 15 — IORef + +- [ ] `IORef a` representation: a dict `{:hk-ioref true :hk-value v}`. + Allocation creates a new dict in the IO monad. Mutation via `dict-set!`. +- [ ] `newIORef :: a -> IO (IORef a)` — wraps a new dict in `IO`. +- [ ] `readIORef :: IORef a -> IO a` — returns `(IO (get ref ":hk-value"))`. +- [ ] `writeIORef :: IORef a -> a -> IO ()` — `(dict-set! ref ":hk-value" v)`, + returns `(IO ("Tuple"))`. +- [ ] `modifyIORef :: IORef a -> (a -> a) -> IO ()` — read + apply + write. +- [ ] `modifyIORef' :: IORef a -> (a -> a) -> IO ()` — strict variant (force + new value before write). +- [ ] `Data.IORef` module wiring. +- [ ] Tests in `lib/haskell/tests/ioref.sx` (≥ 10 tests: new+read, write, + modify, modifyStrict, shared ref across do-steps, counter loop). +- [ ] Conformance programs: + - `counter.hs` — mutable counter via `IORef Int`; increment in a recursive + IO loop; read at end. + - `accumulate.hs` — accumulate results into `IORef [Int]` inside a mapped + IO action, read at the end. + +### Phase 16 — Exception handling + +- [ ] `SomeException` type: `data SomeException = SomeException String`. + `IOException = SomeException`. +- [ ] `throwIO :: Exception e => e -> IO a` — raises `("hk-exception" e)`. +- [ ] `evaluate :: a -> IO a` — forces arg strictly; any embedded `hk-error` + surfaces as a catchable `SomeException`. +- [ ] `catch :: Exception e => IO a -> (e -> IO a) -> IO a` — wraps action in + SX `guard`; on `hk-error` or `hk-exception`, calls the handler with a + `SomeException` value. +- [ ] `try :: Exception e => IO a -> IO (Either e a)` — returns `Right v` on + success, `Left e` on any exception. +- [ ] `handle = flip catch`. +- [ ] Tests in `lib/haskell/tests/exceptions.sx` (≥ 10 tests: catch success, + catch error, try Right, try Left, nested catch, evaluate surfaces error, + throwIO propagates, handle alias). +- [ ] Conformance programs: + - `safediv.hs` — safe division using `catch`; divide-by-zero raises, + handler returns 0. + - `trycatch.hs` — `try` pattern: run an action, branch on Left/Right. + +## Progress log + +_Newest first._