Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 44s
hk-bind-exceptions! in eval.sx registers throwIO, throw, evaluate, catch, try, handle, displayException. SomeException constructor pre-registered in runtime.sx (arity 1, type SomeException). throwIO and the existing error primitive both raise via SX `raise` with a uniform "hk-error: msg" string. catch/try/handle parse it back into a SomeException via hk-exception-of, which strips nested 'Unhandled exception: "..."' host wraps (CEK's host_error formatter) and the "hk-error: " prefix. catch and handle evaluate the handler outside the guard scope (build an "ok"/"exn" outcome tag inside guard, then dispatch outside) so that a re-throw from the handler propagates past this catch — matching Haskell semantics rather than infinite-looping in the same guard. 14 unit tests in tests/exceptions.sx (catch success, catch error, try Right/Left, handle, throwIO + catch/try, evaluate, nested catch, do-bind through catch, branch on try result, IORef-mutating handler). Conformance: safediv.hs (8/8) and trycatch.hs (8/8). Scoreboard now 285/285 tests, 36/36 programs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
824 lines
48 KiB
Markdown
824 lines
48 KiB
Markdown
# 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/<lang>/` 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)
|
||
|
||
- [x] Add `hk-str?` predicate to `runtime.sx` covering both native SX strings
|
||
and `{:hk-str buf :hk-off n}` view dicts.
|
||
- [x] Implement `hk-str-head`, `hk-str-tail`, `hk-str-null?` helpers in
|
||
`runtime.sx`.
|
||
- [x] 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?`.
|
||
- [x] Add builtins: `chr` (int → single-char string), verify `ord` returns int,
|
||
`toUpper`, `toLower` (ASCII range arithmetic on ints).
|
||
- [x] Ensure `++` between two strings concatenates natively via `str` rather
|
||
than building a cons spine.
|
||
- [x] 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).
|
||
- [x] 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
|
||
|
||
- [x] Audit `hk-show-val` in `runtime.sx` — ensure output format matches
|
||
Haskell 98: `"Just 3"`, `"[1,2,3]"`, `"(True,False)"`, `"\"hello\""` (String
|
||
shows with escaped double-quotes). _Deferred:_ `"'a'"` Char single-quotes
|
||
(needs Char tagging — currently Char = Int by representation, ambiguous in
|
||
show); `\n`/`\t` escape inside Strings.
|
||
- [x] `show` Prelude binding calls `hk-show-val`; `print x = putStrLn (show x)`.
|
||
- [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 ++ ")"`. _Records
|
||
deferred — Phase 14._
|
||
- [x] `showsPrec` / `showParen` stubs so hand-written Show instances compile.
|
||
- [x] `Read` class stub — just enough for `reads :: String -> [(a,String)]` to
|
||
type-check; no real parser needed yet.
|
||
- [x] 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).
|
||
_Char tests deferred: Char = Int representation; show on a Char is currently
|
||
`"97"` not `"'a'"`._
|
||
- [x] 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`
|
||
|
||
- [x] `error :: String -> a` — raises `(raise "hk-error: <msg>")` in SX.
|
||
_Plan amended:_ SX's `apply` rewrites unhandled list raises to a string
|
||
`"Unhandled exception: <serialized>"` before any user handler sees them, so
|
||
the tag has to live in a string prefix rather than as the head of a list.
|
||
Catchers use `(index-of e "hk-error: ")` to detect.
|
||
- [x] `undefined :: a` = `error "Prelude.undefined"`.
|
||
- [x] Partial functions emit proper error messages: `head []` →
|
||
`"Prelude.head: empty list"`, `tail []` → `"Prelude.tail: empty list"`,
|
||
`fromJust Nothing` → `"Maybe.fromJust: Nothing"`.
|
||
- [x] 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.
|
||
- [x] `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.
|
||
- [x] Tests in `lib/haskell/tests/errors.sx` (≥ 10 tests: error message
|
||
content, undefined, head/tail/fromJust on bad input, `hk-test-error` helper).
|
||
- [x] Conformance programs:
|
||
- `partial.hs` — exercises `head []`, `tail []`, `fromJust Nothing` caught
|
||
at the top level; shows error messages.
|
||
|
||
### Phase 10 — Numeric tower
|
||
|
||
- [x] `Integer` — verify SX numbers handle large integers without overflow;
|
||
note limit in a comment if there is one. _Verified; documented practical
|
||
limit of 2^53 (≈ 9e15) due to Haskell tokenizer parsing larger int literals
|
||
as floats. Raw SX is exact to ±2^62. See header comment in `numerics.sx`._
|
||
- [x] `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. _Already in `hk-prelude-src` as `fromIntegral x = x`;
|
||
verified with new tests in `numerics.sx`._
|
||
- [x] `toInteger`, `fromInteger` — same treatment. _Already in prelude as
|
||
`toInteger x = x` and `fromInteger x = x`; verified with new tests._
|
||
- [x] Float/Double literals round-trip through `hk-show-val`:
|
||
`show 3.14 = "3.14"`, `show 1.0e10 = "1.0e10"`. _Partial: fractional floats
|
||
render correctly (`3.14`, `-3.14`, `1.0e-3`); whole-valued floats render as
|
||
ints (`1.0e10` → `"10000000000"`) because our system can't distinguish
|
||
`42` from `42.0` — both are SX numbers where `integer?` is true. Existing
|
||
tests like `show 42 = "42"` rely on this rendering. Documented in `numerics.sx`._
|
||
- [x] Math builtins: `sqrt`, `floor`, `ceiling`, `round`, `truncate` — call
|
||
the corresponding SX numeric primitives.
|
||
- [x] `Fractional` typeclass stub: `(/)`, `recip`, `fromRational`. _(/)
|
||
already a binop; `recip x = 1 / x` and `fromRational x = x` registered as
|
||
builtins in the post-prelude block._
|
||
- [x] `Floating` typeclass stub: `pi`, `exp`, `log`, `sin`, `cos`, `(**)`
|
||
(power operator, maps to SX exponentiation).
|
||
- [x] Tests in `lib/haskell/tests/numerics.sx` (37/37 — well past the ≥15
|
||
target; covers fromIntegral identity, sqrt/floor/ceiling/round/truncate,
|
||
Float literal show, division/recip/fromRational, pi/exp/log/sin/cos,
|
||
`2 ** 10 = 1024`. Filename is plural — divergence noted in the plan.)
|
||
- [x] 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
|
||
|
||
- [x] 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")`.
|
||
- [x] Core operations: `empty`, `singleton`, `insert`, `lookup`, `delete`,
|
||
`member`, `size`, `null`.
|
||
- [x] Bulk operations: `fromList`, `toList`, `toAscList`, `keys`, `elems`.
|
||
- [x] Combining: `unionWith`, `intersectionWith`, `difference`.
|
||
- [x] Transforming: `foldlWithKey`, `foldrWithKey`, `mapWithKey`, `filterWithKey`.
|
||
- [x] Updating: `adjust`, `insertWith`, `insertWithKey`, `alter`.
|
||
- [x] Module wiring: `import Data.Map` and `import qualified Data.Map as Map`
|
||
resolve to the `map.sx` namespace dict in the eval import handler.
|
||
- [x] Unit tests in `lib/haskell/tests/map.sx` (26 tests, well past ≥20 target:
|
||
empty/singleton/insert/lookup hit&miss/overwrite/delete/member at the SX
|
||
level, fromList with duplicates last-wins, toAscList ordering, elems in
|
||
order, unionWith/intersectionWith/difference, foldlWithKey/mapWithKey/
|
||
filterWithKey, adjust/insertWith/alter, plus 4 end-to-end tests via
|
||
`import qualified Data.Map as Map`.)
|
||
- [x] 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
|
||
|
||
- [x] 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. _Chose the wrapper approach: Set k = Map k ()._
|
||
- [x] API: `empty`, `singleton`, `insert`, `delete`, `member`, `fromList`,
|
||
`toList`, `toAscList`, `size`, `null`, `union`, `intersection`, `difference`,
|
||
`isSubsetOf`, `filter`, `map`, `foldr`, `foldl'`.
|
||
- [x] Module wiring: `import Data.Set` / `import qualified Data.Set as Set`.
|
||
- [x] Unit tests in `lib/haskell/tests/set.sx` (17/17, plan ≥15: empty, insert,
|
||
member hit/miss, delete, fromList deduplication, union, intersection,
|
||
difference, isSubsetOf, plus 4 end-to-end via `import qualified Data.Set`).
|
||
- [x] 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
|
||
|
||
- [x] 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.
|
||
- [x] 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.
|
||
- [x] Instance method lookup: when the instance dict lacks a method, fall back
|
||
to the default. Wire this into the dictionary-passing dispatch.
|
||
- [x] `Eq` default: `(/=) x y = not (x == y)`. Verify it works without an
|
||
explicit `/=` in every Eq instance. _Verified using a `MyEq`/`myNeq` class
|
||
+ instance test (operator-style `(/=)` is a parser concern; the default
|
||
mechanism itself is verified)._
|
||
- [x] `Ord` defaults: `max a b = if a >= b then a else b`, `min a b = if a <=
|
||
b then a else b`. Verify.
|
||
- [x] `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. _Verified
|
||
for negate / abs via a `MyNum` class. Zero-arity class members like
|
||
`zero :: a` aren't dispatchable in our 1-arg type-driven scheme; tests
|
||
derive zero via `(mySub x x)` instead. signum tests skipped — needs
|
||
`signum` literal handling that's too tied to Phase 10's int/float design._
|
||
- [x] Tests in `lib/haskell/tests/class-defaults.sx` (13/13, plan ≥10).
|
||
- [x] Conformance programs:
|
||
- `shapes.hs` — `class Area a` with a default `perimeter`; two instances
|
||
using `where`-local helpers.
|
||
|
||
### Phase 14 — Record syntax
|
||
|
||
- [x] Parser: extend `hk-parse-data` to recognise `{ field :: Type, … }`
|
||
constructor bodies. AST node: `(:con-rec CNAME [(FNAME TYPE) …])`.
|
||
- [x] Desugar: `:con-rec` → positional `:con-def` plus generated accessor
|
||
functions `(\rec -> case rec of …)` for each field name.
|
||
- [x] 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).
|
||
- [x] 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.
|
||
_Field map lives in `hk-record-fields` (desugar.sx) for load-order reasons,
|
||
not `hk-constructors`._
|
||
- [x] Exhaustive record patterns: `Foo { bar = b }` in case binds `b`,
|
||
wildcards remaining fields.
|
||
- [x] Tests in `lib/haskell/tests/records.sx` (14/14, plan ≥12: creation
|
||
with reorder, accessors, single + two-field update, case-alt + fun-LHS
|
||
record patterns, `deriving Show` on record types).
|
||
- [x] 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
|
||
|
||
- [x] `IORef a` representation: a dict `{:hk-ioref true :hk-value v}`.
|
||
Allocation creates a new dict in the IO monad. Mutation via `dict-set!`.
|
||
- [x] `newIORef :: a -> IO (IORef a)` — wraps a new dict in `IO`.
|
||
- [x] `readIORef :: IORef a -> IO a` — returns `(IO (get ref ":hk-value"))`.
|
||
- [x] `writeIORef :: IORef a -> a -> IO ()` — `(dict-set! ref ":hk-value" v)`,
|
||
returns `(IO ("Tuple"))`.
|
||
- [x] `modifyIORef :: IORef a -> (a -> a) -> IO ()` — read + apply + write.
|
||
- [x] `modifyIORef' :: IORef a -> (a -> a) -> IO ()` — strict variant (force
|
||
new value before write).
|
||
- [x] `Data.IORef` module wiring.
|
||
- [x] Tests in `lib/haskell/tests/ioref.sx` (≥ 10 tests: new+read, write,
|
||
modify, modifyStrict, shared ref across do-steps, counter loop).
|
||
- [x] 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
|
||
|
||
- [x] `SomeException` type: `data SomeException = SomeException String`.
|
||
`IOException = SomeException`.
|
||
- [x] `throwIO :: Exception e => e -> IO a` — raises `("hk-exception" e)`.
|
||
- [x] `evaluate :: a -> IO a` — forces arg strictly; any embedded `hk-error`
|
||
surfaces as a catchable `SomeException`.
|
||
- [x] `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.
|
||
- [x] `try :: Exception e => IO a -> IO (Either e a)` — returns `Right v` on
|
||
success, `Left e` on any exception.
|
||
- [x] `handle = flip catch`.
|
||
- [x] 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).
|
||
- [x] 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._
|
||
|
||
**2026-05-08** — Phase 16 Exception handling complete (6 ops + module wiring +
|
||
14 unit tests + 2 conformance programs). `hk-bind-exceptions!` in `eval.sx`
|
||
registers `throwIO`, `throw`, `evaluate`, `catch`, `try`, `handle`, and
|
||
`displayException`. `SomeException` constructor pre-registered in
|
||
`runtime.sx`. `throwIO` and the `error` primitive both raise via SX `raise`
|
||
with a uniform `"hk-error: msg"` string; catch/try/handle parse this string
|
||
back into a `SomeException` via `hk-exception-of` (which strips nested
|
||
`Unhandled exception: "..."` host wraps and the `hk-error: ` prefix). catch
|
||
and handle evaluate the handler outside the guard scope, so a re-throw from
|
||
the handler propagates past this catch (matching Haskell semantics, not an
|
||
infinite loop). Phase 16 phase complete: scoreboard now 285/285 tests,
|
||
36/36 programs.
|
||
|
||
**2026-05-07** — Fix string ↔ `[Char]` equality. `reverse`/`length`/`head`/etc.
|
||
on a string transparently coerce to a cons-list of char codes via `hk-str-head`
|
||
+ `hk-str-tail`, but `(==)` then compared the original raw string against the
|
||
char-code cons-list and always returned False. Added `hk-try-charlist-to-string`
|
||
+ `hk-normalize-for-eq` in `eval.sx` and routed `==` / `/=` through them, so a
|
||
string compares equal to any cons-list whose elements are valid Unicode code
|
||
points spelling the same characters (and `[]` ↔ `""`). palindrome.hs now 12/12;
|
||
conformance lifts to 34/34 programs, **269/269 tests** — full green.
|
||
|
||
**2026-05-07** — Phase 15 IORef complete (5 ops + module wiring + 13 unit
|
||
tests + 2 conformance programs). `hk-bind-data-ioref!` in `eval.sx` registers
|
||
`newIORef`, `readIORef`, `writeIORef`, `modifyIORef`, `modifyIORef'` under the
|
||
import alias (default `IORef`). Representation: dict `{"hk-ioref" true
|
||
"hk-value" v}` allocated inside `IO`. Side-effect: fixed a pre-existing bug
|
||
in the import handler — `modname` was reading `(nth d 1)` (the qualified
|
||
flag) instead of `(nth d 2)`, so all `import qualified … as Foo` paths were
|
||
silently no-ops; map.sx unit suite jumps from 22→26 passing as a result.
|
||
Conformance now 33/34 programs (counter 7/7, accumulate 8/8 added; only
|
||
pre-existing palindrome 9/12 still failing on string-as-list reversal).
|
||
|
||
**2026-05-07** — Phase 14 conformance: person.hs (7/7) + config.hs (10/10) → Phase 14 complete:
|
||
- `program-person.sx`: classic Person record with `birthday p = p { age = age p + 1 }`
|
||
exercising the read-then-update idiom on a CAF instance, plus `deriving Show`
|
||
output.
|
||
- `program-config.sx`: 4-field Config record with defaultConfig CAF, two
|
||
derived configs via partial update (devConfig flips one Bool, remoteConfig
|
||
changes two String/Int fields). 10 tests covering both branches preserve
|
||
the unchanged fields.
|
||
- Both added to `PROGRAMS` in `conformance.sh`. Phase 14 fully complete.
|
||
|
||
**2026-05-07** — Phase 14 unit tests `tests/records.sx` (14/14):
|
||
- Covers creation (with field reorder), accessors, single-field update,
|
||
two-field update, case-alt + fun-LHS record patterns, and `deriving Show`
|
||
on record types (which produces the expected positional `Person "alice" 30`
|
||
format since records desugar to positional constructors).
|
||
|
||
**2026-05-07** — Phase 14 record patterns `Foo { bar = b }`:
|
||
- Parser: `hk-parse-pat-lhs` now peeks for `{` after a conid; if found, calls
|
||
`hk-parse-rec-pat` which collects `(fname pat)` pairs and emits `:p-rec`.
|
||
- Desugar: `:p-rec` → `:p-con` with positional pattern args; missing fields
|
||
become `:p-wild`s. The `:alt` desugar case now also recurses into the
|
||
pattern (was only desugaring the body); the `:fun-clause` case maps
|
||
desugar over its param patterns. Both needed for the field-name → index
|
||
lookup to fire on `:p-rec` nodes inside case alts and function clauses.
|
||
- Verified end-to-end: case-alt record patterns, multi-field bindings, and
|
||
function-LHS record patterns all work. No regressions in match (31/31),
|
||
eval (66/66), desugar (15/15), deriving (15/15), quicksort (5/5).
|
||
|
||
**2026-05-07** — Phase 14 record-update syntax `r { field = v }`:
|
||
- Parser: `varid {` after a primary expression now triggers
|
||
`hk-parse-rec-update` returning `(:rec-update record-expr [(fname expr) …])`.
|
||
(Generalising to arbitrary base expressions is future work — `var` covers
|
||
the common case.)
|
||
- Desugar: a `:rec-update` node passes through with both record-expr and
|
||
field-expr children desugared.
|
||
- Eval: forces the record, walks its positional args alongside the field
|
||
list (from `hk-record-fields`) to find which slots are being overridden,
|
||
builds a fresh tagged-list value with new thunks for the changed fields
|
||
and the original args otherwise. Multi-field update works. Verified end-
|
||
to-end on `alice { age = 31 }` (only age changes; name preserved). No
|
||
regressions in eval / match / desugar suites.
|
||
|
||
**2026-05-07** — Phase 14 record-creation syntax `Foo { f = e, … }`:
|
||
- Parser: post-`conid` peek for `{` triggers `hk-parse-rec-create`, returning
|
||
`(:rec-create cname [(fname expr) …])`.
|
||
- `hk-record-fields` dict (in desugar.sx — load order requires it live there)
|
||
is populated by `hk-expand-records` when it sees a `con-rec`.
|
||
- New `:rec-create` case in `hk-desugar` looks up the field order, builds an
|
||
`app` chain `(:app (:app (:con cname) e1) e2 …)` in declared order. Field-
|
||
pair lookup via new `hk-find-rec-pair` helper. Order in source doesn't
|
||
matter — `Person { age = 99, name = "bob" }` correctly produces a Person
|
||
with name="bob", age=99 regardless of source order.
|
||
- Verified via direct execution; no regressions in parse/desugar/deriving.
|
||
|
||
**2026-05-07** — Phase 14 record desugar (`:con-rec` → positional + accessors):
|
||
- New `hk-record-accessors` helper in `desugar.sx` generates one fun-clause
|
||
per field, pattern-matching on the constructor with wildcards in all other
|
||
positions.
|
||
- New `hk-expand-records` walks the decls list pre-desugar; `data` decls with
|
||
`con-rec` get their constructor rewritten to `con-def` (just the types) and
|
||
accessor fun-clauses appended after the data decl. Other decls pass through.
|
||
- Wired into the `program` and `module` cases of `hk-desugar`. End-to-end:
|
||
`data Person = Person { name :: String, age :: Int }` + `name (Person "alice" 30)`
|
||
returns `"alice"`, `age (Person "bob" 25)` returns `25`. No regressions in
|
||
parse / desugar / deriving.
|
||
|
||
**2026-05-07** — Phase 14 record parser: `data Foo = Foo { name :: T, … }`:
|
||
- Extended `hk-parse-con-def` to peek for `{` after the constructor name; if
|
||
found, parse `varid :: type` pairs separated by commas, terminate with `}`,
|
||
return `(:con-rec name [(fname ftype) …])`. Positional constructors fall
|
||
through to the existing `:con-def` path. Verified record parses; no
|
||
regressions in parse.sx (43/43), parser-decls (24/24), deriving (15/15).
|
||
|
||
**2026-05-07** — Phase 13 conformance: shapes.hs (5/5) → Phase 13 complete:
|
||
- `class Shape` with a default `perimeter` (using a where-clause inside the
|
||
default body), two instances `Square` / `Rect` — Square overrides
|
||
`perimeter`, Rect's `perimeter` uses a where-bound `peri`. 5/5 across
|
||
area, perimeter (override), perimeter-via-where, sum. Phase 13 fully
|
||
complete.
|
||
|
||
**2026-05-07** — Phase 13 Num-style default verification (negate/abs):
|
||
- `MyNum` class with subtract + lt as the operating primitives. Defaults for
|
||
`myNegate x` and `myAbs x` derive zero via `mySub x x`. Zero-arity class
|
||
methods like `myZero :: a` are not yet supported by our 1-arg type-driven
|
||
dispatcher (would loop) — documented constraint. 3 new tests, 13/13 total.
|
||
|
||
**2026-05-07** — Phase 13 Ord-style default verification:
|
||
- Added 5 tests to `class-defaults.sx` for myMax/myMin defined as defaults
|
||
in terms of `myCmp` (≥). Verified myMax/myMin on (3,5), (8,2), (4,4).
|
||
Suite is now 10/10.
|
||
|
||
**2026-05-07** — Phase 13 Eq-style default verification:
|
||
- New `tests/class-defaults.sx` (5 tests) seeds the class-defaults test file.
|
||
Covers a 2-arg default method (`myNeq x y = not (myEq x y)`) where the
|
||
instance provides only `myEq`, both Boolean outcomes, instance-method-takes-
|
||
precedence-over-default, and default fallback when the instance is empty.
|
||
All 5 pass.
|
||
|
||
**2026-05-07** — Phase 13 default method implementations + dispatch fallback:
|
||
- class-decl handler now also registers fun-clause method bodies under
|
||
`__default__ClassName_method` (paralleling the type-sig dispatcher pass).
|
||
- Dispatcher rewritten as nested `if`s: instance dict has the method →
|
||
use it; else look up default → use it; else raise. Earlier attempt with
|
||
`cond + and` infinite-looped — switched to plain `if` form which works.
|
||
- Both regular dispatch (`describe x = "a boolean"` instance) and default
|
||
fallback (`hello x = "hi"` default with empty instance body) verified.
|
||
No regressions in class/deriving/instance-where/eval suites.
|
||
|
||
**2026-05-07** — Phase 13 `where`-clauses in `instance` bodies:
|
||
- Bug discovered: `hk-desugar` didn't recurse into `instance-decl` method
|
||
bodies, so a `where`-form in an instance method survived to eval and hit
|
||
`eval: unknown node tag 'where'`. Fix: added an `instance-decl` case to
|
||
the desugarer that maps `hk-desugar` over the method-decls list. The
|
||
existing `fun-clause` branch then desugars each method body, including
|
||
the where → let lifting.
|
||
- 4 tests in new `tests/instance-where.sx`: where-helper with literal
|
||
pattern matching, references reused multiple times, and multi-binding
|
||
where. Verified no regression in class.sx (14/14), deriving.sx (15/15),
|
||
desugar.sx (15/15).
|
||
|
||
**2026-05-07** — Phase 12 conformance: uniquewords.hs (4/4) + setops.hs (8/8) → Phase 12 complete:
|
||
- `program-uniquewords.sx`: `foldl Set.insert` over a word list, then check
|
||
`Set.size`/`member`. 4/4.
|
||
- `program-setops.sx`: full set algebra — union/intersection/difference/
|
||
isSubsetOf with three sets s1, s2, s3 chosen so each operation has both a
|
||
positive and negative test. 8/8.
|
||
- Both added to `PROGRAMS` in `conformance.sh`. Phase 12 fully complete.
|
||
|
||
**2026-05-07** — Phase 12 unit tests `tests/set.sx` (17/17):
|
||
- 13 SX-level direct calls + 4 end-to-end via `import qualified Data.Set`.
|
||
Covers all the API + dedupe behavior. Suite is 17/17.
|
||
|
||
**2026-05-07** — Phase 12 module wiring: `import Data.Set`:
|
||
- New `hk-bind-data-set!` registers `Set.empty/singleton/insert/delete/
|
||
member/size/null/union/intersection/difference/isSubsetOf` as Haskell
|
||
builtins.
|
||
- Import handler now dispatches on modname: `Data.Map` → `hk-bind-data-map!`,
|
||
`Data.Set` → `hk-bind-data-set!`. Default alias is now derived from the
|
||
modname suffix instead of being hardcoded `Map` (was a bug for `Data.Set`).
|
||
- `test.sh` and `conformance.sh` load `set.sx` after `map.sx`.
|
||
- Verified `Set.size`, `Set.member`, `Set.union`, `Set.insert` from Haskell.
|
||
|
||
**2026-05-07** — Phase 12 Data.Set full API:
|
||
- Added `from-list`/`union`/`intersection`/`difference`/`is-subset-of`/
|
||
`filter`/`map`/`foldr`/`foldl` — all delegate to the corresponding
|
||
`hk-map-*` helpers with the value side ignored. `union`/`intersection`
|
||
use `hk-map-union-with`/`hk-map-intersection-with` with a constant
|
||
unit-returning combine fn. Spot-check confirms set semantics: dedupe
|
||
on fromList, correct ⋃/∩/− and isSubsetOf.
|
||
|
||
**2026-05-07** — Phase 12 Data.Set skeleton (wraps Data.Map with unit values):
|
||
- New `lib/haskell/set.sx`. `hk-set-empty/singleton/insert/delete/member/
|
||
size/null/to-list` all delegate to the corresponding `hk-map-*`. Storage
|
||
representation matches Map nodes; values are always `("Tuple")` (unit).
|
||
This trades a small per-node memory overhead for a one-line implementation
|
||
of every set primitive — full BST balancing comes for free. Spot-checked.
|
||
|
||
**2026-05-07** — Phase 11 conformance: wordfreq.hs (7/7) + mapgraph.hs (6/6) → Phase 11 complete:
|
||
- Extended `hk-bind-data-map!` with `Map.insertWith`, `Map.adjust`, and
|
||
`Map.findWithDefault` so the conformance programs have what they need.
|
||
- `program-wordfreq.sx`: word-frequency histogram, `foldl Map.insertWith Map.empty`.
|
||
- `program-mapgraph.sx`: adjacency list, `Map.findWithDefault [] n g` for
|
||
default-empty neighbors.
|
||
- Both added to `PROGRAMS` in `conformance.sh`. Phase 11 fully complete.
|
||
|
||
**2026-05-07** — Phase 11 unit tests `tests/map.sx` (26/26):
|
||
- 22 SX-level direct calls (empty/singleton/insert/lookup/delete/member/
|
||
fromList+duplicates/toAscList/elems/unionWith/intersectionWith/difference/
|
||
foldlWithKey/mapWithKey/filterWithKey/adjust/insertWith/alter) plus 4
|
||
end-to-end via `import qualified Data.Map as Map`. Plan asked for ≥20.
|
||
|
||
**2026-05-07** — Phase 11 module wiring: `import Data.Map`:
|
||
- Added `hk-bind-data-map!` helper in `eval.sx` that registers
|
||
`<alias>.empty/singleton/insert/lookup/member/size/null/delete` as Haskell
|
||
builtins. Default alias is `"Map"`.
|
||
- New `:import` case in `hk-bind-decls!` dispatches to `hk-bind-data-map!`
|
||
when modname = `"Data.Map"`. Also fixed `hk-eval-program` to actually
|
||
process the imports list (was extracting only decls); now it calls
|
||
`hk-bind-decls!` once on imports, then once on decls.
|
||
- `test.sh` and `conformance.sh` now load `lib/haskell/map.sx` after
|
||
`eval.sx` so the BST functions exist when the import handler binds.
|
||
- Verified `import qualified Data.Map as Map` and `import Data.Map`
|
||
(default alias) resolve `Map.empty`, `Map.insert`, `Map.lookup`, `Map.size`,
|
||
`Map.member` correctly.
|
||
|
||
**2026-05-07** — Phase 11 updating (adjust/insertWith/insertWithKey/alter):
|
||
- `adjust` recurses to find the key, replaces value with `f(v)`; no-op when
|
||
missing. `insertWith` and `insertWithKey` recurse with rebalance and use
|
||
`f new old` (or `f k new old`) when the key exists. `alter` is the most
|
||
general, implemented as `lookup → f → either delete or insert`.
|
||
|
||
**2026-05-07** — Phase 11 transforming (foldlWithKey/foldrWithKey/mapWithKey/filterWithKey):
|
||
- Folds traverse in-order. `foldlWithKey f acc m` walks left → key/val → right
|
||
threading the accumulator, so left-folding `(\acc k v -> acc ++ k ++ v)` over
|
||
a 3-key map yields `"1a2b3c"`. `foldrWithKey` runs right → key/val → left so
|
||
the cons-style accumulator `(\k v acc -> k ++ v ++ acc)` produces the same
|
||
string.
|
||
- `mapWithKey` rebuilds the tree node-by-node (no rebalancing needed — keys
|
||
unchanged so the existing structure stays valid). `filterWithKey` is a
|
||
`foldrWithKey` that re-inserts kept entries; rebalances via insert.
|
||
|
||
**2026-05-07** — Phase 11 combining (unionWith/intersectionWith/difference):
|
||
- All three implemented via `reduce` over the smaller map's `to-asc-list`,
|
||
inserting / skipping into the result. Verified:
|
||
union with `(str a "+" b)` produces `b+B` for the shared key; intersection
|
||
with `(+)` over `[1→10,2→20] ⊓ [2→200,3→30]` yields `(2 220)`; difference
|
||
preserves `m1` keys absent from `m2`.
|
||
|
||
**2026-05-07** — Phase 11 bulk operations (fromList/toList/toAscList/keys/elems):
|
||
- `hk-map-from-list` uses SX `reduce` — left-to-right, so duplicates resolve
|
||
with last-wins (matches GHC `fromList`). `to-asc-list` is in-order recursive
|
||
traversal returning `(list (list k v) ...)`. `to-list` aliases `to-asc-list`.
|
||
`keys` and `elems` are similar in-order extracts. All take SX-level pairs;
|
||
the Haskell-layer wiring (next iterations) translates Haskell cons + tuple
|
||
representations.
|
||
|
||
**2026-05-07** — Phase 11 core operations on `Data.Map` BST:
|
||
- Added `hk-map-singleton`, `hk-map-insert`, `hk-map-lookup`, `hk-map-delete`,
|
||
`hk-map-member`, `hk-map-null`. Insert recurses with `hk-map-balance` to
|
||
maintain weight invariants. Lookup returns `("Just" v)` / `("Nothing")` —
|
||
matches Haskell ADT layout. Delete uses a `hk-map-glue` helper that picks
|
||
the larger subtree and pulls its extreme element to the root, preserving
|
||
balance without imperative state. Spot-checked: insert+lookup hit/miss,
|
||
member, delete root with successor pulled from right.
|
||
|
||
**2026-05-07** — Phase 11 BST skeleton in `lib/haskell/map.sx`:
|
||
- Adams-style weight-balanced tree: node = `("Map-Node" k v l r size)`,
|
||
empty = `("Map-Empty")`. delta=3 / gamma=2 ratios. Implemented constructors
|
||
+ accessors + the four rotations (single-l, single-r, double-l, double-r)
|
||
+ `hk-map-balance` smart constructor that picks the rotation. Spot-checked
|
||
with eval calls; user-facing operations (insert/lookup/etc.) come next.
|
||
|
||
**2026-05-07** — Phase 10 conformance: statistics.hs (5/5) + newton.hs (5/5) → Phase 10 complete:
|
||
- `program-statistics.sx`: mean / variance / stdDev on a [Double], exercising
|
||
`sum`, `map`, `fromIntegral`, `/`, `sqrt`. 5/5.
|
||
- `program-newton.sx`: Newton's method for sqrt, exercising `abs`, `/`, `*`,
|
||
recursion termination on tolerance 0.0001, and `(<)` to assert convergence
|
||
to within 0.001 of the true value. 5/5.
|
||
- Both added to `PROGRAMS` in `conformance.sh`. Phase 10 fully complete.
|
||
|
||
**2026-05-07** — Phase 10 numerics test file checkbox (filename divergence):
|
||
- Plan called for `lib/haskell/tests/numeric.sx`. From the very first Phase 10
|
||
iteration I created `numerics.sx` (plural) and have been growing it. Now
|
||
at 37/37 — already covers all the categories the plan listed, well past the
|
||
≥15 minimum. Ticked the box; left a note about the filename divergence.
|
||
|
||
**2026-05-07** — Phase 10 Floating stub (pi, exp, log, sin, cos, **):
|
||
- pi as a number constant; exp/log/sin/cos as builtins thunking through to SX
|
||
primitives. `(**)` added as a binop case in `hk-binop` mapping to SX `pow`.
|
||
6 new tests in `numerics.sx` (now 37/37). `2 ** 10 = 1024`, `log (exp 5) = 5`,
|
||
`sin 0 = 0`, `cos 0 = 1`, `pi ≈ 3.14159`, `exp 0 = 1`.
|
||
|
||
**2026-05-07** — Phase 10 Fractional stub (recip, fromRational):
|
||
- `(/)` already a binop. Added `recip` and `fromRational` as builtins
|
||
post-prelude. 3 new tests in `numerics.sx` (now 31/31).
|
||
|
||
**2026-05-07** — Phase 10 math builtins (sqrt/floor/ceiling/round/truncate):
|
||
- Inserted in the post-prelude `begin` block so they override the prelude's
|
||
identity stubs. `ceiling` is the only one needing a definition (SX doesn't
|
||
ship one — derived from `floor`). `sqrt`, `floor`, `round`, `truncate`
|
||
thunk through to SX primitives. 6 new tests in `numerics.sx` (now 28/28).
|
||
|
||
**2026-05-07** — Phase 10 Float display through `hk-show-val`:
|
||
- Added `hk-show-num` and `hk-show-float-sci` helpers in `eval.sx`. Number
|
||
formatting: `integer?` → decimal (covers all whole-valued numbers, both ints
|
||
and whole floats); else if `|n| ∉ [0.1, 10^7)` → scientific (`1.0e-3`); else
|
||
→ decimal with `.0` suffix.
|
||
- `show 3.14` = `"3.14"`, `show 0.001` = `"1.0e-3"`, `show -3.14` = `"-3.14"`.
|
||
- Limit: `show 1.0e10` renders as `"10000000000"` instead of `"1.0e10"` —
|
||
Haskell distinguishes `42` from `42.0` via type, we don't. Documented.
|
||
- 4 new tests in `numerics.sx`. Suite is now 22/22.
|
||
|
||
**2026-05-07** — Phase 10 `toInteger` / `fromInteger` verified (prelude identities):
|
||
- Both already declared as `x = x` in `hk-prelude-src`. Added 4 tests in
|
||
`numerics.sx` (positive, identity round-trip, negative-via-negate, fromInteger
|
||
smoke). Suite now 18/18.
|
||
|
||
**2026-05-07** — Phase 10 `fromIntegral` verified (already an identity in prelude):
|
||
- Pre-existing `fromIntegral x = x` line in `hk-prelude-src` was already
|
||
correct — all numbers share one SX type, so the identity implementation is
|
||
exactly what the plan asked for. Added 4 tests in `numerics.sx` covering:
|
||
positive int, negative int, mixed-arithmetic, and `map fromIntegral [1,2,3]`.
|
||
Suite is now 14/14.
|
||
|
||
**2026-05-07** — Phase 10 large-integer audit (numerics.sx 10/10):
|
||
- Investigated SX number behavior in Haskell context. Findings:
|
||
• Raw SX `*`, `+`, etc. on two ints stay exact up to ±2^62 (~4.6e18).
|
||
• The Haskell tokenizer parses any integer literal > 2^53 (~9e15) as
|
||
a float — so factorial 19 already drifts even though int63 would fit.
|
||
• Once any operand is float, ops promote and decimal precision is lost.
|
||
• `Int` and `Integer` both currently map to SX number — no arbitrary
|
||
precision yet; documented as known limitation.
|
||
- New `tests/numerics.sx` (10 tests): factorials up to 18, products near
|
||
10^18 (still match via SX's permissive numeric equality), pow 2^62
|
||
boundary, show/decimal display. Header comment captures the practical
|
||
limit.
|
||
|
||
**2026-05-07** — Phase 9 conformance: `partial.hs` (7/7) → Phase 9 complete:
|
||
- New `tests/program-partial.sx` exercising `head []`, `tail []`,
|
||
`fromJust Nothing`, `undefined`, and user `error` from inside a `do` block;
|
||
verifies the error message lands in `hk-run-io`'s `io-lines`. Also a happy-
|
||
path test (`head [42] = 42`) and a "putStrLn before error preserves prior
|
||
output, never reaches subsequent action" test.
|
||
- Added `partial` to `PROGRAMS` in `conformance.sh`. Phase 9 done.
|
||
|
||
**2026-05-07** — Phase 9 `tests/errors.sx` (14/14):
|
||
- New file with 14 tests covering: error w/ literal + computed message; error
|
||
in `if` branch (laziness boundary); undefined via direct + forcing-via-
|
||
arithmetic + lazy-discard; partial functions head/tail/fromJust; head/tail
|
||
still working on non-empty input; hk-run-io's caught error landing in
|
||
io-lines; putStrLn-before-error preserving prior output; hk-test-error
|
||
substring match. Spec called for ≥10.
|
||
|
||
**2026-05-07** — Phase 9 `hk-test-error` helper in testlib.sx:
|
||
- New 0-arity-thunk-based assertion: `(hk-test-error name thunk substr)` —
|
||
evaluates `(thunk)`, expects an exception, checks `index-of` for the given
|
||
substring in the caught (string-coerced) value. Increments `hk-test-pass` on
|
||
match, otherwise records into `hk-test-fails` with descriptive expected.
|
||
- Added 2 quick uses to `tests/eval.sx` (error and head []). Suite now 66/66.
|
||
|
||
**2026-05-07** — Phase 9 `hk-run-io` catches errors, appends to io-lines:
|
||
- Wrapped both `hk-run-io` and `hk-run-io-with-input` in `(guard (e (true …)))`
|
||
that appends the caught exception to `hk-io-lines`. Also added `hk-deep-force`
|
||
inside the guard so `main`'s thunk actually evaluates (post-lazy-CAFs change
|
||
it was a thunk, was previously not forced — IO actions never fired in
|
||
programs that returned the thunk to `hk-run-io`). Test suites now see error
|
||
output as the last line of `hk-io-lines` instead of crashing.
|
||
- Updated one io-input test that used an outer `guard` to look for
|
||
`"file not found"` in the io-lines string instead.
|
||
- Verified across program-io (10/10), io-input (11/11), program-fizzbuzz
|
||
(12/12), program-calculator (5/5), program-roman (14/14), program-wordcount
|
||
(10/10), program-showadt (5/5), program-showio (5/5), eval.sx (64/64).
|
||
|
||
**2026-05-07** — Phase 9 partial functions emit proper error messages:
|
||
- Added empty-list catch clauses to `head`, `tail` in the prelude. Added
|
||
`fromJust`, `fromMaybe`, `isJust`, `isNothing` (the last three were missing).
|
||
`fromJust Nothing` raises `"Maybe.fromJust: Nothing"`. Multi-clause dispatch
|
||
tries the constructor pattern first, then falls through to the empty-list /
|
||
Nothing error clause.
|
||
- 5 new tests in `tests/eval.sx`. Suite is 64/64. Verified no regressions in
|
||
match, stdlib, fib, quicksort, program-maybe.
|
||
|
||
**2026-05-07** — Phase 9 `undefined = error "Prelude.undefined"` + lazy CAFs:
|
||
- Added `undefined = error "Prelude.undefined"` to `hk-prelude-src`. Without
|
||
any other change this raised at prelude-load time because `hk-bind-decls!`
|
||
was eagerly evaluating zero-arity definitions (CAFs). Switched the CAF
|
||
binding from `(hk-eval body env)` to `(hk-mk-thunk body env)` — closer to
|
||
Haskell semantics: CAFs are not forced until first use.
|
||
- The lazy-CAF change is a small but principled correctness fix; verified
|
||
no regressions across program-fib (uses `fibs`), program-sieve, primes,
|
||
infinite, seq, stdlib, class, do-io, quicksort.
|
||
- 2 new tests in `tests/eval.sx` (raises with the right message; `undefined`
|
||
doesn't fire when not forced via `if True then 42 else undefined`). 59/59.
|
||
|
||
**2026-05-07** — Phase 9 `error :: String -> a` raises with `hk-error:` prefix:
|
||
- Pre-existing `error` builtin was raising `"*** Exception: <msg>"` (GHC
|
||
console convention). Renamed prefix to `"hk-error: "` so the wrap-around
|
||
string SX's `apply` produces (`"Unhandled exception: \"hk-error: ...\""`)
|
||
contains a stable, searchable tag.
|
||
- Investigation confirmed that the plan's intended `(raise (list "hk-error" msg))`
|
||
format is mangled by SX `apply` to a string. Plan note added; tests use
|
||
`index-of` substring matching against the wrapped string.
|
||
- 2 new tests in `tests/eval.sx` (string and computed-message form). Suite
|
||
is 57/57. Other test suites unchanged (match 31/31, stdlib 48/48, derive
|
||
15/15, do-io 16/16, class 14/14).
|
||
|
||
**2026-05-07** — Phase 8 conformance: `showadt.hs` + `showio.hs` (both 5/5):
|
||
- `program-showadt.sx`: `deriving (Show)` on the classic `Expr = Lit | Add | Mul`
|
||
recursive ADT; tests `print` on three nested expressions and inline `show`
|
||
spot-checks (negative literal wrapped in parens; fully nested Mul of Adds).
|
||
- `program-showio.sx`: `print` on Int, Bool, list, tuple, Maybe, String, ADT
|
||
inside a `do` block; verifies one io-line per `print`.
|
||
- Both added to `PROGRAMS` in `conformance.sh`. Phase 8 conformance complete.
|
||
|
||
**2026-05-07** — Phase 8 `tests/show.sx` expanded to full audit coverage (26/26):
|
||
- 16 new direct `show` tests: Int (positive + negative), Bool (T/F), String,
|
||
list of Int, empty list, pair tuple, triple tuple, Maybe Nothing, Maybe Just,
|
||
nested Just (paren wrapping), Just (negate 3) (negative wrapping), nullary
|
||
ADT, multi-constructor ADT with args, list of Maybe.
|
||
- `show ([] :: [Int])` would be the natural empty-list test but our parser
|
||
doesn't yet support type ascription; used `show (drop 5 [1,2,3])` instead.
|
||
Char `'a'` → `"'a'"` deferred to Char-tagging design (Char = Int currently
|
||
yields `"97"`).
|
||
|
||
**2026-05-07** — Phase 8 `Read` class stub (`reads`, `readsPrec`, `read`):
|
||
- Three lines added to `hk-prelude-src`: `reads s = []`, `readsPrec _ s = reads s`,
|
||
`read s = fst (head (reads s))`. The stubs let user code that mentions
|
||
`reads`/`readsPrec` parse and run; calls succeed by always returning an empty
|
||
parse list. `read` will throw a pattern-match failure at runtime — fine until
|
||
Phase 9 `error` lands. No real parser needed per the plan.
|
||
- 3 new tests in `tests/show.sx` (now 10/10).
|
||
|
||
**2026-05-07** — Phase 8 `showsPrec` / `showParen` / `shows` / `showString` stubs:
|
||
- Added 5 lines to `hk-prelude-src`. `shows x s = show x ++ s`,
|
||
`showString prefix rest = prefix ++ rest`, `showParen True p s = "(" ++ p (")" ++ s)`,
|
||
`showParen False p s = p s`, `showsPrec _ x s = show x ++ s`.
|
||
- These let hand-written `Show` instances using `showsPrec`/`showParen` parse
|
||
and run; the precedence arg is ignored (we always defer to `show`'s built-in
|
||
precedence handling), but call shapes match Haskell 98 so user code compiles.
|
||
- New `lib/haskell/tests/show.sx` (7 tests). The file is intended to grow to
|
||
≥12 covering the full audit (Phase 8 ☐).
|
||
- Function composition `.` is not yet bound; tests use manual composition via
|
||
let-binding. Address in a later iteration.
|
||
|
||
**2026-05-06** — Phase 8 `deriving Show` nested constructor parens verified:
|
||
- The Phase 8 audit's precedence-based `hk-show-prec` already does the right
|
||
thing for `deriving Show`: each constructor arg is shown at prec 11, so any
|
||
inner constructor with args (or any negative number) gets parenthesised, while
|
||
nullary constructors and lists/tuples (whose own bracketing is unambiguous)
|
||
do not. Multi-constructor ADTs (e.g. `Tree = Leaf | Node …`) handled.
|
||
Records deferred to Phase 14.
|
||
- 4 new tests in `tests/deriving.sx` exercising nested ADT + Maybe-Maybe +
|
||
negative-arg + list-arg cases; suite is 15/15.
|
||
|
||
**2026-05-06** — Phase 8 `print` is `putStrLn (show x)` in prelude:
|
||
- Added `print x = putStrLn (show x)` to `hk-prelude-src` and removed the
|
||
standalone `print` builtin. `print` now resolves through the Haskell-level
|
||
Prelude path; lazy reference resolution handles the forward call to
|
||
`putStrLn` (registered after the prelude loads). `show` already calls
|
||
`hk-show-val` from the Phase 8 audit. do-io / program-fib / program-fizzbuzz
|
||
remain green.
|
||
|
||
**2026-05-06** — Phase 8 audit: `hk-show-val` matches Haskell 98 format:
|
||
- `eval.sx`: introduced `hk-show-prec v p` with precedence-based parens.
|
||
Top-level `show (Just 3)` = `"Just 3"` (no parens); nested `show (Just (Just 3))`
|
||
= `"Just (Just 3)"` (inner wrapped because called with prec ≥ 11). Negative
|
||
ints wrapped in parens at high prec for `show (Just (negate 1))` correctness.
|
||
- List/tuple separators changed from `", "` to `","` to match GHC.
|
||
- `hk-show-val` is now a thin shim: `(hk-show-prec v 0)`.
|
||
- Updated `tests/deriving.sx` (3 tests) and `tests/stdlib.sx` (7 tests) to the
|
||
new format. `Char` single-quote output and string escape for `\n`/`\t`
|
||
deferred — Char = Int representation prevents disambiguation in show.
|
||
|
||
**2026-05-06** — Phase 7 conformance complete (runlength-str.hs) + `++` thunk fix:
|
||
- New `lib/haskell/tests/program-runlength-str.sx` (9 tests). Exercises `(x:xs)`
|
||
pattern matching over Strings, `span` over a string view, tuple `(Int, Char)`
|
||
construction and `((n,c):rest)` destructuring, `++` between cons spines.
|
||
- `runlength-str` added to `PROGRAMS` in `conformance.sh`.
|
||
- `eval.sx`: `hk-list-append` now `(hk-force a)` on entry. Pre-existing latent
|
||
bug — when a cons's tail was a thunk (e.g. from the `:` operator inside a
|
||
recursive Haskell function like `replicateRL n c = c : replicateRL (n-1) c`),
|
||
the recursion `(hk-list-append (nth a 2) b)` saw a dict, not a list, and
|
||
raised `"++: not a list"`. Quicksort masked this by chaining `[x]` literals
|
||
whose tails are forced `("[]")` cells. Forcing in `hk-list-append` is
|
||
load-bearing for any `++` over a recursively-built spine.
|
||
|
||
**2026-05-06** — Phase 7 conformance (caesar.hs):
|
||
- New `lib/haskell/tests/program-caesar.sx` (8 tests). Caesar cipher exercising
|
||
`chr`, `ord`, `isUpper`, `isLower`, `mod`, `map`, and `(x:xs)` pattern matching
|
||
over native String values via the Phase 7 string-view path. Adapted from
|
||
https://rosettacode.org/wiki/Caesar_cipher#Haskell.
|
||
- `caesar` added to `PROGRAMS` in `lib/haskell/conformance.sh`. Suite isolated:
|
||
8/8 passing. Note: `else chr c` in `shift` keeps the char-as-string output type
|
||
consistent with the alpha branches (pattern bind on a string view yields an int).
|
||
|
||
**2026-05-06** — Phase 7 complete (string-view O(1) head/tail + `++` native concat):
|
||
- `runtime.sx`: added `hk-str?`, `hk-str-head`, `hk-str-tail`, `hk-str-null?`.
|
||
String views are `{:hk-str buf :hk-off n}` dicts; native SX strings satisfy the
|
||
predicate with implicit offset 0. All helpers are O(1) via `char-at` / `string-length`.
|
||
- `eval.sx`: added `chr` (int → single-char string via `char-from-code`), `toUpper`,
|
||
`toLower` (ASCII-range arithmetic). Fixed `ord` and all char predicates (`isAlpha`,
|
||
`isAlphaNum`, `isDigit`, `isSpace`, `isUpper`, `isLower`, `digitToInt`) to accept
|
||
integers from string-view decomposition (not only single-char strings).
|
||
- `match.sx`: cons-pattern `":"` now checks `hk-str?` before the tagged-list path,
|
||
decomposing to `(hk-str-head, hk-str-tail)`. Empty-list pattern (`p-list []`) also
|
||
accepts `hk-str-null?` values. `hk-match-list-pat` updated to traverse string views
|
||
element-by-element.
|
||
- `runtime.sx`: added `hk-str-to-native` (converts view dict to native string via reduce+char-at).
|
||
- `eval.sx`: `hk-list-append` now checks `hk-str?` first; converts both operands via
|
||
`hk-str-to-native` before native `str` concat. String `++` String no longer builds
|
||
a cons spine.
|
||
- 35 new tests in `lib/haskell/tests/string-char.sx` (35/35 passing).
|
||
- Full suite: 810/810 tests, 0 regressions (was 775).
|