Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
34 KiB
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-treeMCP tools only. - One feature per commit. Keep
## Progress logupdated.
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 vextracts the character at offsetnas an integer (ord value).hk-str-tail vreturns a new view with offsetn+1; O(1).hk-str-null? vis 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 shifts !! n— O(n) (n tail calls)(c:s)construction — O(n) for full[Char]construction (same as real Haskell)++on two strings — nativestrconcat, 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/**andplans/haskell-completeness.md. No edits tospec/,hosts/,shared/, otherlib/<lang>/dirs, orlib/root. - SX files:
sx-treeMCP tools only.sx_validateafter every edit. - Commits: one feature per commit. Keep
## Progress logupdated. - Tests:
bash lib/haskell/test.shmust be green before any commit. After adding new programs, runbash lib/haskell/conformance.shand commit the updatedscoreboard.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.shPROGRAMS 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 toruntime.sxcovering both native SX strings and{:hk-str buf :hk-off n}view dicts. - Implement
hk-str-head,hk-str-tail,hk-str-null?helpers inruntime.sx. - In
match.sx, intercept cons-pattern":"when scrutinee satisfieshk-str?; decompose to (char-int, view) instead of the tagged-list path. Nil-pattern"[]"matcheshk-str-null?. - Add builtins:
chr(int → single-char string), verifyordreturns int,toUpper,toLower(ASCII range arithmetic on ints). - Ensure
++between two strings concatenates natively viastrrather 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. Exercisesmap,chr,ord,toUpper,toLoweron 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-valinruntime.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/\tescape inside Strings. showPrelude binding callshk-show-val;print x = putStrLn (show x).deriving Showauto-generates proper show for record-style and multi-constructor ADTs. Nested application arguments wrapped in parens: ifshow argcontains a space, emit"(" ++ show arg ++ ")". Records deferred — Phase 14.showsPrec/showParenstubs so hand-written Show instances compile.Readclass stub — just enough forreads :: 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.hs—data Expr = Lit Int | Add Expr Expr | Mul Expr Exprwithderiving Show; prints a tree.showio.hs—printon various types in adoblock.
Phase 9 — error / undefined
error :: String -> a— raises(raise "hk-error: <msg>")in SX. Plan amended: SX'sapplyrewrites 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-iocatcheshk-errortag and returns it as a tagged error result so test suites can inspect it without crashing. hk-test-errorhelper intestlib.sx:(hk-test-error "desc" thunk expected-substring)— asserts the thunk raises anhk-errorwhose 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-errorhelper). - Conformance programs:
partial.hs— exerciseshead [],tail [],fromJust Nothingcaught 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 innumerics.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 inhk-prelude-srcasfromIntegral x = x; verified with new tests innumerics.sx.toInteger,fromInteger— same treatment. Already in prelude astoInteger x = xandfromInteger 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 distinguish42from42.0— both are SX numbers whereinteger?is true. Existing tests likeshow 42 = "42"rely on this rendering. Documented innumerics.sx. - Math builtins:
sqrt,floor,ceiling,round,truncate— call the corresponding SX numeric primitives. Fractionaltypeclass stub:(/),recip,fromRational. (/) already a binop;recip x = 1 / xandfromRational x = xregistered as builtins in the post-prelude block.Floatingtypeclass 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]. ExercisesfromIntegral,sqrt,/.newton.hs— Newton's method for square root. ExercisesFloat,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.Mapandimport qualified Data.Map as Mapresolve to themap.sxnamespace 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 usingData.Map. Source from Rosetta Code "Word frequency" Haskell entry.mapgraph.hs— adjacency-list BFS usingData.Map.
Phase 12 — Data.Set
- Implement
Data.Setinlib/haskell/set.sx. Use a standalone weight-balanced BST (same structure as Map but no value field) or wrapData.Mapwith 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 usingData.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 ininstancebodies desugar correctly. Thehk-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-classcollects 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.
Eqdefault:(/=) x y = not (x == y). Verify it works without an explicit/=in every Eq instance.Orddefaults:max a b = if a >= b then a else b,min a b = if a <= b then a else b. Verify.Numdefaults: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 awith a defaultperimeter; two instances usingwhere-local helpers.
Phase 14 — Record syntax
- Parser: extend
hk-parse-datato recognise{ field :: Type, … }constructor bodies. AST node:(:con-rec CNAME [(FNAME TYPE) …]). - Desugar:
:con-rec→ positional:con-defplus 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 inhk-constructorsat registration. - Exhaustive record patterns:
Foo { bar = b }in case bindsb, wildcards remaining fields. - Tests in
lib/haskell/tests/records.sx(≥ 12 tests: creation, accessor, update one field, update two fields, record pattern,deriving Showon 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 arepresentation: a dict{:hk-ioref true :hk-value v}. Allocation creates a new dict in the IO monad. Mutation viadict-set!.newIORef :: a -> IO (IORef a)— wraps a new dict inIO.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.IORefmodule 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 viaIORef Int; increment in a recursive IO loop; read at end.accumulate.hs— accumulate results intoIORef [Int]inside a mapped IO action, read at the end.
Phase 16 — Exception handling
SomeExceptiontype:data SomeException = SomeException String.IOException = SomeException.throwIO :: Exception e => e -> IO a— raises("hk-exception" e).evaluate :: a -> IO a— forces arg strictly; any embeddedhk-errorsurfaces as a catchableSomeException.catch :: Exception e => IO a -> (e -> IO a) -> IO a— wraps action in SXguard; onhk-errororhk-exception, calls the handler with aSomeExceptionvalue.try :: Exception e => IO a -> IO (Either e a)— returnsRight von success,Left eon 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 usingcatch; divide-by-zero raises, handler returns 0.trycatch.hs—trypattern: run an action, branch on Left/Right.
Progress log
Newest first.
2026-05-07 — Phase 11 module wiring: import Data.Map:
- Added
hk-bind-data-map!helper ineval.sxthat registers<alias>.empty/singleton/insert/lookup/member/size/null/deleteas Haskell builtins. Default alias is"Map". - New
:importcase inhk-bind-decls!dispatches tohk-bind-data-map!when modname ="Data.Map". Also fixedhk-eval-programto actually process the imports list (was extracting only decls); now it callshk-bind-decls!once on imports, then once on decls. test.shandconformance.shnow loadlib/haskell/map.sxaftereval.sxso the BST functions exist when the import handler binds.- Verified
import qualified Data.Map as Mapandimport Data.Map(default alias) resolveMap.empty,Map.insert,Map.lookup,Map.size,Map.membercorrectly.
2026-05-07 — Phase 11 updating (adjust/insertWith/insertWithKey/alter):
adjustrecurses to find the key, replaces value withf(v); no-op when missing.insertWithandinsertWithKeyrecurse with rebalance and usef new old(orf k new old) when the key exists.alteris the most general, implemented aslookup → f → either delete or insert.
2026-05-07 — Phase 11 transforming (foldlWithKey/foldrWithKey/mapWithKey/filterWithKey):
- Folds traverse in-order.
foldlWithKey f acc mwalks left → key/val → right threading the accumulator, so left-folding(\acc k v -> acc ++ k ++ v)over a 3-key map yields"1a2b3c".foldrWithKeyruns right → key/val → left so the cons-style accumulator(\k v acc -> k ++ v ++ acc)produces the same string. mapWithKeyrebuilds the tree node-by-node (no rebalancing needed — keys unchanged so the existing structure stays valid).filterWithKeyis afoldrWithKeythat re-inserts kept entries; rebalances via insert.
2026-05-07 — Phase 11 combining (unionWith/intersectionWith/difference):
- All three implemented via
reduceover the smaller map'sto-asc-list, inserting / skipping into the result. Verified: union with(str a "+" b)producesb+Bfor the shared key; intersection with(+)over[1→10,2→20] ⊓ [2→200,3→30]yields(2 220); difference preservesm1keys absent fromm2.
2026-05-07 — Phase 11 bulk operations (fromList/toList/toAscList/keys/elems):
hk-map-from-listuses SXreduce— left-to-right, so duplicates resolve with last-wins (matches GHCfromList).to-asc-listis in-order recursive traversal returning(list (list k v) ...).to-listaliasesto-asc-list.keysandelemsare 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 withhk-map-balanceto maintain weight invariants. Lookup returns("Just" v)/("Nothing")— matches Haskell ADT layout. Delete uses ahk-map-gluehelper 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-balancesmart 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], exercisingsum,map,fromIntegral,/,sqrt. 5/5.program-newton.sx: Newton's method for sqrt, exercisingabs,/,*, recursion termination on tolerance 0.0001, and(<)to assert convergence to within 0.001 of the true value. 5/5.- Both added to
PROGRAMSinconformance.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 creatednumerics.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 inhk-binopmapping to SXpow. 6 new tests innumerics.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. AddedrecipandfromRationalas builtins post-prelude. 3 new tests innumerics.sx(now 31/31).
2026-05-07 — Phase 10 math builtins (sqrt/floor/ceiling/round/truncate):
- Inserted in the post-prelude
beginblock so they override the prelude's identity stubs.ceilingis the only one needing a definition (SX doesn't ship one — derived fromfloor).sqrt,floor,round,truncatethunk through to SX primitives. 6 new tests innumerics.sx(now 28/28).
2026-05-07 — Phase 10 Float display through hk-show-val:
- Added
hk-show-numandhk-show-float-scihelpers ineval.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.0suffix. show 3.14="3.14",show 0.001="1.0e-3",show -3.14="-3.14".- Limit:
show 1.0e10renders as"10000000000"instead of"1.0e10"— Haskell distinguishes42from42.0via 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 = xinhk-prelude-src. Added 4 tests innumerics.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 = xline inhk-prelude-srcwas already correct — all numbers share one SX type, so the identity implementation is exactly what the plan asked for. Added 4 tests innumerics.sxcovering: positive int, negative int, mixed-arithmetic, andmap 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. •IntandIntegerboth 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.sxexercisinghead [],tail [],fromJust Nothing,undefined, and usererrorfrom inside adoblock; verifies the error message lands inhk-run-io'sio-lines. Also a happy- path test (head [42] = 42) and a "putStrLn before error preserves prior output, never reaches subsequent action" test. - Added
partialtoPROGRAMSinconformance.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
ifbranch (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, checksindex-offor the given substring in the caught (string-coerced) value. Incrementshk-test-passon match, otherwise records intohk-test-failswith 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-ioandhk-run-io-with-inputin(guard (e (true …)))that appends the caught exception tohk-io-lines. Also addedhk-deep-forceinside the guard somain'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 tohk-run-io). Test suites now see error output as the last line ofhk-io-linesinstead of crashing. - Updated one io-input test that used an outer
guardto 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,tailin the prelude. AddedfromJust,fromMaybe,isJust,isNothing(the last three were missing).fromJust Nothingraises"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"tohk-prelude-src. Without any other change this raised at prelude-load time becausehk-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;undefineddoesn't fire when not forced viaif True then 42 else undefined). 59/59.
2026-05-07 — Phase 9 error :: String -> a raises with hk-error: prefix:
- Pre-existing
errorbuiltin was raising"*** Exception: <msg>"(GHC console convention). Renamed prefix to"hk-error: "so the wrap-around string SX'sapplyproduces ("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 SXapplyto a string. Plan note added; tests useindex-ofsubstring 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 classicExpr = Lit | Add | Mulrecursive ADT; testsprinton three nested expressions and inlineshowspot-checks (negative literal wrapped in parens; fully nested Mul of Adds).program-showio.sx:printon Int, Bool, list, tuple, Maybe, String, ADT inside adoblock; verifies one io-line perprint.- Both added to
PROGRAMSinconformance.sh. Phase 8 conformance complete.
2026-05-07 — Phase 8 tests/show.sx expanded to full audit coverage (26/26):
- 16 new direct
showtests: 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; usedshow (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 mentionsreads/readsPrecparse and run; calls succeed by always returning an empty parse list.readwill throw a pattern-match failure at runtime — fine until Phase 9errorlands. 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
Showinstances usingshowsPrec/showParenparse and run; the precedence arg is ignored (we always defer toshow'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-precalready does the right thing forderiving 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.sxexercising 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)tohk-prelude-srcand removed the standaloneprintbuiltin.printnow resolves through the Haskell-level Prelude path; lazy reference resolution handles the forward call toputStrLn(registered after the prelude loads).showalready callshk-show-valfrom 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: introducedhk-show-prec v pwith precedence-based parens. Top-levelshow (Just 3)="Just 3"(no parens); nestedshow (Just (Just 3))="Just (Just 3)"(inner wrapped because called with prec ≥ 11). Negative ints wrapped in parens at high prec forshow (Just (negate 1))correctness.- List/tuple separators changed from
", "to","to match GHC. hk-show-valis now a thin shim:(hk-show-prec v 0).- Updated
tests/deriving.sx(3 tests) andtests/stdlib.sx(7 tests) to the new format.Charsingle-quote output and string escape for\n/\tdeferred — 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,spanover a string view, tuple(Int, Char)construction and((n,c):rest)destructuring,++between cons spines. runlength-stradded toPROGRAMSinconformance.sh.eval.sx:hk-list-appendnow(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 likereplicateRL 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 inhk-list-appendis 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 exercisingchr,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. caesaradded toPROGRAMSinlib/haskell/conformance.sh. Suite isolated: 8/8 passing. Note:else chr cinshiftkeeps 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: addedhk-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) viachar-at/string-length.eval.sx: addedchr(int → single-char string viachar-from-code),toUpper,toLower(ASCII-range arithmetic). Fixedordand 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 checkshk-str?before the tagged-list path, decomposing to(hk-str-head, hk-str-tail). Empty-list pattern (p-list []) also acceptshk-str-null?values.hk-match-list-patupdated to traverse string views element-by-element.runtime.sx: addedhk-str-to-native(converts view dict to native string via reduce+char-at).eval.sx:hk-list-appendnow checkshk-str?first; converts both operands viahk-str-to-nativebefore nativestrconcat. 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).