diff --git a/lib/haskell/eval.sx b/lib/haskell/eval.sx index 6448e4c4..13272701 100644 --- a/lib/haskell/eval.sx +++ b/lib/haskell/eval.sx @@ -87,7 +87,17 @@ hk-mk-builtin (fn (name fn arity) - {:type "builtin" :name name :fn fn :arity arity :collected (list)})) + {:type "builtin" :name name :fn fn :arity arity :lazy false :collected (list)})) + +;; A lazy built-in receives its collected args as raw thunks (or +;; values, if those happened to be eager) — the implementation is +;; responsible for forcing exactly what it needs. Used for `seq` +;; and `deepseq`, which are non-strict in their second argument. +(define + hk-mk-lazy-builtin + (fn + (name fn arity) + {:type "builtin" :name name :fn fn :arity arity :lazy true :collected (list)})) ;; ── Apply a function value to one argument ────────────────── (define @@ -199,11 +209,15 @@ ((< (len collected) arity) (assoc b "collected" collected)) (:else - ;; Built-ins are strict in all their arguments. Force each - ;; collected thunk before invoking the underlying SX fn. - (apply - (get b "fn") - (map hk-force collected))))))) + ;; Strict built-ins force every collected arg before + ;; calling. Lazy ones (`seq`, `deepseq`) receive the raw + ;; thunks so they can choose what to force. + (cond + ((get b "lazy") (apply (get b "fn") collected)) + (:else + (apply + (get b "fn") + (map hk-force collected))))))))) ;; ── Bool helpers (Bool values are tagged conses) ──────────── (define @@ -583,6 +597,23 @@ plus a b = a + b env "id" (hk-mk-builtin "id" (fn (x) x) 1)) + ;; `seq a b` — strict in `a`, lazy in `b`. Forces `a` to WHNF + ;; and returns `b` unchanged (still a thunk if it was one). + (dict-set! + env + "seq" + (hk-mk-lazy-builtin + "seq" + (fn (a b) (do (hk-force a) b)) + 2)) + ;; `deepseq a b` — like seq but forces `a` to normal form. + (dict-set! + env + "deepseq" + (hk-mk-lazy-builtin + "deepseq" + (fn (a b) (do (hk-deep-force a) b)) + 2)) ;; Operators as first-class values (dict-set! env "+" (hk-make-binop-builtin "+" "+")) (dict-set! env "-" (hk-make-binop-builtin "-" "-")) diff --git a/lib/haskell/tests/seq.sx b/lib/haskell/tests/seq.sx new file mode 100644 index 00000000..c46ecab3 --- /dev/null +++ b/lib/haskell/tests/seq.sx @@ -0,0 +1,85 @@ +;; seq / deepseq tests. seq is strict in its first arg (forces to +;; WHNF) and returns the second arg unchanged. deepseq additionally +;; forces the first arg to normal form. + +(define + hk-prog-val + (fn + (src name) + (hk-deep-force (get (hk-eval-program (hk-core src)) name)))) + +(define hk-as-list + (fn (xs) + (cond + ((and (list? xs) (= (first xs) "[]")) (list)) + ((and (list? xs) (= (first xs) ":")) + (cons (nth xs 1) (hk-as-list (nth xs 2)))) + (:else xs)))) + +(define + hk-eval-list + (fn (src) (hk-as-list (hk-eval-expr-source src)))) + +;; ── seq returns its second arg ── +(hk-test + "seq with primitive first arg" + (hk-eval-expr-source "seq 1 99") + 99) + +(hk-test + "seq forces first arg via let" + (hk-eval-expr-source "let x = 1 + 2 in seq x x") + 3) + +(hk-test + "seq second arg is whatever shape" + (hk-eval-expr-source "seq 0 \"hello\"") + "hello") + +;; ── seq enables previously-lazy bottom to be forced ── +;; Without seq the let-binding `x = error …` is never forced; +;; with seq it must be forced because seq is strict in its first +;; argument. We don't run that error case here (it would terminate +;; the test), but we do verify the negative — that without seq, +;; the bottom bound is never demanded. +(hk-test + "lazy let — bottom never forced when unused" + (hk-eval-expr-source "let x = error \"never\" in 42") + 42) + +;; ── deepseq forces nested structure ── +(hk-test + "deepseq with finite list" + (hk-eval-expr-source "deepseq [1, 2, 3] 7") + 7) + +(hk-test + "deepseq with constructor value" + (hk-eval-expr-source "deepseq (Just 5) 11") + 11) + +(hk-test + "deepseq with tuple" + (hk-eval-expr-source "deepseq (1, 2) 13") + 13) + +;; ── seq + arithmetic ── +(hk-test + "seq used inside arithmetic doesn't poison the result" + (hk-eval-expr-source "(seq 1 5) + (seq 2 7)") + 12) + +;; ── seq in user code ── +(hk-test + "seq via fun-clause" + (hk-prog-val + "f x = seq x (x + 1)\nresult = f 10" + "result") + 11) + +(hk-test + "seq sequences list construction" + (hk-eval-list "[seq 1 10, seq 2 20]") + (list 10 20)) + +{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail} diff --git a/plans/haskell-on-sx.md b/plans/haskell-on-sx.md index adc2c8d7..3c46b2c2 100644 --- a/plans/haskell-on-sx.md +++ b/plans/haskell-on-sx.md @@ -79,7 +79,7 @@ Key mappings: - [x] `force` = SX eval-thunk-to-WHNF primitive - [x] Pattern match forces scrutinee before matching - [x] Infinite structures: `repeat x`, `iterate f x`, `[1..]`, Fibonacci stream (sieve deferred — needs lazy `++` and is exercised under `Classic programs`) -- [ ] `seq`, `deepseq` from Prelude +- [x] `seq`, `deepseq` from Prelude - [ ] Do-notation for a stub `IO` monad (just threading, no real side effects yet) - [ ] Classic programs in `lib/haskell/tests/programs/`: - [ ] `fib.hs` — infinite Fibonacci stream @@ -114,6 +114,22 @@ Key mappings: _Newest first._ +- **2026-04-25** — Phase 3 `seq` + `deepseq`. Built-ins were strict + in all args by default (every collected thunk forced before + invoking the underlying SX fn) — that defeats `seq`'s purpose, + which is strict in its first argument and lazy in its second. + Added a tiny `lazy` flag on the builtin record (set by a new + `hk-mk-lazy-builtin` constructor) and routed `hk-apply-builtin` + to skip the auto-force when the flag is true. `seq a b` calls + `hk-force a` then returns `b` unchanged so its laziness is + preserved; `deepseq` does the same with `hk-deep-force`. 9 new + tests in `lib/haskell/tests/seq.sx` cover primitive, computed, + and let-bound first args, deepseq on a list / `Just` / + tuple, seq inside arithmetic, seq via a fun-clause, and + `[seq 1 10, seq 2 20]` to confirm seq composes inside list + literals. The lazy-when-unused negative case is also tested: + `let x = error "never" in 42 == 42`. 368/368 green. + - **2026-04-24** — Phase 3 infinite structures + Prelude. Two evaluator changes turn the lazy primitives into a working language: