Files
rose-ash/plans/haskell-completeness.md
giles 208953667b
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 35s
haskell: Phase 12 — Data.Set skeleton (wraps Data.Map with unit values)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 11:37:39 +00:00

36 KiB
Raw Blame History

Haskell-on-SX: completeness roadmap (Phases 716)

Continuation of plans/haskell-on-sx.md. Phases 16 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)

  • 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)", "\"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.
  • 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 ++ ")". Records deferred — Phase 14.
  • 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). Char tests deferred: Char = Int representation; show on a Char is currently "97" not "'a'".
  • Conformance programs:
    • showadt.hsdata Expr = Lit Int | Add Expr Expr | Mul Expr Expr with deriving Show; prints a tree.
    • showio.hsprint on various types in a do block.

Phase 9 — error / undefined

  • 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.
  • 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. 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.
  • 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.
  • toInteger, fromInteger — same treatment. Already in prelude as toInteger x = x and fromInteger x = x; verified with new tests.
  • 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.
  • Math builtins: sqrt, floor, ceiling, round, truncate — call the corresponding SX numeric primitives.
  • Fractional typeclass stub: (/), recip, fromRational. (/) already a binop; recip x = 1 / x and fromRational x = x registered as builtins in the post-prelude block.
  • Floating typeclass stub: pi, exp, log, sin, cos, (**) (power operator, maps to SX exponentiation).
  • 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.)
  • 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 (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.)
  • 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. Chose the wrapper approach: Set k = Map k ().
  • 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.hsclass 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.hsdata 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.hstry pattern: run an action, branch on Left/Right.

Progress log

Newest first.

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).