Files
rose-ash/plans/haskell-completeness.md
giles 1b7bd86b43
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 1m8s
haskell: Phase 10 — Float show with .0 suffix and scientific form (+4 tests, 22/22)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 07:55:54 +00:00

29 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.
  • Floating typeclass stub: pi, exp, log, sin, cos, (**) (power operator, maps to SX exponentiation).
  • Tests in lib/haskell/tests/numeric.sx (≥ 15 tests: fromIntegral identity, sqrt/floor/ceiling/round on known values, Float literal show, division, pi, 2 ** 10 = 1024.0).
  • Conformance programs:
    • statistics.hs — mean, variance, std-dev on a [Double]. Exercises fromIntegral, sqrt, /.
    • newton.hs — Newton's method for square root. Exercises Float, abs, iteration.

Phase 11 — Data.Map

  • Implement a weight-balanced BST in pure SX in lib/haskell/map.sx. Internal node representation: ("Map-Node" key val left right size). Leaf: ("Map-Empty").
  • Core operations: empty, singleton, insert, lookup, delete, member, size, null.
  • Bulk operations: fromList, toList, toAscList, keys, elems.
  • Combining: unionWith, intersectionWith, difference.
  • Transforming: foldlWithKey, foldrWithKey, mapWithKey, filterWithKey.
  • Updating: adjust, insertWith, insertWithKey, alter.
  • Module wiring: import Data.Map and import qualified Data.Map as Map resolve to the map.sx namespace dict in the eval import handler.
  • Unit tests in lib/haskell/tests/map.sx (≥ 20 tests: empty, singleton, insert + lookup hit/miss, delete root, fromList with duplicates, toAscList ordering, unionWith, foldlWithKey).
  • Conformance programs:
    • wordfreq.hs — word-frequency histogram using Data.Map. Source from Rosetta Code "Word frequency" Haskell entry.
    • mapgraph.hs — adjacency-list BFS using Data.Map.

Phase 12 — Data.Set

  • Implement Data.Set in lib/haskell/set.sx. Use a standalone weight-balanced BST (same structure as Map but no value field) or wrap Data.Map with unit values.
  • API: empty, singleton, insert, delete, member, fromList, toList, toAscList, size, null, union, intersection, difference, isSubsetOf, filter, map, foldr, foldl'.
  • Module wiring: import Data.Set / import qualified Data.Set as Set.
  • Unit tests in lib/haskell/tests/set.sx (≥ 15 tests: empty, insert, member hit/miss, delete, fromList deduplication, union, intersection, difference, isSubsetOf).
  • Conformance programs:
    • uniquewords.hs — unique words in a string using Data.Set.
    • setops.hs — set union/intersection/difference on integer sets; exercises all three combining operations.

Phase 13 — where in typeclass instances + default methods

  • Verify where-clauses in instance bodies desugar correctly. The hk-bind-decls! instance arm must call the same where-lifting logic as top-level function clauses. Write a targeted test to confirm.
  • Class declarations may include default method implementations. Parser: hk-parse-class collects method decls; eval registers defaults under "__default__ClassName_method" in the class dict.
  • Instance method lookup: when the instance dict lacks a method, fall back to the default. Wire this into the dictionary-passing dispatch.
  • Eq default: (/=) x y = not (x == y). Verify it works without an explicit /= in every Eq instance.
  • Ord defaults: max a b = if a >= b then a else b, min a b = if a <= b then a else b. Verify.
  • Num defaults: negate x = 0 - x, abs x = if x < 0 then negate x else x, signum x = if x > 0 then 1 else if x < 0 then -1 else 0. Verify.
  • Tests in lib/haskell/tests/class-defaults.sx (≥ 10 tests).
  • Conformance programs:
    • shapes.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 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).