diff --git a/lib/haskell/conformance.sh b/lib/haskell/conformance.sh index ddd0a45c..fa34edae 100755 --- a/lib/haskell/conformance.sh +++ b/lib/haskell/conformance.sh @@ -20,7 +20,7 @@ if [ ! -x "$SX_SERVER" ]; then fi fi -PROGRAMS=(fib sieve quicksort nqueens calculator collatz palindrome maybe fizzbuzz anagram roman binary either primes zipwith matrix wordcount powers caesar runlength-str showadt showio partial statistics newton wordfreq mapgraph uniquewords setops shapes person config) +PROGRAMS=(fib sieve quicksort nqueens calculator collatz palindrome maybe fizzbuzz anagram roman binary either primes zipwith matrix wordcount powers caesar runlength-str showadt showio partial statistics newton wordfreq mapgraph uniquewords setops shapes person config counter accumulate) PASS_COUNTS=() FAIL_COUNTS=() diff --git a/lib/haskell/eval.sx b/lib/haskell/eval.sx index fb74cfef..84fead3b 100644 --- a/lib/haskell/eval.sx +++ b/lib/haskell/eval.sx @@ -1242,6 +1242,78 @@ (hk-of-bool (hk-set-is-subset-of (hk-force a) (hk-force b)))) 2)))))) +(define + hk-bind-data-ioref! + (fn + (env alias) + (let + ((p (str alias "."))) + (begin + (dict-set! + env + (str p "newIORef") + (hk-mk-lazy-builtin + "IORef.newIORef" + (fn + (v) + (let + ((ref (dict))) + (begin + (dict-set! ref "hk-ioref" true) + (dict-set! ref "hk-value" v) + (list "IO" ref)))) + 1)) + (dict-set! + env + (str p "readIORef") + (hk-mk-lazy-builtin + "IORef.readIORef" + (fn (r) (list "IO" (get (hk-force r) "hk-value"))) + 1)) + (dict-set! + env + (str p "writeIORef") + (hk-mk-lazy-builtin + "IORef.writeIORef" + (fn + (r v) + (begin + (dict-set! (hk-force r) "hk-value" v) + (list "IO" (list "Tuple")))) + 2)) + (dict-set! + env + (str p "modifyIORef") + (hk-mk-lazy-builtin + "IORef.modifyIORef" + (fn + (r f) + (let + ((ref (hk-force r))) + (begin + (dict-set! + ref + "hk-value" + (hk-apply f (get ref "hk-value"))) + (list "IO" (list "Tuple"))))) + 2)) + (dict-set! + env + (str p "modifyIORef'") + (hk-mk-lazy-builtin + "IORef.modifyIORef'" + (fn + (r f) + (let + ((ref (hk-force r))) + (begin + (dict-set! + ref + "hk-value" + (hk-deep-force (hk-apply f (get ref "hk-value")))) + (list "IO" (list "Tuple"))))) + 2)))))) + (define hk-bind-decls! (fn @@ -1450,10 +1522,12 @@ (let ((modname (nth d 2)) (as-name (nth d 3))) (let - ((alias (cond ((not (nil? as-name)) as-name) ((= modname "Data.Map") "Map") ((= modname "Data.Set") "Set") (:else modname)))) + ((alias (cond ((not (nil? as-name)) as-name) ((= modname "Data.Map") "Map") ((= modname "Data.Set") "Set") ((= modname "Data.IORef") "IORef") (:else modname)))) (cond ((= modname "Data.Map") (hk-bind-data-map! env alias)) ((= modname "Data.Set") (hk-bind-data-set! env alias)) + ((= modname "Data.IORef") + (hk-bind-data-ioref! env alias)) (:else nil))))) (:else nil))) decls) diff --git a/lib/haskell/scoreboard.json b/lib/haskell/scoreboard.json index 6f7884c9..61573d6e 100644 --- a/lib/haskell/scoreboard.json +++ b/lib/haskell/scoreboard.json @@ -1,7 +1,7 @@ { - "date": "2026-05-06", - "total_pass": 156, - "total_fail": 0, + "date": "2026-05-07", + "total_pass": 266, + "total_fail": 3, "programs": { "fib": {"pass": 2, "fail": 0}, "sieve": {"pass": 2, "fail": 0}, @@ -9,7 +9,7 @@ "nqueens": {"pass": 2, "fail": 0}, "calculator": {"pass": 5, "fail": 0}, "collatz": {"pass": 11, "fail": 0}, - "palindrome": {"pass": 8, "fail": 0}, + "palindrome": {"pass": 9, "fail": 3}, "maybe": {"pass": 12, "fail": 0}, "fizzbuzz": {"pass": 12, "fail": 0}, "anagram": {"pass": 9, "fail": 0}, @@ -19,7 +19,23 @@ "primes": {"pass": 12, "fail": 0}, "zipwith": {"pass": 9, "fail": 0}, "matrix": {"pass": 8, "fail": 0}, - "wordcount": {"pass": 7, "fail": 0}, - "powers": {"pass": 14, "fail": 0} + "wordcount": {"pass": 10, "fail": 0}, + "powers": {"pass": 14, "fail": 0}, + "caesar": {"pass": 8, "fail": 0}, + "runlength-str": {"pass": 9, "fail": 0}, + "showadt": {"pass": 5, "fail": 0}, + "showio": {"pass": 5, "fail": 0}, + "partial": {"pass": 7, "fail": 0}, + "statistics": {"pass": 5, "fail": 0}, + "newton": {"pass": 5, "fail": 0}, + "wordfreq": {"pass": 7, "fail": 0}, + "mapgraph": {"pass": 6, "fail": 0}, + "uniquewords": {"pass": 4, "fail": 0}, + "setops": {"pass": 8, "fail": 0}, + "shapes": {"pass": 5, "fail": 0}, + "person": {"pass": 7, "fail": 0}, + "config": {"pass": 10, "fail": 0}, + "counter": {"pass": 7, "fail": 0}, + "accumulate": {"pass": 8, "fail": 0} } } diff --git a/lib/haskell/scoreboard.md b/lib/haskell/scoreboard.md index 500f8394..4b656ca9 100644 --- a/lib/haskell/scoreboard.md +++ b/lib/haskell/scoreboard.md @@ -1,6 +1,6 @@ # Haskell-on-SX Scoreboard -Updated 2026-05-06 · Phase 6 (prelude extras + 18 programs) +Updated 2026-05-07 · Phase 6 (prelude extras + 18 programs) | Program | Tests | Status | |---------|-------|--------| @@ -10,7 +10,7 @@ Updated 2026-05-06 · Phase 6 (prelude extras + 18 programs) | nqueens.hs | 2/2 | ✓ | | calculator.hs | 5/5 | ✓ | | collatz.hs | 11/11 | ✓ | -| palindrome.hs | 8/8 | ✓ | +| palindrome.hs | 9/12 | ✗ | | maybe.hs | 12/12 | ✓ | | fizzbuzz.hs | 12/12 | ✓ | | anagram.hs | 9/9 | ✓ | @@ -20,6 +20,22 @@ Updated 2026-05-06 · Phase 6 (prelude extras + 18 programs) | primes.hs | 12/12 | ✓ | | zipwith.hs | 9/9 | ✓ | | matrix.hs | 8/8 | ✓ | -| wordcount.hs | 7/7 | ✓ | +| wordcount.hs | 10/10 | ✓ | | powers.hs | 14/14 | ✓ | -| **Total** | **156/156** | **18/18 programs** | +| caesar.hs | 8/8 | ✓ | +| runlength-str.hs | 9/9 | ✓ | +| showadt.hs | 5/5 | ✓ | +| showio.hs | 5/5 | ✓ | +| partial.hs | 7/7 | ✓ | +| statistics.hs | 5/5 | ✓ | +| newton.hs | 5/5 | ✓ | +| wordfreq.hs | 7/7 | ✓ | +| mapgraph.hs | 6/6 | ✓ | +| uniquewords.hs | 4/4 | ✓ | +| setops.hs | 8/8 | ✓ | +| shapes.hs | 5/5 | ✓ | +| person.hs | 7/7 | ✓ | +| config.hs | 10/10 | ✓ | +| counter.hs | 7/7 | ✓ | +| accumulate.hs | 8/8 | ✓ | +| **Total** | **266/269** | **33/34 programs** | diff --git a/lib/haskell/tests/ioref.sx b/lib/haskell/tests/ioref.sx new file mode 100644 index 00000000..5331c648 --- /dev/null +++ b/lib/haskell/tests/ioref.sx @@ -0,0 +1,94 @@ +;; Phase 15 — IORef unit tests. + +(hk-test + "newIORef + readIORef returns initial value" + (hk-deep-force + (hk-run + "import qualified Data.IORef as IORef\nmain = do { r <- IORef.newIORef 42; v <- IORef.readIORef r; return v }")) + (list "IO" 42)) + +(hk-test + "writeIORef updates the cell" + (hk-deep-force + (hk-run + "import qualified Data.IORef as IORef\nmain = do { r <- IORef.newIORef 0; IORef.writeIORef r 99; v <- IORef.readIORef r; return v }")) + (list "IO" 99)) + +(hk-test + "writeIORef returns IO ()" + (hk-deep-force + (hk-run + "import qualified Data.IORef as IORef\nmain = do { r <- IORef.newIORef 0; IORef.writeIORef r 1 }")) + (list "IO" (list "Tuple"))) + +(hk-test + "modifyIORef applies a function" + (hk-deep-force + (hk-run + "import qualified Data.IORef as IORef\nmain = do { r <- IORef.newIORef 5; IORef.modifyIORef r (\\x -> x * 2); v <- IORef.readIORef r; return v }")) + (list "IO" 10)) + +(hk-test + "modifyIORef' (strict) applies a function" + (hk-deep-force + (hk-run + "import qualified Data.IORef as IORef\nmain = do { r <- IORef.newIORef 7; IORef.modifyIORef' r (\\x -> x + 3); v <- IORef.readIORef r; return v }")) + (list "IO" 10)) + +(hk-test + "two reads return the same value" + (hk-deep-force + (hk-run + "import qualified Data.IORef as IORef\nmain = do { r <- IORef.newIORef 11; a <- IORef.readIORef r; b <- IORef.readIORef r; return (a + b) }")) + (list "IO" 22)) + +(hk-test + "shared ref across do-steps: write then read" + (hk-deep-force + (hk-run + "import qualified Data.IORef as IORef\nmain = do { r <- IORef.newIORef 1; IORef.writeIORef r 2; IORef.writeIORef r 3; v <- IORef.readIORef r; return v }")) + (list "IO" 3)) + +(hk-test + "two refs are independent" + (hk-deep-force + (hk-run + "import qualified Data.IORef as IORef\nmain = do { r1 <- IORef.newIORef 1; r2 <- IORef.newIORef 2; IORef.writeIORef r1 10; a <- IORef.readIORef r1; b <- IORef.readIORef r2; return (a + b) }")) + (list "IO" 12)) + +(hk-test + "string-valued IORef" + (hk-deep-force + (hk-run + "import qualified Data.IORef as IORef\nmain = do { r <- IORef.newIORef \"hi\"; IORef.writeIORef r \"bye\"; v <- IORef.readIORef r; return v }")) + (list "IO" "bye")) + +(hk-test + "list-valued IORef + cons" + (hk-deep-force + (hk-run + "import qualified Data.IORef as IORef\nmain = do { r <- IORef.newIORef [1,2,3]; IORef.modifyIORef r (\\xs -> 0 : xs); v <- IORef.readIORef r; return v }")) + (list + "IO" + (list ":" 0 (list ":" 1 (list ":" 2 (list ":" 3 (list "[]"))))))) + +(hk-test + "counter loop: increment N times" + (hk-deep-force + (hk-run + "import qualified Data.IORef as IORef\nloop r 0 = return ()\nloop r n = do { IORef.modifyIORef r (\\x -> x + 1); loop r (n - 1) }\nmain = do { r <- IORef.newIORef 0; loop r 10; v <- IORef.readIORef r; return v }")) + (list "IO" 10)) + +(hk-test + "modifyIORef' inside a loop" + (hk-deep-force + (hk-run + "import qualified Data.IORef as IORef\ngo r 0 = return ()\ngo r n = do { IORef.modifyIORef' r (\\x -> x + n); go r (n - 1) }\nmain = do { r <- IORef.newIORef 0; go r 5; v <- IORef.readIORef r; return v }")) + (list "IO" 15)) + +(hk-test + "newIORef inside a function passed via parameter" + (hk-deep-force + (hk-run + "import qualified Data.IORef as IORef\nbump r = IORef.modifyIORef r (\\x -> x + 100)\nmain = do { r <- IORef.newIORef 1; bump r; v <- IORef.readIORef r; return v }")) + (list "IO" 101)) diff --git a/lib/haskell/tests/program-accumulate.sx b/lib/haskell/tests/program-accumulate.sx new file mode 100644 index 00000000..56f59398 --- /dev/null +++ b/lib/haskell/tests/program-accumulate.sx @@ -0,0 +1,81 @@ +;; accumulate.hs — accumulate results into an IORef [Int] (Phase 15 conformance). + +(define + hk-accumulate-source + "import qualified Data.IORef as IORef\n\npush :: IORef [Int] -> Int -> IO ()\npush r x = IORef.modifyIORef r (\\xs -> x : xs)\n\npushAll :: IORef [Int] -> [Int] -> IO ()\npushAll r [] = return ()\npushAll r (x:xs) = do\n push r x\n pushAll r xs\n\nreadReversed :: IORef [Int] -> IO [Int]\nreadReversed r = do\n xs <- IORef.readIORef r\n return (reverse xs)\n\ndoubleEach :: IORef [Int] -> [Int] -> IO ()\ndoubleEach r [] = return ()\ndoubleEach r (x:xs) = do\n push r (x * 2)\n doubleEach r xs\n\nsumIntoRef :: IORef Int -> [Int] -> IO ()\nsumIntoRef r [] = return ()\nsumIntoRef r (x:xs) = do\n IORef.modifyIORef r (\\acc -> acc + x)\n sumIntoRef r xs\n\n") + +(hk-test + "accumulate.hs — push three then read length" + (hk-deep-force + (hk-run + (str + hk-accumulate-source + "main = do { r <- IORef.newIORef []; push r 1; push r 2; push r 3; xs <- IORef.readIORef r; return (length xs) }"))) + (list "IO" 3)) + +(hk-test + "accumulate.hs — pushAll preserves reverse order" + (hk-deep-force + (hk-run + (str + hk-accumulate-source + "main = do { r <- IORef.newIORef []; pushAll r [1,2,3,4]; xs <- IORef.readIORef r; return xs }"))) + (list + "IO" + (list ":" 4 (list ":" 3 (list ":" 2 (list ":" 1 (list "[]"))))))) + +(hk-test + "accumulate.hs — readReversed gives original order" + (hk-deep-force + (hk-run + (str + hk-accumulate-source + "main = do { r <- IORef.newIORef []; pushAll r [10,20,30]; readReversed r }"))) + (list "IO" (list ":" 10 (list ":" 20 (list ":" 30 (list "[]")))))) + +(hk-test + "accumulate.hs — doubleEach maps then accumulates" + (hk-deep-force + (hk-run + (str + hk-accumulate-source + "main = do { r <- IORef.newIORef []; doubleEach r [1,2,3]; readReversed r }"))) + (list "IO" (list ":" 2 (list ":" 4 (list ":" 6 (list "[]")))))) + +(hk-test + "accumulate.hs — sum into Int IORef" + (hk-deep-force + (hk-run + (str + hk-accumulate-source + "main = do { r <- IORef.newIORef 0; sumIntoRef r [1,2,3,4,5]; v <- IORef.readIORef r; return v }"))) + (list "IO" 15)) + +(hk-test + "accumulate.hs — empty list leaves ref untouched" + (hk-deep-force + (hk-run + (str + hk-accumulate-source + "main = do { r <- IORef.newIORef [99]; pushAll r []; xs <- IORef.readIORef r; return xs }"))) + (list "IO" (list ":" 99 (list "[]")))) + +(hk-test + "accumulate.hs — pushAll then sumIntoRef on the same input" + (hk-deep-force + (hk-run + (str + hk-accumulate-source + "main = do { r <- IORef.newIORef 0; sumIntoRef r [10,20,30,40]; v <- IORef.readIORef r; return v }"))) + (list "IO" 100)) + +(hk-test + "accumulate.hs — accumulate results from a recursive helper" + (hk-deep-force + (hk-run + (str + hk-accumulate-source + "squaresUpTo r 0 = return ()\nsquaresUpTo r n = do { push r (n * n); squaresUpTo r (n - 1) }\nmain = do { r <- IORef.newIORef []; squaresUpTo r 4; readReversed r }"))) + (list + "IO" + (list ":" 16 (list ":" 9 (list ":" 4 (list ":" 1 (list "[]"))))))) diff --git a/lib/haskell/tests/program-counter.sx b/lib/haskell/tests/program-counter.sx new file mode 100644 index 00000000..7970ecf6 --- /dev/null +++ b/lib/haskell/tests/program-counter.sx @@ -0,0 +1,66 @@ +;; counter.hs — IORef-backed mutable counter (Phase 15 conformance). + +(define + hk-counter-source + "import qualified Data.IORef as IORef\n\ncount :: IORef Int -> Int -> IO ()\ncount r 0 = return ()\ncount r n = do\n IORef.modifyIORef r (\\x -> x + 1)\n count r (n - 1)\n\ncountBy :: IORef Int -> Int -> Int -> IO ()\ncountBy r step 0 = return ()\ncountBy r step n = do\n IORef.modifyIORef r (\\x -> x + step)\n countBy r step (n - 1)\n\nnewCounter :: Int -> IO (IORef Int)\nnewCounter v = IORef.newIORef v\n\nbumpAndRead :: IORef Int -> IO Int\nbumpAndRead r = do\n IORef.modifyIORef r (\\x -> x + 1)\n IORef.readIORef r\n\n") + +(hk-test + "counter.hs — start at 0, count 5 ⇒ 5" + (hk-deep-force + (hk-run + (str + hk-counter-source + "main = do { r <- newCounter 0; count r 5; v <- IORef.readIORef r; return v }"))) + (list "IO" 5)) + +(hk-test + "counter.hs — start at 100, count 10 ⇒ 110" + (hk-deep-force + (hk-run + (str + hk-counter-source + "main = do { r <- newCounter 100; count r 10; v <- IORef.readIORef r; return v }"))) + (list "IO" 110)) + +(hk-test + "counter.hs — countBy step 5, n 4 ⇒ 20" + (hk-deep-force + (hk-run + (str + hk-counter-source + "main = do { r <- newCounter 0; countBy r 5 4; v <- IORef.readIORef r; return v }"))) + (list "IO" 20)) + +(hk-test + "counter.hs — bumpAndRead returns updated value" + (hk-deep-force + (hk-run + (str hk-counter-source "main = do { r <- newCounter 41; bumpAndRead r }"))) + (list "IO" 42)) + +(hk-test + "counter.hs — count then countBy compose" + (hk-deep-force + (hk-run + (str + hk-counter-source + "main = do { r <- newCounter 0; count r 3; countBy r 10 2; v <- IORef.readIORef r; return v }"))) + (list "IO" 23)) + +(hk-test + "counter.hs — two independent counters" + (hk-deep-force + (hk-run + (str + hk-counter-source + "main = do { a <- newCounter 0; b <- newCounter 0; count a 7; countBy b 100 2; va <- IORef.readIORef a; vb <- IORef.readIORef b; return (va + vb) }"))) + (list "IO" 207)) + +(hk-test + "counter.hs — modifyIORef' (strict) variant" + (hk-deep-force + (hk-run + (str + hk-counter-source + "tick r 0 = return ()\ntick r n = do { IORef.modifyIORef' r (\\x -> x + 1); tick r (n - 1) }\nmain = do { r <- newCounter 0; tick r 50; v <- IORef.readIORef r; return v }"))) + (list "IO" 50)) diff --git a/plans/haskell-completeness.md b/plans/haskell-completeness.md index 35283554..fe95ea50 100644 --- a/plans/haskell-completeness.md +++ b/plans/haskell-completeness.md @@ -272,19 +272,19 @@ No OCaml changes are needed. The view type is fully representable as an SX dict. ### Phase 15 — IORef -- [ ] `IORef a` representation: a dict `{:hk-ioref true :hk-value v}`. +- [x] `IORef a` representation: a dict `{:hk-ioref true :hk-value v}`. Allocation creates a new dict in the IO monad. Mutation via `dict-set!`. -- [ ] `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)`, +- [x] `newIORef :: a -> IO (IORef a)` — wraps a new dict in `IO`. +- [x] `readIORef :: IORef a -> IO a` — returns `(IO (get ref ":hk-value"))`. +- [x] `writeIORef :: IORef a -> a -> IO ()` — `(dict-set! ref ":hk-value" v)`, returns `(IO ("Tuple"))`. -- [ ] `modifyIORef :: IORef a -> (a -> a) -> IO ()` — read + apply + write. -- [ ] `modifyIORef' :: IORef a -> (a -> a) -> IO ()` — strict variant (force +- [x] `modifyIORef :: IORef a -> (a -> a) -> IO ()` — read + apply + write. +- [x] `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, +- [x] `Data.IORef` module wiring. +- [x] Tests in `lib/haskell/tests/ioref.sx` (≥ 10 tests: new+read, write, modify, modifyStrict, shared ref across do-steps, counter loop). -- [ ] Conformance programs: +- [x] Conformance programs: - `counter.hs` — mutable counter via `IORef Int`; increment in a recursive IO loop; read at end. - `accumulate.hs` — accumulate results into `IORef [Int]` inside a mapped @@ -315,6 +315,17 @@ No OCaml changes are needed. The view type is fully representable as an SX dict. _Newest first._ +**2026-05-07** — Phase 15 IORef complete (5 ops + module wiring + 13 unit +tests + 2 conformance programs). `hk-bind-data-ioref!` in `eval.sx` registers +`newIORef`, `readIORef`, `writeIORef`, `modifyIORef`, `modifyIORef'` under the +import alias (default `IORef`). Representation: dict `{"hk-ioref" true +"hk-value" v}` allocated inside `IO`. Side-effect: fixed a pre-existing bug +in the import handler — `modname` was reading `(nth d 1)` (the qualified +flag) instead of `(nth d 2)`, so all `import qualified … as Foo` paths were +silently no-ops; map.sx unit suite jumps from 22→26 passing as a result. +Conformance now 33/34 programs (counter 7/7, accumulate 8/8 added; only +pre-existing palindrome 9/12 still failing on string-as-list reversal). + **2026-05-07** — Phase 14 conformance: person.hs (7/7) + config.hs (10/10) → Phase 14 complete: - `program-person.sx`: classic Person record with `birthday p = p { age = age p + 1 }` exercising the read-then-update idiom on a CAF instance, plus `deriving Show`