Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
16 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)","'a'"(Char shows with single-quotes),"\"hello\""(String shows with escaped double-quotes). 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 ++ ")".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). - 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 (list "hk-error" msg))in SX.undefined :: a=error "Prelude.undefined".- Partial functions emit proper error messages:
head []→"Prelude.head: empty list",tail []→"Prelude.tail: empty list",fromJust Nothing→"Maybe.fromJust: Nothing". - Top-level
hk-run-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.fromIntegral :: (Integral a, Num b) => a -> b— identity in our runtime (all numbers share one SX type); register as a builtin no-op with the correct typeclass signature.toInteger,fromInteger— same treatment.- Float/Double literals round-trip through
hk-show-val:show 3.14 = "3.14",show 1.0e10 = "1.0e10". - Math builtins:
sqrt,floor,ceiling,round,truncate— call the corresponding SX numeric primitives. Fractionaltypeclass stub:(/),recip,fromRational.Floatingtypeclass stub:pi,exp,log,sin,cos,(**)(power operator, maps to SX exponentiation).- Tests in
lib/haskell/tests/numeric.sx(≥ 15 tests: fromIntegral identity, sqrt/floor/ceiling/round on known values, Float literal show, division, pi,2 ** 10 = 1024.0). - Conformance programs:
statistics.hs— mean, variance, std-dev on a[Double]. 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-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).