27 Commits

Author SHA1 Message Date
973085e15f plans: tick conformance.sh + Phase 3 complete
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 12s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 19:26:56 +00:00
9f71706bc8 haskell: conformance.sh runner + scoreboard.json + scoreboard.md (16/16, 5/5)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 19:26:26 +00:00
161fa613f2 plans: tick calculator.hs + 5/5 classic programs target
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 18:57:59 +00:00
ba63cdf8c4 haskell: classic program calculator.hs + nested constructor patterns (+5 tests, 402/402)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 18:57:44 +00:00
2b117288f6 plans: tick nqueens.hs, progress log 2026-04-25
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 18:40:56 +00:00
8a9168c8d5 haskell: n-queens via list comprehension + where (+2 tests, 397/397)
- fix hk-eval-let: multi-clause where/let now uses hk-bind-decls!
  grouping (enables go 0 / go k pattern)
- add concatMap/concat/abs/negate to Prelude (list comprehension support)
- cache init env in hk-env0 (eval-expr-source 5x faster)
2026-04-25 18:40:27 +00:00
9facbb4836 plans: tick quicksort.hs, progress log 2026-04-25
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 18:06:58 +00:00
a12dcef327 haskell: naive quicksort classic program (+5 tests, 395/395) 2026-04-25 18:06:41 +00:00
d33c520318 plans: tick sieve.hs, progress log 2026-04-25
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 18:00:02 +00:00
9be65d7d60 haskell: lazy sieve of Eratosthenes (+mod/div/rem/quot, +2 tests, 390/390) 2026-04-25 17:59:39 +00:00
4ed7ffe9dd haskell: classic program fib.hs + source-order top-level binding (+2 tests, 388/388)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 08:53:47 +00:00
cd489b19be haskell: do-notation desugar + stub IO monad (return/>>=/>>) (+14 tests, 382/382)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 00:59:42 +00:00
04a25d17d0 haskell: seq + deepseq via lazy-builtin flag (+9 tests, 368/368)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 00:28:19 +00:00
cc5315a5e6 haskell: lazy : + ranges + Prelude (repeat/iterate/fibs/take, +25 tests, 359/359)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-24 23:58:21 +00:00
0e53e88b02 haskell: thunks + force, app args become lazy (+6 tests, 333/333)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-24 23:22:21 +00:00
fba92c2b69 haskell: strict evaluator + 38 eval tests, Phase 2 complete (329/329)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-24 22:49:12 +00:00
1aa06237f1 haskell: value-level pattern matcher (+31 tests, 281/281)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-24 22:15:13 +00:00
e9c8f803b5 haskell: runtime constructor registry (+24 tests, 250/250)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-24 21:45:51 +00:00
ef81fffb6f haskell: desugar guards/where/list-comp → core AST (+15 tests, 226/226)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-24 21:16:53 +00:00
cab7ca883f haskell: operator sections + list comprehensions, Phase 1 parser complete (+22 tests, 211/211)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-24 20:47:51 +00:00
bf0d72fd2f haskell: module header + imports (+16 tests, 189/189)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-24 20:08:30 +00:00
defbe0a612 haskell: guards + where clauses (+11 tests, 173/173)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-24 19:37:52 +00:00
869b0b552d haskell: top-level decls (fn-clause, type-sig, data, type, newtype, fixity) + type parser (+24 tests, 162/162)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-24 19:06:38 +00:00
58dbbc5d8b haskell: full patterns — as/lazy/negative/infix + lambda & let pat LHS (+18 tests, 138/138)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-24 18:34:47 +00:00
36234f0132 haskell: case/do + minimal patterns (+19 tests, 119/119)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-24 18:00:58 +00:00
6ccef45ce4 haskell: expression parser + precedence climbing (+42 tests, 100/100)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-24 17:31:38 +00:00
c07ff90f6b haskell: layout rule per §10.3 (+15 tests, 58/58)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-24 17:05:35 +00:00
61 changed files with 8014 additions and 3574 deletions

View File

@@ -1,578 +0,0 @@
;; Common Lisp evaluator — evaluates CL AST forms.
;;
;; Depends on: lib/common-lisp/reader.sx, lib/common-lisp/parser.sx
;;
;; Environment:
;; {:vars {"NAME" val ...} :fns {"NAME" cl-fn ...}}
;; CL function:
;; {:cl-type "function" :params ll :body forms :env env}
;;
;; Public API:
;; (cl-make-env) — create empty environment
;; (cl-eval form env) — evaluate one CL AST form
;; (cl-eval-str src env) — read+eval a CL source string
;; (cl-eval-all-str src env) — read-all+eval-each, return last
;; cl-global-env — global mutable environment
;; ── environment ──────────────────────────────────────────────────
(define cl-make-env (fn () {:vars {} :fns {}}))
(define cl-global-env (cl-make-env))
(define cl-env-get-var (fn (env name) (get (get env "vars") name)))
(define cl-env-has-var? (fn (env name) (has-key? (get env "vars") name)))
(define cl-env-get-fn (fn (env name) (get (get env "fns") name)))
(define cl-env-has-fn? (fn (env name) (has-key? (get env "fns") name)))
(define cl-env-bind-var
(fn (env name value)
{:vars (assoc (get env "vars") name value)
:fns (get env "fns")}))
(define cl-env-bind-fn
(fn (env name fn-obj)
{:vars (get env "vars")
:fns (assoc (get env "fns") name fn-obj)}))
;; ── body evaluation ───────────────────────────────────────────────
(define cl-eval-body
(fn (forms env)
(cond
((= (len forms) 0) nil)
((= (len forms) 1) (cl-eval (nth forms 0) env))
(:else
(do
(cl-eval (nth forms 0) env)
(cl-eval-body (rest forms) env))))))
;; ── lambda-list binding helpers ───────────────────────────────────
(define cl-bind-required
(fn (names args env)
(if (= (len names) 0)
env
(cl-bind-required
(rest names)
(if (> (len args) 0) (rest args) args)
(cl-env-bind-var env
(nth names 0)
(if (> (len args) 0) (nth args 0) nil))))))
;; returns {:env e :rest remaining-args}
(define cl-bind-optional
(fn (opts args env)
(if (= (len opts) 0)
{:env env :rest args}
(let ((spec (nth opts 0))
(has-val (> (len args) 0)))
(let ((val (if has-val (nth args 0) nil))
(rem (if has-val (rest args) args)))
(let ((e1 (cl-env-bind-var env (get spec "name")
(if has-val val
(if (get spec "default")
(cl-eval (get spec "default") env) nil)))))
(let ((e2 (if (get spec "supplied")
(cl-env-bind-var e1 (get spec "supplied") has-val)
e1)))
(cl-bind-optional (rest opts) rem e2))))))))
;; returns {:found bool :value v}
(define cl-find-kw-arg
(fn (kw args i)
(if (>= i (len args))
{:found false :value nil}
(let ((a (nth args i)))
(if (and (dict? a)
(= (get a "cl-type") "keyword")
(= (get a "name") kw))
{:found true
:value (if (< (+ i 1) (len args)) (nth args (+ i 1)) nil)}
(cl-find-kw-arg kw args (+ i 2)))))))
(define cl-bind-key
(fn (key-specs all-args env)
(if (= (len key-specs) 0)
env
(let ((spec (nth key-specs 0))
(r (cl-find-kw-arg (get (nth key-specs 0) "keyword") all-args 0)))
(let ((found (get r "found"))
(kval (get r "value")))
(let ((e1 (cl-env-bind-var env (get spec "name")
(if found kval
(if (get spec "default")
(cl-eval (get spec "default") env) nil)))))
(let ((e2 (if (get spec "supplied")
(cl-env-bind-var e1 (get spec "supplied") found)
e1)))
(cl-bind-key (rest key-specs) all-args e2))))))))
(define cl-bind-aux
(fn (aux-specs env)
(if (= (len aux-specs) 0)
env
(let ((spec (nth aux-specs 0)))
(cl-bind-aux
(rest aux-specs)
(cl-env-bind-var env (get spec "name")
(if (get spec "init") (cl-eval (get spec "init") env) nil)))))))
;; ── function creation ─────────────────────────────────────────────
;; ll-and-body: (list lambda-list-form body-form ...)
(define cl-make-lambda
(fn (ll-and-body env)
{:cl-type "function"
:params (cl-parse-lambda-list (nth ll-and-body 0))
:body (rest ll-and-body)
:env env}))
;; ── function application ──────────────────────────────────────────
(define cl-apply
(fn (fn-obj args)
(cond
((and (dict? fn-obj) (has-key? fn-obj "builtin-fn"))
((get fn-obj "builtin-fn") args))
((or (not (dict? fn-obj)) (not (= (get fn-obj "cl-type") "function")))
{:cl-type "error" :message "Not a function"})
(:else
(let ((params (get fn-obj "params"))
(body (get fn-obj "body"))
(cenv (get fn-obj "env")))
(let ((req (get params "required"))
(opt (get params "optional"))
(rest-name (get params "rest"))
(key-specs (get params "key"))
(aux-specs (get params "aux")))
(let ((e1 (cl-bind-required req args cenv)))
(let ((opt-r (cl-bind-optional
opt (slice args (len req) (len args)) e1)))
(let ((e2 (get opt-r "env"))
(rem (get opt-r "rest")))
(let ((e3 (if rest-name
(cl-env-bind-var e2 rest-name rem)
e2)))
(let ((e4 (cl-bind-key key-specs args e3)))
(let ((e5 (cl-bind-aux aux-specs e4)))
(cl-eval-body body e5)))))))))))))
;; ── built-in functions ────────────────────────────────────────────
(define cl-builtins
(dict
"+" (fn (args) (reduce (fn (a b) (+ a b)) 0 args))
"-" (fn (args)
(cond
((= (len args) 0) 0)
((= (len args) 1) (- 0 (nth args 0)))
(:else (reduce (fn (a b) (- a b)) (nth args 0) (rest args)))))
"*" (fn (args) (reduce (fn (a b) (* a b)) 1 args))
"/" (fn (args)
(cond
((= (len args) 0) 1)
((= (len args) 1) (/ 1 (nth args 0)))
(:else (reduce (fn (a b) (/ a b)) (nth args 0) (rest args)))))
"1+" (fn (args) (+ (nth args 0) 1))
"1-" (fn (args) (- (nth args 0) 1))
"=" (fn (args) (if (= (nth args 0) (nth args 1)) true nil))
"/=" (fn (args) (if (not (= (nth args 0) (nth args 1))) true nil))
"<" (fn (args) (if (< (nth args 0) (nth args 1)) true nil))
">" (fn (args) (if (> (nth args 0) (nth args 1)) true nil))
"<=" (fn (args) (if (<= (nth args 0) (nth args 1)) true nil))
">=" (fn (args) (if (>= (nth args 0) (nth args 1)) true nil))
"NOT" (fn (args) (if (nth args 0) nil true))
"NULL" (fn (args) (if (= (nth args 0) nil) true nil))
"NUMBERP" (fn (args) (if (number? (nth args 0)) true nil))
"STRINGP" (fn (args) (if (string? (nth args 0)) true nil))
"SYMBOLP" (fn (args) nil)
"LISTP" (fn (args)
(if (or (list? (nth args 0)) (= (nth args 0) nil)) true nil))
"CONSP" (fn (args)
(let ((x (nth args 0)))
(if (and (dict? x) (= (get x "cl-type") "cons")) true nil)))
"ATOM" (fn (args)
(let ((x (nth args 0)))
(if (and (dict? x) (= (get x "cl-type") "cons")) nil true)))
"FUNCTIONP" (fn (args)
(let ((x (nth args 0)))
(if (and (dict? x) (= (get x "cl-type") "function")) true nil)))
"ZEROP" (fn (args) (if (= (nth args 0) 0) true nil))
"PLUSP" (fn (args) (if (> (nth args 0) 0) true nil))
"MINUSP" (fn (args) (if (< (nth args 0) 0) true nil))
"EVENP" (fn (args)
(let ((n (nth args 0)))
(if (= (mod n 2) 0) true nil)))
"ODDP" (fn (args)
(let ((n (nth args 0)))
(if (not (= (mod n 2) 0)) true nil)))
"ABS" (fn (args) (let ((n (nth args 0))) (if (< n 0) (- 0 n) n)))
"MAX" (fn (args) (reduce (fn (a b) (if (> a b) a b)) (nth args 0) (rest args)))
"MIN" (fn (args) (reduce (fn (a b) (if (< a b) a b)) (nth args 0) (rest args)))
"CONS" (fn (args) {:cl-type "cons" :car (nth args 0) :cdr (nth args 1)})
"CAR" (fn (args)
(let ((x (nth args 0)))
(if (and (dict? x) (= (get x "cl-type") "cons"))
(get x "car")
(if (and (list? x) (> (len x) 0)) (nth x 0) nil))))
"CDR" (fn (args)
(let ((x (nth args 0)))
(if (and (dict? x) (= (get x "cl-type") "cons"))
(get x "cdr")
(if (list? x) (rest x) nil))))
"LIST" (fn (args) args)
"APPEND" (fn (args)
(if (= (len args) 0) (list)
(reduce (fn (a b)
(if (= a nil) b (if (= b nil) a (concat a b))))
(list) args)))
"LENGTH" (fn (args)
(let ((x (nth args 0)))
(if (= x nil) 0 (len x))))
"NTH" (fn (args) (nth (nth args 1) (nth args 0)))
"FIRST" (fn (args)
(let ((x (nth args 0)))
(if (and (list? x) (> (len x) 0)) (nth x 0) nil)))
"SECOND" (fn (args)
(let ((x (nth args 0)))
(if (and (list? x) (> (len x) 1)) (nth x 1) nil)))
"THIRD" (fn (args)
(let ((x (nth args 0)))
(if (and (list? x) (> (len x) 2)) (nth x 2) nil)))
"REST" (fn (args) (rest (nth args 0)))
"REVERSE" (fn (args)
(reduce (fn (acc x) (concat (list x) acc))
(list) (nth args 0)))
"IDENTITY" (fn (args) (nth args 0))
"VALUES" (fn (args) (if (> (len args) 0) (nth args 0) nil))
"PRINT" (fn (args) (nth args 0))
"PRIN1" (fn (args) (nth args 0))
"PRINC" (fn (args) (nth args 0))
"TERPRI" (fn (args) nil)
"WRITE" (fn (args) (nth args 0))
"STRING-UPCASE" (fn (args) (upcase (nth args 0)))
"STRING-DOWNCASE" (fn (args) (downcase (nth args 0)))
"STRING=" (fn (args) (if (= (nth args 0) (nth args 1)) true nil))
"CONCATENATE" (fn (args) (reduce (fn (a b) (str a b)) "" (rest args)))
"EQ" (fn (args) (if (= (nth args 0) (nth args 1)) true nil))
"EQL" (fn (args) (if (= (nth args 0) (nth args 1)) true nil))
"EQUAL" (fn (args) (if (= (nth args 0) (nth args 1)) true nil))))
;; Register builtins in cl-global-env so (function #'name) resolves them
(for-each
(fn (name)
(dict-set! (get cl-global-env "fns") name
{:cl-type "function" :builtin-fn (get cl-builtins name)}))
(keys cl-builtins))
;; ── special form evaluators ───────────────────────────────────────
(define cl-eval-if
(fn (args env)
(let ((cond-val (cl-eval (nth args 0) env))
(then-form (nth args 1))
(else-form (if (> (len args) 2) (nth args 2) nil)))
(if cond-val
(cl-eval then-form env)
(if else-form (cl-eval else-form env) nil)))))
(define cl-eval-and
(fn (args env)
(if (= (len args) 0)
true
(let ((val (cl-eval (nth args 0) env)))
(if (not val)
nil
(if (= (len args) 1)
val
(cl-eval-and (rest args) env)))))))
(define cl-eval-or
(fn (args env)
(if (= (len args) 0)
nil
(let ((val (cl-eval (nth args 0) env)))
(if val
val
(cl-eval-or (rest args) env))))))
(define cl-eval-cond
(fn (clauses env)
(if (= (len clauses) 0)
nil
(let ((clause (nth clauses 0)))
(let ((test-val (cl-eval (nth clause 0) env)))
(if test-val
(if (= (len clause) 1)
test-val
(cl-eval-body (rest clause) env))
(cl-eval-cond (rest clauses) env)))))))
;; Parallel LET and sequential LET*
(define cl-eval-let
(fn (args env sequential)
(let ((bindings (nth args 0))
(body (rest args)))
(if sequential
;; LET*: each binding sees previous ones
(let ((new-env env))
(define bind-seq
(fn (bs e)
(if (= (len bs) 0)
e
(let ((b (nth bs 0)))
(let ((name (if (list? b) (nth b 0) b))
(init (if (and (list? b) (> (len b) 1)) (nth b 1) nil)))
(bind-seq (rest bs)
(cl-env-bind-var e name (cl-eval init e))))))))
(cl-eval-body body (bind-seq bindings env)))
;; LET: evaluate all inits in current env, then bind
(let ((pairs (map
(fn (b)
(let ((name (if (list? b) (nth b 0) b))
(init (if (and (list? b) (> (len b) 1)) (nth b 1) nil)))
{:name name :value (cl-eval init env)}))
bindings)))
(let ((new-env (reduce
(fn (e pair)
(cl-env-bind-var e (get pair "name") (get pair "value")))
env pairs)))
(cl-eval-body body new-env)))))))
;; SETQ / SETF (simplified: mutate nearest scope or global)
(define cl-eval-setq
(fn (args env)
(if (< (len args) 2)
nil
(let ((name (nth args 0))
(val (cl-eval (nth args 1) env)))
(if (has-key? (get env "vars") name)
(dict-set! (get env "vars") name val)
(dict-set! (get cl-global-env "vars") name val))
(if (> (len args) 2)
(cl-eval-setq (rest (rest args)) env)
val)))))
;; FUNCTION: get function value or create lambda
(define cl-eval-function
(fn (args env)
(let ((spec (nth args 0)))
(cond
((and (list? spec) (> (len spec) 0) (= (nth spec 0) "LAMBDA"))
(cl-make-lambda (rest spec) env))
((string? spec)
(cond
((cl-env-has-fn? env spec) (cl-env-get-fn env spec))
((cl-env-has-fn? cl-global-env spec)
(cl-env-get-fn cl-global-env spec))
(:else {:cl-type "error" :message (str "Undefined function: " spec)})))
(:else {:cl-type "error" :message "FUNCTION: invalid spec"})))))
;; FLET: local functions (non-recursive, close over outer env)
(define cl-eval-flet
(fn (args env)
(let ((fn-defs (nth args 0))
(body (rest args)))
(let ((new-env (reduce
(fn (e def)
(let ((name (nth def 0))
(ll (nth def 1))
(fn-body (rest (rest def))))
(cl-env-bind-fn e name
{:cl-type "function"
:params (cl-parse-lambda-list ll)
:body fn-body
:env env})))
env fn-defs)))
(cl-eval-body body new-env)))))
;; LABELS: mutually-recursive local functions
(define cl-eval-labels
(fn (args env)
(let ((fn-defs (nth args 0))
(body (rest args)))
;; Build env with placeholder nil entries for each name
(let ((new-env (reduce
(fn (e def) (cl-env-bind-fn e (nth def 0) nil))
env fn-defs)))
;; Fill in real function objects that capture new-env
(for-each
(fn (def)
(let ((name (nth def 0))
(ll (nth def 1))
(fn-body (rest (rest def))))
(dict-set! (get new-env "fns") name
{:cl-type "function"
:params (cl-parse-lambda-list ll)
:body fn-body
:env new-env})))
fn-defs)
(cl-eval-body body new-env)))))
;; EVAL-WHEN: evaluate body only if :execute is in situations
(define cl-eval-eval-when
(fn (args env)
(let ((situations (nth args 0))
(body (rest args)))
(define has-exec
(some (fn (s)
(or
(and (dict? s)
(= (get s "cl-type") "keyword")
(= (get s "name") "EXECUTE"))
(= s "EXECUTE")))
situations))
(if has-exec (cl-eval-body body env) nil))))
;; DEFUN: define function in global fns namespace
(define cl-eval-defun
(fn (args env)
(let ((name (nth args 0))
(ll (nth args 1))
(fn-body (rest (rest args))))
(let ((fn-obj {:cl-type "function"
:params (cl-parse-lambda-list ll)
:body fn-body
:env env}))
(dict-set! (get cl-global-env "fns") name fn-obj)
name))))
;; DEFVAR / DEFPARAMETER / DEFCONSTANT
(define cl-eval-defvar
(fn (args env always-assign)
(let ((name (nth args 0))
(has-init (> (len args) 1)))
(let ((val (if has-init (cl-eval (nth args 1) env) nil)))
(when (or always-assign
(not (cl-env-has-var? cl-global-env name)))
(dict-set! (get cl-global-env "vars") name val))
name))))
;; Function call: evaluate name → look up fns, builtins; evaluate args
(define cl-call-fn
(fn (name args env)
(let ((evaled (map (fn (a) (cl-eval a env)) args)))
(cond
;; FUNCALL: (funcall fn arg...)
((= name "FUNCALL")
(cl-apply (nth evaled 0) (rest evaled)))
;; APPLY: (apply fn arg... list)
((= name "APPLY")
(let ((fn-obj (nth evaled 0))
(all-args (rest evaled)))
(let ((leading (slice all-args 0 (- (len all-args) 1)))
(last-arg (nth all-args (- (len all-args) 1))))
(cl-apply fn-obj (concat leading (if (= last-arg nil) (list) last-arg))))))
;; MAPCAR: (mapcar fn list)
((= name "MAPCAR")
(let ((fn-obj (nth evaled 0))
(lst (nth evaled 1)))
(if (= lst nil) (list)
(map (fn (x) (cl-apply fn-obj (list x))) lst))))
;; Look up in local fns namespace
((cl-env-has-fn? env name)
(cl-apply (cl-env-get-fn env name) evaled))
;; Look up in global fns namespace
((cl-env-has-fn? cl-global-env name)
(cl-apply (cl-env-get-fn cl-global-env name) evaled))
;; Look up in builtins
((has-key? cl-builtins name)
((get cl-builtins name) evaled))
(:else
{:cl-type "error" :message (str "Undefined function: " name)})))))
;; ── main evaluator ────────────────────────────────────────────────
(define cl-eval
(fn (form env)
(cond
;; Nil and booleans are self-evaluating
((= form nil) nil)
((= form true) true)
;; Numbers are self-evaluating
((number? form) form)
;; Dicts: typed CL values
((dict? form)
(let ((ct (get form "cl-type")))
(cond
((= ct "string") (get form "value")) ;; CL string → SX string
(:else form)))) ;; keywords, floats, chars, etc.
;; Symbol reference (variable lookup)
((string? form)
(cond
((cl-env-has-var? env form) (cl-env-get-var env form))
((cl-env-has-var? cl-global-env form)
(cl-env-get-var cl-global-env form))
(:else {:cl-type "error" :message (str "Undefined variable: " form)})))
;; List: special forms or function call
((list? form) (cl-eval-list form env))
;; Anything else self-evaluates
(:else form))))
(define cl-eval-list
(fn (form env)
(if (= (len form) 0)
nil
(let ((head (nth form 0))
(args (rest form)))
(cond
((= head "QUOTE") (nth args 0))
((= head "IF") (cl-eval-if args env))
((= head "PROGN") (cl-eval-body args env))
((= head "LET") (cl-eval-let args env false))
((= head "LET*") (cl-eval-let args env true))
((= head "AND") (cl-eval-and args env))
((= head "OR") (cl-eval-or args env))
((= head "COND") (cl-eval-cond args env))
((= head "WHEN")
(if (cl-eval (nth args 0) env)
(cl-eval-body (rest args) env) nil))
((= head "UNLESS")
(if (not (cl-eval (nth args 0) env))
(cl-eval-body (rest args) env) nil))
((= head "SETQ") (cl-eval-setq args env))
((= head "SETF") (cl-eval-setq args env))
((= head "FUNCTION") (cl-eval-function args env))
((= head "LAMBDA") (cl-make-lambda args env))
((= head "FLET") (cl-eval-flet args env))
((= head "LABELS") (cl-eval-labels args env))
((= head "THE") (cl-eval (nth args 1) env))
((= head "LOCALLY") (cl-eval-body args env))
((= head "EVAL-WHEN") (cl-eval-eval-when args env))
((= head "DEFUN") (cl-eval-defun args env))
((= head "DEFVAR") (cl-eval-defvar args env false))
((= head "DEFPARAMETER") (cl-eval-defvar args env true))
((= head "DEFCONSTANT") (cl-eval-defvar args env true))
((= head "DECLAIM") nil)
((= head "PROCLAIM") nil)
;; Named function call
((string? head)
(cl-call-fn head args env))
;; Anonymous call: ((lambda ...) args)
(:else
(let ((fn-obj (cl-eval head env)))
(if (and (dict? fn-obj) (= (get fn-obj "cl-type") "function"))
(cl-apply fn-obj (map (fn (a) (cl-eval a env)) args))
{:cl-type "error" :message "Not callable"}))))))))
;; ── public API ────────────────────────────────────────────────────
(define cl-eval-str
(fn (src env)
(cl-eval (cl-read src) env)))
(define cl-eval-all-str
(fn (src env)
(let ((forms (cl-read-all src)))
(if (= (len forms) 0)
nil
(let ((result nil) (i 0))
(define loop (fn ()
(when (< i (len forms))
(do
(set! result (cl-eval (nth forms i) env))
(set! i (+ i 1))
(loop)))))
(loop)
result)))))

View File

@@ -1,377 +0,0 @@
;; Common Lisp reader — converts token stream to CL AST forms.
;;
;; Depends on: lib/common-lisp/reader.sx (cl-tokenize)
;;
;; AST representation:
;; integer/float → SX number (or {:cl-type "float"/:ratio ...})
;; string → SX string
;; symbol FOO → SX string "FOO" (upcase)
;; symbol NIL → nil
;; symbol T → true
;; :keyword → {:cl-type "keyword" :name "FOO"}
;; #\char → {:cl-type "char" :value "a"}
;; #:uninterned → {:cl-type "uninterned" :name "FOO"}
;; ratio 1/3 → {:cl-type "ratio" :value "1/3"}
;; float 3.14 → {:cl-type "float" :value "3.14"}
;; proper list (a b c) → SX list (a b c)
;; dotted pair (a . b) → {:cl-type "cons" :car a :cdr b}
;; vector #(a b) → {:cl-type "vector" :elements (list a b)}
;; 'x → ("QUOTE" x)
;; `x → ("QUASIQUOTE" x)
;; ,x → ("UNQUOTE" x)
;; ,@x → ("UNQUOTE-SPLICING" x)
;; #'x → ("FUNCTION" x)
;;
;; Public API:
;; (cl-read src) — parse first form from string, return form
;; (cl-read-all src) — parse all top-level forms, return list
;; ── number conversion ─────────────────────────────────────────────
(define
cl-hex-val
(fn
(c)
(let
((o (cl-ord c)))
(cond
((and (>= o 48) (<= o 57)) (- o 48))
((and (>= o 65) (<= o 70)) (+ 10 (- o 65)))
((and (>= o 97) (<= o 102)) (+ 10 (- o 97)))
(:else 0)))))
(define
cl-parse-radix-str
(fn
(s radix start)
(let
((n (string-length s)) (i start) (acc 0))
(define
loop
(fn
()
(when
(< i n)
(do
(set! acc (+ (* acc radix) (cl-hex-val (substring s i (+ i 1)))))
(set! i (+ i 1))
(loop)))))
(loop)
acc)))
(define
cl-convert-integer
(fn
(s)
(let
((n (string-length s)) (neg false))
(cond
((and (> n 2) (= (substring s 0 1) "#"))
(let
((letter (downcase (substring s 1 2))))
(cond
((= letter "x") (cl-parse-radix-str s 16 2))
((= letter "b") (cl-parse-radix-str s 2 2))
((= letter "o") (cl-parse-radix-str s 8 2))
(:else (parse-int s 0)))))
(:else (parse-int s 0))))))
;; ── reader ────────────────────────────────────────────────────────
;; Read one form from token list.
;; Returns {:form F :rest remaining-toks} or {:form nil :rest toks :eof true}
(define
cl-read-form
(fn
(toks)
(if
(not toks)
{:form nil :rest toks :eof true}
(let
((tok (nth toks 0)) (nxt (rest toks)))
(let
((type (get tok "type")) (val (get tok "value")))
(cond
((= type "eof") {:form nil :rest toks :eof true})
((= type "integer") {:form (cl-convert-integer val) :rest nxt})
((= type "float") {:form {:cl-type "float" :value val} :rest nxt})
((= type "ratio") {:form {:cl-type "ratio" :value val} :rest nxt})
((= type "string") {:form val :rest nxt})
((= type "char") {:form {:cl-type "char" :value val} :rest nxt})
((= type "keyword") {:form {:cl-type "keyword" :name val} :rest nxt})
((= type "uninterned") {:form {:cl-type "uninterned" :name val} :rest nxt})
((= type "symbol")
(cond
((= val "NIL") {:form nil :rest nxt})
((= val "T") {:form true :rest nxt})
(:else {:form val :rest nxt})))
;; list forms
((= type "lparen") (cl-read-list nxt))
((= type "hash-paren") (cl-read-vector nxt))
;; reader macros that wrap the next form
((= type "quote") (cl-read-wrap "QUOTE" nxt))
((= type "backquote") (cl-read-wrap "QUASIQUOTE" nxt))
((= type "comma") (cl-read-wrap "UNQUOTE" nxt))
((= type "comma-at") (cl-read-wrap "UNQUOTE-SPLICING" nxt))
((= type "hash-quote") (cl-read-wrap "FUNCTION" nxt))
;; skip unrecognised tokens
(:else (cl-read-form nxt))))))))
;; Wrap next form in a list: (name form)
(define
cl-read-wrap
(fn
(name toks)
(let
((inner (cl-read-form toks)))
{:form (list name (get inner "form")) :rest (get inner "rest")})))
;; Read list forms until ')'; handles dotted pair (a . b)
;; Called after consuming '('
(define
cl-read-list
(fn
(toks)
(let
((result (cl-read-list-items toks (list))))
{:form (get result "items") :rest (get result "rest")})))
(define
cl-read-list-items
(fn
(toks acc)
(if
(not toks)
{:items acc :rest toks}
(let
((tok (nth toks 0)))
(let
((type (get tok "type")))
(cond
((= type "eof") {:items acc :rest toks})
((= type "rparen") {:items acc :rest (rest toks)})
;; dotted pair: read one more form then expect ')'
((= type "dot")
(let
((cdr-result (cl-read-form (rest toks))))
(let
((cdr-form (get cdr-result "form"))
(after-cdr (get cdr-result "rest")))
;; skip the closing ')'
(let
((close (if after-cdr (nth after-cdr 0) nil)))
(let
((remaining
(if
(and close (= (get close "type") "rparen"))
(rest after-cdr)
after-cdr)))
;; build dotted structure
(let
((dotted (cl-build-dotted acc cdr-form)))
{:items dotted :rest remaining}))))))
(:else
(let
((item (cl-read-form toks)))
(cl-read-list-items
(get item "rest")
(concat acc (list (get item "form"))))))))))))
;; Build dotted form: (a b . c) → ((DOTTED a b) . c) style
;; In CL (a b c . d) means a proper dotted structure.
;; We represent it as {:cl-type "cons" :car a :cdr (list->dotted b c d)}
(define
cl-build-dotted
(fn
(head-items tail)
(if
(= (len head-items) 0)
tail
(if
(= (len head-items) 1)
{:cl-type "cons" :car (nth head-items 0) :cdr tail}
(let
((last-item (nth head-items (- (len head-items) 1)))
(but-last (slice head-items 0 (- (len head-items) 1))))
{:cl-type "cons"
:car (cl-build-dotted but-last (list last-item))
:cdr tail})))))
;; Read vector #(…) elements until ')'
(define
cl-read-vector
(fn
(toks)
(let
((result (cl-read-vector-items toks (list))))
{:form {:cl-type "vector" :elements (get result "items")} :rest (get result "rest")})))
(define
cl-read-vector-items
(fn
(toks acc)
(if
(not toks)
{:items acc :rest toks}
(let
((tok (nth toks 0)))
(let
((type (get tok "type")))
(cond
((= type "eof") {:items acc :rest toks})
((= type "rparen") {:items acc :rest (rest toks)})
(:else
(let
((item (cl-read-form toks)))
(cl-read-vector-items
(get item "rest")
(concat acc (list (get item "form"))))))))))))
;; ── lambda-list parser ───────────────────────────────────────────
;;
;; (cl-parse-lambda-list forms) — parse a list of CL forms (already read)
;; into a structured dict:
;; {:required (list sym ...)
;; :optional (list {:name N :default D :supplied S} ...)
;; :rest nil | "SYM"
;; :key (list {:name N :keyword K :default D :supplied S} ...)
;; :allow-other-keys false | true
;; :aux (list {:name N :init I} ...)}
;;
;; Symbols arrive as SX strings (upcase). &-markers are strings like "&OPTIONAL".
;; Key params: keyword is the upcase name string; caller uses it as :keyword.
;; Supplied-p: nil when absent.
(define
cl-parse-opt-spec
(fn
(spec)
(if
(list? spec)
{:name (nth spec 0)
:default (if (> (len spec) 1) (nth spec 1) nil)
:supplied (if (> (len spec) 2) (nth spec 2) nil)}
{:name spec :default nil :supplied nil})))
(define
cl-parse-key-spec
(fn
(spec)
(if
(list? spec)
(let
((first (nth spec 0)))
(if
(list? first)
;; ((:keyword var) default supplied-p)
{:name (nth first 1)
:keyword (get first "name")
:default (if (> (len spec) 1) (nth spec 1) nil)
:supplied (if (> (len spec) 2) (nth spec 2) nil)}
;; (var default supplied-p)
{:name first
:keyword first
:default (if (> (len spec) 1) (nth spec 1) nil)
:supplied (if (> (len spec) 2) (nth spec 2) nil)}))
{:name spec :keyword spec :default nil :supplied nil})))
(define
cl-parse-aux-spec
(fn
(spec)
(if
(list? spec)
{:name (nth spec 0) :init (if (> (len spec) 1) (nth spec 1) nil)}
{:name spec :init nil})))
(define
cl-parse-lambda-list
(fn
(forms)
(let
((state "required")
(required (list))
(optional (list))
(rest-name nil)
(key (list))
(allow-other-keys false)
(aux (list)))
(define
scan
(fn
(items)
(when
(> (len items) 0)
(let
((item (nth items 0)) (tail (rest items)))
(cond
((= item "&OPTIONAL")
(do (set! state "optional") (scan tail)))
((= item "&REST")
(do (set! state "rest") (scan tail)))
((= item "&BODY")
(do (set! state "rest") (scan tail)))
((= item "&KEY")
(do (set! state "key") (scan tail)))
((= item "&AUX")
(do (set! state "aux") (scan tail)))
((= item "&ALLOW-OTHER-KEYS")
(do (set! allow-other-keys true) (scan tail)))
((= state "required")
(do (append! required item) (scan tail)))
((= state "optional")
(do (append! optional (cl-parse-opt-spec item)) (scan tail)))
((= state "rest")
(do (set! rest-name item) (set! state "done") (scan tail)))
((= state "key")
(do (append! key (cl-parse-key-spec item)) (scan tail)))
((= state "aux")
(do (append! aux (cl-parse-aux-spec item)) (scan tail)))
(:else (scan tail)))))))
(scan forms)
{:required required
:optional optional
:rest rest-name
:key key
:allow-other-keys allow-other-keys
:aux aux})))
;; Convenience: parse lambda list from a CL source string
(define
cl-parse-lambda-list-str
(fn
(src)
(cl-parse-lambda-list (cl-read src))))
;; ── public API ────────────────────────────────────────────────────
(define
cl-read
(fn
(src)
(let
((toks (cl-tokenize src)))
(get (cl-read-form toks) "form"))))
(define
cl-read-all
(fn
(src)
(let
((toks (cl-tokenize src)))
(define
loop
(fn
(toks acc)
(if
(or (not toks) (= (get (nth toks 0) "type") "eof"))
acc
(let
((result (cl-read-form toks)))
(if
(get result "eof")
acc
(loop (get result "rest") (concat acc (list (get result "form")))))))))
(loop toks (list)))))

View File

@@ -1,381 +0,0 @@
;; Common Lisp tokenizer
;;
;; Tokens: {:type T :value V :pos P}
;;
;; Types:
;; "symbol" — FOO, PKG:SYM, PKG::SYM, T, NIL (upcase)
;; "keyword" — :foo (value is upcase name without colon)
;; "integer" — 42, -5, #xFF, #b1010, #o17 (string)
;; "float" — 3.14, 1.0e10 (string)
;; "ratio" — 1/3 (string "N/D")
;; "string" — unescaped content
;; "char" — single-character string
;; "lparen" "rparen" "quote" "backquote" "comma" "comma-at"
;; "hash-quote" — #'
;; "hash-paren" — #(
;; "uninterned" — #:foo (upcase name)
;; "dot" — standalone . (dotted pair separator)
;; "eof"
(define cl-make-tok (fn (type value pos) {:type type :value value :pos pos}))
;; ── char ordinal table ────────────────────────────────────────────
(define
cl-ord-table
(let
((t (dict)) (i 0))
(define
cl-fill
(fn
()
(when
(< i 128)
(do
(dict-set! t (char-from-code i) i)
(set! i (+ i 1))
(cl-fill)))))
(cl-fill)
t))
(define cl-ord (fn (c) (or (get cl-ord-table c) 0)))
;; ── character predicates ──────────────────────────────────────────
(define cl-digit? (fn (c) (and (>= (cl-ord c) 48) (<= (cl-ord c) 57))))
(define
cl-hex?
(fn
(c)
(or
(cl-digit? c)
(and (>= (cl-ord c) 65) (<= (cl-ord c) 70))
(and (>= (cl-ord c) 97) (<= (cl-ord c) 102)))))
(define cl-octal? (fn (c) (and (>= (cl-ord c) 48) (<= (cl-ord c) 55))))
(define cl-binary? (fn (c) (or (= c "0") (= c "1"))))
(define cl-ws? (fn (c) (or (= c " ") (= c "\t") (= c "\n") (= c "\r"))))
(define
cl-alpha?
(fn
(c)
(or
(and (>= (cl-ord c) 65) (<= (cl-ord c) 90))
(and (>= (cl-ord c) 97) (<= (cl-ord c) 122)))))
;; Characters that end a token (whitespace + terminating macro chars)
(define
cl-terminating?
(fn
(c)
(or
(cl-ws? c)
(= c "(")
(= c ")")
(= c "\"")
(= c ";")
(= c "`")
(= c ","))))
;; Symbol constituent: not terminating, not reader-special
(define
cl-sym-char?
(fn
(c)
(not
(or
(cl-terminating? c)
(= c "#")
(= c "|")
(= c "\\")
(= c "'")))))
;; ── named character table ─────────────────────────────────────────
(define
cl-named-chars
{:space " "
:newline "\n"
:tab "\t"
:return "\r"
:backspace (char-from-code 8)
:rubout (char-from-code 127)
:delete (char-from-code 127)
:escape (char-from-code 27)
:altmode (char-from-code 27)
:null (char-from-code 0)
:nul (char-from-code 0)
:page (char-from-code 12)
:formfeed (char-from-code 12)})
;; ── main tokenizer ────────────────────────────────────────────────
(define
cl-tokenize
(fn
(src)
(let
((pos 0) (n (string-length src)) (toks (list)))
(define at (fn () (if (< pos n) (substring src pos (+ pos 1)) nil)))
(define peek1 (fn () (if (< (+ pos 1) n) (substring src (+ pos 1) (+ pos 2)) nil)))
(define adv (fn () (set! pos (+ pos 1))))
;; Advance while predicate holds; return substring from start to end
(define
read-while
(fn
(pred)
(let
((start pos))
(define
rw-loop
(fn
()
(when
(and (at) (pred (at)))
(do (adv) (rw-loop)))))
(rw-loop)
(substring src start pos))))
(define
skip-line
(fn
()
(when
(and (at) (not (= (at) "\n")))
(do (adv) (skip-line)))))
(define
skip-block
(fn
(depth)
(when
(at)
(cond
((and (= (at) "#") (= (peek1) "|"))
(do (adv) (adv) (skip-block (+ depth 1))))
((and (= (at) "|") (= (peek1) "#"))
(do
(adv)
(adv)
(when (> depth 1) (skip-block (- depth 1)))))
(:else (do (adv) (skip-block depth)))))))
;; Read string literal — called with pos just past opening "
(define
read-str
(fn
(acc)
(if
(not (at))
acc
(cond
((= (at) "\"") (do (adv) acc))
((= (at) "\\")
(do
(adv)
(let
((e (at)))
(adv)
(read-str
(str
acc
(cond
((= e "n") "\n")
((= e "t") "\t")
((= e "r") "\r")
((= e "\"") "\"")
((= e "\\") "\\")
(:else e)))))))
(:else
(let
((c (at)))
(adv)
(read-str (str acc c))))))))
;; Read #\ char literal — called with pos just past the backslash
(define
read-char-lit
(fn
()
(let
((first (at)))
(adv)
(let
((rest (if (and (at) (cl-alpha? (at))) (read-while cl-alpha?) "")))
(if
(= rest "")
first
(let
((name (downcase (str first rest))))
(or (get cl-named-chars name) first)))))))
;; Number scanner — called with pos just past first digit(s).
;; acc holds what was already consumed (first digit or sign+digit).
(define
scan-num
(fn
(p acc)
(let
((more (read-while cl-digit?)))
(set! acc (str acc more))
(cond
;; ratio N/D
((and (at) (= (at) "/") (peek1) (cl-digit? (peek1)))
(do
(adv)
(let
((denom (read-while cl-digit?)))
{:type "ratio" :value (str acc "/" denom) :pos p})))
;; float: decimal point N.M[eE]
((and (at) (= (at) ".") (peek1) (cl-digit? (peek1)))
(do
(adv)
(let
((frac (read-while cl-digit?)))
(set! acc (str acc "." frac))
(when
(and (at) (or (= (at) "e") (= (at) "E")))
(do
(set! acc (str acc (at)))
(adv)
(when
(and (at) (or (= (at) "+") (= (at) "-")))
(do (set! acc (str acc (at))) (adv)))
(set! acc (str acc (read-while cl-digit?)))))
{:type "float" :value acc :pos p})))
;; float: exponent only NeE
((and (at) (or (= (at) "e") (= (at) "E")))
(do
(set! acc (str acc (at)))
(adv)
(when
(and (at) (or (= (at) "+") (= (at) "-")))
(do (set! acc (str acc (at))) (adv)))
(set! acc (str acc (read-while cl-digit?)))
{:type "float" :value acc :pos p}))
(:else {:type "integer" :value acc :pos p})))))
(define
read-radix
(fn
(letter p)
(let
((pred
(cond
((or (= letter "x") (= letter "X")) cl-hex?)
((or (= letter "b") (= letter "B")) cl-binary?)
((or (= letter "o") (= letter "O")) cl-octal?)
(:else cl-digit?))))
{:type "integer"
:value (str "#" letter (read-while pred))
:pos p})))
(define emit (fn (tok) (append! toks tok)))
(define
scan
(fn
()
(when
(< pos n)
(let
((c (at)) (p pos))
(cond
((cl-ws? c) (do (adv) (scan)))
((= c ";") (do (adv) (skip-line) (scan)))
((= c "(") (do (adv) (emit (cl-make-tok "lparen" "(" p)) (scan)))
((= c ")") (do (adv) (emit (cl-make-tok "rparen" ")" p)) (scan)))
((= c "'") (do (adv) (emit (cl-make-tok "quote" "'" p)) (scan)))
((= c "`") (do (adv) (emit (cl-make-tok "backquote" "`" p)) (scan)))
((= c ",")
(do
(adv)
(if
(= (at) "@")
(do (adv) (emit (cl-make-tok "comma-at" ",@" p)))
(emit (cl-make-tok "comma" "," p)))
(scan)))
((= c "\"")
(do
(adv)
(emit (cl-make-tok "string" (read-str "") p))
(scan)))
;; :keyword
((= c ":")
(do
(adv)
(emit (cl-make-tok "keyword" (upcase (read-while cl-sym-char?)) p))
(scan)))
;; dispatch macro #
((= c "#")
(do
(adv)
(let
((d (at)))
(cond
((= d "'") (do (adv) (emit (cl-make-tok "hash-quote" "#'" p)) (scan)))
((= d "(") (do (adv) (emit (cl-make-tok "hash-paren" "#(" p)) (scan)))
((= d ":")
(do
(adv)
(emit
(cl-make-tok "uninterned" (upcase (read-while cl-sym-char?)) p))
(scan)))
((= d "|") (do (adv) (skip-block 1) (scan)))
((= d "\\")
(do (adv) (emit (cl-make-tok "char" (read-char-lit) p)) (scan)))
((or (= d "x") (= d "X"))
(do (adv) (emit (read-radix d p)) (scan)))
((or (= d "b") (= d "B"))
(do (adv) (emit (read-radix d p)) (scan)))
((or (= d "o") (= d "O"))
(do (adv) (emit (read-radix d p)) (scan)))
(:else (scan))))))
;; standalone dot, float .5, or symbol starting with dots
((= c ".")
(do
(adv)
(cond
((or (not (at)) (cl-terminating? (at)))
(do (emit (cl-make-tok "dot" "." p)) (scan)))
((cl-digit? (at))
(do
(emit
(cl-make-tok "float" (str "0." (read-while cl-digit?)) p))
(scan)))
(:else
(do
(emit
(cl-make-tok "symbol" (upcase (str "." (read-while cl-sym-char?))) p))
(scan))))))
;; sign followed by digit → number
((and (or (= c "+") (= c "-")) (peek1) (cl-digit? (peek1)))
(do
(adv)
(let
((first-d (at)))
(adv)
(emit (scan-num p (str c first-d))))
(scan)))
;; decimal digit → number
((cl-digit? c)
(do
(adv)
(emit (scan-num p c))
(scan)))
;; symbol constituent (includes bare +, -, etc.)
((cl-sym-char? c)
(do
(emit (cl-make-tok "symbol" (upcase (read-while cl-sym-char?)) p))
(scan)))
(:else (do (adv) (scan))))))))
(scan)
(append! toks (cl-make-tok "eof" nil n))
toks)))

View File

@@ -1,100 +0,0 @@
#!/usr/bin/env bash
# Common Lisp on SX test runner — pipes directly to sx_server.exe
#
# Usage:
# bash lib/common-lisp/test.sh # all tests
# bash lib/common-lisp/test.sh -v # verbose
# bash lib/common-lisp/test.sh tests/read.sx # one file
set -euo pipefail
cd "$(git rev-parse --show-toplevel)"
SX_SERVER="hosts/ocaml/_build/default/bin/sx_server.exe"
if [ ! -x "$SX_SERVER" ]; then
MAIN_ROOT=$(git worktree list | awk 'NR==1{print $1}')
if [ -x "$MAIN_ROOT/$SX_SERVER" ]; then
SX_SERVER="$MAIN_ROOT/$SX_SERVER"
else
echo "ERROR: sx_server.exe not found"
exit 1
fi
fi
VERBOSE=""
FILES=()
for arg in "$@"; do
case "$arg" in
-v|--verbose) VERBOSE=1 ;;
*) FILES+=("$arg") ;;
esac
done
if [ ${#FILES[@]} -eq 0 ]; then
mapfile -t FILES < <(find lib/common-lisp/tests -maxdepth 2 -name '*.sx' | sort)
fi
TOTAL_PASS=0
TOTAL_FAIL=0
FAILED_FILES=()
for FILE in "${FILES[@]}"; do
[ -f "$FILE" ] || { echo "skip $FILE (not found)"; continue; }
TMPFILE=$(mktemp)
cat > "$TMPFILE" <<EPOCHS
(epoch 1)
(load "lib/common-lisp/reader.sx")
(load "lib/common-lisp/parser.sx")
(epoch 2)
(load "$FILE")
(epoch 3)
(eval "(list cl-test-pass cl-test-fail)")
EPOCHS
OUTPUT=$(timeout 60 "$SX_SERVER" < "$TMPFILE" 2>&1 || true)
rm -f "$TMPFILE"
LINE=$(echo "$OUTPUT" | awk '/^\(ok-len 3 / {getline; print; exit}' || true)
if [ -z "$LINE" ]; then
LINE=$(echo "$OUTPUT" | grep -E '^\(ok 3 \([0-9]+ [0-9]+\)\)' | tail -1 \
| sed -E 's/^\(ok 3 //; s/\)$//' || true)
fi
if [ -z "$LINE" ]; then
echo "$FILE: could not extract summary"
echo "$OUTPUT" | tail -20
TOTAL_FAIL=$((TOTAL_FAIL + 1))
FAILED_FILES+=("$FILE")
continue
fi
P=$(echo "$LINE" | sed -E 's/^\(([0-9]+) ([0-9]+)\).*/\1/')
F=$(echo "$LINE" | sed -E 's/^\(([0-9]+) ([0-9]+)\).*/\2/')
TOTAL_PASS=$((TOTAL_PASS + P))
TOTAL_FAIL=$((TOTAL_FAIL + F))
if [ "$F" -gt 0 ]; then
FAILED_FILES+=("$FILE")
printf '✗ %-40s %d/%d\n' "$FILE" "$P" "$((P+F))"
TMPFILE2=$(mktemp)
cat > "$TMPFILE2" <<EPOCHS
(epoch 1)
(load "lib/common-lisp/reader.sx")
(load "lib/common-lisp/parser.sx")
(epoch 2)
(load "$FILE")
(epoch 3)
(eval "(map (fn (f) (get f \"name\")) cl-test-fails)")
EPOCHS
FAILS=$(timeout 60 "$SX_SERVER" < "$TMPFILE2" 2>&1 | grep -E '^\(ok 3 ' || true)
rm -f "$TMPFILE2"
echo " $FAILS"
elif [ "$VERBOSE" = "1" ]; then
printf '✓ %-40s %d passed\n' "$FILE" "$P"
fi
done
TOTAL=$((TOTAL_PASS + TOTAL_FAIL))
if [ $TOTAL_FAIL -eq 0 ]; then
echo "$TOTAL_PASS/$TOTAL common-lisp-on-sx tests passed"
else
echo "$TOTAL_PASS/$TOTAL passed, $TOTAL_FAIL failed in: ${FAILED_FILES[*]}"
fi
[ $TOTAL_FAIL -eq 0 ]

View File

@@ -1,285 +0,0 @@
;; CL evaluator tests
(define cl-test-pass 0)
(define cl-test-fail 0)
(define cl-test-fails (list))
(define
cl-deep=
(fn
(a b)
(cond
((= a b) true)
((and (dict? a) (dict? b))
(let
((ak (keys a)) (bk (keys b)))
(if
(not (= (len ak) (len bk)))
false
(every?
(fn (k) (and (has-key? b k) (cl-deep= (get a k) (get b k))))
ak))))
((and (list? a) (list? b))
(if
(not (= (len a) (len b)))
false
(let
((i 0) (ok true))
(define
chk
(fn
()
(when
(and ok (< i (len a)))
(do
(when
(not (cl-deep= (nth a i) (nth b i)))
(set! ok false))
(set! i (+ i 1))
(chk)))))
(chk)
ok)))
(:else false))))
(define
cl-test
(fn
(name actual expected)
(if
(cl-deep= actual expected)
(set! cl-test-pass (+ cl-test-pass 1))
(do
(set! cl-test-fail (+ cl-test-fail 1))
(append! cl-test-fails {:name name :expected expected :actual actual})))))
;; Convenience: evaluate CL string with fresh env each time
(define ev (fn (src) (cl-eval-str src (cl-make-env))))
(define evall (fn (src) (cl-eval-all-str src (cl-make-env))))
;; ── self-evaluating literals ──────────────────────────────────────
(cl-test "lit: nil" (ev "nil") nil)
(cl-test "lit: t" (ev "t") true)
(cl-test "lit: integer" (ev "42") 42)
(cl-test "lit: negative" (ev "-7") -7)
(cl-test "lit: zero" (ev "0") 0)
(cl-test "lit: string" (ev "\"hello\"") "hello")
(cl-test "lit: empty string" (ev "\"\"") "")
(cl-test "lit: keyword type" (get (ev ":foo") "cl-type") "keyword")
(cl-test "lit: keyword name" (get (ev ":foo") "name") "FOO")
(cl-test "lit: float type" (get (ev "3.14") "cl-type") "float")
;; ── QUOTE ─────────────────────────────────────────────────────────
(cl-test "quote: symbol" (ev "'x") "X")
(cl-test "quote: list" (ev "'(a b c)") (list "A" "B" "C"))
(cl-test "quote: nil" (ev "'nil") nil)
(cl-test "quote: integer" (ev "'42") 42)
(cl-test "quote: nested" (ev "'(a (b c))") (list "A" (list "B" "C")))
;; ── IF ────────────────────────────────────────────────────────────
(cl-test "if: true branch" (ev "(if t 1 2)") 1)
(cl-test "if: false branch" (ev "(if nil 1 2)") 2)
(cl-test "if: no else nil" (ev "(if nil 99)") nil)
(cl-test "if: number truthy" (ev "(if 0 'yes 'no)") "YES")
(cl-test "if: empty string truthy" (ev "(if \"\" 'yes 'no)") "YES")
(cl-test "if: nested" (ev "(if t (if nil 1 2) 3)") 2)
;; ── PROGN ────────────────────────────────────────────────────────
(cl-test "progn: single" (ev "(progn 42)") 42)
(cl-test "progn: multiple" (ev "(progn 1 2 3)") 3)
(cl-test "progn: nil last" (ev "(progn 1 nil)") nil)
;; ── AND / OR ─────────────────────────────────────────────────────
(cl-test "and: empty" (ev "(and)") true)
(cl-test "and: all true" (ev "(and 1 2 3)") 3)
(cl-test "and: short-circuit" (ev "(and nil 99)") nil)
(cl-test "and: returns last" (ev "(and 1 2)") 2)
(cl-test "or: empty" (ev "(or)") nil)
(cl-test "or: first truthy" (ev "(or 1 2)") 1)
(cl-test "or: all nil" (ev "(or nil nil)") nil)
(cl-test "or: short-circuit" (ev "(or nil 42)") 42)
;; ── COND ─────────────────────────────────────────────────────────
(cl-test "cond: first match" (ev "(cond (t 1) (t 2))") 1)
(cl-test "cond: second match" (ev "(cond (nil 1) (t 2))") 2)
(cl-test "cond: no match" (ev "(cond (nil 1) (nil 2))") nil)
(cl-test "cond: returns test value" (ev "(cond (42))") 42)
;; ── WHEN / UNLESS ─────────────────────────────────────────────────
(cl-test "when: true" (ev "(when t 1 2 3)") 3)
(cl-test "when: nil" (ev "(when nil 99)") nil)
(cl-test "unless: nil runs" (ev "(unless nil 42)") 42)
(cl-test "unless: true skips" (ev "(unless t 99)") nil)
;; ── LET ──────────────────────────────────────────────────────────
(cl-test "let: empty bindings" (ev "(let () 42)") 42)
(cl-test "let: single binding" (ev "(let ((x 5)) x)") 5)
(cl-test "let: two bindings" (ev "(let ((x 3) (y 4)) (+ x y))") 7)
(cl-test "let: parallel" (ev "(let ((x 1)) (let ((x 2) (y x)) y))") 1)
(cl-test "let: nested" (ev "(let ((x 1)) (let ((y 2)) (+ x y)))") 3)
(cl-test "let: progn body" (ev "(let ((x 5)) (+ x 1) (* x 2))") 10)
(cl-test "let: bare name nil" (ev "(let (x) x)") nil)
;; ── LET* ─────────────────────────────────────────────────────────
(cl-test "let*: sequential" (ev "(let* ((x 1) (y (+ x 1))) y)") 2)
(cl-test "let*: chain" (ev "(let* ((a 2) (b (* a 3)) (c (+ b 1))) c)") 7)
(cl-test "let*: shadow" (ev "(let ((x 1)) (let* ((x 2) (y x)) y))") 2)
;; ── SETQ / SETF ──────────────────────────────────────────────────
(cl-test "setq: basic" (ev "(let ((x 0)) (setq x 5) x)") 5)
(cl-test "setq: returns value" (ev "(let ((x 0)) (setq x 99))") 99)
(cl-test "setf: basic" (ev "(let ((x 0)) (setf x 7) x)") 7)
;; ── LAMBDA ────────────────────────────────────────────────────────
(cl-test "lambda: call" (ev "((lambda (x) x) 42)") 42)
(cl-test "lambda: multi-arg" (ev "((lambda (x y) (+ x y)) 3 4)") 7)
(cl-test "lambda: closure" (ev "(let ((n 10)) ((lambda (x) (+ x n)) 5))") 15)
(cl-test "lambda: rest arg"
(ev "((lambda (x &rest xs) (cons x xs)) 1 2 3)")
{:cl-type "cons" :car 1 :cdr (list 2 3)})
(cl-test "lambda: optional no default"
(ev "((lambda (&optional x) x))")
nil)
(cl-test "lambda: optional with arg"
(ev "((lambda (&optional (x 99)) x) 42)")
42)
(cl-test "lambda: optional default used"
(ev "((lambda (&optional (x 7)) x))")
7)
;; ── FUNCTION ─────────────────────────────────────────────────────
(cl-test "function: lambda" (get (ev "(function (lambda (x) x))") "cl-type") "function")
;; ── DEFUN ────────────────────────────────────────────────────────
(cl-test "defun: returns name" (evall "(defun sq (x) (* x x))") "SQ")
(cl-test "defun: call" (evall "(defun sq (x) (* x x)) (sq 5)") 25)
(cl-test "defun: multi-arg" (evall "(defun add (x y) (+ x y)) (add 3 4)") 7)
(cl-test "defun: recursive factorial"
(evall "(defun fact (n) (if (<= n 1) 1 (* n (fact (- n 1))))) (fact 5)")
120)
(cl-test "defun: multiple calls"
(evall "(defun double (x) (* x 2)) (+ (double 3) (double 5))")
16)
;; ── FLET ─────────────────────────────────────────────────────────
(cl-test "flet: basic"
(ev "(flet ((double (x) (* x 2))) (double 5))")
10)
(cl-test "flet: sees outer vars"
(ev "(let ((n 3)) (flet ((add-n (x) (+ x n))) (add-n 7)))")
10)
(cl-test "flet: non-recursive"
(ev "(flet ((f (x) (+ x 1))) (flet ((f (x) (f (f x)))) (f 5)))")
7)
;; ── LABELS ────────────────────────────────────────────────────────
(cl-test "labels: basic"
(ev "(labels ((greet (x) x)) (greet 42))")
42)
(cl-test "labels: recursive"
(ev "(labels ((count (n) (if (<= n 0) 0 (+ 1 (count (- n 1)))))) (count 5))")
5)
(cl-test "labels: mutual recursion"
(ev "(labels
((even? (n) (if (= n 0) t (odd? (- n 1))))
(odd? (n) (if (= n 0) nil (even? (- n 1)))))
(list (even? 4) (odd? 3)))")
(list true true))
;; ── THE / LOCALLY / EVAL-WHEN ────────────────────────────────────
(cl-test "the: passthrough" (ev "(the integer 42)") 42)
(cl-test "the: string" (ev "(the string \"hi\")") "hi")
(cl-test "locally: body" (ev "(locally 1 2 3)") 3)
(cl-test "eval-when: execute" (ev "(eval-when (:execute) 99)") 99)
(cl-test "eval-when: no execute" (ev "(eval-when (:compile-toplevel) 99)") nil)
;; ── DEFVAR / DEFPARAMETER ────────────────────────────────────────
(cl-test "defvar: returns name" (evall "(defvar *x* 10)") "*X*")
(cl-test "defparameter: sets value" (evall "(defparameter *y* 42) *y*") 42)
(cl-test "defvar: no reinit" (evall "(defvar *z* 1) (defvar *z* 99) *z*") 1)
;; ── built-in arithmetic ───────────────────────────────────────────
(cl-test "arith: +" (ev "(+ 1 2 3)") 6)
(cl-test "arith: + zero" (ev "(+)") 0)
(cl-test "arith: -" (ev "(- 10 3 2)") 5)
(cl-test "arith: - negate" (ev "(- 5)") -5)
(cl-test "arith: *" (ev "(* 2 3 4)") 24)
(cl-test "arith: * one" (ev "(*)") 1)
(cl-test "arith: /" (ev "(/ 12 3)") 4)
(cl-test "arith: max" (ev "(max 3 1 4 1 5)") 5)
(cl-test "arith: min" (ev "(min 3 1 4 1 5)") 1)
(cl-test "arith: abs neg" (ev "(abs -7)") 7)
(cl-test "arith: abs pos" (ev "(abs 7)") 7)
;; ── built-in comparisons ──────────────────────────────────────────
(cl-test "cmp: = true" (ev "(= 3 3)") true)
(cl-test "cmp: = false" (ev "(= 3 4)") nil)
(cl-test "cmp: /=" (ev "(/= 3 4)") true)
(cl-test "cmp: <" (ev "(< 1 2)") true)
(cl-test "cmp: > false" (ev "(> 1 2)") nil)
(cl-test "cmp: <=" (ev "(<= 2 2)") true)
;; ── built-in predicates ───────────────────────────────────────────
(cl-test "pred: null nil" (ev "(null nil)") true)
(cl-test "pred: null non-nil" (ev "(null 5)") nil)
(cl-test "pred: not nil" (ev "(not nil)") true)
(cl-test "pred: not truthy" (ev "(not 5)") nil)
(cl-test "pred: numberp" (ev "(numberp 5)") true)
(cl-test "pred: numberp str" (ev "(numberp \"x\")") nil)
(cl-test "pred: stringp" (ev "(stringp \"hello\")") true)
(cl-test "pred: listp list" (ev "(listp '(1))") true)
(cl-test "pred: listp nil" (ev "(listp nil)") true)
(cl-test "pred: zerop" (ev "(zerop 0)") true)
(cl-test "pred: plusp" (ev "(plusp 3)") true)
(cl-test "pred: evenp" (ev "(evenp 4)") true)
(cl-test "pred: oddp" (ev "(oddp 3)") true)
;; ── built-in list ops ─────────────────────────────────────────────
(cl-test "list: car" (ev "(car '(1 2 3))") 1)
(cl-test "list: cdr" (ev "(cdr '(1 2 3))") (list 2 3))
(cl-test "list: cons" (get (ev "(cons 1 2)") "car") 1)
(cl-test "list: list fn" (ev "(list 1 2 3)") (list 1 2 3))
(cl-test "list: length" (ev "(length '(a b c))") 3)
(cl-test "list: length nil" (ev "(length nil)") 0)
(cl-test "list: append" (ev "(append '(1 2) '(3 4))") (list 1 2 3 4))
(cl-test "list: first" (ev "(first '(10 20 30))") 10)
(cl-test "list: second" (ev "(second '(10 20 30))") 20)
(cl-test "list: third" (ev "(third '(10 20 30))") 30)
(cl-test "list: rest" (ev "(rest '(1 2 3))") (list 2 3))
(cl-test "list: nth" (ev "(nth 1 '(a b c))") "B")
(cl-test "list: reverse" (ev "(reverse '(1 2 3))") (list 3 2 1))
;; ── FUNCALL / APPLY / MAPCAR ─────────────────────────────────────
(cl-test "funcall: lambda"
(ev "(funcall (lambda (x) (* x x)) 5)")
25)
(cl-test "apply: basic"
(ev "(apply #'+ '(1 2 3))")
6)
(cl-test "apply: leading args"
(ev "(apply #'+ 1 2 '(3 4))")
10)
(cl-test "mapcar: basic"
(ev "(mapcar (lambda (x) (* x 2)) '(1 2 3))")
(list 2 4 6))

View File

@@ -1,204 +0,0 @@
;; Lambda list parser tests
(define cl-test-pass 0)
(define cl-test-fail 0)
(define cl-test-fails (list))
;; Deep structural equality for dicts and lists
(define
cl-deep=
(fn
(a b)
(cond
((= a b) true)
((and (dict? a) (dict? b))
(let
((ak (keys a)) (bk (keys b)))
(if
(not (= (len ak) (len bk)))
false
(every?
(fn (k) (and (has-key? b k) (cl-deep= (get a k) (get b k))))
ak))))
((and (list? a) (list? b))
(if
(not (= (len a) (len b)))
false
(let
((i 0) (ok true))
(define
chk
(fn
()
(when
(and ok (< i (len a)))
(do
(when
(not (cl-deep= (nth a i) (nth b i)))
(set! ok false))
(set! i (+ i 1))
(chk)))))
(chk)
ok)))
(:else false))))
(define
cl-test
(fn
(name actual expected)
(if
(cl-deep= actual expected)
(set! cl-test-pass (+ cl-test-pass 1))
(do
(set! cl-test-fail (+ cl-test-fail 1))
(append! cl-test-fails {:name name :expected expected :actual actual})))))
;; Helper: parse lambda list from string "(x y ...)"
(define ll (fn (src) (cl-parse-lambda-list-str src)))
(define ll-req (fn (src) (get (ll src) "required")))
(define ll-opt (fn (src) (get (ll src) "optional")))
(define ll-rest (fn (src) (get (ll src) "rest")))
(define ll-key (fn (src) (get (ll src) "key")))
(define ll-aok (fn (src) (get (ll src) "allow-other-keys")))
(define ll-aux (fn (src) (get (ll src) "aux")))
;; ── required parameters ───────────────────────────────────────────
(cl-test "required: empty" (ll-req "()") (list))
(cl-test "required: one" (ll-req "(x)") (list "X"))
(cl-test "required: two" (ll-req "(x y)") (list "X" "Y"))
(cl-test "required: three" (ll-req "(a b c)") (list "A" "B" "C"))
(cl-test "required: upcased" (ll-req "(foo bar)") (list "FOO" "BAR"))
;; ── &optional ─────────────────────────────────────────────────────
(cl-test "optional: none" (ll-opt "(x)") (list))
(cl-test
"optional: bare symbol"
(ll-opt "(x &optional z)")
(list {:name "Z" :default nil :supplied nil}))
(cl-test
"optional: with default"
(ll-opt "(x &optional (z 0))")
(list {:name "Z" :default 0 :supplied nil}))
(cl-test
"optional: with supplied-p"
(ll-opt "(x &optional (z 0 z-p))")
(list {:name "Z" :default 0 :supplied "Z-P"}))
(cl-test
"optional: two params"
(ll-opt "(&optional a (b 1))")
(list {:name "A" :default nil :supplied nil} {:name "B" :default 1 :supplied nil}))
(cl-test
"optional: string default"
(ll-opt "(&optional (name \"world\"))")
(list {:name "NAME" :default {:cl-type "string" :value "world"} :supplied nil}))
;; ── &rest ─────────────────────────────────────────────────────────
(cl-test "rest: none" (ll-rest "(x)") nil)
(cl-test "rest: present" (ll-rest "(x &rest args)") "ARGS")
(cl-test "rest: with required" (ll-rest "(a b &rest tail)") "TAIL")
;; &body is an alias for &rest
(cl-test "body: alias for rest" (ll-rest "(&body forms)") "FORMS")
;; rest doesn't consume required params
(cl-test "rest: required still there" (ll-req "(a b &rest rest)") (list "A" "B"))
;; ── &key ──────────────────────────────────────────────────────────
(cl-test "key: none" (ll-key "(x)") (list))
(cl-test
"key: bare symbol"
(ll-key "(&key x)")
(list {:name "X" :keyword "X" :default nil :supplied nil}))
(cl-test
"key: with default"
(ll-key "(&key (x 42))")
(list {:name "X" :keyword "X" :default 42 :supplied nil}))
(cl-test
"key: with supplied-p"
(ll-key "(&key (x 42 x-p))")
(list {:name "X" :keyword "X" :default 42 :supplied "X-P"}))
(cl-test
"key: two params"
(ll-key "(&key a b)")
(list
{:name "A" :keyword "A" :default nil :supplied nil}
{:name "B" :keyword "B" :default nil :supplied nil}))
;; ── &allow-other-keys ─────────────────────────────────────────────
(cl-test "aok: absent" (ll-aok "(x)") false)
(cl-test "aok: present" (ll-aok "(&key x &allow-other-keys)") true)
;; ── &aux ──────────────────────────────────────────────────────────
(cl-test "aux: none" (ll-aux "(x)") (list))
(cl-test
"aux: bare symbol"
(ll-aux "(&aux temp)")
(list {:name "TEMP" :init nil}))
(cl-test
"aux: with init"
(ll-aux "(&aux (count 0))")
(list {:name "COUNT" :init 0}))
(cl-test
"aux: two vars"
(ll-aux "(&aux a (b 1))")
(list {:name "A" :init nil} {:name "B" :init 1}))
;; ── combined ──────────────────────────────────────────────────────
(cl-test
"combined: full lambda list"
(let
((parsed (ll "(x y &optional (z 0 z-p) &rest args &key a (b nil b-p) &aux temp)")))
(list
(get parsed "required")
(get (nth (get parsed "optional") 0) "name")
(get (nth (get parsed "optional") 0) "default")
(get (nth (get parsed "optional") 0) "supplied")
(get parsed "rest")
(get (nth (get parsed "key") 0) "name")
(get (nth (get parsed "key") 1) "supplied")
(get (nth (get parsed "aux") 0) "name")))
(list
(list "X" "Y")
"Z"
0
"Z-P"
"ARGS"
"A"
"B-P"
"TEMP"))
(cl-test
"combined: required only stops before &"
(ll-req "(a b &optional c)")
(list "A" "B"))
(cl-test
"combined: required only with &key"
(ll-req "(x &key y)")
(list "X"))
(cl-test
"combined: &rest and &key together"
(let
((parsed (ll "(&rest args &key verbose)")))
(list (get parsed "rest") (get (nth (get parsed "key") 0) "name")))
(list "ARGS" "VERBOSE"))

View File

@@ -1,160 +0,0 @@
;; Common Lisp reader/parser tests
(define cl-test-pass 0)
(define cl-test-fail 0)
(define cl-test-fails (list))
(define
cl-deep=
(fn
(a b)
(cond
((= a b) true)
((and (dict? a) (dict? b))
(let
((ak (keys a)) (bk (keys b)))
(if
(not (= (len ak) (len bk)))
false
(every?
(fn (k) (and (has-key? b k) (cl-deep= (get a k) (get b k))))
ak))))
((and (list? a) (list? b))
(if
(not (= (len a) (len b)))
false
(let
((i 0) (ok true))
(define
chk
(fn
()
(when
(and ok (< i (len a)))
(do
(when
(not (cl-deep= (nth a i) (nth b i)))
(set! ok false))
(set! i (+ i 1))
(chk)))))
(chk)
ok)))
(:else false))))
(define
cl-test
(fn
(name actual expected)
(if
(cl-deep= actual expected)
(set! cl-test-pass (+ cl-test-pass 1))
(do
(set! cl-test-fail (+ cl-test-fail 1))
(append! cl-test-fails {:name name :expected expected :actual actual})))))
;; ── atoms ─────────────────────────────────────────────────────────
(cl-test "integer: 42" (cl-read "42") 42)
(cl-test "integer: 0" (cl-read "0") 0)
(cl-test "integer: negative" (cl-read "-5") -5)
(cl-test "integer: positive sign" (cl-read "+3") 3)
(cl-test "integer: hex #xFF" (cl-read "#xFF") 255)
(cl-test "integer: hex #xAB" (cl-read "#xAB") 171)
(cl-test "integer: binary #b1010" (cl-read "#b1010") 10)
(cl-test "integer: octal #o17" (cl-read "#o17") 15)
(cl-test "float: type" (get (cl-read "3.14") "cl-type") "float")
(cl-test "float: value" (get (cl-read "3.14") "value") "3.14")
(cl-test "float: neg" (get (cl-read "-2.5") "value") "-2.5")
(cl-test "float: exp" (get (cl-read "1.0e10") "value") "1.0e10")
(cl-test "ratio: type" (get (cl-read "1/3") "cl-type") "ratio")
(cl-test "ratio: value" (get (cl-read "1/3") "value") "1/3")
(cl-test "ratio: 22/7" (get (cl-read "22/7") "value") "22/7")
(cl-test "string: basic" (cl-read "\"hello\"") {:cl-type "string" :value "hello"})
(cl-test "string: empty" (cl-read "\"\"") {:cl-type "string" :value ""})
(cl-test "string: with escape" (cl-read "\"a\\nb\"") {:cl-type "string" :value "a\nb"})
(cl-test "symbol: foo" (cl-read "foo") "FOO")
(cl-test "symbol: BAR" (cl-read "BAR") "BAR")
(cl-test "symbol: pkg:sym" (cl-read "cl:car") "CL:CAR")
(cl-test "symbol: pkg::sym" (cl-read "pkg::foo") "PKG::FOO")
(cl-test "nil: symbol" (cl-read "nil") nil)
(cl-test "nil: uppercase" (cl-read "NIL") nil)
(cl-test "t: symbol" (cl-read "t") true)
(cl-test "t: uppercase" (cl-read "T") true)
(cl-test "keyword: type" (get (cl-read ":foo") "cl-type") "keyword")
(cl-test "keyword: name" (get (cl-read ":foo") "name") "FOO")
(cl-test "keyword: :test" (get (cl-read ":test") "name") "TEST")
(cl-test "char: type" (get (cl-read "#\\a") "cl-type") "char")
(cl-test "char: value" (get (cl-read "#\\a") "value") "a")
(cl-test "char: Space" (get (cl-read "#\\Space") "value") " ")
(cl-test "char: Newline" (get (cl-read "#\\Newline") "value") "\n")
(cl-test "uninterned: type" (get (cl-read "#:foo") "cl-type") "uninterned")
(cl-test "uninterned: name" (get (cl-read "#:foo") "name") "FOO")
;; ── lists ─────────────────────────────────────────────────────────
(cl-test "list: empty" (cl-read "()") (list))
(cl-test "list: one element" (cl-read "(foo)") (list "FOO"))
(cl-test "list: two elements" (cl-read "(foo bar)") (list "FOO" "BAR"))
(cl-test "list: nested" (cl-read "((a b) c)") (list (list "A" "B") "C"))
(cl-test "list: with integer" (cl-read "(+ 1 2)") (list "+" 1 2))
(cl-test "list: with string" (cl-read "(print \"hi\")") (list "PRINT" {:cl-type "string" :value "hi"}))
(cl-test "list: nil element" (cl-read "(a nil b)") (list "A" nil "B"))
(cl-test "list: t element" (cl-read "(a t b)") (list "A" true "B"))
;; ── dotted pairs ──────────────────────────────────────────────<E29480><E29480>──
(cl-test "dotted: type" (get (cl-read "(a . b)") "cl-type") "cons")
(cl-test "dotted: car" (get (cl-read "(a . b)") "car") "A")
(cl-test "dotted: cdr" (get (cl-read "(a . b)") "cdr") "B")
(cl-test "dotted: number cdr" (get (cl-read "(x . 42)") "cdr") 42)
;; ── reader macros ────────────────────────────────────────────────<E29480><E29480>
(cl-test "quote: form" (cl-read "'x") (list "QUOTE" "X"))
(cl-test "quote: list" (cl-read "'(a b)") (list "QUOTE" (list "A" "B")))
(cl-test "backquote: form" (cl-read "`x") (list "QUASIQUOTE" "X"))
(cl-test "unquote: form" (cl-read ",x") (list "UNQUOTE" "X"))
(cl-test "comma-at: form" (cl-read ",@x") (list "UNQUOTE-SPLICING" "X"))
(cl-test "function: form" (cl-read "#'foo") (list "FUNCTION" "FOO"))
;; ── vector ────────────────────────────────────────────────────────
(cl-test "vector: type" (get (cl-read "#(1 2 3)") "cl-type") "vector")
(cl-test "vector: elements" (get (cl-read "#(1 2 3)") "elements") (list 1 2 3))
(cl-test "vector: empty" (get (cl-read "#()") "elements") (list))
(cl-test "vector: mixed" (get (cl-read "#(a 1 \"s\")") "elements") (list "A" 1 {:cl-type "string" :value "s"}))
;; ── cl-read-all ───────────────────────────────────────────────────
(cl-test
"read-all: empty"
(cl-read-all "")
(list))
(cl-test
"read-all: two forms"
(cl-read-all "42 foo")
(list 42 "FOO"))
(cl-test
"read-all: three forms"
(cl-read-all "(+ 1 2) (+ 3 4) hello")
(list (list "+" 1 2) (list "+" 3 4) "HELLO"))
(cl-test
"read-all: with comments"
(cl-read-all "; this is a comment\n42 ; inline\nfoo")
(list 42 "FOO"))
(cl-test
"read-all: defun form"
(nth (cl-read-all "(defun square (x) (* x x))") 0)
(list "DEFUN" "SQUARE" (list "X") (list "*" "X" "X")))

View File

@@ -1,180 +0,0 @@
;; Common Lisp tokenizer tests
(define cl-test-pass 0)
(define cl-test-fail 0)
(define cl-test-fails (list))
(define
cl-test
(fn
(name actual expected)
(if
(= actual expected)
(set! cl-test-pass (+ cl-test-pass 1))
(do
(set! cl-test-fail (+ cl-test-fail 1))
(append! cl-test-fails {:name name :expected expected :actual actual})))))
;; Helpers: extract types and values from token stream (drops eof)
(define
cl-tok-types
(fn
(src)
(map
(fn (t) (get t "type"))
(filter (fn (t) (not (= (get t "type") "eof"))) (cl-tokenize src)))))
(define
cl-tok-values
(fn
(src)
(map
(fn (t) (get t "value"))
(filter (fn (t) (not (= (get t "type") "eof"))) (cl-tokenize src)))))
(define
cl-tok-first
(fn (src) (nth (cl-tokenize src) 0)))
;; ── symbols ───────────────────────────────────────────────────────
(cl-test "symbol: bare lowercase" (cl-tok-values "foo") (list "FOO"))
(cl-test "symbol: uppercase" (cl-tok-values "BAR") (list "BAR"))
(cl-test "symbol: mixed case folded" (cl-tok-values "FooBar") (list "FOOBAR"))
(cl-test "symbol: with hyphen" (cl-tok-values "foo-bar") (list "FOO-BAR"))
(cl-test "symbol: with star" (cl-tok-values "*special*") (list "*SPECIAL*"))
(cl-test "symbol: with question" (cl-tok-values "null?") (list "NULL?"))
(cl-test "symbol: with exclamation" (cl-tok-values "set!") (list "SET!"))
(cl-test "symbol: plus sign alone" (cl-tok-values "+") (list "+"))
(cl-test "symbol: minus sign alone" (cl-tok-values "-") (list "-"))
(cl-test "symbol: type is symbol" (cl-tok-types "foo") (list "symbol"))
;; ── package-qualified symbols ─────────────────────────────────────
(cl-test "symbol: pkg:sym external" (cl-tok-values "cl:car") (list "CL:CAR"))
(cl-test "symbol: pkg::sym internal" (cl-tok-values "pkg::foo") (list "PKG::FOO"))
(cl-test "symbol: cl:car type" (cl-tok-types "cl:car") (list "symbol"))
;; ── keywords ──────────────────────────────────────────────────────
(cl-test "keyword: basic" (cl-tok-values ":foo") (list "FOO"))
(cl-test "keyword: type" (cl-tok-types ":foo") (list "keyword"))
(cl-test "keyword: upcase" (cl-tok-values ":hello-world") (list "HELLO-WORLD"))
(cl-test "keyword: multiple" (cl-tok-types ":a :b :c") (list "keyword" "keyword" "keyword"))
;; ── integers ──────────────────────────────────────────────────────
(cl-test "integer: zero" (cl-tok-values "0") (list "0"))
(cl-test "integer: positive" (cl-tok-values "42") (list "42"))
(cl-test "integer: negative" (cl-tok-values "-5") (list "-5"))
(cl-test "integer: positive-sign" (cl-tok-values "+3") (list "+3"))
(cl-test "integer: type" (cl-tok-types "42") (list "integer"))
(cl-test "integer: multi-digit" (cl-tok-values "12345678") (list "12345678"))
;; ── hex, binary, octal ───────────────────────────────────────────
(cl-test "hex: lowercase x" (cl-tok-values "#xFF") (list "#xFF"))
(cl-test "hex: uppercase X" (cl-tok-values "#XFF") (list "#XFF"))
(cl-test "hex: type" (cl-tok-types "#xFF") (list "integer"))
(cl-test "hex: zero" (cl-tok-values "#x0") (list "#x0"))
(cl-test "binary: #b" (cl-tok-values "#b1010") (list "#b1010"))
(cl-test "binary: type" (cl-tok-types "#b1010") (list "integer"))
(cl-test "octal: #o" (cl-tok-values "#o17") (list "#o17"))
(cl-test "octal: type" (cl-tok-types "#o17") (list "integer"))
;; ── floats ────────────────────────────────────────────────────────
(cl-test "float: basic" (cl-tok-values "3.14") (list "3.14"))
(cl-test "float: type" (cl-tok-types "3.14") (list "float"))
(cl-test "float: negative" (cl-tok-values "-2.5") (list "-2.5"))
(cl-test "float: exponent" (cl-tok-values "1.0e10") (list "1.0e10"))
(cl-test "float: neg exponent" (cl-tok-values "1.5e-3") (list "1.5e-3"))
(cl-test "float: leading dot" (cl-tok-values ".5") (list "0.5"))
(cl-test "float: exp only" (cl-tok-values "1e5") (list "1e5"))
;; ── ratios ────────────────────────────────────────────────────────
(cl-test "ratio: 1/3" (cl-tok-values "1/3") (list "1/3"))
(cl-test "ratio: type" (cl-tok-types "1/3") (list "ratio"))
(cl-test "ratio: 22/7" (cl-tok-values "22/7") (list "22/7"))
(cl-test "ratio: negative" (cl-tok-values "-1/2") (list "-1/2"))
;; ── strings ───────────────────────────────────────────────────────
(cl-test "string: empty" (cl-tok-values "\"\"") (list ""))
(cl-test "string: basic" (cl-tok-values "\"hello\"") (list "hello"))
(cl-test "string: type" (cl-tok-types "\"hello\"") (list "string"))
(cl-test "string: with space" (cl-tok-values "\"hello world\"") (list "hello world"))
(cl-test "string: escaped quote" (cl-tok-values "\"say \\\"hi\\\"\"") (list "say \"hi\""))
(cl-test "string: escaped backslash" (cl-tok-values "\"a\\\\b\"") (list "a\\b"))
(cl-test "string: newline escape" (cl-tok-values "\"a\\nb\"") (list "a\nb"))
(cl-test "string: tab escape" (cl-tok-values "\"a\\tb\"") (list "a\tb"))
;; ── characters ────────────────────────────────────────────────────
(cl-test "char: lowercase a" (cl-tok-values "#\\a") (list "a"))
(cl-test "char: uppercase A" (cl-tok-values "#\\A") (list "A"))
(cl-test "char: digit" (cl-tok-values "#\\1") (list "1"))
(cl-test "char: type" (cl-tok-types "#\\a") (list "char"))
(cl-test "char: Space" (cl-tok-values "#\\Space") (list " "))
(cl-test "char: Newline" (cl-tok-values "#\\Newline") (list "\n"))
(cl-test "char: Tab" (cl-tok-values "#\\Tab") (list "\t"))
(cl-test "char: Return" (cl-tok-values "#\\Return") (list "\r"))
;; ── reader macros ─────────────────────────────────────────────────
(cl-test "quote: type" (cl-tok-types "'x") (list "quote" "symbol"))
(cl-test "backquote: type" (cl-tok-types "`x") (list "backquote" "symbol"))
(cl-test "comma: type" (cl-tok-types ",x") (list "comma" "symbol"))
(cl-test "comma-at: type" (cl-tok-types ",@x") (list "comma-at" "symbol"))
(cl-test "hash-quote: type" (cl-tok-types "#'foo") (list "hash-quote" "symbol"))
(cl-test "hash-paren: type" (cl-tok-types "#(1 2)") (list "hash-paren" "integer" "integer" "rparen"))
;; ── uninterned ────────────────────────────────────────────────────
(cl-test "uninterned: type" (cl-tok-types "#:foo") (list "uninterned"))
(cl-test "uninterned: value upcase" (cl-tok-values "#:foo") (list "FOO"))
(cl-test "uninterned: compound" (cl-tok-values "#:my-sym") (list "MY-SYM"))
;; ── parens and structure ──────────────────────────────────────────
(cl-test "paren: empty list" (cl-tok-types "()") (list "lparen" "rparen"))
(cl-test "paren: nested" (cl-tok-types "((a))") (list "lparen" "lparen" "symbol" "rparen" "rparen"))
(cl-test "dot: standalone" (cl-tok-types "(a . b)") (list "lparen" "symbol" "dot" "symbol" "rparen"))
;; ── comments ──────────────────────────────────────────────────────
(cl-test "comment: line" (cl-tok-types "; comment\nfoo") (list "symbol"))
(cl-test "comment: inline" (cl-tok-values "foo ; bar\nbaz") (list "FOO" "BAZ"))
(cl-test "block-comment: basic" (cl-tok-types "#| hello |# foo") (list "symbol"))
(cl-test "block-comment: nested" (cl-tok-types "#| a #| b |# c |# x") (list "symbol"))
;; ── combined ──────────────────────────────────────────────────────
(cl-test
"combined: defun skeleton"
(cl-tok-types "(defun foo (x) x)")
(list "lparen" "symbol" "symbol" "lparen" "symbol" "rparen" "symbol" "rparen"))
(cl-test
"combined: let form"
(cl-tok-types "(let ((x 1)) x)")
(list
"lparen"
"symbol"
"lparen"
"lparen"
"symbol"
"integer"
"rparen"
"rparen"
"symbol"
"rparen"))
(cl-test
"combined: whitespace skip"
(cl-tok-values " foo bar baz ")
(list "FOO" "BAR" "BAZ"))
(cl-test "eof: present" (get (nth (cl-tokenize "") 0) "type") "eof")
(cl-test "eof: at end of tokens" (get (nth (cl-tokenize "x") 1) "type") "eof")

140
lib/haskell/conformance.sh Executable file
View File

@@ -0,0 +1,140 @@
#!/usr/bin/env bash
# lib/haskell/conformance.sh — run the 5 classic-program test suites.
# Writes lib/haskell/scoreboard.json and lib/haskell/scoreboard.md.
#
# Usage:
# bash lib/haskell/conformance.sh # run + write scoreboards
# bash lib/haskell/conformance.sh --check # run only, exit 1 on failure
set -euo pipefail
cd "$(git rev-parse --show-toplevel)"
SX_SERVER="hosts/ocaml/_build/default/bin/sx_server.exe"
if [ ! -x "$SX_SERVER" ]; then
MAIN_ROOT=$(git worktree list | head -1 | awk '{print $1}')
if [ -x "$MAIN_ROOT/$SX_SERVER" ]; then
SX_SERVER="$MAIN_ROOT/$SX_SERVER"
else
echo "ERROR: sx_server.exe not found. Run: cd hosts/ocaml && dune build"
exit 1
fi
fi
PROGRAMS=(fib sieve quicksort nqueens calculator)
PASS_COUNTS=()
FAIL_COUNTS=()
run_suite() {
local prog="$1"
local FILE="lib/haskell/tests/program-${prog}.sx"
local TMPFILE
TMPFILE=$(mktemp)
cat > "$TMPFILE" <<EPOCHS
(epoch 1)
(load "lib/haskell/tokenizer.sx")
(load "lib/haskell/layout.sx")
(load "lib/haskell/parser.sx")
(load "lib/haskell/desugar.sx")
(load "lib/haskell/runtime.sx")
(load "lib/haskell/match.sx")
(load "lib/haskell/eval.sx")
(load "lib/haskell/testlib.sx")
(epoch 2)
(load "$FILE")
(epoch 3)
(eval "(list hk-test-pass hk-test-fail)")
EPOCHS
local OUTPUT
OUTPUT=$(timeout 120 "$SX_SERVER" < "$TMPFILE" 2>&1 || true)
rm -f "$TMPFILE"
local LINE
LINE=$(echo "$OUTPUT" | awk '/^\(ok-len 3 / {getline; print; exit}')
if [ -z "$LINE" ]; then
LINE=$(echo "$OUTPUT" | grep -E '^\(ok 3 \([0-9]+ [0-9]+\)\)' | tail -1 \
| sed -E 's/^\(ok 3 //; s/\)$//' || true)
fi
if [ -z "$LINE" ]; then
echo "0 1"
else
local P F
P=$(echo "$LINE" | sed -E 's/^\(([0-9]+) ([0-9]+)\).*/\1/' || echo "0")
F=$(echo "$LINE" | sed -E 's/^\(([0-9]+) ([0-9]+)\).*/\2/' || echo "1")
echo "$P $F"
fi
}
for prog in "${PROGRAMS[@]}"; do
RESULT=$(run_suite "$prog")
P=$(echo "$RESULT" | cut -d' ' -f1)
F=$(echo "$RESULT" | cut -d' ' -f2)
PASS_COUNTS+=("$P")
FAIL_COUNTS+=("$F")
T=$((P + F))
if [ "$F" -eq 0 ]; then
printf '✓ %-14s %d/%d\n' "${prog}.hs" "$P" "$T"
else
printf '✗ %-14s %d/%d\n' "${prog}.hs" "$P" "$T"
fi
done
TOTAL_PASS=0
TOTAL_FAIL=0
PROG_PASS=0
for i in "${!PROGRAMS[@]}"; do
TOTAL_PASS=$((TOTAL_PASS + PASS_COUNTS[i]))
TOTAL_FAIL=$((TOTAL_FAIL + FAIL_COUNTS[i]))
[ "${FAIL_COUNTS[$i]}" -eq 0 ] && PROG_PASS=$((PROG_PASS + 1))
done
PROG_TOTAL=${#PROGRAMS[@]}
echo ""
echo "Classic programs: ${TOTAL_PASS}/$((TOTAL_PASS + TOTAL_FAIL)) tests | ${PROG_PASS}/${PROG_TOTAL} programs passing"
if [[ "${1:-}" == "--check" ]]; then
[ $TOTAL_FAIL -eq 0 ]
exit $?
fi
DATE=$(date '+%Y-%m-%d')
# scoreboard.json
{
printf '{\n'
printf ' "date": "%s",\n' "$DATE"
printf ' "total_pass": %d,\n' "$TOTAL_PASS"
printf ' "total_fail": %d,\n' "$TOTAL_FAIL"
printf ' "programs": {\n'
last=$((${#PROGRAMS[@]} - 1))
for i in "${!PROGRAMS[@]}"; do
prog="${PROGRAMS[$i]}"
if [ $i -lt $last ]; then
printf ' "%s": {"pass": %d, "fail": %d},\n' "$prog" "${PASS_COUNTS[$i]}" "${FAIL_COUNTS[$i]}"
else
printf ' "%s": {"pass": %d, "fail": %d}\n' "$prog" "${PASS_COUNTS[$i]}" "${FAIL_COUNTS[$i]}"
fi
done
printf ' }\n'
printf '}\n'
} > lib/haskell/scoreboard.json
# scoreboard.md
{
printf '# Haskell-on-SX Scoreboard\n\n'
printf 'Updated %s · Phase 3 (laziness + classic programs)\n\n' "$DATE"
printf '| Program | Tests | Status |\n'
printf '|---------|-------|--------|\n'
for i in "${!PROGRAMS[@]}"; do
prog="${PROGRAMS[$i]}"
P=${PASS_COUNTS[$i]}
F=${FAIL_COUNTS[$i]}
T=$((P + F))
[ "$F" -eq 0 ] && STATUS="✓" || STATUS="✗"
printf '| %s | %d/%d | %s |\n' "${prog}.hs" "$P" "$T" "$STATUS"
done
printf '| **Total** | **%d/%d** | **%d/%d programs** |\n' \
"$TOTAL_PASS" "$((TOTAL_PASS + TOTAL_FAIL))" "$PROG_PASS" "$PROG_TOTAL"
} > lib/haskell/scoreboard.md
echo "Wrote lib/haskell/scoreboard.json and lib/haskell/scoreboard.md"
[ $TOTAL_FAIL -eq 0 ]

249
lib/haskell/desugar.sx Normal file
View File

@@ -0,0 +1,249 @@
;; Desugar the Haskell surface AST into a smaller core AST.
;;
;; Eliminates the three surface-only shapes produced by the parser:
;; :where BODY DECLS → :let DECLS BODY
;; :guarded GUARDS → :if C1 E1 (:if C2 E2 … (:app error …))
;; :list-comp EXPR QUALS → concatMap-based expression (§3.11)
;;
;; Everything else (:app, :op, :lambda, :let, :case, :do, :tuple,
;; :list, :range, :if, :neg, :sect-left / :sect-right, plus all
;; leaf forms and pattern / type nodes) is passed through after
;; recursing into children.
(define
hk-guards-to-if
(fn
(guards)
(cond
((empty? guards)
(list
:app
(list :var "error")
(list :string "Non-exhaustive guards")))
(:else
(let
((g (first guards)))
(list
:if
(hk-desugar (nth g 1))
(hk-desugar (nth g 2))
(hk-guards-to-if (rest guards))))))))
;; do-notation desugaring (Haskell 98 §3.14):
;; do { e } = e
;; do { e ; ss } = e >> do { ss }
;; do { p <- e ; ss } = e >>= \p -> do { ss }
;; do { let decls ; ss } = let decls in do { ss }
(define
hk-desugar-do
(fn
(stmts)
(cond
((empty? stmts) (raise "empty do block"))
((empty? (rest stmts))
(let ((s (first stmts)))
(cond
((= (first s) "do-expr") (hk-desugar (nth s 1)))
(:else
(raise "do block must end with an expression")))))
(:else
(let
((s (first stmts)) (rest-stmts (rest stmts)))
(let
((rest-do (hk-desugar-do rest-stmts)))
(cond
((= (first s) "do-expr")
(list
:app
(list
:app
(list :var ">>")
(hk-desugar (nth s 1)))
rest-do))
((= (first s) "do-bind")
(list
:app
(list
:app
(list :var ">>=")
(hk-desugar (nth s 2)))
(list :lambda (list (nth s 1)) rest-do)))
((= (first s) "do-let")
(list
:let
(map hk-desugar (nth s 1))
rest-do))
(:else (raise "unknown do-stmt tag")))))))))
;; List-comprehension desugaring (Haskell 98 §3.11):
;; [e | ] = [e]
;; [e | b, Q ] = if b then [e | Q] else []
;; [e | p <- l, Q ] = concatMap (\p -> [e | Q]) l
;; [e | let ds, Q ] = let ds in [e | Q]
(define
hk-lc-desugar
(fn
(e quals)
(cond
((empty? quals) (list :list (list e)))
(:else
(let
((q (first quals)))
(let
((qtag (first q)))
(cond
((= qtag "q-guard")
(list
:if
(hk-desugar (nth q 1))
(hk-lc-desugar e (rest quals))
(list :list (list))))
((= qtag "q-gen")
(list
:app
(list
:app
(list :var "concatMap")
(list
:lambda
(list (nth q 1))
(hk-lc-desugar e (rest quals))))
(hk-desugar (nth q 2))))
((= qtag "q-let")
(list
:let
(map hk-desugar (nth q 1))
(hk-lc-desugar e (rest quals))))
(:else
(raise
(str
"hk-lc-desugar: unknown qualifier tag "
qtag))))))))))
(define
hk-desugar
(fn
(node)
(cond
((not (list? node)) node)
((empty? node) node)
(:else
(let
((tag (first node)))
(cond
;; Transformations
((= tag "where")
(list
:let
(map hk-desugar (nth node 2))
(hk-desugar (nth node 1))))
((= tag "guarded") (hk-guards-to-if (nth node 1)))
((= tag "list-comp")
(hk-lc-desugar
(hk-desugar (nth node 1))
(nth node 2)))
;; Expression nodes
((= tag "app")
(list
:app
(hk-desugar (nth node 1))
(hk-desugar (nth node 2))))
((= tag "op")
(list
:op
(nth node 1)
(hk-desugar (nth node 2))
(hk-desugar (nth node 3))))
((= tag "neg") (list :neg (hk-desugar (nth node 1))))
((= tag "if")
(list
:if
(hk-desugar (nth node 1))
(hk-desugar (nth node 2))
(hk-desugar (nth node 3))))
((= tag "tuple")
(list :tuple (map hk-desugar (nth node 1))))
((= tag "list")
(list :list (map hk-desugar (nth node 1))))
((= tag "range")
(list
:range
(hk-desugar (nth node 1))
(hk-desugar (nth node 2))))
((= tag "range-step")
(list
:range-step
(hk-desugar (nth node 1))
(hk-desugar (nth node 2))
(hk-desugar (nth node 3))))
((= tag "lambda")
(list
:lambda
(nth node 1)
(hk-desugar (nth node 2))))
((= tag "let")
(list
:let
(map hk-desugar (nth node 1))
(hk-desugar (nth node 2))))
((= tag "case")
(list
:case
(hk-desugar (nth node 1))
(map hk-desugar (nth node 2))))
((= tag "alt")
(list :alt (nth node 1) (hk-desugar (nth node 2))))
((= tag "do") (hk-desugar-do (nth node 1)))
((= tag "sect-left")
(list
:sect-left
(nth node 1)
(hk-desugar (nth node 2))))
((= tag "sect-right")
(list
:sect-right
(nth node 1)
(hk-desugar (nth node 2))))
;; Top-level
((= tag "program")
(list :program (map hk-desugar (nth node 1))))
((= tag "module")
(list
:module
(nth node 1)
(nth node 2)
(nth node 3)
(map hk-desugar (nth node 4))))
;; Decls carrying a body
((= tag "fun-clause")
(list
:fun-clause
(nth node 1)
(nth node 2)
(hk-desugar (nth node 3))))
((= tag "pat-bind")
(list
:pat-bind
(nth node 1)
(hk-desugar (nth node 2))))
((= tag "bind")
(list
:bind
(nth node 1)
(hk-desugar (nth node 2))))
;; Everything else: leaf literals, vars, cons, patterns,
;; types, imports, type-sigs, data / newtype / fixity, …
(:else node)))))))
;; Convenience — tokenize + layout + parse + desugar.
(define
hk-core
(fn (src) (hk-desugar (hk-parse-top src))))
(define
hk-core-expr
(fn (src) (hk-desugar (hk-parse src))))

792
lib/haskell/eval.sx Normal file
View File

@@ -0,0 +1,792 @@
;; Haskell strict evaluator (Phase 2).
;;
;; Consumes the post-desugar core AST and produces SX values. Strict
;; throughout — laziness and thunks are Phase 3.
;;
;; Value representation:
;; numbers / strings / chars → raw SX values
;; constructor values → tagged lists (con-name first)
;; functions: closure / multifun → {:type "fn" :kind … …}
;; constructor partials → {:type "con-partial" …}
;; built-ins → {:type "builtin" …}
;;
;; Multi-clause top-level definitions are bundled into a single
;; multifun keyed by name; arguments are gathered through currying
;; until arity is reached, then each clause's pattern list is matched
;; in order. Recursive let bindings work because the binding env is
;; built mutably so closures captured during evaluation see the
;; eventual full env.
(define
hk-dict-copy
(fn
(d)
(let ((nd (dict)))
(for-each
(fn (k) (dict-set! nd k (get d k)))
(keys d))
nd)))
;; ── Thunks (Phase 3 — laziness) ─────────────────────────────
;; A thunk wraps an unevaluated AST plus the env in which it was
;; created. The first call to `hk-force` evaluates the body, replaces
;; the body with the cached value, and flips `forced`. Subsequent
;; forces return the cached value directly.
(define
hk-mk-thunk
(fn
(body env)
{:type "thunk" :body body :env env :forced false :value nil}))
(define
hk-is-thunk?
(fn (v) (and (dict? v) (= (get v "type") "thunk"))))
(define
hk-force
(fn
(v)
(cond
((hk-is-thunk? v)
(cond
((get v "forced") (get v "value"))
(:else
(let
((res (hk-force (hk-eval (get v "body") (get v "env")))))
(dict-set! v "forced" true)
(dict-set! v "value" res)
res))))
(:else v))))
;; Recursive force — used at the test/output boundary so test
;; expectations can compare against fully-evaluated structures.
(define
hk-deep-force
(fn
(v)
(let ((fv (hk-force v)))
(cond
((not (list? fv)) fv)
((empty? fv) fv)
(:else (map hk-deep-force fv))))))
;; ── Function value constructors ──────────────────────────────
(define
hk-mk-closure
(fn
(params body env)
{:type "fn" :kind "closure" :params params :body body :env env}))
(define
hk-mk-multifun
(fn
(arity clauses env)
{:type "fn" :kind "multi" :arity arity :clauses clauses :env env :collected (list)}))
(define
hk-mk-builtin
(fn
(name fn arity)
{: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
hk-apply
(fn
(f arg)
(let ((f (hk-force f)))
(cond
((not (dict? f))
(raise (str "apply: not a function value: " f)))
((= (get f "type") "fn")
(cond
((= (get f "kind") "closure") (hk-apply-closure f arg))
((= (get f "kind") "multi") (hk-apply-multi f arg))
(:else (raise "apply: unknown fn kind"))))
((= (get f "type") "con-partial") (hk-apply-con-partial f arg))
((= (get f "type") "builtin") (hk-apply-builtin f arg))
(:else (raise "apply: not a function dict"))))))
(define
hk-apply-closure
(fn
(cl arg)
(let
((params (get cl "params"))
(body (get cl "body"))
(env (get cl "env")))
(cond
((empty? params) (raise "apply-closure: no params"))
(:else
(let
((p1 (first params)) (rest-p (rest params)))
(let
((env-after (hk-match p1 arg env)))
(cond
((nil? env-after)
(raise "pattern match failure in lambda"))
((empty? rest-p) (hk-eval body env-after))
(:else
(hk-mk-closure rest-p body env-after))))))))))
(define
hk-apply-multi
(fn
(mf arg)
(let
((arity (get mf "arity"))
(clauses (get mf "clauses"))
(env (get mf "env"))
(collected (append (get mf "collected") (list arg))))
(cond
((< (len collected) arity)
(assoc mf "collected" collected))
(:else (hk-dispatch-multi clauses collected env))))))
(define
hk-dispatch-multi
(fn
(clauses args env)
(cond
((empty? clauses)
(raise "non-exhaustive patterns in function definition"))
(:else
(let
((c (first clauses)))
(let
((pats (first c)) (body (first (rest c))))
(let
((env-after (hk-match-args pats args env)))
(cond
((nil? env-after)
(hk-dispatch-multi (rest clauses) args env))
(:else (hk-eval body env-after))))))))))
(define
hk-match-args
(fn
(pats args env)
(cond
((empty? pats) env)
(:else
(let
((res (hk-match (first pats) (first args) env)))
(cond
((nil? res) nil)
(:else
(hk-match-args (rest pats) (rest args) res))))))))
(define
hk-apply-con-partial
(fn
(cp arg)
(let
((name (get cp "name"))
(arity (get cp "arity"))
(args (append (get cp "args") (list arg))))
(cond
((= (len args) arity) (hk-mk-con name args))
(:else (assoc cp "args" args))))))
(define
hk-apply-builtin
(fn
(b arg)
(let
((arity (get b "arity"))
(collected (append (get b "collected") (list arg))))
(cond
((< (len collected) arity)
(assoc b "collected" collected))
(:else
;; 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
hk-truthy?
(fn
(v)
(and (list? v) (not (empty? v)) (= (first v) "True"))))
(define hk-true (hk-mk-con "True" (list)))
(define hk-false (hk-mk-con "False" (list)))
(define hk-of-bool (fn (b) (if b hk-true hk-false)))
;; ── Core eval ───────────────────────────────────────────────
(define
hk-eval
(fn
(node env)
(cond
((not (list? node)) (raise (str "eval: not a list: " node)))
((empty? node) (raise "eval: empty list node"))
(:else
(let
((tag (first node)))
(cond
((= tag "int") (nth node 1))
((= tag "float") (nth node 1))
((= tag "string") (nth node 1))
((= tag "char") (nth node 1))
((= tag "var") (hk-eval-var (nth node 1) env))
((= tag "con") (hk-eval-con-ref (nth node 1)))
((= tag "neg")
(- 0 (hk-force (hk-eval (nth node 1) env))))
((= tag "if") (hk-eval-if node env))
((= tag "let") (hk-eval-let (nth node 1) (nth node 2) env))
((= tag "lambda")
(hk-mk-closure (nth node 1) (nth node 2) env))
((= tag "app")
(hk-apply
(hk-eval (nth node 1) env)
(hk-mk-thunk (nth node 2) env)))
((= tag "op")
(hk-eval-op
(nth node 1)
(nth node 2)
(nth node 3)
env))
((= tag "case")
(hk-eval-case (nth node 1) (nth node 2) env))
((= tag "tuple")
(hk-mk-tuple
(map (fn (e) (hk-eval e env)) (nth node 1))))
((= tag "list")
(hk-mk-list
(map (fn (e) (hk-eval e env)) (nth node 1))))
((= tag "range")
(let
((from (hk-force (hk-eval (nth node 1) env)))
(to (hk-force (hk-eval (nth node 2) env))))
(hk-build-range from to 1)))
((= tag "range-step")
(let
((from (hk-force (hk-eval (nth node 1) env)))
(nxt (hk-force (hk-eval (nth node 2) env)))
(to (hk-force (hk-eval (nth node 3) env))))
(hk-build-range from to (- nxt from))))
((= tag "range-from")
;; [from..] = iterate (+ 1) from — uses the Prelude.
(hk-eval
(list
:app
(list
:app
(list :var "iterate")
(list
:sect-right
"+"
(list :int 1)))
(nth node 1))
env))
((= tag "sect-left")
(hk-eval-sect-left (nth node 1) (nth node 2) env))
((= tag "sect-right")
(hk-eval-sect-right (nth node 1) (nth node 2) env))
(:else
(raise (str "eval: unknown node tag '" tag "'")))))))))
(define
hk-eval-var
(fn
(name env)
(cond
((has-key? env name) (get env name))
((hk-is-con? name) (hk-eval-con-ref name))
(:else (raise (str "unbound variable: " name))))))
(define
hk-eval-con-ref
(fn
(name)
(let ((arity (hk-con-arity name)))
(cond
((nil? arity) (raise (str "unknown constructor: " name)))
((= arity 0) (hk-mk-con name (list)))
(:else
{:type "con-partial" :name name :arity arity :args (list)})))))
(define
hk-eval-if
(fn
(node env)
(let ((cv (hk-force (hk-eval (nth node 1) env))))
(cond
((hk-truthy? cv) (hk-eval (nth node 2) env))
((and (list? cv) (= (first cv) "False"))
(hk-eval (nth node 3) env))
((= cv true) (hk-eval (nth node 2) env))
((= cv false) (hk-eval (nth node 3) env))
(:else (raise "if: condition is not Bool"))))))
(define
hk-extend-env-with-match!
(fn
(env match-env)
(for-each
(fn (k) (dict-set! env k (get match-env k)))
(keys match-env))))
(define
hk-eval-let-bind!
(fn
(b env)
(let ((tag (first b)))
(cond
((= tag "fun-clause")
(let
((name (nth b 1))
(pats (nth b 2))
(body (nth b 3)))
(cond
((empty? pats)
(dict-set! env name (hk-eval body env)))
(:else
(dict-set! env name (hk-mk-closure pats body env))))))
((or (= tag "bind") (= tag "pat-bind"))
(let ((pat (nth b 1)) (body (nth b 2)))
(let ((val (hk-eval body env)))
(let ((res (hk-match pat val env)))
(cond
((nil? res)
(raise "let: pattern bind failure"))
(:else
(hk-extend-env-with-match! env res)))))))
(:else nil)))))
(define
hk-eval-let
(fn
(binds body env)
;; Reuse hk-bind-decls! so multi-clause fun bindings in where/let
;; are grouped into multifuns, enabling patterns like:
;; let { go 0 = [[]]; go k = [...] } in go n
(let ((new-env (hk-dict-copy env)))
(hk-bind-decls! new-env binds)
(hk-eval body new-env))))
(define
hk-eval-case
(fn
(scrut alts env)
(let ((sv (hk-force (hk-eval scrut env))))
(hk-try-alts alts sv env))))
(define
hk-try-alts
(fn
(alts val env)
(cond
((empty? alts) (raise "case: non-exhaustive patterns"))
(:else
(let
((alt (first alts)))
(let
((pat (nth alt 1)) (body (nth alt 2)))
(let
((res (hk-match pat val env)))
(cond
((nil? res) (hk-try-alts (rest alts) val env))
(:else (hk-eval body res))))))))))
(define
hk-eval-op
(fn
(op left right env)
(cond
;; Cons is non-strict in both args: build a cons cell whose
;; head and tail are deferred. This is what makes `repeat x =
;; x : repeat x` and `fibs = 0 : 1 : zipWith (+) fibs (tail
;; fibs)` terminate.
((= op ":")
(hk-mk-cons
(hk-mk-thunk left env)
(hk-mk-thunk right env)))
(:else
(let
((lv (hk-force (hk-eval left env)))
(rv (hk-force (hk-eval right env))))
(hk-binop op lv rv))))))
(define
hk-list-append
(fn
(a b)
(cond
((and (list? a) (= (first a) "[]")) b)
((and (list? a) (= (first a) ":"))
(hk-mk-cons (nth a 1) (hk-list-append (nth a 2) b)))
(:else (raise "++: not a list")))))
;; Eager finite-range spine — handles [from..to] and [from,next..to].
;; Step direction is governed by the sign of `step`; when step > 0 we
;; stop at to; when step < 0 we stop at to going down.
(define
hk-build-range
(fn
(from to step)
(cond
((and (> step 0) (> from to)) (hk-mk-nil))
((and (< step 0) (< from to)) (hk-mk-nil))
((= step 0) (hk-mk-nil))
(:else
(hk-mk-cons from (hk-build-range (+ from step) to step))))))
(define
hk-binop
(fn
(op lv rv)
(cond
((= op "+") (+ lv rv))
((= op "-") (- lv rv))
((= op "*") (* lv rv))
((= op "/") (/ lv rv))
((= op "==") (hk-of-bool (= lv rv)))
((= op "/=") (hk-of-bool (not (= lv rv))))
((= op "<") (hk-of-bool (< lv rv)))
((= op "<=") (hk-of-bool (<= lv rv)))
((= op ">") (hk-of-bool (> lv rv)))
((= op ">=") (hk-of-bool (>= lv rv)))
((= op "&&") (hk-of-bool (and (hk-truthy? lv) (hk-truthy? rv))))
((= op "||") (hk-of-bool (or (hk-truthy? lv) (hk-truthy? rv))))
((= op ":") (hk-mk-cons lv rv))
((= op "++") (hk-list-append lv rv))
((= op "mod") (mod lv rv))
((= op "div") (floor (/ lv rv)))
((= op "rem") (mod lv rv))
((= op "quot") (truncate (/ lv rv)))
(:else (raise (str "unknown operator: " op))))))
(define
hk-eval-sect-left
(fn
(op e env)
;; (e op) = \x -> e op x — bind e once, defer the operator call.
(let ((ev (hk-eval e env)))
(let ((cenv (hk-dict-copy env)))
(dict-set! cenv "__hk-sect-l" ev)
(hk-mk-closure
(list (list :p-var "__hk-sect-x"))
(list
:op
op
(list :var "__hk-sect-l")
(list :var "__hk-sect-x"))
cenv)))))
(define
hk-eval-sect-right
(fn
(op e env)
(let ((ev (hk-eval e env)))
(let ((cenv (hk-dict-copy env)))
(dict-set! cenv "__hk-sect-r" ev)
(hk-mk-closure
(list (list :p-var "__hk-sect-x"))
(list
:op
op
(list :var "__hk-sect-x")
(list :var "__hk-sect-r"))
cenv)))))
;; ── Top-level program evaluation ────────────────────────────
;; Operator-as-value built-ins — let `(+)`, `(*)`, etc. work as
;; first-class functions for `zipWith (+)` and friends. Strict in
;; both args (built-ins are forced via hk-apply-builtin).
(define
hk-make-binop-builtin
(fn
(name op-name)
(hk-mk-builtin
name
(fn (a b) (hk-binop op-name a b))
2)))
;; Inline Prelude source — loaded into the initial env so simple
;; programs can use `head`, `take`, `repeat`, etc. without each
;; user file redefining them. The Prelude itself uses lazy `:` for
;; the recursive list-building functions.
(define
hk-prelude-src
"head (x:_) = x
tail (_:xs) = xs
fst (a, _) = a
snd (_, b) = b
take 0 _ = []
take _ [] = []
take n (x:xs) = x : take (n - 1) xs
drop 0 xs = xs
drop _ [] = []
drop n (_:xs) = drop (n - 1) xs
repeat x = x : repeat x
iterate f x = x : iterate f (f x)
length [] = 0
length (_:xs) = 1 + length xs
map _ [] = []
map f (x:xs) = f x : map f xs
filter _ [] = []
filter p (x:xs) = if p x then x : filter p xs else filter p xs
zipWith _ [] _ = []
zipWith _ _ [] = []
zipWith f (x:xs) (y:ys) = f x y : zipWith f xs ys
fibs = 0 : 1 : zipWith plus fibs (tail fibs)
plus a b = a + b
concat [] = []
concat (xs:xss) = xs ++ concat xss
concatMap f [] = []
concatMap f (x:xs) = f x ++ concatMap f xs
abs x = if x < 0 then 0 - x else x
negate x = 0 - x
")
(define
hk-load-into!
(fn
(env src)
(let ((ast (hk-core src)))
(hk-register-program! ast)
(let
((decls
(cond
((= (first ast) "program") (nth ast 1))
((= (first ast) "module") (nth ast 4))
(:else (list)))))
(hk-bind-decls! env decls)))))
(define
hk-init-env
(fn
()
(let ((env (dict)))
(dict-set! env "otherwise" hk-true)
(dict-set!
env
"error"
(hk-mk-builtin
"error"
(fn (msg) (raise (str "*** Exception: " msg)))
1))
(dict-set!
env
"not"
(hk-mk-builtin
"not"
(fn (b) (hk-of-bool (not (hk-truthy? b))))
1))
(dict-set!
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))
;; ── Stub IO monad ─────────────────────────────────────
;; IO actions are tagged values `("IO" payload)`; `>>=` and
;; `>>` chain them. Lazy in the action arguments so do-blocks
;; can be deeply structured without forcing the whole chain
;; up front.
(dict-set!
env
"return"
(hk-mk-lazy-builtin
"return"
(fn (x) (list "IO" x))
1))
(dict-set!
env
">>="
(hk-mk-lazy-builtin
">>="
(fn (m f)
(let ((io-val (hk-force m)))
(cond
((and
(list? io-val)
(= (first io-val) "IO"))
(hk-apply (hk-force f) (nth io-val 1)))
(:else
(raise "(>>=): left side is not an IO action")))))
2))
(dict-set!
env
">>"
(hk-mk-lazy-builtin
">>"
(fn (m n)
(let ((io-val (hk-force m)))
(cond
((and
(list? io-val)
(= (first io-val) "IO"))
(hk-force n))
(:else
(raise "(>>): left side is not an IO action")))))
2))
;; Operators as first-class values
(dict-set! env "+" (hk-make-binop-builtin "+" "+"))
(dict-set! env "-" (hk-make-binop-builtin "-" "-"))
(dict-set! env "*" (hk-make-binop-builtin "*" "*"))
(dict-set! env "/" (hk-make-binop-builtin "/" "/"))
(dict-set! env "==" (hk-make-binop-builtin "==" "=="))
(dict-set! env "/=" (hk-make-binop-builtin "/=" "/="))
(dict-set! env "<" (hk-make-binop-builtin "<" "<"))
(dict-set! env "<=" (hk-make-binop-builtin "<=" "<="))
(dict-set! env ">" (hk-make-binop-builtin ">" ">"))
(dict-set! env ">=" (hk-make-binop-builtin ">=" ">="))
(dict-set! env "&&" (hk-make-binop-builtin "&&" "&&"))
(dict-set! env "||" (hk-make-binop-builtin "||" "||"))
(dict-set! env "++" (hk-make-binop-builtin "++" "++"))
(dict-set! env "mod" (hk-make-binop-builtin "mod" "mod"))
(dict-set! env "div" (hk-make-binop-builtin "div" "div"))
(dict-set! env "rem" (hk-make-binop-builtin "rem" "rem"))
(dict-set! env "quot" (hk-make-binop-builtin "quot" "quot"))
(hk-load-into! env hk-prelude-src)
env)))
(define
hk-bind-decls!
(fn
(env decls)
(let
((groups (dict))
(group-order (list))
(pat-binds (list)))
;; Pass 1: collect fun-clause groups by name; track first-seen
;; order so pass 3 can evaluate 0-arity bodies in source order
;; (forward references to other 0-arity definitions still need
;; the earlier name to be bound first).
(for-each
(fn (d)
(cond
((= (first d) "fun-clause")
(let
((name (nth d 1)))
(when (not (has-key? groups name))
(append! group-order name))
(dict-set!
groups
name
(append
(if
(has-key? groups name)
(get groups name)
(list))
(list (list (nth d 2) (nth d 3)))))
(when
(not (has-key? env name))
(dict-set! env name nil))))
((or (= (first d) "bind") (= (first d) "pat-bind"))
(append! pat-binds d))
(:else nil)))
decls)
;; Pass 2: install multifuns (arity > 0) — order doesn't matter
;; because they're closures; collect 0-arity names in source
;; order for pass 3.
(let ((zero-arity (list)))
(for-each
(fn (name)
(let ((clauses (get groups name)))
(let ((arity (len (first (first clauses)))))
(cond
((> arity 0)
(dict-set!
env
name
(hk-mk-multifun arity clauses env)))
(:else (append! zero-arity name))))))
group-order)
;; Pass 3: evaluate 0-arity bodies and pat-binds in source
;; order — forward references to a later 0-arity name will
;; still see its placeholder (nil) and fail noisily, but the
;; common case of a top-down program works.
(for-each
(fn (name)
(let ((clauses (get groups name)))
(dict-set!
env
name
(hk-eval (first (rest (first clauses))) env))))
zero-arity)
(for-each
(fn (d)
(let ((pat (nth d 1)) (body (nth d 2)))
(let ((val (hk-eval body env)))
(let ((res (hk-match pat val env)))
(cond
((nil? res)
(raise "top-level pattern bind failure"))
(:else (hk-extend-env-with-match! env res)))))))
pat-binds))
env)))
(define
hk-eval-program
(fn
(ast)
(cond
((nil? ast) (raise "eval-program: nil ast"))
((not (list? ast)) (raise "eval-program: not a list"))
(:else
(do
(hk-register-program! ast)
(let ((env (hk-init-env)))
(let
((decls
(cond
((= (first ast) "program") (nth ast 1))
((= (first ast) "module") (nth ast 4))
(:else (raise "eval-program: bad shape")))))
(hk-bind-decls! env decls))))))))
;; ── Source-level convenience ────────────────────────────────
(define
hk-run
(fn
(src)
(let ((env (hk-eval-program (hk-core src))))
(cond
((has-key? env "main") (get env "main"))
(:else env)))))
;; Eagerly build the Prelude env once at load time; each call to
;; hk-eval-expr-source copies it instead of re-parsing the whole Prelude.
(define hk-env0 (hk-init-env))
(define
hk-eval-expr-source
(fn
(src)
(hk-deep-force (hk-eval (hk-core-expr src) (hk-dict-copy hk-env0)))))

329
lib/haskell/layout.sx Normal file
View File

@@ -0,0 +1,329 @@
;; Haskell 98 layout algorithm (§10.3).
;;
;; Consumes the raw token stream produced by hk-tokenize and inserts
;; virtual braces / semicolons (types vlbrace / vrbrace / vsemi) based
;; on indentation. Newline tokens are consumed and stripped.
;;
;; (hk-layout (hk-tokenize src)) → tokens-with-virtual-layout
;; ── Pre-pass ──────────────────────────────────────────────────────
;;
;; Walks the raw token list and emits an augmented stream containing
;; two fresh pseudo-tokens:
;;
;; {:type "layout-open" :col N :keyword K}
;; At stream start (K = "<module>") unless the first real token is
;; `module` or `{`. Also immediately after every `let` / `where` /
;; `do` / `of` whose following token is NOT `{`. N is the column
;; of the token that follows.
;;
;; {:type "layout-indent" :col N}
;; Before any token whose line is strictly greater than the line
;; of the previously emitted real token, EXCEPT when that token
;; is already preceded by a layout-open (Haskell 98 §10.3 note 3).
;;
;; Raw newline tokens are dropped.
(define
hk-layout-keyword?
(fn
(tok)
(and
(= (get tok "type") "reserved")
(or
(= (get tok "value") "let")
(= (get tok "value") "where")
(= (get tok "value") "do")
(= (get tok "value") "of")))))
(define
hk-layout-pre
(fn
(tokens)
(let
((result (list))
(n (len tokens))
(i 0)
(prev-line -1)
(first-real-emitted false)
(suppress-next-indent false))
(define
hk-next-real-idx
(fn
(start)
(let
((j start))
(define
hk-nri-loop
(fn
()
(when
(and
(< j n)
(= (get (nth tokens j) "type") "newline"))
(do (set! j (+ j 1)) (hk-nri-loop)))))
(hk-nri-loop)
j)))
(define
hk-pre-step
(fn
()
(when
(< i n)
(let
((tok (nth tokens i)) (ty (get tok "type")))
(cond
((= ty "newline") (do (set! i (+ i 1)) (hk-pre-step)))
(:else
(do
(when
(not first-real-emitted)
(do
(set! first-real-emitted true)
(when
(not
(or
(and
(= ty "reserved")
(= (get tok "value") "module"))
(= ty "lbrace")))
(do
(append!
result
{:type "layout-open"
:col (get tok "col")
:keyword "<module>"
:line (get tok "line")})
(set! suppress-next-indent true)))))
(when
(and
(>= prev-line 0)
(> (get tok "line") prev-line)
(not suppress-next-indent))
(append!
result
{:type "layout-indent"
:col (get tok "col")
:line (get tok "line")}))
(set! suppress-next-indent false)
(set! prev-line (get tok "line"))
(append! result tok)
(when
(hk-layout-keyword? tok)
(let
((j (hk-next-real-idx (+ i 1))))
(cond
((>= j n)
(do
(append!
result
{:type "layout-open"
:col 0
:keyword (get tok "value")
:line (get tok "line")})
(set! suppress-next-indent true)))
((= (get (nth tokens j) "type") "lbrace") nil)
(:else
(do
(append!
result
{:type "layout-open"
:col (get (nth tokens j) "col")
:keyword (get tok "value")
:line (get tok "line")})
(set! suppress-next-indent true))))))
(set! i (+ i 1))
(hk-pre-step))))))))
(hk-pre-step)
result)))
;; ── Main pass: L algorithm ────────────────────────────────────────
;;
;; Stack is a list; the head is the top of stack. Each entry is
;; either the keyword :explicit (pushed by an explicit `{`) or a dict
;; {:col N :keyword K} pushed by a layout-open marker.
;;
;; Rules (following Haskell 98 §10.3):
;;
;; layout-open(n) vs stack:
;; empty or explicit top → push n; emit {
;; n > top-col → push n; emit {
;; otherwise → emit { }; retry as indent(n)
;;
;; layout-indent(n) vs stack:
;; empty or explicit top → drop
;; n == top-col → emit ;
;; n < top-col → emit }; pop; recurse
;; n > top-col → drop
;;
;; lbrace → push :explicit; emit {
;; rbrace → pop if :explicit; emit }
;; `in` with implicit let on top → emit }; pop; emit in
;; any other token → emit
;;
;; EOF: emit } for every remaining implicit context.
(define
hk-layout-L
(fn
(pre-toks)
(let
((result (list))
(stack (list))
(n (len pre-toks))
(i 0))
(define hk-emit (fn (t) (append! result t)))
(define
hk-indent-at
(fn
(col line)
(cond
((or (empty? stack) (= (first stack) :explicit)) nil)
(:else
(let
((top-col (get (first stack) "col")))
(cond
((= col top-col)
(hk-emit
{:type "vsemi" :value ";" :line line :col col}))
((< col top-col)
(do
(hk-emit
{:type "vrbrace" :value "}" :line line :col col})
(set! stack (rest stack))
(hk-indent-at col line)))
(:else nil)))))))
(define
hk-open-at
(fn
(col keyword line)
(cond
((and
(> col 0)
(or
(empty? stack)
(= (first stack) :explicit)
(> col (get (first stack) "col"))))
(do
(hk-emit
{:type "vlbrace" :value "{" :line line :col col})
(set! stack (cons {:col col :keyword keyword} stack))))
(:else
(do
(hk-emit
{:type "vlbrace" :value "{" :line line :col col})
(hk-emit
{:type "vrbrace" :value "}" :line line :col col})
(hk-indent-at col line))))))
(define
hk-close-eof
(fn
()
(when
(and
(not (empty? stack))
(not (= (first stack) :explicit)))
(do
(hk-emit {:type "vrbrace" :value "}" :line 0 :col 0})
(set! stack (rest stack))
(hk-close-eof)))))
;; Peek past further layout-indent / layout-open markers to find
;; the next real token's value when its type is `reserved`.
;; Returns nil if no such token.
(define
hk-peek-next-reserved
(fn
(start)
(let ((j (+ start 1)) (found nil) (done false))
(define
hk-pnr-loop
(fn
()
(when
(and (not done) (< j n))
(let
((t (nth pre-toks j)) (ty (get t "type")))
(cond
((or
(= ty "layout-indent")
(= ty "layout-open"))
(do (set! j (+ j 1)) (hk-pnr-loop)))
((= ty "reserved")
(do (set! found (get t "value")) (set! done true)))
(:else (set! done true)))))))
(hk-pnr-loop)
found)))
(define
hk-layout-step
(fn
()
(when
(< i n)
(let
((tok (nth pre-toks i)) (ty (get tok "type")))
(cond
((= ty "eof")
(do
(hk-close-eof)
(hk-emit tok)
(set! i (+ i 1))
(hk-layout-step)))
((= ty "layout-open")
(do
(hk-open-at
(get tok "col")
(get tok "keyword")
(get tok "line"))
(set! i (+ i 1))
(hk-layout-step)))
((= ty "layout-indent")
(cond
((= (hk-peek-next-reserved i) "in")
(do (set! i (+ i 1)) (hk-layout-step)))
(:else
(do
(hk-indent-at (get tok "col") (get tok "line"))
(set! i (+ i 1))
(hk-layout-step)))))
((= ty "lbrace")
(do
(set! stack (cons :explicit stack))
(hk-emit tok)
(set! i (+ i 1))
(hk-layout-step)))
((= ty "rbrace")
(do
(when
(and
(not (empty? stack))
(= (first stack) :explicit))
(set! stack (rest stack)))
(hk-emit tok)
(set! i (+ i 1))
(hk-layout-step)))
((and
(= ty "reserved")
(= (get tok "value") "in")
(not (empty? stack))
(not (= (first stack) :explicit))
(= (get (first stack) "keyword") "let"))
(do
(hk-emit
{:type "vrbrace"
:value "}"
:line (get tok "line")
:col (get tok "col")})
(set! stack (rest stack))
(hk-emit tok)
(set! i (+ i 1))
(hk-layout-step)))
(:else
(do
(hk-emit tok)
(set! i (+ i 1))
(hk-layout-step))))))))
(hk-layout-step)
(hk-close-eof)
result)))
(define hk-layout (fn (tokens) (hk-layout-L (hk-layout-pre tokens))))

201
lib/haskell/match.sx Normal file
View File

@@ -0,0 +1,201 @@
;; Value-level pattern matching.
;;
;; Constructor values are tagged lists whose first element is the
;; constructor name (a string). Tuples use the special tag "Tuple".
;; Lists use the spine of `:` cons and `[]` nil.
;;
;; Just 5 → ("Just" 5)
;; Nothing → ("Nothing")
;; (1, 2) → ("Tuple" 1 2)
;; [1, 2] → (":" 1 (":" 2 ("[]")))
;; () → ("()")
;;
;; Primitive values (numbers, strings, chars) are stored raw.
;;
;; The matcher takes a pattern AST node, a value, and an environment
;; dict; it returns an extended dict on success, or `nil` on failure.
;; ── Value builders ──────────────────────────────────────────
(define
hk-mk-con
(fn
(cname args)
(let ((result (list cname)))
(for-each (fn (a) (append! result a)) args)
result)))
(define
hk-mk-tuple
(fn
(items)
(let ((result (list "Tuple")))
(for-each (fn (x) (append! result x)) items)
result)))
(define hk-mk-nil (fn () (list "[]")))
(define hk-mk-cons (fn (h t) (list ":" h t)))
(define
hk-mk-list
(fn
(items)
(cond
((empty? items) (hk-mk-nil))
(:else
(hk-mk-cons (first items) (hk-mk-list (rest items)))))))
;; ── Predicates / accessors on constructor values ───────────
(define
hk-is-con-val?
(fn
(v)
(and
(list? v)
(not (empty? v))
(string? (first v)))))
(define hk-val-con-name (fn (v) (first v)))
(define hk-val-con-args (fn (v) (rest v)))
;; ── The matcher ────────────────────────────────────────────
;;
;; Pattern match forces the scrutinee to WHNF before inspecting it
;; — except for `p-wild`, `p-var`, and `p-lazy`, which never need
;; to look at the value. Args of constructor / tuple / list values
;; remain thunked (they're forced only when their own pattern needs
;; to inspect them, recursively).
(define
hk-match
(fn
(pat val env)
(cond
((not (list? pat)) nil)
((empty? pat) nil)
(:else
(let
((tag (first pat)))
(cond
((= tag "p-wild") env)
((= tag "p-var") (assoc env (nth pat 1) val))
((= tag "p-lazy") (hk-match (nth pat 1) val env))
((= tag "p-as")
(let
((res (hk-match (nth pat 2) val env)))
(cond
((nil? res) nil)
(:else (assoc res (nth pat 1) val)))))
(:else
(let ((fv (hk-force val)))
(cond
((= tag "p-int")
(if
(and (number? fv) (= fv (nth pat 1)))
env
nil))
((= tag "p-float")
(if
(and (number? fv) (= fv (nth pat 1)))
env
nil))
((= tag "p-string")
(if
(and (string? fv) (= fv (nth pat 1)))
env
nil))
((= tag "p-char")
(if
(and (string? fv) (= fv (nth pat 1)))
env
nil))
((= tag "p-con")
(let
((pat-name (nth pat 1)) (pat-args (nth pat 2)))
(cond
((not (hk-is-con-val? fv)) nil)
((not (= (hk-val-con-name fv) pat-name)) nil)
(:else
(let
((val-args (hk-val-con-args fv)))
(cond
((not (= (len pat-args) (len val-args)))
nil)
(:else
(hk-match-all
pat-args
val-args
env))))))))
((= tag "p-tuple")
(let
((items (nth pat 1)))
(cond
((not (hk-is-con-val? fv)) nil)
((not (= (hk-val-con-name fv) "Tuple")) nil)
((not (= (len (hk-val-con-args fv)) (len items)))
nil)
(:else
(hk-match-all
items
(hk-val-con-args fv)
env)))))
((= tag "p-list")
(hk-match-list-pat (nth pat 1) fv env))
(:else nil))))))))))
(define
hk-match-all
(fn
(pats vals env)
(cond
((empty? pats) env)
(:else
(let
((res (hk-match (first pats) (first vals) env)))
(cond
((nil? res) nil)
(:else
(hk-match-all (rest pats) (rest vals) res))))))))
(define
hk-match-list-pat
(fn
(items val env)
(let ((fv (hk-force val)))
(cond
((empty? items)
(if
(and
(hk-is-con-val? fv)
(= (hk-val-con-name fv) "[]"))
env
nil))
(:else
(cond
((not (hk-is-con-val? fv)) nil)
((not (= (hk-val-con-name fv) ":")) nil)
(:else
(let
((args (hk-val-con-args fv)))
(let
((h (first args)) (t (first (rest args))))
(let
((res (hk-match (first items) h env)))
(cond
((nil? res) nil)
(:else
(hk-match-list-pat
(rest items)
t
res)))))))))))))
;; ── Convenience: parse a pattern from source for tests ─────
;; (Uses the parser's case-alt entry — `case _ of pat -> 0` —
;; to extract a pattern AST.)
(define
hk-parse-pat-source
(fn
(src)
(let
((expr (hk-parse (str "case 0 of " src " -> 0"))))
(nth (nth (nth expr 2) 0) 1))))

1994
lib/haskell/parser.sx Normal file

File diff suppressed because it is too large Load Diff

130
lib/haskell/runtime.sx Normal file
View File

@@ -0,0 +1,130 @@
;; Haskell runtime: constructor registry.
;;
;; A mutable dict keyed by constructor name (e.g. "Just", "[]") with
;; entries of shape {:arity N :type TYPE-NAME-STRING}.
;; Populated by ingesting `data` / `newtype` decls from parsed ASTs.
;; Pre-registers a small set of constructors tied to Haskell syntactic
;; forms (Bool, list, unit) — every nontrivial program depends on
;; these, and the parser/desugar pipeline emits them as (:var "True")
;; etc. without a corresponding `data` decl.
(define hk-constructors (dict))
(define
hk-register-con!
(fn
(cname arity type-name)
(dict-set!
hk-constructors
cname
{:arity arity :type type-name})))
(define hk-is-con? (fn (name) (has-key? hk-constructors name)))
(define
hk-con-arity
(fn
(name)
(if
(has-key? hk-constructors name)
(get (get hk-constructors name) "arity")
nil)))
(define
hk-con-type
(fn
(name)
(if
(has-key? hk-constructors name)
(get (get hk-constructors name) "type")
nil)))
(define hk-con-names (fn () (keys hk-constructors)))
;; ── Registration from AST ────────────────────────────────────
;; (:data NAME TVARS ((:con-def CNAME FIELDS) …))
(define
hk-register-data!
(fn
(data-node)
(let
((type-name (nth data-node 1))
(cons-list (nth data-node 3)))
(for-each
(fn
(cd)
(hk-register-con!
(nth cd 1)
(len (nth cd 2))
type-name))
cons-list))))
;; (:newtype NAME TVARS CNAME FIELD)
(define
hk-register-newtype!
(fn
(nt-node)
(hk-register-con!
(nth nt-node 3)
1
(nth nt-node 1))))
;; Walk a decls list, registering every `data` / `newtype` decl.
(define
hk-register-decls!
(fn
(decls)
(for-each
(fn
(d)
(cond
((and
(list? d)
(not (empty? d))
(= (first d) "data"))
(hk-register-data! d))
((and
(list? d)
(not (empty? d))
(= (first d) "newtype"))
(hk-register-newtype! d))
(:else nil)))
decls)))
(define
hk-register-program!
(fn
(ast)
(cond
((nil? ast) nil)
((not (list? ast)) nil)
((empty? ast) nil)
((= (first ast) "program")
(hk-register-decls! (nth ast 1)))
((= (first ast) "module")
(hk-register-decls! (nth ast 4)))
(:else nil))))
;; Convenience: source → AST → desugar → register.
(define
hk-load-source!
(fn (src) (hk-register-program! (hk-core src))))
;; ── Built-in constructors pre-registered ─────────────────────
;; Bool — used implicitly by `if`, comparison operators.
(hk-register-con! "True" 0 "Bool")
(hk-register-con! "False" 0 "Bool")
;; List — used by list literals, range syntax, and cons operator.
(hk-register-con! "[]" 0 "List")
(hk-register-con! ":" 2 "List")
;; Unit — produced by empty parens `()`.
(hk-register-con! "()" 0 "Unit")
;; Standard Prelude types — pre-registered so expression-level
;; programs can use them without a `data` decl.
(hk-register-con! "Nothing" 0 "Maybe")
(hk-register-con! "Just" 1 "Maybe")
(hk-register-con! "Left" 1 "Either")
(hk-register-con! "Right" 1 "Either")
(hk-register-con! "LT" 0 "Ordering")
(hk-register-con! "EQ" 0 "Ordering")
(hk-register-con! "GT" 0 "Ordering")

View File

@@ -0,0 +1,12 @@
{
"date": "2026-04-25",
"total_pass": 16,
"total_fail": 0,
"programs": {
"fib": {"pass": 2, "fail": 0},
"sieve": {"pass": 2, "fail": 0},
"quicksort": {"pass": 5, "fail": 0},
"nqueens": {"pass": 2, "fail": 0},
"calculator": {"pass": 5, "fail": 0}
}
}

12
lib/haskell/scoreboard.md Normal file
View File

@@ -0,0 +1,12 @@
# Haskell-on-SX Scoreboard
Updated 2026-04-25 · Phase 3 (laziness + classic programs)
| Program | Tests | Status |
|---------|-------|--------|
| fib.hs | 2/2 | ✓ |
| sieve.hs | 2/2 | ✓ |
| quicksort.hs | 5/5 | ✓ |
| nqueens.hs | 2/2 | ✓ |
| calculator.hs | 5/5 | ✓ |
| **Total** | **16/16** | **5/5 programs** |

View File

@@ -46,6 +46,13 @@ for FILE in "${FILES[@]}"; do
cat > "$TMPFILE" <<EPOCHS
(epoch 1)
(load "lib/haskell/tokenizer.sx")
(load "lib/haskell/layout.sx")
(load "lib/haskell/parser.sx")
(load "lib/haskell/desugar.sx")
(load "lib/haskell/runtime.sx")
(load "lib/haskell/match.sx")
(load "lib/haskell/eval.sx")
(load "lib/haskell/testlib.sx")
(epoch 2)
(load "$FILE")
(epoch 3)
@@ -81,6 +88,13 @@ EPOCHS
cat > "$TMPFILE2" <<EPOCHS
(epoch 1)
(load "lib/haskell/tokenizer.sx")
(load "lib/haskell/layout.sx")
(load "lib/haskell/parser.sx")
(load "lib/haskell/desugar.sx")
(load "lib/haskell/runtime.sx")
(load "lib/haskell/match.sx")
(load "lib/haskell/eval.sx")
(load "lib/haskell/testlib.sx")
(epoch 2)
(load "$FILE")
(epoch 3)

58
lib/haskell/testlib.sx Normal file
View File

@@ -0,0 +1,58 @@
;; Shared test harness for Haskell-on-SX tests.
;; Each test file expects hk-test / hk-deep=? / counters to already be bound.
(define
hk-deep=?
(fn
(a b)
(cond
((= a b) true)
((and (dict? a) (dict? b))
(let
((ak (keys a)) (bk (keys b)))
(if
(not (= (len ak) (len bk)))
false
(every?
(fn
(k)
(and (has-key? b k) (hk-deep=? (get a k) (get b k))))
ak))))
((and (list? a) (list? b))
(if
(not (= (len a) (len b)))
false
(let
((i 0) (ok true))
(define
hk-de-loop
(fn
()
(when
(and ok (< i (len a)))
(do
(when
(not (hk-deep=? (nth a i) (nth b i)))
(set! ok false))
(set! i (+ i 1))
(hk-de-loop)))))
(hk-de-loop)
ok)))
(:else false))))
(define hk-test-pass 0)
(define hk-test-fail 0)
(define hk-test-fails (list))
(define
hk-test
(fn
(name actual expected)
(if
(hk-deep=? actual expected)
(set! hk-test-pass (+ hk-test-pass 1))
(do
(set! hk-test-fail (+ hk-test-fail 1))
(append!
hk-test-fails
{:actual actual :expected expected :name name})))))

View File

@@ -0,0 +1,305 @@
;; Desugar tests — surface AST → core AST.
;; :guarded → nested :if
;; :where → :let
;; :list-comp → concatMap-based tree
(define
hk-prog
(fn (&rest decls) (list :program decls)))
;; ── Guards → if ──
(hk-test
"two-way guarded rhs"
(hk-desugar (hk-parse-top "abs x | x < 0 = - x\n | otherwise = x"))
(hk-prog
(list
:fun-clause
"abs"
(list (list :p-var "x"))
(list
:if
(list :op "<" (list :var "x") (list :int 0))
(list :neg (list :var "x"))
(list
:if
(list :var "otherwise")
(list :var "x")
(list
:app
(list :var "error")
(list :string "Non-exhaustive guards")))))))
(hk-test
"three-way guarded rhs"
(hk-desugar
(hk-parse-top "sign n | n > 0 = 1\n | n < 0 = -1\n | otherwise = 0"))
(hk-prog
(list
:fun-clause
"sign"
(list (list :p-var "n"))
(list
:if
(list :op ">" (list :var "n") (list :int 0))
(list :int 1)
(list
:if
(list :op "<" (list :var "n") (list :int 0))
(list :neg (list :int 1))
(list
:if
(list :var "otherwise")
(list :int 0)
(list
:app
(list :var "error")
(list :string "Non-exhaustive guards"))))))))
(hk-test
"case-alt guards desugared too"
(hk-desugar
(hk-parse "case x of\n Just y | y > 0 -> y\n | otherwise -> 0\n Nothing -> -1"))
(list
:case
(list :var "x")
(list
(list
:alt
(list :p-con "Just" (list (list :p-var "y")))
(list
:if
(list :op ">" (list :var "y") (list :int 0))
(list :var "y")
(list
:if
(list :var "otherwise")
(list :int 0)
(list
:app
(list :var "error")
(list :string "Non-exhaustive guards")))))
(list
:alt
(list :p-con "Nothing" (list))
(list :neg (list :int 1))))))
;; ── Where → let ──
(hk-test
"where with single binding"
(hk-desugar (hk-parse-top "f x = y\n where y = x + 1"))
(hk-prog
(list
:fun-clause
"f"
(list (list :p-var "x"))
(list
:let
(list
(list
:fun-clause
"y"
(list)
(list :op "+" (list :var "x") (list :int 1))))
(list :var "y")))))
(hk-test
"where with two bindings"
(hk-desugar
(hk-parse-top "f x = y + z\n where y = x + 1\n z = x - 1"))
(hk-prog
(list
:fun-clause
"f"
(list (list :p-var "x"))
(list
:let
(list
(list
:fun-clause
"y"
(list)
(list :op "+" (list :var "x") (list :int 1)))
(list
:fun-clause
"z"
(list)
(list :op "-" (list :var "x") (list :int 1))))
(list :op "+" (list :var "y") (list :var "z"))))))
(hk-test
"guards + where — guarded body inside let"
(hk-desugar
(hk-parse-top "f x | x > 0 = y\n | otherwise = 0\n where y = 99"))
(hk-prog
(list
:fun-clause
"f"
(list (list :p-var "x"))
(list
:let
(list (list :fun-clause "y" (list) (list :int 99)))
(list
:if
(list :op ">" (list :var "x") (list :int 0))
(list :var "y")
(list
:if
(list :var "otherwise")
(list :int 0)
(list
:app
(list :var "error")
(list :string "Non-exhaustive guards"))))))))
;; ── List comprehensions → concatMap / if / let ──
(hk-test
"list-comp: single generator"
(hk-core-expr "[x | x <- xs]")
(list
:app
(list
:app
(list :var "concatMap")
(list
:lambda
(list (list :p-var "x"))
(list :list (list (list :var "x")))))
(list :var "xs")))
(hk-test
"list-comp: generator then guard"
(hk-core-expr "[x * 2 | x <- xs, x > 0]")
(list
:app
(list
:app
(list :var "concatMap")
(list
:lambda
(list (list :p-var "x"))
(list
:if
(list :op ">" (list :var "x") (list :int 0))
(list
:list
(list (list :op "*" (list :var "x") (list :int 2))))
(list :list (list)))))
(list :var "xs")))
(hk-test
"list-comp: generator then let"
(hk-core-expr "[y | x <- xs, let y = x + 1]")
(list
:app
(list
:app
(list :var "concatMap")
(list
:lambda
(list (list :p-var "x"))
(list
:let
(list
(list
:bind
(list :p-var "y")
(list :op "+" (list :var "x") (list :int 1))))
(list :list (list (list :var "y"))))))
(list :var "xs")))
(hk-test
"list-comp: two generators (nested concatMap)"
(hk-core-expr "[(x, y) | x <- xs, y <- ys]")
(list
:app
(list
:app
(list :var "concatMap")
(list
:lambda
(list (list :p-var "x"))
(list
:app
(list
:app
(list :var "concatMap")
(list
:lambda
(list (list :p-var "y"))
(list
:list
(list
(list
:tuple
(list (list :var "x") (list :var "y")))))))
(list :var "ys"))))
(list :var "xs")))
;; ── Pass-through cases ──
(hk-test
"plain int literal unchanged"
(hk-core-expr "42")
(list :int 42))
(hk-test
"lambda + if passes through"
(hk-core-expr "\\x -> if x > 0 then x else - x")
(list
:lambda
(list (list :p-var "x"))
(list
:if
(list :op ">" (list :var "x") (list :int 0))
(list :var "x")
(list :neg (list :var "x")))))
(hk-test
"simple fun-clause (no guards/where) passes through"
(hk-desugar (hk-parse-top "id x = x"))
(hk-prog
(list
:fun-clause
"id"
(list (list :p-var "x"))
(list :var "x"))))
(hk-test
"data decl passes through"
(hk-desugar (hk-parse-top "data Maybe a = Nothing | Just a"))
(hk-prog
(list
:data
"Maybe"
(list "a")
(list
(list :con-def "Nothing" (list))
(list :con-def "Just" (list (list :t-var "a")))))))
(hk-test
"module header passes through, body desugared"
(hk-desugar
(hk-parse-top "module M where\nf x | x > 0 = 1\n | otherwise = 0"))
(list
:module
"M"
nil
(list)
(list
(list
:fun-clause
"f"
(list (list :p-var "x"))
(list
:if
(list :op ">" (list :var "x") (list :int 0))
(list :int 1)
(list
:if
(list :var "otherwise")
(list :int 0)
(list
:app
(list :var "error")
(list :string "Non-exhaustive guards"))))))))
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}

117
lib/haskell/tests/do-io.sx Normal file
View File

@@ -0,0 +1,117 @@
;; do-notation + stub IO monad. Desugaring is per Haskell 98 §3.14:
;; do { e ; ss } = e >> do { ss }
;; do { p <- e ; ss } = e >>= \p -> do { ss }
;; do { let ds ; ss } = let ds in do { ss }
;; do { e } = e
;; The IO type is just `("IO" payload)` for now — no real side
;; effects yet. `return`, `>>=`, `>>` are built-ins.
(define
hk-prog-val
(fn
(src name)
(hk-deep-force (get (hk-eval-program (hk-core src)) name))))
;; ── Single-statement do ──
(hk-test
"do with a single expression"
(hk-eval-expr-source "do { return 5 }")
(list "IO" 5))
(hk-test
"return wraps any expression"
(hk-eval-expr-source "return (1 + 2 * 3)")
(list "IO" 7))
;; ── Bind threads results ──
(hk-test
"single bind"
(hk-eval-expr-source
"do { x <- return 5 ; return (x + 1) }")
(list "IO" 6))
(hk-test
"two binds"
(hk-eval-expr-source
"do\n x <- return 5\n y <- return 7\n return (x + y)")
(list "IO" 12))
(hk-test
"three binds — accumulating"
(hk-eval-expr-source
"do\n a <- return 1\n b <- return 2\n c <- return 3\n return (a + b + c)")
(list "IO" 6))
;; ── Mixing >> and >>= ──
(hk-test
">> sequencing — last wins"
(hk-eval-expr-source
"do\n return 1\n return 2\n return 3")
(list "IO" 3))
(hk-test
">> then >>= — last bind wins"
(hk-eval-expr-source
"do\n return 99\n x <- return 5\n return x")
(list "IO" 5))
;; ── do-let ──
(hk-test
"do-let single binding"
(hk-eval-expr-source
"do\n let x = 3\n return (x * 2)")
(list "IO" 6))
(hk-test
"do-let multi-bind, used after"
(hk-eval-expr-source
"do\n let x = 4\n y = 5\n return (x * y)")
(list "IO" 20))
(hk-test
"do-let interleaved with bind"
(hk-eval-expr-source
"do\n x <- return 10\n let y = x + 1\n return (x * y)")
(list "IO" 110))
;; ── Bind + pattern ──
(hk-test
"bind to constructor pattern"
(hk-eval-expr-source
"do\n Just x <- return (Just 7)\n return (x + 100)")
(list "IO" 107))
(hk-test
"bind to tuple pattern"
(hk-eval-expr-source
"do\n (a, b) <- return (3, 4)\n return (a * b)")
(list "IO" 12))
;; ── User-defined IO functions ──
(hk-test
"do inside top-level fun"
(hk-prog-val
"addM x y = do\n a <- return x\n b <- return y\n return (a + b)\nresult = addM 5 6"
"result")
(list "IO" 11))
(hk-test
"nested do"
(hk-eval-expr-source
"do\n x <- do { y <- return 3 ; return (y + 1) }\n return (x * 2)")
(list "IO" 8))
;; ── (>>=) and (>>) used directly as functions ──
(hk-test
">>= used directly"
(hk-eval-expr-source
"(return 4) >>= (\\x -> return (x + 100))")
(list "IO" 104))
(hk-test
">> used directly"
(hk-eval-expr-source
"(return 1) >> (return 2)")
(list "IO" 2))
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}

278
lib/haskell/tests/eval.sx Normal file
View File

@@ -0,0 +1,278 @@
;; Strict evaluator tests. Each test parses, desugars, and evaluates
;; either an expression (hk-eval-expr-source) or a full program
;; (hk-eval-program → look up a named value).
(define
hk-prog-val
(fn
(src name)
(hk-deep-force (get (hk-eval-program (hk-core src)) name))))
;; ── Literals ──
(hk-test "int literal" (hk-eval-expr-source "42") 42)
(hk-test "float literal" (hk-eval-expr-source "3.14") 3.14)
(hk-test "string literal" (hk-eval-expr-source "\"hi\"") "hi")
(hk-test "char literal" (hk-eval-expr-source "'a'") "a")
(hk-test "negative literal" (hk-eval-expr-source "- 5") -5)
;; ── Arithmetic ──
(hk-test "addition" (hk-eval-expr-source "1 + 2") 3)
(hk-test
"precedence"
(hk-eval-expr-source "1 + 2 * 3")
7)
(hk-test
"parens override precedence"
(hk-eval-expr-source "(1 + 2) * 3")
9)
(hk-test
"subtraction left-assoc"
(hk-eval-expr-source "10 - 3 - 2")
5)
;; ── Comparison + Bool ──
(hk-test
"less than is True"
(hk-eval-expr-source "3 < 5")
(list "True"))
(hk-test
"equality is False"
(hk-eval-expr-source "1 == 2")
(list "False"))
(hk-test
"&& shortcuts"
(hk-eval-expr-source "(1 == 1) && (2 == 2)")
(list "True"))
;; ── if / otherwise ──
(hk-test
"if True"
(hk-eval-expr-source "if True then 1 else 2")
1)
(hk-test
"if comparison branch"
(hk-eval-expr-source "if 5 > 3 then \"yes\" else \"no\"")
"yes")
(hk-test "otherwise is True" (hk-eval-expr-source "otherwise") (list "True"))
;; ── let ──
(hk-test
"let single binding"
(hk-eval-expr-source "let x = 5 in x + 1")
6)
(hk-test
"let two bindings"
(hk-eval-expr-source "let x = 1; y = 2 in x + y")
3)
(hk-test
"let recursive: factorial 5"
(hk-eval-expr-source
"let f n = if n == 0 then 1 else n * f (n - 1) in f 5")
120)
;; ── Lambdas ──
(hk-test
"lambda apply"
(hk-eval-expr-source "(\\x -> x + 1) 5")
6)
(hk-test
"lambda multi-arg"
(hk-eval-expr-source "(\\x y -> x * y) 3 4")
12)
(hk-test
"lambda with constructor pattern"
(hk-eval-expr-source "(\\(Just x) -> x + 1) (Just 7)")
8)
;; ── Constructors ──
(hk-test
"0-arity constructor"
(hk-eval-expr-source "Nothing")
(list "Nothing"))
(hk-test
"1-arity constructor applied"
(hk-eval-expr-source "Just 5")
(list "Just" 5))
(hk-test
"True / False as bools"
(hk-eval-expr-source "True")
(list "True"))
;; ── case ──
(hk-test
"case Just"
(hk-eval-expr-source
"case Just 7 of Just x -> x ; Nothing -> 0")
7)
(hk-test
"case Nothing"
(hk-eval-expr-source
"case Nothing of Just x -> x ; Nothing -> 99")
99)
(hk-test
"case literal pattern"
(hk-eval-expr-source
"case 0 of 0 -> \"zero\" ; n -> \"other\"")
"zero")
(hk-test
"case tuple"
(hk-eval-expr-source
"case (1, 2) of (a, b) -> a + b")
3)
(hk-test
"case wildcard fallback"
(hk-eval-expr-source
"case 5 of 0 -> \"z\" ; _ -> \"nz\"")
"nz")
;; ── List literals + cons ──
(hk-test
"list literal as cons spine"
(hk-eval-expr-source "[1, 2, 3]")
(list ":" 1 (list ":" 2 (list ":" 3 (list "[]")))))
(hk-test
"empty list literal"
(hk-eval-expr-source "[]")
(list "[]"))
(hk-test
"cons via :"
(hk-eval-expr-source "1 : []")
(list ":" 1 (list "[]")))
(hk-test
"++ concatenates lists"
(hk-eval-expr-source "[1, 2] ++ [3]")
(list ":" 1 (list ":" 2 (list ":" 3 (list "[]")))))
;; ── Tuples ──
(hk-test
"2-tuple"
(hk-eval-expr-source "(1, 2)")
(list "Tuple" 1 2))
(hk-test
"3-tuple"
(hk-eval-expr-source "(\"a\", 5, True)")
(list "Tuple" "a" 5 (list "True")))
;; ── Sections ──
(hk-test
"right section (+ 1) applied"
(hk-eval-expr-source "(+ 1) 5")
6)
(hk-test
"left section (10 -) applied"
(hk-eval-expr-source "(10 -) 4")
6)
;; ── Multi-clause top-level functions ──
(hk-test
"multi-clause: factorial"
(hk-prog-val
"fact 0 = 1\nfact n = n * fact (n - 1)\nresult = fact 6"
"result")
720)
(hk-test
"multi-clause: list length via cons pattern"
(hk-prog-val
"len [] = 0\nlen (x:xs) = 1 + len xs\nresult = len [10, 20, 30, 40]"
"result")
4)
(hk-test
"multi-clause: Maybe handler"
(hk-prog-val
"fromMaybe d Nothing = d\nfromMaybe _ (Just x) = x\nresult = fromMaybe 0 (Just 9)"
"result")
9)
(hk-test
"multi-clause: Maybe with default"
(hk-prog-val
"fromMaybe d Nothing = d\nfromMaybe _ (Just x) = x\nresult = fromMaybe 0 Nothing"
"result")
0)
;; ── User-defined data and matching ──
(hk-test
"custom data with pattern match"
(hk-prog-val
"data Color = Red | Green | Blue\nname Red = \"red\"\nname Green = \"green\"\nname Blue = \"blue\"\nresult = name Green"
"result")
"green")
(hk-test
"custom binary tree height"
(hk-prog-val
"data Tree = Leaf | Node Tree Tree\nh Leaf = 0\nh (Node l r) = 1 + max (h l) (h r)\nmax a b = if a > b then a else b\nresult = h (Node (Node Leaf Leaf) Leaf)"
"result")
2)
;; ── Currying ──
(hk-test
"partial application"
(hk-prog-val
"add x y = x + y\nadd5 = add 5\nresult = add5 7"
"result")
12)
;; ── Higher-order ──
(hk-test
"higher-order: function as arg"
(hk-prog-val
"twice f x = f (f x)\ninc x = x + 1\nresult = twice inc 10"
"result")
12)
;; ── Error built-in ──
(hk-test
"error short-circuits via if"
(hk-eval-expr-source
"if True then 1 else error \"unreachable\"")
1)
;; ── Laziness: app args evaluate only when forced ──
(hk-test
"second arg never forced"
(hk-eval-expr-source
"(\\x y -> x) 1 (error \"never\")")
1)
(hk-test
"first arg never forced"
(hk-eval-expr-source
"(\\x y -> y) (error \"never\") 99")
99)
(hk-test
"constructor argument is lazy under wildcard pattern"
(hk-eval-expr-source
"case Just (error \"deeply\") of Just _ -> 7 ; Nothing -> 0")
7)
(hk-test
"lazy: const drops its second argument"
(hk-prog-val
"const x y = x\nresult = const 5 (error \"boom\")"
"result")
5)
(hk-test
"lazy: head ignores tail"
(hk-prog-val
"myHead (x:_) = x\nresult = myHead (1 : (error \"tail\") : [])"
"result")
1)
(hk-test
"lazy: Just on undefined evaluates only on force"
(hk-prog-val
"wrapped = Just (error \"oh no\")\nresult = case wrapped of Just _ -> True ; Nothing -> False"
"result")
(list "True"))
;; ── not / id built-ins ──
(hk-test "not True" (hk-eval-expr-source "not True") (list "False"))
(hk-test "not False" (hk-eval-expr-source "not False") (list "True"))
(hk-test "id" (hk-eval-expr-source "id 42") 42)
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}

View File

@@ -0,0 +1,137 @@
;; Infinite structures + Prelude tests. The lazy `:` operator builds
;; cons cells with thunked head/tail so recursive list-defining
;; functions terminate when only a finite prefix is consumed.
(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))))
;; ── Prelude basics ──
(hk-test "head of literal" (hk-eval-expr-source "head [1, 2, 3]") 1)
(hk-test
"tail of literal"
(hk-eval-list "tail [1, 2, 3]")
(list 2 3))
(hk-test "length" (hk-eval-expr-source "length [10, 20, 30, 40]") 4)
(hk-test "length empty" (hk-eval-expr-source "length []") 0)
(hk-test
"map with section"
(hk-eval-list "map (+ 1) [1, 2, 3]")
(list 2 3 4))
(hk-test
"filter"
(hk-eval-list "filter (\\x -> x > 2) [1, 2, 3, 4, 5]")
(list 3 4 5))
(hk-test
"drop"
(hk-eval-list "drop 2 [10, 20, 30, 40]")
(list 30 40))
(hk-test "fst" (hk-eval-expr-source "fst (7, 9)") 7)
(hk-test "snd" (hk-eval-expr-source "snd (7, 9)") 9)
(hk-test
"zipWith"
(hk-eval-list "zipWith plus [1, 2, 3] [10, 20, 30]")
(list 11 22 33))
;; ── Infinite structures ──
(hk-test
"take from repeat"
(hk-eval-list "take 5 (repeat 7)")
(list 7 7 7 7 7))
(hk-test
"take 0 from repeat returns empty"
(hk-eval-list "take 0 (repeat 7)")
(list))
(hk-test
"take from iterate"
(hk-eval-list "take 5 (iterate (\\x -> x + 1) 0)")
(list 0 1 2 3 4))
(hk-test
"iterate with multiplication"
(hk-eval-list "take 4 (iterate (\\x -> x * 2) 1)")
(list 1 2 4 8))
(hk-test
"head of repeat"
(hk-eval-expr-source "head (repeat 99)")
99)
;; ── Fibonacci stream ──
(hk-test
"first 10 Fibonacci numbers"
(hk-eval-list "take 10 fibs")
(list 0 1 1 2 3 5 8 13 21 34))
(hk-test
"fib at position 8"
(hk-eval-expr-source "head (drop 8 fibs)")
21)
;; ── Building infinite structures in user code ──
(hk-test
"user-defined infinite ones"
(hk-prog-val
"ones = 1 : ones\nresult = take 6 ones"
"result")
(list ":" 1 (list ":" 1 (list ":" 1 (list ":" 1 (list ":" 1 (list ":" 1 (list "[]"))))))))
(hk-test
"user-defined nats"
(hk-prog-val
"nats = naturalsFrom 1\nnaturalsFrom n = n : naturalsFrom (n + 1)\nresult = take 5 nats"
"result")
(list ":" 1 (list ":" 2 (list ":" 3 (list ":" 4 (list ":" 5 (list "[]")))))))
;; ── Range syntax ──
(hk-test
"finite range [1..5]"
(hk-eval-list "[1..5]")
(list 1 2 3 4 5))
(hk-test
"empty range when from > to"
(hk-eval-list "[10..3]")
(list))
(hk-test
"stepped range"
(hk-eval-list "[1, 3..10]")
(list 1 3 5 7 9))
(hk-test
"open range — head"
(hk-eval-expr-source "head [1..]")
1)
(hk-test
"open range — drop then head"
(hk-eval-expr-source "head (drop 99 [1..])")
100)
(hk-test
"open range — take 5"
(hk-eval-list "take 5 [10..]")
(list 10 11 12 13 14))
;; ── Composing Prelude functions ──
(hk-test
"map then filter"
(hk-eval-list
"filter (\\x -> x > 5) (map (\\x -> x * 2) [1, 2, 3, 4])")
(list 6 8))
(hk-test
"sum-via-foldless"
(hk-prog-val
"mySum [] = 0\nmySum (x:xs) = x + mySum xs\nresult = mySum (take 5 (iterate (\\x -> x + 1) 1))"
"result")
15)
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}

245
lib/haskell/tests/layout.sx Normal file
View File

@@ -0,0 +1,245 @@
;; Haskell layout-rule tests. hk-tokenizer + hk-layout produce a
;; virtual-brace-annotated stream; these tests cover the algorithm
;; from Haskell 98 §10.3 plus the pragmatic let/in single-line rule.
;; Convenience — tokenize, run layout, strip eof, keep :type/:value.
(define
hk-lay
(fn
(src)
(map
(fn (tok) {:value (get tok "value") :type (get tok "type")})
(filter
(fn (tok) (not (= (get tok "type") "eof")))
(hk-layout (hk-tokenize src))))))
;; ── 1. Basics ──
(hk-test
"empty input produces empty module { }"
(hk-lay "")
(list
{:value "{" :type "vlbrace"}
{:value "}" :type "vrbrace"}))
(hk-test
"single token → module open+close"
(hk-lay "foo")
(list
{:value "{" :type "vlbrace"}
{:value "foo" :type "varid"}
{:value "}" :type "vrbrace"}))
(hk-test
"two top-level decls get vsemi between"
(hk-lay "foo = 1\nbar = 2")
(list
{:value "{" :type "vlbrace"}
{:value "foo" :type "varid"}
{:value "=" :type "reservedop"}
{:value 1 :type "integer"}
{:value ";" :type "vsemi"}
{:value "bar" :type "varid"}
{:value "=" :type "reservedop"}
{:value 2 :type "integer"}
{:value "}" :type "vrbrace"}))
;; ── 2. Layout keywords — do / let / where / of ──
(hk-test
"do block with two stmts"
(hk-lay "f = do\n x\n y")
(list
{:value "{" :type "vlbrace"}
{:value "f" :type "varid"}
{:value "=" :type "reservedop"}
{:value "do" :type "reserved"}
{:value "{" :type "vlbrace"}
{:value "x" :type "varid"}
{:value ";" :type "vsemi"}
{:value "y" :type "varid"}
{:value "}" :type "vrbrace"}
{:value "}" :type "vrbrace"}))
(hk-test
"single-line let ... in"
(hk-lay "let x = 1 in x")
(list
{:value "{" :type "vlbrace"}
{:value "let" :type "reserved"}
{:value "{" :type "vlbrace"}
{:value "x" :type "varid"}
{:value "=" :type "reservedop"}
{:value 1 :type "integer"}
{:value "}" :type "vrbrace"}
{:value "in" :type "reserved"}
{:value "x" :type "varid"}
{:value "}" :type "vrbrace"}))
(hk-test
"where block with two bindings"
(hk-lay "f = g\n where\n g = 1\n h = 2")
(list
{:value "{" :type "vlbrace"}
{:value "f" :type "varid"}
{:value "=" :type "reservedop"}
{:value "g" :type "varid"}
{:value "where" :type "reserved"}
{:value "{" :type "vlbrace"}
{:value "g" :type "varid"}
{:value "=" :type "reservedop"}
{:value 1 :type "integer"}
{:value ";" :type "vsemi"}
{:value "h" :type "varid"}
{:value "=" :type "reservedop"}
{:value 2 :type "integer"}
{:value "}" :type "vrbrace"}
{:value "}" :type "vrbrace"}))
(hk-test
"case … of with arms"
(hk-lay "f x = case x of\n Just y -> y\n Nothing -> 0")
(list
{:value "{" :type "vlbrace"}
{:value "f" :type "varid"}
{:value "x" :type "varid"}
{:value "=" :type "reservedop"}
{:value "case" :type "reserved"}
{:value "x" :type "varid"}
{:value "of" :type "reserved"}
{:value "{" :type "vlbrace"}
{:value "Just" :type "conid"}
{:value "y" :type "varid"}
{:value "->" :type "reservedop"}
{:value "y" :type "varid"}
{:value ";" :type "vsemi"}
{:value "Nothing" :type "conid"}
{:value "->" :type "reservedop"}
{:value 0 :type "integer"}
{:value "}" :type "vrbrace"}
{:value "}" :type "vrbrace"}))
;; ── 3. Explicit braces disable layout ──
(hk-test
"explicit braces — no implicit vlbrace/vsemi/vrbrace inside"
(hk-lay "do { x ; y }")
(list
{:value "{" :type "vlbrace"}
{:value "do" :type "reserved"}
{:value "{" :type "lbrace"}
{:value "x" :type "varid"}
{:value ";" :type "semi"}
{:value "y" :type "varid"}
{:value "}" :type "rbrace"}
{:value "}" :type "vrbrace"}))
;; ── 4. Dedent closes nested blocks ──
(hk-test
"dedent back to module level closes do block"
(hk-lay "f = do\n x\n y\ng = 2")
(list
{:value "{" :type "vlbrace"}
{:value "f" :type "varid"}
{:value "=" :type "reservedop"}
{:value "do" :type "reserved"}
{:value "{" :type "vlbrace"}
{:value "x" :type "varid"}
{:value ";" :type "vsemi"}
{:value "y" :type "varid"}
{:value "}" :type "vrbrace"}
{:value ";" :type "vsemi"}
{:value "g" :type "varid"}
{:value "=" :type "reservedop"}
{:value 2 :type "integer"}
{:value "}" :type "vrbrace"}))
(hk-test
"dedent closes inner let, emits vsemi at outer do level"
(hk-lay "main = do\n let x = 1\n print x")
(list
{:value "{" :type "vlbrace"}
{:value "main" :type "varid"}
{:value "=" :type "reservedop"}
{:value "do" :type "reserved"}
{:value "{" :type "vlbrace"}
{:value "let" :type "reserved"}
{:value "{" :type "vlbrace"}
{:value "x" :type "varid"}
{:value "=" :type "reservedop"}
{:value 1 :type "integer"}
{:value "}" :type "vrbrace"}
{:value ";" :type "vsemi"}
{:value "print" :type "varid"}
{:value "x" :type "varid"}
{:value "}" :type "vrbrace"}
{:value "}" :type "vrbrace"}))
;; ── 5. Module header skips outer implicit open ──
(hk-test
"module M where — only where opens a block"
(hk-lay "module M where\n f = 1")
(list
{:value "module" :type "reserved"}
{:value "M" :type "conid"}
{:value "where" :type "reserved"}
{:value "{" :type "vlbrace"}
{:value "f" :type "varid"}
{:value "=" :type "reservedop"}
{:value 1 :type "integer"}
{:value "}" :type "vrbrace"}))
;; ── 6. Newlines are stripped ──
(hk-test
"newline tokens do not appear in output"
(let
((toks (hk-layout (hk-tokenize "foo\nbar"))))
(every?
(fn (t) (not (= (get t "type") "newline")))
toks))
true)
;; ── 7. Continuation — deeper indent does NOT emit vsemi ──
(hk-test
"line continuation (deeper indent) just merges"
(hk-lay "foo = 1 +\n 2")
(list
{:value "{" :type "vlbrace"}
{:value "foo" :type "varid"}
{:value "=" :type "reservedop"}
{:value 1 :type "integer"}
{:value "+" :type "varsym"}
{:value 2 :type "integer"}
{:value "}" :type "vrbrace"}))
;; ── 8. Stack closing at EOF ──
(hk-test
"EOF inside nested do closes all implicit blocks"
(let
((toks (hk-lay "main = do\n do\n x")))
(let
((n (len toks)))
(list
(get (nth toks (- n 1)) "type")
(get (nth toks (- n 2)) "type")
(get (nth toks (- n 3)) "type"))))
(list "vrbrace" "vrbrace" "vrbrace"))
;; ── 9. Qualified-newline: x at deeper col than stack top does nothing ──
(hk-test
"mixed where + do"
(hk-lay "f = do\n x\n where\n x = 1")
(list
{:value "{" :type "vlbrace"}
{:value "f" :type "varid"}
{:value "=" :type "reservedop"}
{:value "do" :type "reserved"}
{:value "{" :type "vlbrace"}
{:value "x" :type "varid"}
{:value "}" :type "vrbrace"}
{:value "where" :type "reserved"}
{:value "{" :type "vlbrace"}
{:value "x" :type "varid"}
{:value "=" :type "reservedop"}
{:value 1 :type "integer"}
{:value "}" :type "vrbrace"}
{:value "}" :type "vrbrace"}))
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}

256
lib/haskell/tests/match.sx Normal file
View File

@@ -0,0 +1,256 @@
;; Pattern-matcher tests. The matcher takes (pat val env) and returns
;; an extended env dict on success, or `nil` on failure. Constructor
;; values are tagged lists (con-name first); tuples use the "Tuple"
;; tag; lists use chained `:` cons with `[]` nil.
;; ── Atomic patterns ──
(hk-test
"wildcard always matches"
(hk-match (list :p-wild) 42 (dict))
(dict))
(hk-test
"var binds value"
(hk-match (list :p-var "x") 42 (dict))
{:x 42})
(hk-test
"var preserves prior env"
(hk-match (list :p-var "y") 7 {:x 1})
{:x 1 :y 7})
(hk-test
"int literal matches equal"
(hk-match (list :p-int 5) 5 (dict))
(dict))
(hk-test
"int literal fails on mismatch"
(hk-match (list :p-int 5) 6 (dict))
nil)
(hk-test
"negative int literal matches"
(hk-match (list :p-int -3) -3 (dict))
(dict))
(hk-test
"string literal matches"
(hk-match (list :p-string "hi") "hi" (dict))
(dict))
(hk-test
"string literal fails"
(hk-match (list :p-string "hi") "bye" (dict))
nil)
(hk-test
"char literal matches"
(hk-match (list :p-char "a") "a" (dict))
(dict))
;; ── Constructor patterns ──
(hk-test
"0-arity con matches"
(hk-match
(list :p-con "Nothing" (list))
(hk-mk-con "Nothing" (list))
(dict))
(dict))
(hk-test
"1-arity con matches and binds"
(hk-match
(list :p-con "Just" (list (list :p-var "y")))
(hk-mk-con "Just" (list 9))
(dict))
{:y 9})
(hk-test
"con name mismatch fails"
(hk-match
(list :p-con "Just" (list (list :p-var "y")))
(hk-mk-con "Nothing" (list))
(dict))
nil)
(hk-test
"con arity mismatch fails"
(hk-match
(list :p-con "Pair" (list (list :p-var "a") (list :p-var "b")))
(hk-mk-con "Pair" (list 1))
(dict))
nil)
(hk-test
"nested con: Just (Just x)"
(hk-match
(list
:p-con
"Just"
(list
(list
:p-con
"Just"
(list (list :p-var "x")))))
(hk-mk-con "Just" (list (hk-mk-con "Just" (list 42))))
(dict))
{:x 42})
;; ── Tuple patterns ──
(hk-test
"2-tuple matches and binds"
(hk-match
(list
:p-tuple
(list (list :p-var "a") (list :p-var "b")))
(hk-mk-tuple (list 10 20))
(dict))
{:a 10 :b 20})
(hk-test
"tuple arity mismatch fails"
(hk-match
(list
:p-tuple
(list (list :p-var "a") (list :p-var "b")))
(hk-mk-tuple (list 10 20 30))
(dict))
nil)
;; ── List patterns ──
(hk-test
"[] pattern matches empty list"
(hk-match (list :p-list (list)) (hk-mk-nil) (dict))
(dict))
(hk-test
"[] pattern fails on non-empty"
(hk-match (list :p-list (list)) (hk-mk-list (list 1)) (dict))
nil)
(hk-test
"[a] pattern matches singleton"
(hk-match
(list :p-list (list (list :p-var "a")))
(hk-mk-list (list 7))
(dict))
{:a 7})
(hk-test
"[a, b] pattern matches pair-list and binds"
(hk-match
(list
:p-list
(list (list :p-var "a") (list :p-var "b")))
(hk-mk-list (list 1 2))
(dict))
{:a 1 :b 2})
(hk-test
"[a, b] fails on too-long list"
(hk-match
(list
:p-list
(list (list :p-var "a") (list :p-var "b")))
(hk-mk-list (list 1 2 3))
(dict))
nil)
;; Cons-style infix pattern (which the parser produces as :p-con ":")
(hk-test
"cons (h:t) on non-empty list"
(hk-match
(list
:p-con
":"
(list (list :p-var "h") (list :p-var "t")))
(hk-mk-list (list 1 2 3))
(dict))
{:h 1 :t (list ":" 2 (list ":" 3 (list "[]")))})
(hk-test
"cons fails on empty list"
(hk-match
(list
:p-con
":"
(list (list :p-var "h") (list :p-var "t")))
(hk-mk-nil)
(dict))
nil)
;; ── as patterns ──
(hk-test
"as binds whole + sub-pattern"
(hk-match
(list
:p-as
"all"
(list :p-con "Just" (list (list :p-var "x"))))
(hk-mk-con "Just" (list 99))
(dict))
{:all (list "Just" 99) :x 99})
(hk-test
"as on wildcard binds whole"
(hk-match
(list :p-as "v" (list :p-wild))
"anything"
(dict))
{:v "anything"})
(hk-test
"as fails when sub-pattern fails"
(hk-match
(list
:p-as
"n"
(list :p-con "Just" (list (list :p-var "x"))))
(hk-mk-con "Nothing" (list))
(dict))
nil)
;; ── lazy ~ pattern (eager equivalent for now) ──
(hk-test
"lazy pattern eager-matches its inner"
(hk-match
(list :p-lazy (list :p-var "y"))
42
(dict))
{:y 42})
;; ── Source-driven: parse a real Haskell pattern, match a value ──
(hk-test
"parsed pattern: Just x against Just 5"
(hk-match
(hk-parse-pat-source "Just x")
(hk-mk-con "Just" (list 5))
(dict))
{:x 5})
(hk-test
"parsed pattern: x : xs against [10, 20, 30]"
(hk-match
(hk-parse-pat-source "x : xs")
(hk-mk-list (list 10 20 30))
(dict))
{:x 10 :xs (list ":" 20 (list ":" 30 (list "[]")))})
(hk-test
"parsed pattern: (a, b) against (1, 2)"
(hk-match
(hk-parse-pat-source "(a, b)")
(hk-mk-tuple (list 1 2))
(dict))
{:a 1 :b 2})
(hk-test
"parsed pattern: n@(Just x) against Just 7"
(hk-match
(hk-parse-pat-source "n@(Just x)")
(hk-mk-con "Just" (list 7))
(dict))
{:n (list "Just" 7) :x 7})
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}

View File

@@ -3,60 +3,8 @@
;; Lightweight runner: each test checks actual vs expected with
;; structural (deep) equality and accumulates pass/fail counters.
;; Final value of this file is a summary dict with :pass :fail :fails.
(define
hk-deep=?
(fn
(a b)
(cond
((= a b) true)
((and (dict? a) (dict? b))
(let
((ak (keys a)) (bk (keys b)))
(if
(not (= (len ak) (len bk)))
false
(every?
(fn
(k)
(and (has-key? b k) (hk-deep=? (get a k) (get b k))))
ak))))
((and (list? a) (list? b))
(if
(not (= (len a) (len b)))
false
(let
((i 0) (ok true))
(define
hk-de-loop
(fn
()
(when
(and ok (< i (len a)))
(do
(when
(not (hk-deep=? (nth a i) (nth b i)))
(set! ok false))
(set! i (+ i 1))
(hk-de-loop)))))
(hk-de-loop)
ok)))
(:else false))))
(define hk-test-pass 0)
(define hk-test-fail 0)
(define hk-test-fails (list))
(define
hk-test
(fn
(name actual expected)
(if
(hk-deep=? actual expected)
(set! hk-test-pass (+ hk-test-pass 1))
(do
(set! hk-test-fail (+ hk-test-fail 1))
(append! hk-test-fails {:actual actual :expected expected :name name})))))
;; The hk-test / hk-deep=? helpers live in lib/haskell/testlib.sx
;; and are preloaded by lib/haskell/test.sh.
;; Convenience: tokenize and drop newline + eof tokens so tests focus
;; on meaningful content. Returns list of {:type :value} pairs.

View File

@@ -0,0 +1,278 @@
;; case-of and do-notation parser tests.
;; Covers the minimal patterns needed to make these meaningful: var,
;; wildcard, literal, constructor (with and without args), tuple, list.
;; ── Patterns (in case arms) ──
(hk-test
"wildcard pat"
(hk-parse "case x of _ -> 0")
(list
:case
(list :var "x")
(list (list :alt (list :p-wild) (list :int 0)))))
(hk-test
"var pat"
(hk-parse "case x of y -> y")
(list
:case
(list :var "x")
(list
(list :alt (list :p-var "y") (list :var "y")))))
(hk-test
"0-arity constructor pat"
(hk-parse "case x of\n Nothing -> 0\n Just y -> y")
(list
:case
(list :var "x")
(list
(list :alt (list :p-con "Nothing" (list)) (list :int 0))
(list
:alt
(list :p-con "Just" (list (list :p-var "y")))
(list :var "y")))))
(hk-test
"int literal pat"
(hk-parse "case n of\n 0 -> 1\n _ -> n")
(list
:case
(list :var "n")
(list
(list :alt (list :p-int 0) (list :int 1))
(list :alt (list :p-wild) (list :var "n")))))
(hk-test
"string literal pat"
(hk-parse "case s of\n \"hi\" -> 1\n _ -> 0")
(list
:case
(list :var "s")
(list
(list :alt (list :p-string "hi") (list :int 1))
(list :alt (list :p-wild) (list :int 0)))))
(hk-test
"tuple pat"
(hk-parse "case p of (a, b) -> a")
(list
:case
(list :var "p")
(list
(list
:alt
(list
:p-tuple
(list (list :p-var "a") (list :p-var "b")))
(list :var "a")))))
(hk-test
"list pat"
(hk-parse "case xs of\n [] -> 0\n [a] -> a")
(list
:case
(list :var "xs")
(list
(list :alt (list :p-list (list)) (list :int 0))
(list
:alt
(list :p-list (list (list :p-var "a")))
(list :var "a")))))
(hk-test
"nested constructor pat"
(hk-parse "case x of\n Just (a, b) -> a\n _ -> 0")
(list
:case
(list :var "x")
(list
(list
:alt
(list
:p-con
"Just"
(list
(list
:p-tuple
(list (list :p-var "a") (list :p-var "b")))))
(list :var "a"))
(list :alt (list :p-wild) (list :int 0)))))
(hk-test
"constructor with multiple var args"
(hk-parse "case t of Pair a b -> a")
(list
:case
(list :var "t")
(list
(list
:alt
(list
:p-con
"Pair"
(list (list :p-var "a") (list :p-var "b")))
(list :var "a")))))
;; ── case-of shapes ──
(hk-test
"case with explicit braces"
(hk-parse "case x of { Just y -> y ; Nothing -> 0 }")
(list
:case
(list :var "x")
(list
(list
:alt
(list :p-con "Just" (list (list :p-var "y")))
(list :var "y"))
(list :alt (list :p-con "Nothing" (list)) (list :int 0)))))
(hk-test
"case scrutinee is a full expression"
(hk-parse "case f x + 1 of\n y -> y")
(list
:case
(list
:op
"+"
(list :app (list :var "f") (list :var "x"))
(list :int 1))
(list (list :alt (list :p-var "y") (list :var "y")))))
(hk-test
"case arm body is full expression"
(hk-parse "case x of\n Just y -> y + 1")
(list
:case
(list :var "x")
(list
(list
:alt
(list :p-con "Just" (list (list :p-var "y")))
(list :op "+" (list :var "y") (list :int 1))))))
;; ── do blocks ──
(hk-test
"do with two expressions"
(hk-parse "do\n putStrLn \"hi\"\n return 0")
(list
:do
(list
(list
:do-expr
(list :app (list :var "putStrLn") (list :string "hi")))
(list
:do-expr
(list :app (list :var "return") (list :int 0))))))
(hk-test
"do with bind"
(hk-parse "do\n x <- getLine\n putStrLn x")
(list
:do
(list
(list :do-bind (list :p-var "x") (list :var "getLine"))
(list
:do-expr
(list :app (list :var "putStrLn") (list :var "x"))))))
(hk-test
"do with let"
(hk-parse "do\n let y = 5\n print y")
(list
:do
(list
(list
:do-let
(list (list :bind (list :p-var "y") (list :int 5))))
(list
:do-expr
(list :app (list :var "print") (list :var "y"))))))
(hk-test
"do with multiple let bindings"
(hk-parse "do\n let x = 1\n y = 2\n print (x + y)")
(list
:do
(list
(list
:do-let
(list
(list :bind (list :p-var "x") (list :int 1))
(list :bind (list :p-var "y") (list :int 2))))
(list
:do-expr
(list
:app
(list :var "print")
(list :op "+" (list :var "x") (list :var "y")))))))
(hk-test
"do with bind using constructor pat"
(hk-parse "do\n Just x <- getMaybe\n return x")
(list
:do
(list
(list
:do-bind
(list :p-con "Just" (list (list :p-var "x")))
(list :var "getMaybe"))
(list
:do-expr
(list :app (list :var "return") (list :var "x"))))))
(hk-test
"do with explicit braces"
(hk-parse "do { x <- a ; y <- b ; return (x + y) }")
(list
:do
(list
(list :do-bind (list :p-var "x") (list :var "a"))
(list :do-bind (list :p-var "y") (list :var "b"))
(list
:do-expr
(list
:app
(list :var "return")
(list :op "+" (list :var "x") (list :var "y")))))))
;; ── Mixing case/do inside expressions ──
(hk-test
"case inside let"
(hk-parse "let f = \\x -> case x of\n Just y -> y\n _ -> 0\nin f 5")
(list
:let
(list
(list
:bind
(list :p-var "f")
(list
:lambda
(list (list :p-var "x"))
(list
:case
(list :var "x")
(list
(list
:alt
(list :p-con "Just" (list (list :p-var "y")))
(list :var "y"))
(list :alt (list :p-wild) (list :int 0)))))))
(list :app (list :var "f") (list :int 5))))
(hk-test
"lambda containing do"
(hk-parse "\\x -> do\n y <- x\n return y")
(list
:lambda
(list (list :p-var "x"))
(list
:do
(list
(list :do-bind (list :p-var "y") (list :var "x"))
(list
:do-expr
(list :app (list :var "return") (list :var "y")))))))
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}

View File

@@ -0,0 +1,273 @@
;; Top-level declarations: function clauses, type signatures, data,
;; type, newtype, fixity. Driven by hk-parse-top which produces
;; a (:program DECLS) node.
(define
hk-prog
(fn
(&rest decls)
(list :program decls)))
;; ── Function clauses & pattern bindings ──
(hk-test
"simple fun-clause"
(hk-parse-top "f x = x + 1")
(hk-prog
(list
:fun-clause
"f"
(list (list :p-var "x"))
(list :op "+" (list :var "x") (list :int 1)))))
(hk-test
"nullary decl"
(hk-parse-top "answer = 42")
(hk-prog
(list :fun-clause "answer" (list) (list :int 42))))
(hk-test
"multi-clause fn (separate defs for each pattern)"
(hk-parse-top "fact 0 = 1\nfact n = n")
(hk-prog
(list :fun-clause "fact" (list (list :p-int 0)) (list :int 1))
(list
:fun-clause
"fact"
(list (list :p-var "n"))
(list :var "n"))))
(hk-test
"constructor pattern in fn args"
(hk-parse-top "fromJust (Just x) = x")
(hk-prog
(list
:fun-clause
"fromJust"
(list (list :p-con "Just" (list (list :p-var "x"))))
(list :var "x"))))
(hk-test
"pattern binding at top level"
(hk-parse-top "(a, b) = pair")
(hk-prog
(list
:pat-bind
(list
:p-tuple
(list (list :p-var "a") (list :p-var "b")))
(list :var "pair"))))
;; ── Type signatures ──
(hk-test
"single-name sig"
(hk-parse-top "f :: Int -> Int")
(hk-prog
(list
:type-sig
(list "f")
(list :t-fun (list :t-con "Int") (list :t-con "Int")))))
(hk-test
"multi-name sig"
(hk-parse-top "f, g, h :: Int -> Bool")
(hk-prog
(list
:type-sig
(list "f" "g" "h")
(list :t-fun (list :t-con "Int") (list :t-con "Bool")))))
(hk-test
"sig with type application"
(hk-parse-top "f :: Maybe a -> a")
(hk-prog
(list
:type-sig
(list "f")
(list
:t-fun
(list :t-app (list :t-con "Maybe") (list :t-var "a"))
(list :t-var "a")))))
(hk-test
"sig with list type"
(hk-parse-top "len :: [a] -> Int")
(hk-prog
(list
:type-sig
(list "len")
(list
:t-fun
(list :t-list (list :t-var "a"))
(list :t-con "Int")))))
(hk-test
"sig with tuple and right-assoc ->"
(hk-parse-top "pair :: a -> b -> (a, b)")
(hk-prog
(list
:type-sig
(list "pair")
(list
:t-fun
(list :t-var "a")
(list
:t-fun
(list :t-var "b")
(list
:t-tuple
(list (list :t-var "a") (list :t-var "b"))))))))
(hk-test
"sig + implementation together"
(hk-parse-top "id :: a -> a\nid x = x")
(hk-prog
(list
:type-sig
(list "id")
(list :t-fun (list :t-var "a") (list :t-var "a")))
(list
:fun-clause
"id"
(list (list :p-var "x"))
(list :var "x"))))
;; ── data declarations ──
(hk-test
"data Maybe"
(hk-parse-top "data Maybe a = Nothing | Just a")
(hk-prog
(list
:data
"Maybe"
(list "a")
(list
(list :con-def "Nothing" (list))
(list :con-def "Just" (list (list :t-var "a")))))))
(hk-test
"data Either"
(hk-parse-top "data Either a b = Left a | Right b")
(hk-prog
(list
:data
"Either"
(list "a" "b")
(list
(list :con-def "Left" (list (list :t-var "a")))
(list :con-def "Right" (list (list :t-var "b")))))))
(hk-test
"data with no type parameters"
(hk-parse-top "data Bool = True | False")
(hk-prog
(list
:data
"Bool"
(list)
(list
(list :con-def "True" (list))
(list :con-def "False" (list))))))
(hk-test
"recursive data type"
(hk-parse-top "data Tree a = Leaf | Node (Tree a) a (Tree a)")
(hk-prog
(list
:data
"Tree"
(list "a")
(list
(list :con-def "Leaf" (list))
(list
:con-def
"Node"
(list
(list :t-app (list :t-con "Tree") (list :t-var "a"))
(list :t-var "a")
(list :t-app (list :t-con "Tree") (list :t-var "a"))))))))
;; ── type synonyms ──
(hk-test
"simple type synonym"
(hk-parse-top "type Name = String")
(hk-prog
(list :type-syn "Name" (list) (list :t-con "String"))))
(hk-test
"parameterised type synonym"
(hk-parse-top "type Pair a = (a, a)")
(hk-prog
(list
:type-syn
"Pair"
(list "a")
(list
:t-tuple
(list (list :t-var "a") (list :t-var "a"))))))
;; ── newtype ──
(hk-test
"newtype"
(hk-parse-top "newtype Age = Age Int")
(hk-prog (list :newtype "Age" (list) "Age" (list :t-con "Int"))))
(hk-test
"parameterised newtype"
(hk-parse-top "newtype Wrap a = Wrap a")
(hk-prog
(list :newtype "Wrap" (list "a") "Wrap" (list :t-var "a"))))
;; ── fixity declarations ──
(hk-test
"infixl with precedence"
(hk-parse-top "infixl 5 +:, -:")
(hk-prog (list :fixity "l" 5 (list "+:" "-:"))))
(hk-test
"infixr"
(hk-parse-top "infixr 9 .")
(hk-prog (list :fixity "r" 9 (list "."))))
(hk-test
"infix (non-assoc) default prec"
(hk-parse-top "infix ==")
(hk-prog (list :fixity "n" 9 (list "=="))))
(hk-test
"fixity with backtick operator name"
(hk-parse-top "infixl 7 `div`")
(hk-prog (list :fixity "l" 7 (list "div"))))
;; ── Several decls combined ──
(hk-test
"mixed: data + sig + fn + type"
(hk-parse-top "data Maybe a = Nothing | Just a\ntype Entry = Maybe Int\nf :: Entry -> Int\nf (Just x) = x\nf Nothing = 0")
(hk-prog
(list
:data
"Maybe"
(list "a")
(list
(list :con-def "Nothing" (list))
(list :con-def "Just" (list (list :t-var "a")))))
(list
:type-syn
"Entry"
(list)
(list :t-app (list :t-con "Maybe") (list :t-con "Int")))
(list
:type-sig
(list "f")
(list :t-fun (list :t-con "Entry") (list :t-con "Int")))
(list
:fun-clause
"f"
(list (list :p-con "Just" (list (list :p-var "x"))))
(list :var "x"))
(list
:fun-clause
"f"
(list (list :p-con "Nothing" (list)))
(list :int 0))))
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}

View File

@@ -0,0 +1,258 @@
;; Haskell expression parser tests.
;; hk-parse tokenises, runs layout, then parses. Output is an AST
;; whose head is a keyword tag (evaluates to its string name).
;; ── 1. Literals ──
(hk-test "integer" (hk-parse "42") (list :int 42))
(hk-test "float" (hk-parse "3.14") (list :float 3.14))
(hk-test "string" (hk-parse "\"hi\"") (list :string "hi"))
(hk-test "char" (hk-parse "'a'") (list :char "a"))
;; ── 2. Variables and constructors ──
(hk-test "varid" (hk-parse "foo") (list :var "foo"))
(hk-test "conid" (hk-parse "Nothing") (list :con "Nothing"))
(hk-test "qvarid" (hk-parse "Data.Map.lookup") (list :var "Data.Map.lookup"))
(hk-test "qconid" (hk-parse "Data.Map") (list :con "Data.Map"))
;; ── 3. Parens / unit / tuple ──
(hk-test "parens strip" (hk-parse "(42)") (list :int 42))
(hk-test "unit" (hk-parse "()") (list :con "()"))
(hk-test
"2-tuple"
(hk-parse "(1, 2)")
(list :tuple (list (list :int 1) (list :int 2))))
(hk-test
"3-tuple"
(hk-parse "(x, y, z)")
(list
:tuple
(list (list :var "x") (list :var "y") (list :var "z"))))
;; ── 4. Lists ──
(hk-test "empty list" (hk-parse "[]") (list :list (list)))
(hk-test
"singleton list"
(hk-parse "[1]")
(list :list (list (list :int 1))))
(hk-test
"list of ints"
(hk-parse "[1, 2, 3]")
(list
:list
(list (list :int 1) (list :int 2) (list :int 3))))
(hk-test
"range"
(hk-parse "[1..10]")
(list :range (list :int 1) (list :int 10)))
(hk-test
"range with step"
(hk-parse "[1, 3..10]")
(list
:range-step
(list :int 1)
(list :int 3)
(list :int 10)))
;; ── 5. Application ──
(hk-test
"one-arg app"
(hk-parse "f x")
(list :app (list :var "f") (list :var "x")))
(hk-test
"multi-arg app is left-assoc"
(hk-parse "f x y z")
(list
:app
(list
:app
(list :app (list :var "f") (list :var "x"))
(list :var "y"))
(list :var "z")))
(hk-test
"app with con"
(hk-parse "Just 5")
(list :app (list :con "Just") (list :int 5)))
;; ── 6. Infix operators ──
(hk-test
"simple +"
(hk-parse "1 + 2")
(list :op "+" (list :int 1) (list :int 2)))
(hk-test
"precedence: * binds tighter than +"
(hk-parse "1 + 2 * 3")
(list
:op
"+"
(list :int 1)
(list :op "*" (list :int 2) (list :int 3))))
(hk-test
"- is left-assoc"
(hk-parse "10 - 3 - 2")
(list
:op
"-"
(list :op "-" (list :int 10) (list :int 3))
(list :int 2)))
(hk-test
": is right-assoc"
(hk-parse "a : b : c")
(list
:op
":"
(list :var "a")
(list :op ":" (list :var "b") (list :var "c"))))
(hk-test
"app binds tighter than op"
(hk-parse "f x + g y")
(list
:op
"+"
(list :app (list :var "f") (list :var "x"))
(list :app (list :var "g") (list :var "y"))))
(hk-test
"$ is lowest precedence, right-assoc"
(hk-parse "f $ g x")
(list
:op
"$"
(list :var "f")
(list :app (list :var "g") (list :var "x"))))
;; ── 7. Backticks (varid-as-operator) ──
(hk-test
"backtick operator"
(hk-parse "x `mod` 3")
(list :op "mod" (list :var "x") (list :int 3)))
;; ── 8. Unary negation ──
(hk-test
"unary -"
(hk-parse "- 5")
(list :neg (list :int 5)))
(hk-test
"unary - on application"
(hk-parse "- f x")
(list :neg (list :app (list :var "f") (list :var "x"))))
(hk-test
"- n + m → (- n) + m"
(hk-parse "- 1 + 2")
(list
:op
"+"
(list :neg (list :int 1))
(list :int 2)))
;; ── 9. Lambda ──
(hk-test
"lambda single param"
(hk-parse "\\x -> x")
(list :lambda (list (list :p-var "x")) (list :var "x")))
(hk-test
"lambda multi-param"
(hk-parse "\\x y -> x + y")
(list
:lambda
(list (list :p-var "x") (list :p-var "y"))
(list :op "+" (list :var "x") (list :var "y"))))
(hk-test
"lambda body is full expression"
(hk-parse "\\f -> f 1 + f 2")
(list
:lambda
(list (list :p-var "f"))
(list
:op
"+"
(list :app (list :var "f") (list :int 1))
(list :app (list :var "f") (list :int 2)))))
;; ── 10. if-then-else ──
(hk-test
"if basic"
(hk-parse "if x then 1 else 2")
(list :if (list :var "x") (list :int 1) (list :int 2)))
(hk-test
"if with infix cond"
(hk-parse "if x == 0 then y else z")
(list
:if
(list :op "==" (list :var "x") (list :int 0))
(list :var "y")
(list :var "z")))
;; ── 11. let-in ──
(hk-test
"let single binding"
(hk-parse "let x = 1 in x")
(list
:let
(list (list :bind (list :p-var "x") (list :int 1)))
(list :var "x")))
(hk-test
"let two bindings (multi-line)"
(hk-parse "let x = 1\n y = 2\nin x + y")
(list
:let
(list
(list :bind (list :p-var "x") (list :int 1))
(list :bind (list :p-var "y") (list :int 2)))
(list :op "+" (list :var "x") (list :var "y"))))
(hk-test
"let with explicit braces"
(hk-parse "let { x = 1 ; y = 2 } in x + y")
(list
:let
(list
(list :bind (list :p-var "x") (list :int 1))
(list :bind (list :p-var "y") (list :int 2)))
(list :op "+" (list :var "x") (list :var "y"))))
;; ── 12. Mixed / nesting ──
(hk-test
"nested application"
(hk-parse "f (g x) y")
(list
:app
(list
:app
(list :var "f")
(list :app (list :var "g") (list :var "x")))
(list :var "y")))
(hk-test
"lambda applied"
(hk-parse "(\\x -> x + 1) 5")
(list
:app
(list
:lambda
(list (list :p-var "x"))
(list :op "+" (list :var "x") (list :int 1)))
(list :int 5)))
(hk-test
"lambda + if"
(hk-parse "\\n -> if n == 0 then 1 else n")
(list
:lambda
(list (list :p-var "n"))
(list
:if
(list :op "==" (list :var "n") (list :int 0))
(list :int 1)
(list :var "n"))))
;; ── 13. Precedence corners ──
(hk-test
". is right-assoc (prec 9)"
(hk-parse "f . g . h")
(list
:op
"."
(list :var "f")
(list :op "." (list :var "g") (list :var "h"))))
(hk-test
"== is non-associative (single use)"
(hk-parse "x == y")
(list :op "==" (list :var "x") (list :var "y")))
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}

View File

@@ -0,0 +1,261 @@
;; Guards and where-clauses — on fun-clauses, case alts, and
;; let-bindings (which now also accept funclause-style LHS like
;; `let f x = e` or `let f x | g = e | g = e`).
(define
hk-prog
(fn (&rest decls) (list :program decls)))
;; ── Guarded fun-clauses ──
(hk-test
"simple guards (two branches)"
(hk-parse-top "abs x | x < 0 = - x\n | otherwise = x")
(hk-prog
(list
:fun-clause
"abs"
(list (list :p-var "x"))
(list
:guarded
(list
(list
:guard
(list :op "<" (list :var "x") (list :int 0))
(list :neg (list :var "x")))
(list :guard (list :var "otherwise") (list :var "x")))))))
(hk-test
"three-way guard"
(hk-parse-top "sign n | n > 0 = 1\n | n < 0 = -1\n | otherwise = 0")
(hk-prog
(list
:fun-clause
"sign"
(list (list :p-var "n"))
(list
:guarded
(list
(list
:guard
(list :op ">" (list :var "n") (list :int 0))
(list :int 1))
(list
:guard
(list :op "<" (list :var "n") (list :int 0))
(list :neg (list :int 1)))
(list
:guard
(list :var "otherwise")
(list :int 0)))))))
(hk-test
"mixed: one eq clause plus one guarded clause"
(hk-parse-top "sign 0 = 0\nsign n | n > 0 = 1\n | otherwise = -1")
(hk-prog
(list
:fun-clause
"sign"
(list (list :p-int 0))
(list :int 0))
(list
:fun-clause
"sign"
(list (list :p-var "n"))
(list
:guarded
(list
(list
:guard
(list :op ">" (list :var "n") (list :int 0))
(list :int 1))
(list
:guard
(list :var "otherwise")
(list :neg (list :int 1))))))))
;; ── where on fun-clauses ──
(hk-test
"where with one binding"
(hk-parse-top "f x = y + y\n where y = x + 1")
(hk-prog
(list
:fun-clause
"f"
(list (list :p-var "x"))
(list
:where
(list :op "+" (list :var "y") (list :var "y"))
(list
(list
:fun-clause
"y"
(list)
(list :op "+" (list :var "x") (list :int 1))))))))
(hk-test
"where with multiple bindings"
(hk-parse-top "f x = y * z\n where y = x + 1\n z = x - 1")
(hk-prog
(list
:fun-clause
"f"
(list (list :p-var "x"))
(list
:where
(list :op "*" (list :var "y") (list :var "z"))
(list
(list
:fun-clause
"y"
(list)
(list :op "+" (list :var "x") (list :int 1)))
(list
:fun-clause
"z"
(list)
(list :op "-" (list :var "x") (list :int 1))))))))
(hk-test
"guards + where"
(hk-parse-top "f x | x > 0 = y\n | otherwise = 0\n where y = 99")
(hk-prog
(list
:fun-clause
"f"
(list (list :p-var "x"))
(list
:where
(list
:guarded
(list
(list
:guard
(list :op ">" (list :var "x") (list :int 0))
(list :var "y"))
(list
:guard
(list :var "otherwise")
(list :int 0))))
(list
(list :fun-clause "y" (list) (list :int 99)))))))
;; ── Guards in case alts ──
(hk-test
"case alt with guards"
(hk-parse "case x of\n Just y | y > 0 -> y\n | otherwise -> 0\n Nothing -> 0")
(list
:case
(list :var "x")
(list
(list
:alt
(list :p-con "Just" (list (list :p-var "y")))
(list
:guarded
(list
(list
:guard
(list :op ">" (list :var "y") (list :int 0))
(list :var "y"))
(list
:guard
(list :var "otherwise")
(list :int 0)))))
(list :alt (list :p-con "Nothing" (list)) (list :int 0)))))
(hk-test
"case alt with where"
(hk-parse "case x of\n Just y -> y + z where z = 5\n Nothing -> 0")
(list
:case
(list :var "x")
(list
(list
:alt
(list :p-con "Just" (list (list :p-var "y")))
(list
:where
(list :op "+" (list :var "y") (list :var "z"))
(list
(list :fun-clause "z" (list) (list :int 5)))))
(list :alt (list :p-con "Nothing" (list)) (list :int 0)))))
;; ── let-bindings: funclause form, guards, where ──
(hk-test
"let with funclause shorthand"
(hk-parse "let f x = x + 1 in f 5")
(list
:let
(list
(list
:fun-clause
"f"
(list (list :p-var "x"))
(list :op "+" (list :var "x") (list :int 1))))
(list :app (list :var "f") (list :int 5))))
(hk-test
"let with guards"
(hk-parse "let f x | x > 0 = x\n | otherwise = 0\nin f 3")
(list
:let
(list
(list
:fun-clause
"f"
(list (list :p-var "x"))
(list
:guarded
(list
(list
:guard
(list :op ">" (list :var "x") (list :int 0))
(list :var "x"))
(list
:guard
(list :var "otherwise")
(list :int 0))))))
(list :app (list :var "f") (list :int 3))))
(hk-test
"let funclause + where"
(hk-parse "let f x = y where y = x + 1\nin f 7")
(list
:let
(list
(list
:fun-clause
"f"
(list (list :p-var "x"))
(list
:where
(list :var "y")
(list
(list
:fun-clause
"y"
(list)
(list :op "+" (list :var "x") (list :int 1)))))))
(list :app (list :var "f") (list :int 7))))
;; ── Nested: where inside where (via recursive hk-parse-decl) ──
(hk-test
"where block can contain a type signature"
(hk-parse-top "f x = y\n where y :: Int\n y = x")
(hk-prog
(list
:fun-clause
"f"
(list (list :p-var "x"))
(list
:where
(list :var "y")
(list
(list :type-sig (list "y") (list :t-con "Int"))
(list
:fun-clause
"y"
(list)
(list :var "x")))))))
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}

View File

@@ -0,0 +1,202 @@
;; Module header + imports. The parser switches from (:program DECLS)
;; to (:module NAME EXPORTS IMPORTS DECLS) as soon as a module header
;; or any `import` decl appears.
;; ── Module header ──
(hk-test
"simple module, no exports"
(hk-parse-top "module M where\n f = 1")
(list
:module
"M"
nil
(list)
(list (list :fun-clause "f" (list) (list :int 1)))))
(hk-test
"module with dotted name"
(hk-parse-top "module Data.Map where\nf = 1")
(list
:module
"Data.Map"
nil
(list)
(list (list :fun-clause "f" (list) (list :int 1)))))
(hk-test
"module with empty export list"
(hk-parse-top "module M () where\nf = 1")
(list
:module
"M"
(list)
(list)
(list (list :fun-clause "f" (list) (list :int 1)))))
(hk-test
"module with exports (var, tycon-all, tycon-with)"
(hk-parse-top "module M (f, g, Maybe(..), List(Cons, Nil)) where\nf = 1\ng = 2")
(list
:module
"M"
(list
(list :ent-var "f")
(list :ent-var "g")
(list :ent-all "Maybe")
(list :ent-with "List" (list "Cons" "Nil")))
(list)
(list
(list :fun-clause "f" (list) (list :int 1))
(list :fun-clause "g" (list) (list :int 2)))))
(hk-test
"module export list including another module"
(hk-parse-top "module M (module Foo, f) where\nf = 1")
(list
:module
"M"
(list (list :ent-module "Foo") (list :ent-var "f"))
(list)
(list (list :fun-clause "f" (list) (list :int 1)))))
(hk-test
"module export with operator"
(hk-parse-top "module M ((+:), f) where\nf = 1")
(list
:module
"M"
(list (list :ent-var "+:") (list :ent-var "f"))
(list)
(list (list :fun-clause "f" (list) (list :int 1)))))
(hk-test
"empty module body"
(hk-parse-top "module M where")
(list :module "M" nil (list) (list)))
;; ── Imports ──
(hk-test
"plain import"
(hk-parse-top "import Foo")
(list
:module
nil
nil
(list (list :import false "Foo" nil nil))
(list)))
(hk-test
"qualified import"
(hk-parse-top "import qualified Data.Map")
(list
:module
nil
nil
(list (list :import true "Data.Map" nil nil))
(list)))
(hk-test
"import with alias"
(hk-parse-top "import Data.Map as M")
(list
:module
nil
nil
(list (list :import false "Data.Map" "M" nil))
(list)))
(hk-test
"import with explicit list"
(hk-parse-top "import Foo (bar, Baz(..), Quux(X, Y))")
(list
:module
nil
nil
(list
(list
:import
false
"Foo"
nil
(list
:spec-items
(list
(list :ent-var "bar")
(list :ent-all "Baz")
(list :ent-with "Quux" (list "X" "Y"))))))
(list)))
(hk-test
"import hiding"
(hk-parse-top "import Foo hiding (x, y)")
(list
:module
nil
nil
(list
(list
:import
false
"Foo"
nil
(list
:spec-hiding
(list (list :ent-var "x") (list :ent-var "y")))))
(list)))
(hk-test
"qualified + alias + hiding"
(hk-parse-top "import qualified Data.List as L hiding (sort)")
(list
:module
nil
nil
(list
(list
:import
true
"Data.List"
"L"
(list :spec-hiding (list (list :ent-var "sort")))))
(list)))
;; ── Combinations ──
(hk-test
"module with multiple imports and a decl"
(hk-parse-top "module M where\nimport Foo\nimport qualified Bar as B\nf = 1")
(list
:module
"M"
nil
(list
(list :import false "Foo" nil nil)
(list :import true "Bar" "B" nil))
(list (list :fun-clause "f" (list) (list :int 1)))))
(hk-test
"headerless file with imports"
(hk-parse-top "import Foo\nimport Bar (baz)\nf = 1")
(list
:module
nil
nil
(list
(list :import false "Foo" nil nil)
(list
:import
false
"Bar"
nil
(list :spec-items (list (list :ent-var "baz")))))
(list (list :fun-clause "f" (list) (list :int 1)))))
(hk-test
"plain program (no header, no imports) still uses :program"
(hk-parse-top "f = 1\ng = 2")
(list
:program
(list
(list :fun-clause "f" (list) (list :int 1))
(list :fun-clause "g" (list) (list :int 2)))))
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}

View File

@@ -0,0 +1,234 @@
;; Full-pattern parser tests: as-patterns, lazy ~, negative literals,
;; infix constructor patterns (`:`, any consym), lambda pattern args,
;; and let pattern-bindings.
;; ── as-patterns ──
(hk-test
"as pattern, wraps constructor"
(hk-parse "case x of n@(Just y) -> n")
(list
:case
(list :var "x")
(list
(list
:alt
(list
:p-as
"n"
(list :p-con "Just" (list (list :p-var "y"))))
(list :var "n")))))
(hk-test
"as pattern, wraps wildcard"
(hk-parse "case x of all@_ -> all")
(list
:case
(list :var "x")
(list
(list
:alt
(list :p-as "all" (list :p-wild))
(list :var "all")))))
(hk-test
"as in lambda"
(hk-parse "\\xs@(a : rest) -> xs")
(list
:lambda
(list
(list
:p-as
"xs"
(list
:p-con
":"
(list (list :p-var "a") (list :p-var "rest")))))
(list :var "xs")))
;; ── lazy patterns ──
(hk-test
"lazy var"
(hk-parse "case x of ~y -> y")
(list
:case
(list :var "x")
(list
(list :alt (list :p-lazy (list :p-var "y")) (list :var "y")))))
(hk-test
"lazy constructor"
(hk-parse "\\(~(Just x)) -> x")
(list
:lambda
(list
(list
:p-lazy
(list :p-con "Just" (list (list :p-var "x")))))
(list :var "x")))
;; ── negative literal patterns ──
(hk-test
"negative int pattern"
(hk-parse "case n of\n -1 -> 0\n _ -> n")
(list
:case
(list :var "n")
(list
(list :alt (list :p-int -1) (list :int 0))
(list :alt (list :p-wild) (list :var "n")))))
(hk-test
"negative float pattern"
(hk-parse "case x of -0.5 -> 1")
(list
:case
(list :var "x")
(list (list :alt (list :p-float -0.5) (list :int 1)))))
;; ── infix constructor patterns (`:` and any consym) ──
(hk-test
"cons pattern"
(hk-parse "case xs of x : rest -> x")
(list
:case
(list :var "xs")
(list
(list
:alt
(list
:p-con
":"
(list (list :p-var "x") (list :p-var "rest")))
(list :var "x")))))
(hk-test
"cons is right-associative in pats"
(hk-parse "case xs of a : b : rest -> rest")
(list
:case
(list :var "xs")
(list
(list
:alt
(list
:p-con
":"
(list
(list :p-var "a")
(list
:p-con
":"
(list (list :p-var "b") (list :p-var "rest")))))
(list :var "rest")))))
(hk-test
"consym pattern"
(hk-parse "case p of a :+: b -> a")
(list
:case
(list :var "p")
(list
(list
:alt
(list
:p-con
":+:"
(list (list :p-var "a") (list :p-var "b")))
(list :var "a")))))
;; ── lambda with pattern args ──
(hk-test
"lambda with constructor pattern"
(hk-parse "\\(Just x) -> x")
(list
:lambda
(list (list :p-con "Just" (list (list :p-var "x"))))
(list :var "x")))
(hk-test
"lambda with tuple pattern"
(hk-parse "\\(a, b) -> a + b")
(list
:lambda
(list
(list
:p-tuple
(list (list :p-var "a") (list :p-var "b"))))
(list :op "+" (list :var "a") (list :var "b"))))
(hk-test
"lambda with wildcard"
(hk-parse "\\_ -> 42")
(list :lambda (list (list :p-wild)) (list :int 42)))
(hk-test
"lambda with mixed apats"
(hk-parse "\\x _ (Just y) -> y")
(list
:lambda
(list
(list :p-var "x")
(list :p-wild)
(list :p-con "Just" (list (list :p-var "y"))))
(list :var "y")))
;; ── let pattern-bindings ──
(hk-test
"let tuple pattern-binding"
(hk-parse "let (x, y) = pair in x + y")
(list
:let
(list
(list
:bind
(list
:p-tuple
(list (list :p-var "x") (list :p-var "y")))
(list :var "pair")))
(list :op "+" (list :var "x") (list :var "y"))))
(hk-test
"let constructor pattern-binding"
(hk-parse "let Just x = m in x")
(list
:let
(list
(list
:bind
(list :p-con "Just" (list (list :p-var "x")))
(list :var "m")))
(list :var "x")))
(hk-test
"let cons pattern-binding"
(hk-parse "let (x : rest) = xs in x")
(list
:let
(list
(list
:bind
(list
:p-con
":"
(list (list :p-var "x") (list :p-var "rest")))
(list :var "xs")))
(list :var "x")))
;; ── do with constructor-pattern binds ──
(hk-test
"do bind to tuple pattern"
(hk-parse "do\n (a, b) <- pairs\n return a")
(list
:do
(list
(list
:do-bind
(list
:p-tuple
(list (list :p-var "a") (list :p-var "b")))
(list :var "pairs"))
(list
:do-expr
(list :app (list :var "return") (list :var "a"))))))
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}

View File

@@ -0,0 +1,191 @@
;; Operator sections and list comprehensions.
;; ── Operator references (unchanged expr shape) ──
(hk-test
"op as value (+)"
(hk-parse "(+)")
(list :var "+"))
(hk-test
"op as value (-)"
(hk-parse "(-)")
(list :var "-"))
(hk-test
"op as value (:)"
(hk-parse "(:)")
(list :var ":"))
(hk-test
"backtick op as value"
(hk-parse "(`div`)")
(list :var "div"))
;; ── Right sections (op expr) ──
(hk-test
"right section (+ 5)"
(hk-parse "(+ 5)")
(list :sect-right "+" (list :int 5)))
(hk-test
"right section (* x)"
(hk-parse "(* x)")
(list :sect-right "*" (list :var "x")))
(hk-test
"right section with backtick op"
(hk-parse "(`div` 2)")
(list :sect-right "div" (list :int 2)))
;; `-` is unary in expr position — (- 5) is negation, not a right section
(hk-test
"(- 5) is negation, not a section"
(hk-parse "(- 5)")
(list :neg (list :int 5)))
;; ── Left sections (expr op) ──
(hk-test
"left section (5 +)"
(hk-parse "(5 +)")
(list :sect-left "+" (list :int 5)))
(hk-test
"left section with backtick"
(hk-parse "(x `mod`)")
(list :sect-left "mod" (list :var "x")))
(hk-test
"left section with cons (x :)"
(hk-parse "(x :)")
(list :sect-left ":" (list :var "x")))
;; ── Mixed / nesting ──
(hk-test
"map (+ 1) xs"
(hk-parse "map (+ 1) xs")
(list
:app
(list
:app
(list :var "map")
(list :sect-right "+" (list :int 1)))
(list :var "xs")))
(hk-test
"filter (< 0) xs"
(hk-parse "filter (< 0) xs")
(list
:app
(list
:app
(list :var "filter")
(list :sect-right "<" (list :int 0)))
(list :var "xs")))
;; ── Plain parens and tuples still work ──
(hk-test
"plain parens unwrap"
(hk-parse "(1 + 2)")
(list :op "+" (list :int 1) (list :int 2)))
(hk-test
"tuple still parses"
(hk-parse "(a, b, c)")
(list
:tuple
(list (list :var "a") (list :var "b") (list :var "c"))))
;; ── List comprehensions ──
(hk-test
"simple list comprehension"
(hk-parse "[x | x <- xs]")
(list
:list-comp
(list :var "x")
(list
(list :q-gen (list :p-var "x") (list :var "xs")))))
(hk-test
"comprehension with filter"
(hk-parse "[x * 2 | x <- xs, x > 0]")
(list
:list-comp
(list :op "*" (list :var "x") (list :int 2))
(list
(list :q-gen (list :p-var "x") (list :var "xs"))
(list
:q-guard
(list :op ">" (list :var "x") (list :int 0))))))
(hk-test
"comprehension with let"
(hk-parse "[y | x <- xs, let y = x + 1]")
(list
:list-comp
(list :var "y")
(list
(list :q-gen (list :p-var "x") (list :var "xs"))
(list
:q-let
(list
(list
:bind
(list :p-var "y")
(list :op "+" (list :var "x") (list :int 1))))))))
(hk-test
"nested generators"
(hk-parse "[(x, y) | x <- xs, y <- ys]")
(list
:list-comp
(list :tuple (list (list :var "x") (list :var "y")))
(list
(list :q-gen (list :p-var "x") (list :var "xs"))
(list :q-gen (list :p-var "y") (list :var "ys")))))
(hk-test
"comprehension with constructor pattern"
(hk-parse "[v | Just v <- xs]")
(list
:list-comp
(list :var "v")
(list
(list
:q-gen
(list :p-con "Just" (list (list :p-var "v")))
(list :var "xs")))))
(hk-test
"comprehension with tuple pattern"
(hk-parse "[x + y | (x, y) <- pairs]")
(list
:list-comp
(list :op "+" (list :var "x") (list :var "y"))
(list
(list
:q-gen
(list
:p-tuple
(list (list :p-var "x") (list :p-var "y")))
(list :var "pairs")))))
(hk-test
"combination: generator, let, guard"
(hk-parse "[z | x <- xs, let z = x * 2, z > 10]")
(list
:list-comp
(list :var "z")
(list
(list :q-gen (list :p-var "x") (list :var "xs"))
(list
:q-let
(list
(list
:bind
(list :p-var "z")
(list :op "*" (list :var "x") (list :int 2)))))
(list
:q-guard
(list :op ">" (list :var "z") (list :int 10))))))
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}

View File

@@ -0,0 +1,55 @@
;; calculator.hs — recursive descent expression evaluator.
;;
;; Exercises:
;; - ADTs with constructor fields: TNum Int, TOp String, R Int [Token]
;; - Nested constructor pattern matching: (R v (TOp "+":rest))
;; - let bindings in function bodies
;; - Integer arithmetic including `div` (backtick infix)
;; - Left-associative multi-level operator precedence
(define
hk-prog-val
(fn
(src name)
(hk-deep-force (get (hk-eval-program (hk-core src)) name))))
(define
hk-calc-src
"data Token = TNum Int | TOp String\ndata Result = R Int [Token]\ngetV (R v _) = v\ngetR (R _ r) = r\neval ts = getV (parseExpr ts)\nparseExpr ts = parseExprRest (parseTerm ts)\nparseExprRest (R v (TOp \"+\":rest)) =\n let t = parseTerm rest\n in parseExprRest (R (v + getV t) (getR t))\nparseExprRest (R v (TOp \"-\":rest)) =\n let t = parseTerm rest\n in parseExprRest (R (v - getV t) (getR t))\nparseExprRest r = r\nparseTerm ts = parseTermRest (parseFactor ts)\nparseTermRest (R v (TOp \"*\":rest)) =\n let t = parseFactor rest\n in parseTermRest (R (v * getV t) (getR t))\nparseTermRest (R v (TOp \"/\":rest)) =\n let t = parseFactor rest\n in parseTermRest (R (v `div` getV t) (getR t))\nparseTermRest r = r\nparseFactor (TNum n:rest) = R n rest\n")
(hk-test
"calculator: 2 + 3 = 5"
(hk-prog-val
(str hk-calc-src "result = eval [TNum 2, TOp \"+\", TNum 3]\n")
"result")
5)
(hk-test
"calculator: 2 + 3 * 4 = 14 (precedence)"
(hk-prog-val
(str hk-calc-src "result = eval [TNum 2, TOp \"+\", TNum 3, TOp \"*\", TNum 4]\n")
"result")
14)
(hk-test
"calculator: 10 - 3 - 2 = 5 (left-assoc)"
(hk-prog-val
(str hk-calc-src "result = eval [TNum 10, TOp \"-\", TNum 3, TOp \"-\", TNum 2]\n")
"result")
5)
(hk-test
"calculator: 6 / 2 * 3 = 9 (left-assoc)"
(hk-prog-val
(str hk-calc-src "result = eval [TNum 6, TOp \"/\", TNum 2, TOp \"*\", TNum 3]\n")
"result")
9)
(hk-test
"calculator: single number"
(hk-prog-val
(str hk-calc-src "result = eval [TNum 42]\n")
"result")
42)
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}

View File

@@ -0,0 +1,45 @@
;; fib.hs — infinite Fibonacci stream classic program.
;;
;; The canonical artefact lives at lib/haskell/tests/programs/fib.hs.
;; The source is mirrored here as an SX string because the evaluator
;; doesn't have read-file in the default env. If you change one, keep
;; the other in sync — there's a runner-level cross-check against the
;; expected first-15 list.
(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-fib-source
"zipPlus (x:xs) (y:ys) = x + y : zipPlus xs ys
zipPlus _ _ = []
myFibs = 0 : 1 : zipPlus myFibs (tail myFibs)
result = take 15 myFibs
")
(hk-test
"fib.hs — first 15 Fibonacci numbers"
(hk-as-list (hk-prog-val hk-fib-source "result"))
(list 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377))
;; Spot-check that the user-defined zipPlus is also reachable
(hk-test
"fib.hs — zipPlus is a multi-clause user fn"
(hk-as-list
(hk-prog-val
(str hk-fib-source "extra = zipPlus [1, 2, 3] [10, 20, 30]\n")
"extra"))
(list 11 22 33))
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}

View File

@@ -0,0 +1,38 @@
;; nqueens.hs — n-queens solver via list comprehension + where.
;;
;; Also exercises:
;; - multi-clause let/where binding (go 0 = ...; go k = ...)
;; - list comprehensions (desugared to concatMap)
;; - abs (from Prelude)
;; - [1..n] finite range
;;
;; n=8 is too slow for a 60s timeout; n=4 and n=5 run in ~17s combined.
(define
hk-prog-val
(fn
(src name)
(hk-deep-force (get (hk-eval-program (hk-core src)) name))))
(define
hk-nq-base
"queens n = go n
where
go 0 = [[]]
go k = [q:qs | qs <- go (k - 1), q <- [1..n], safe q qs]
safe q qs = check q qs 1
check q [] _ = True
check q (c:cs) d = q /= c && abs (q - c) /= d && check q cs (d + 1)
")
(hk-test
"nqueens: queens 4 has 2 solutions"
(hk-prog-val (str hk-nq-base "result = length (queens 4)\n") "result")
2)
(hk-test
"nqueens: queens 5 has 10 solutions"
(hk-prog-val (str hk-nq-base "result = length (queens 5)\n") "result")
10)
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}

View File

@@ -0,0 +1,65 @@
;; quicksort.hs — naive functional quicksort.
(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-prog-val
(fn
(src name)
(hk-deep-force (get (hk-eval-program (hk-core src)) name))))
(define
hk-qs-source
"qsort [] = []
qsort (x:xs) = qsort smaller ++ [x] ++ qsort larger
where
smaller = filter (< x) xs
larger = filter (>= x) xs
result = qsort [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
")
(hk-test
"quicksort.hs — sort a list of ints"
(hk-as-list (hk-prog-val hk-qs-source "result"))
(list 1 1 2 3 3 4 5 5 5 6 9))
(hk-test
"quicksort.hs — empty list"
(hk-as-list
(hk-prog-val
(str hk-qs-source "e = qsort []\n")
"e"))
(list))
(hk-test
"quicksort.hs — singleton"
(hk-as-list
(hk-prog-val
(str hk-qs-source "s = qsort [42]\n")
"s"))
(list 42))
(hk-test
"quicksort.hs — already sorted"
(hk-as-list
(hk-prog-val
(str hk-qs-source "asc = qsort [1, 2, 3, 4, 5]\n")
"asc"))
(list 1 2 3 4 5))
(hk-test
"quicksort.hs — reverse sorted"
(hk-as-list
(hk-prog-val
(str hk-qs-source "desc = qsort [5, 4, 3, 2, 1]\n")
"desc"))
(list 1 2 3 4 5))
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}

View File

@@ -0,0 +1,48 @@
;; sieve.hs — lazy sieve of Eratosthenes.
;;
;; The canonical artefact lives at lib/haskell/tests/programs/sieve.hs.
;; Mirrored here as an SX string because the default eval env has no
;; read-file. Uses filter + backtick `mod` + lazy [2..] — all of which
;; are now wired in via Phase 3 + the mod/div additions to hk-binop.
(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-prog-val
(fn
(src name)
(hk-deep-force (get (hk-eval-program (hk-core src)) name))))
(define
hk-sieve-source
"sieve (p:xs) = p : sieve (filter (\\x -> x `mod` p /= 0) xs)
sieve [] = []
primes = sieve [2..]
result = take 10 primes
")
(hk-test
"sieve.hs — first 10 primes"
(hk-as-list (hk-prog-val hk-sieve-source "result"))
(list 2 3 5 7 11 13 17 19 23 29))
(hk-test
"sieve.hs — 20th prime is 71"
(nth
(hk-as-list
(hk-prog-val
(str
hk-sieve-source
"result20 = take 20 primes\n")
"result20"))
19)
71)
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}

View File

@@ -0,0 +1,40 @@
-- calculator.hs — recursive descent expression evaluator.
--
-- Tokens are represented as an ADT; the parser threads a [Token] list
-- through a custom Result type so pattern matching can destructure the
-- pair (value, remaining-tokens) directly inside constructor patterns.
--
-- Operator precedence: * and / bind tighter than + and -.
-- All operators are left-associative.
data Token = TNum Int | TOp String
data Result = R Int [Token]
getV (R v _) = v
getR (R _ r) = r
eval ts = getV (parseExpr ts)
parseExpr ts = parseExprRest (parseTerm ts)
parseExprRest (R v (TOp "+":rest)) =
let t = parseTerm rest
in parseExprRest (R (v + getV t) (getR t))
parseExprRest (R v (TOp "-":rest)) =
let t = parseTerm rest
in parseExprRest (R (v - getV t) (getR t))
parseExprRest r = r
parseTerm ts = parseTermRest (parseFactor ts)
parseTermRest (R v (TOp "*":rest)) =
let t = parseFactor rest
in parseTermRest (R (v * getV t) (getR t))
parseTermRest (R v (TOp "/":rest)) =
let t = parseFactor rest
in parseTermRest (R (v `div` getV t) (getR t))
parseTermRest r = r
parseFactor (TNum n:rest) = R n rest
result = eval [TNum 2, TOp "+", TNum 3, TOp "*", TNum 4]

View File

@@ -0,0 +1,15 @@
-- fib.hs — infinite Fibonacci stream.
--
-- The classic two-line definition: `fibs` is a self-referential
-- lazy list built by zipping itself with its own tail, summing the
-- pair at each step. Without lazy `:` (cons cell with thunked head
-- and tail) this would diverge before producing any output; with
-- it, `take 15 fibs` evaluates exactly as much of the spine as
-- demanded.
zipPlus (x:xs) (y:ys) = x + y : zipPlus xs ys
zipPlus _ _ = []
myFibs = 0 : 1 : zipPlus myFibs (tail myFibs)
result = take 15 myFibs

View File

@@ -0,0 +1,18 @@
-- nqueens.hs — n-queens backtracking solver.
--
-- `queens n` returns all solutions as lists of column positions,
-- one per row. Each call to `go k` extends all partial `(k-1)`-row
-- solutions by one safe queen, using a list comprehension whose guard
-- checks the new queen against all already-placed queens.
queens n = go n
where
go 0 = [[]]
go k = [q:qs | qs <- go (k - 1), q <- [1..n], safe q qs]
safe q qs = check q qs 1
check q [] _ = True
check q (c:cs) d = q /= c && abs (q - c) /= d && check q cs (d + 1)
result = length (queens 8)

View File

@@ -0,0 +1,12 @@
-- quicksort.hs — naive functional quicksort.
--
-- Partition by pivot, recurse on each half, concatenate.
-- Uses right sections `(< x)` and `(>= x)` with filter.
qsort [] = []
qsort (x:xs) = qsort smaller ++ [x] ++ qsort larger
where
smaller = filter (< x) xs
larger = filter (>= x) xs
result = qsort [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]

View File

@@ -0,0 +1,13 @@
-- sieve.hs — lazy sieve of Eratosthenes.
--
-- Each recursive call to `sieve` consumes one prime `p` off the front
-- of the input stream and produces an infinite stream of composites
-- filtered out via `filter`. Because cons is lazy, only as much of
-- the stream is forced as demanded by `take`.
sieve (p:xs) = p : sieve (filter (\x -> x `mod` p /= 0) xs)
sieve [] = []
primes = sieve [2..]
result = take 10 primes

View File

@@ -0,0 +1,127 @@
;; Runtime constructor-registry tests. Built-ins are pre-registered
;; when lib/haskell/runtime.sx loads; user types are registered by
;; walking a parsed+desugared AST with hk-register-program! (or the
;; `hk-load-source!` convenience).
;; ── Pre-registered built-ins ──
(hk-test "True is a con" (hk-is-con? "True") true)
(hk-test "False is a con" (hk-is-con? "False") true)
(hk-test "[] is a con" (hk-is-con? "[]") true)
(hk-test ": (cons) is a con" (hk-is-con? ":") true)
(hk-test "() is a con" (hk-is-con? "()") true)
(hk-test "True arity 0" (hk-con-arity "True") 0)
(hk-test ": arity 2" (hk-con-arity ":") 2)
(hk-test "[] arity 0" (hk-con-arity "[]") 0)
(hk-test "True type Bool" (hk-con-type "True") "Bool")
(hk-test "False type Bool" (hk-con-type "False") "Bool")
(hk-test ": type List" (hk-con-type ":") "List")
(hk-test "() type Unit" (hk-con-type "()") "Unit")
;; ── Unknown names ──
(hk-test "is-con? false for varid" (hk-is-con? "foo") false)
(hk-test "arity nil for unknown" (hk-con-arity "NotACon") nil)
(hk-test "type nil for unknown" (hk-con-type "NotACon") nil)
;; ── data MyBool = Yes | No ──
(hk-test
"register simple data"
(do
(hk-load-source! "data MyBool = Yes | No")
(list
(hk-con-arity "Yes")
(hk-con-arity "No")
(hk-con-type "Yes")
(hk-con-type "No")))
(list 0 0 "MyBool" "MyBool"))
;; ── data Maybe a = Nothing | Just a ──
(hk-test
"register Maybe"
(do
(hk-load-source! "data Maybe a = Nothing | Just a")
(list
(hk-con-arity "Nothing")
(hk-con-arity "Just")
(hk-con-type "Nothing")
(hk-con-type "Just")))
(list 0 1 "Maybe" "Maybe"))
;; ── data Either a b = Left a | Right b ──
(hk-test
"register Either"
(do
(hk-load-source! "data Either a b = Left a | Right b")
(list
(hk-con-arity "Left")
(hk-con-arity "Right")
(hk-con-type "Left")
(hk-con-type "Right")))
(list 1 1 "Either" "Either"))
;; ── Recursive data ──
(hk-test
"register recursive Tree"
(do
(hk-load-source!
"data Tree a = Leaf | Node (Tree a) a (Tree a)")
(list
(hk-con-arity "Leaf")
(hk-con-arity "Node")
(hk-con-type "Leaf")
(hk-con-type "Node")))
(list 0 3 "Tree" "Tree"))
;; ── newtype ──
(hk-test
"register newtype"
(do
(hk-load-source! "newtype Age = MkAge Int")
(list
(hk-con-arity "MkAge")
(hk-con-type "MkAge")))
(list 1 "Age"))
;; ── Multiple data decls in one program ──
(hk-test
"multiple data decls"
(do
(hk-load-source!
"data Color = Red | Green | Blue\ndata Shape = Circle | Square\nf x = x")
(list
(hk-con-type "Red")
(hk-con-type "Green")
(hk-con-type "Blue")
(hk-con-type "Circle")
(hk-con-type "Square")))
(list "Color" "Color" "Color" "Shape" "Shape"))
;; ── Inside a module header ──
(hk-test
"register from module body"
(do
(hk-load-source!
"module M where\ndata Pair a = Pair a a")
(list
(hk-con-arity "Pair")
(hk-con-type "Pair")))
(list 2 "Pair"))
;; ── Non-data decls are ignored ──
(hk-test
"program with only fun-decl leaves registry unchanged for that name"
(do
(hk-load-source! "myFunctionNotACon x = x + 1")
(hk-is-con? "myFunctionNotACon"))
false)
;; ── Re-registering overwrites (last wins) ──
(hk-test
"re-registration overwrites the entry"
(do
(hk-load-source! "data Foo = Bar Int")
(hk-load-source! "data Foo = Bar Int Int")
(hk-con-arity "Bar"))
2)
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}

85
lib/haskell/tests/seq.sx Normal file
View File

@@ -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}

View File

@@ -1,81 +0,0 @@
# apl-on-sx loop agent (single agent, queue-driven)
Role: iterates `plans/apl-on-sx.md` forever. Rank-polymorphic primitives + 6 operators on the JIT is the headline showcase — APL is the densest combinator algebra you can put on top of a primitive table. Every program is `array → array` pure pipelines, exactly what the JIT was built for.
```
description: apl-on-sx queue loop
subagent_type: general-purpose
run_in_background: true
isolation: worktree
```
## Prompt
You are the sole background agent working `/root/rose-ash/plans/apl-on-sx.md`. Isolated worktree, forever, one commit per feature. Never push.
## Restart baseline — check before iterating
1. Read `plans/apl-on-sx.md` — roadmap + Progress log.
2. `ls lib/apl/` — pick up from the most advanced file.
3. If `lib/apl/tests/*.sx` exist, run them. Green before new work.
4. If `lib/apl/scoreboard.md` exists, that's your baseline.
## The queue
Phase order per `plans/apl-on-sx.md`:
- **Phase 1** — tokenizer + parser. Unicode glyphs, `¯` for negative, strands (juxtaposition), right-to-left, valence resolution by syntactic position
- **Phase 2** — array model + scalar primitives. `make-array {shape, ravel}`, scalar promotion, broadcast for `+ - × ÷ ⌈ ⌊ * ⍟ | ! ○`, comparison, logical, ``, `⎕IO`
- **Phase 3** — structural primitives + indexing. ` , ⍉ ↑ ↓ ⌽ ⊖ ⌷ ⍋ ⍒ ⊂ ⊃ ∊`
- **Phase 4** — **THE SHOWCASE**: operators. `f/` (reduce), `f¨` (each), `∘.f` (outer), `f.g` (inner), `f⍨` (commute), `f∘g` (compose), `f⍣n` (power), `f⍤k` (rank), `@` (at)
- **Phase 5** — dfns + tradfns + control flow. `{+⍵}`, `∇` recurse, `←default`, tradfn header, `:If/:While/:For/:Select`
- **Phase 6** — classic programs (life, mandelbrot, primes, n-queens, quicksort) + idiom corpus + drive to 100+
Within a phase, pick the checkbox that unlocks the most tests per effort.
Every iteration: implement → test → commit → tick `[ ]` → Progress log → next.
## Ground rules (hard)
- **Scope:** only `lib/apl/**` and `plans/apl-on-sx.md`. Do **not** edit `spec/`, `hosts/`, `shared/`, other `lib/<lang>/` dirs, `lib/stdlib.sx`, or `lib/` root. APL primitives go in `lib/apl/runtime.sx`.
- **NEVER call `sx_build`.** 600s watchdog. If sx_server binary broken → Blockers entry, stop.
- **Shared-file issues** → plan's Blockers with minimal repro.
- **SX files:** `sx-tree` MCP tools ONLY. `sx_validate` after edits.
- **Unicode in `.sx`:** raw UTF-8 only, never `\uXXXX` escapes. Glyphs land directly in source.
- **Worktree:** commit locally. Never push. Never touch `main`.
- **Commit granularity:** one feature per commit.
- **Plan file:** update Progress log + tick boxes every commit.
## APL-specific gotchas
- **Right-to-left, no precedence among functions.** `2 × 3 + 4` is `2 × (3 + 4)` = 14, not 10. Operators bind tighter than functions: `+/ 5` is `+/(5)`, and `2 +.× 3 4` is `2 (+.×) 3 4`.
- **Valence by position.** `-3` is monadic negate (`-` with no left arg). `5-3` is dyadic subtract. The parser must look left to decide. Same glyph; different fn.
- **`¯` is part of a number literal**, not a prefix function. `¯3` is the literal negative three; `-3` is the function call. Tokenizer eats `¯` into the numeric token.
- **Strands.** `1 2 3` is a 3-element vector, not three separate calls. Adjacent literals fuse into a strand at parse time. Adjacent names do *not* fuse — `a b c` is three separate references.
- **Scalar promotion.** `1 + 2 3 4``3 4 5`. Any scalar broadcasts against any-rank conformable shape.
- **Conformability** = exactly matching shapes, OR one side scalar, OR (in some dialects) one side rank-1 cycling against rank-N. Keep strict in v1: matching shape or scalar only.
- **`` is overloaded.** Monadic `N` = vector 1..N (or 0..N-1 if `⎕IO=0`). Dyadic `V W` = first-index lookup, returns `≢V+1` for not-found.
- **Reduce with `+/0`** = `0` (identity for `+`). Each scalar primitive has a defined identity used by reduce-on-empty. Don't crash; return identity.
- **Reduce direction.** `f/` reduces the *last* axis. `f⌿` reduces the *first*. Matters for matrices.
- **Indexing is 1-based** by default (`⎕IO=1`). Do not silently translate to 0-based; respect `⎕IO`.
- **Bracket indexing** `A[I]` is sugar for `I⌷A` (squad-quad). Multi-axis: `A[I;J]` is `I J⌷A` with semicolon-separated axes; `A[;J]` selects all of axis 0.
- **Dfn `{...}`** — `` = left arg (may be unbound for monadic call → check with `←default`), `⍵` = right arg, `∇` = recurse. Default left arg syntax: `←0`.
- **Tradfn vs dfn** — tradfns use line-numbered `→linenum` for goto; dfns use guards `cond:expr`. Pick the right one for the user's syntax.
- **Empty array** = rank-N array where some dim is 0. `00` is empty rank-1. Scalar prototype matters for empty-array operations; ignore in v1, return 0/space.
- **Test corpus:** custom + idioms. Place programs in `lib/apl/tests/programs/` with `.apl` extension.
## General gotchas (all loops)
- SX `do` = R7RS iteration. Use `begin` for multi-expr sequences.
- `cond`/`when`/`let` clauses evaluate only the last expr.
- `type-of` on user fn returns `"lambda"`.
- Shell heredoc `||` gets eaten — escape or use `case`.
## Style
- No comments in `.sx` unless non-obvious.
- No new planning docs — update `plans/apl-on-sx.md` inline.
- Short, factual commit messages (`apl: outer product ∘. (+9)`).
- One feature per iteration. Commit. Log. Next.
Go. Read the plan; find first `[ ]`; implement.

View File

@@ -1,80 +0,0 @@
# common-lisp-on-sx loop agent (single agent, queue-driven)
Role: iterates `plans/common-lisp-on-sx.md` forever. Conditions + restarts on delimited continuations is the headline showcase — every other Lisp reinvents resumable exceptions on the host stack. On SX `signal`/`invoke-restart` is just a captured continuation. Plus CLOS, the LOOP macro, packages.
```
description: common-lisp-on-sx queue loop
subagent_type: general-purpose
run_in_background: true
isolation: worktree
```
## Prompt
You are the sole background agent working `/root/rose-ash/plans/common-lisp-on-sx.md`. Isolated worktree, forever, one commit per feature. Never push.
## Restart baseline — check before iterating
1. Read `plans/common-lisp-on-sx.md` — roadmap + Progress log.
2. `ls lib/common-lisp/` — pick up from the most advanced file.
3. If `lib/common-lisp/tests/*.sx` exist, run them. Green before new work.
4. If `lib/common-lisp/scoreboard.md` exists, that's your baseline.
## The queue
Phase order per `plans/common-lisp-on-sx.md`:
- **Phase 1** — reader + parser (read macros `#'` `'` `` ` `` `,` `,@` `#( … )` `#:` `#\char` `#xFF` `#b1010`, ratios, dispatch chars, lambda lists with `&optional`/`&rest`/`&key`/`&aux`)
- **Phase 2** — sequential eval + special forms (`let`/`let*`/`flet`/`labels`, `block`/`return-from`, `tagbody`/`go`, `unwind-protect`, multiple values, `setf` subset, dynamic variables)
- **Phase 3** — **THE SHOWCASE**: condition system + restarts. `define-condition`, `signal`/`error`/`cerror`/`warn`, `handler-bind` (non-unwinding), `handler-case` (unwinding), `restart-case`, `restart-bind`, `find-restart`/`invoke-restart`/`compute-restarts`, `with-condition-restarts`. Classic programs (restart-demo, parse-recover, interactive-debugger) green.
- **Phase 4** — CLOS: `defclass`, `defgeneric`, `defmethod` with `:before`/`:after`/`:around`, `call-next-method`, multiple dispatch
- **Phase 5** — macros + LOOP macro + reader macros
- **Phase 6** — packages + stdlib (sequence functions, FORMAT directives, drive corpus to 200+)
Within a phase, pick the checkbox that unlocks the most tests per effort.
Every iteration: implement → test → commit → tick `[ ]` → Progress log → next.
## Ground rules (hard)
- **Scope:** only `lib/common-lisp/**` and `plans/common-lisp-on-sx.md`. Do **not** edit `spec/`, `hosts/`, `shared/`, other `lib/<lang>/` dirs, `lib/stdlib.sx`, or `lib/` root. CL primitives go in `lib/common-lisp/runtime.sx`.
- **NEVER call `sx_build`.** 600s watchdog. If sx_server binary broken → Blockers entry, stop.
- **Shared-file issues** → plan's Blockers with minimal repro.
- **Delimited continuations** are in `lib/callcc.sx` + `spec/evaluator.sx` Step 5. `sx_summarise` spec/evaluator.sx first — 2300+ lines.
- **SX files:** `sx-tree` MCP tools ONLY. `sx_validate` after edits.
- **Worktree:** commit locally. Never push. Never touch `main`.
- **Commit granularity:** one feature per commit.
- **Plan file:** update Progress log + tick boxes every commit.
## Common-Lisp-specific gotchas
- **`handler-bind` is non-unwinding** — handlers can decline by returning normally, in which case `signal` keeps walking the chain. **`handler-case` is unwinding** — picking a handler aborts the protected form via a captured continuation. Don't conflate them.
- **Restarts are not handlers.** `restart-case` establishes named *resumption points*; `signal` runs handler code with restarts visible; the handler chooses a restart by calling `invoke-restart`, which abandons handler stack and resumes at the restart point. Two stacks: handlers walk down, restarts wait to be invoked.
- **`block` / `return-from`** is lexical. `block name … (return-from name v) …` captures `^k` once at entry; `return-from` invokes it. `return-from` to a name not in scope is an error (don't fall back to outer block).
- **`tagbody` / `go`** — each tag in tagbody is a continuation; `go tag` invokes it. Tags are lexical, can only target tagbodies in scope.
- **`unwind-protect`** runs cleanup on *any* non-local exit (return-from, throw, condition unwind). Implement as a scope frame fired by the cleanup machinery.
- **Multiple values**: primary-value-only contexts (function args, `if` test, etc.) drop extras silently. `values` produces multiple. `multiple-value-bind` / `multiple-value-call` consume them. Don't auto-list.
- **CLOS dispatch:** sort applicable methods by argument-list specificity (`subclassp` per arg, left-to-right); standard method combination calls primary methods most-specific-first via `call-next-method` chain. `:before` runs all before primaries; `:after` runs all after, in reverse-specificity. `:around` wraps everything.
- **`call-next-method`** is a *continuation* available only inside a method body. Implement as a thunk stored in a dynamic-extent variable.
- **Generalised reference (`setf`)**: `(setf (foo x) v)``(setf-foo v x)`. Look up the setf-expander, not just a writer fn. `define-setf-expander` is mandatory for non-trivial places. Start with the symbolic / list / aref / slot-value cases.
- **Dynamic variables (specials):** `defvar`/`defparameter` mark a symbol as special. `let` over a special name *rebinds* in dynamic extent (use parameterize-style scope), not lexical.
- **Symbols are package-qualified.** Reader resolves `cl:car`, `mypkg::internal`, bare `foo` (current package). Internal vs external matters for `:` (one colon) reads.
- **`nil` is also `()` is also the empty list.** Same object. `nil` is also false. CL has no distinct unit value.
- **LOOP macro is huge.** Build incrementally — start with `for/in`, `for/from`, `collect`, `sum`, `count`, `repeat`. Add conditional clauses (`when`, `if`, `else`) once iteration drivers stable. `named` blocks + `return-from named` last.
- **Test corpus:** custom + curated `ansi-test` slice. Place programs in `lib/common-lisp/tests/programs/` with `.lisp` extension.
## General gotchas (all loops)
- SX `do` = R7RS iteration. Use `begin` for multi-expr sequences.
- `cond`/`when`/`let` clauses evaluate only the last expr.
- `type-of` on user fn returns `"lambda"`.
- Shell heredoc `||` gets eaten — escape or use `case`.
## Style
- No comments in `.sx` unless non-obvious.
- No new planning docs — update `plans/common-lisp-on-sx.md` inline.
- Short, factual commit messages (`common-lisp: handler-bind + 12 tests`).
- One feature per iteration. Commit. Log. Next.
Go. Read the plan; find first `[ ]`; implement.

View File

@@ -1,83 +0,0 @@
# ruby-on-sx loop agent (single agent, queue-driven)
Role: iterates `plans/ruby-on-sx.md` forever. Fibers via delcc is the headline showcase — `Fiber.new`/`Fiber.yield`/`Fiber.resume` are textbook delimited continuations with sugar, where MRI does it via C-stack swapping. Plus blocks/yield (lexical escape continuations, same shape as Smalltalk's non-local return), method_missing, and singleton classes.
```
description: ruby-on-sx queue loop
subagent_type: general-purpose
run_in_background: true
isolation: worktree
```
## Prompt
You are the sole background agent working `/root/rose-ash/plans/ruby-on-sx.md`. Isolated worktree, forever, one commit per feature. Never push.
## Restart baseline — check before iterating
1. Read `plans/ruby-on-sx.md` — roadmap + Progress log.
2. `ls lib/ruby/` — pick up from the most advanced file.
3. If `lib/ruby/tests/*.sx` exist, run them. Green before new work.
4. If `lib/ruby/scoreboard.md` exists, that's your baseline.
## The queue
Phase order per `plans/ruby-on-sx.md`:
- **Phase 1** — tokenizer + parser. Keywords, identifier sigils (`@` ivar, `@@` cvar, `$` global), strings with interpolation, `%w[]`/`%i[]`, symbols, blocks `{|x| …}` and `do |x| … end`, splats, default args, method def
- **Phase 2** — object model + sequential eval. Class table, ancestor-chain dispatch, `super`, singleton classes, `method_missing` fallback, dynamic constant lookup
- **Phase 3** — blocks + procs + lambdas. Method captures escape continuation `^k`; `yield` / `return` / `break` / `next` / `redo` semantics; lambda strict arity vs proc lax
- **Phase 4** — **THE SHOWCASE**: fibers via delcc. `Fiber.new`/`Fiber.resume`/`Fiber.yield`/`Fiber.transfer`. Classic programs (generator, producer-consumer, tree-walk) green
- **Phase 5** — modules + mixins + metaprogramming. `include`/`prepend`/`extend`, `define_method`, `class_eval`/`instance_eval`, `respond_to?`/`respond_to_missing?`, hooks
- **Phase 6** — stdlib drive. `Enumerable` mixin, `Comparable`, Array/Hash/Range/String/Integer methods, drive corpus to 200+
Within a phase, pick the checkbox that unlocks the most tests per effort.
Every iteration: implement → test → commit → tick `[ ]` → Progress log → next.
## Ground rules (hard)
- **Scope:** only `lib/ruby/**` and `plans/ruby-on-sx.md`. Do **not** edit `spec/`, `hosts/`, `shared/`, other `lib/<lang>/` dirs, `lib/stdlib.sx`, or `lib/` root. Ruby primitives go in `lib/ruby/runtime.sx`.
- **NEVER call `sx_build`.** 600s watchdog. If sx_server binary broken → Blockers entry, stop.
- **Shared-file issues** → plan's Blockers with minimal repro.
- **Delimited continuations** are in `lib/callcc.sx` + `spec/evaluator.sx` Step 5. `sx_summarise` spec/evaluator.sx first — 2300+ lines.
- **SX files:** `sx-tree` MCP tools ONLY. `sx_validate` after edits.
- **Worktree:** commit locally. Never push. Never touch `main`.
- **Commit granularity:** one feature per commit.
- **Plan file:** update Progress log + tick boxes every commit.
## Ruby-specific gotchas
- **Block `return` vs lambda `return`.** Inside a block `{ ... return v }`, `return` invokes the *enclosing method's* escape continuation (non-local return). Inside a lambda `->(){ ... return v }`, `return` returns from the *lambda*. Don't conflate. Implement: blocks bind their `^method-k`; lambdas bind their own `^lambda-k`.
- **`break` from inside a block** invokes a different escape — the *iteration loop's* escape — and the loop returns the break-value. `next` is escape from current iteration, returns iteration value. `redo` re-enters current iteration without advancing.
- **Proc arity is lax.** `proc { |a, b, c| … }.call(1, 2)``c = nil`. Lambda is strict — same call raises ArgumentError. Check arity at call site for lambdas only.
- **Block argument unpacking.** `[[1,2],[3,4]].each { |a, b| … }` — single Array arg auto-unpacks for blocks (not lambdas). One arg, one Array → unpack. Frequent footgun.
- **Method dispatch chain order:** prepended modules → class methods → included modules → superclass → BasicObject → method_missing. `super` walks from the *defining* class's position, not the receiver class's.
- **Singleton classes** are lazily allocated. Looking up the chain for an object passes through its singleton class first, then its actual class. `class << obj; …; end` opens the singleton.
- **`method_missing`** — fallback when ancestor walk misses. Receives `(name_symbol, *args, &blk)`. Pair with `respond_to_missing?` for `respond_to?` to also report true. Do **not** swallow NoMethodError silently.
- **Ivars are per-object dicts.** Reading an unset ivar yields `nil` and a warning (`-W`). Don't error.
- **Constant lookup** is first lexical (Module.nesting), then inheritance (Module.ancestors of the innermost class). Different from method lookup.
- **`Object#send`** invokes private and public methods alike; `Object#public_send` skips privates.
- **Class reopening.** `class Foo; def bar; …; end; end` plus a later `class Foo; def baz; …; end; end` adds methods to the same class. Class table lookups must be by-name, mutable; methods dict is mutable.
- **Fiber semantics.** `Fiber.new { |arg| … }` creates a fiber suspended at entry. First `Fiber.resume(v)` enters with `arg = v`. Inside, `Fiber.yield(w)` returns `w` to the resumer; the next `Fiber.resume(v')` returns `v'` to the yield site. End of block returns final value to last resumer; subsequent `Fiber.resume` raises FiberError.
- **`Fiber.transfer`** is symmetric — either side can transfer to the other; no resume/yield asymmetry. Implement on top of the same continuation pair, just don't enforce direction.
- **Symbols are interned.** `:foo == :foo` is identity. Use SX symbols.
- **Strings are mutable.** `s = "abc"; s << "d"; s == "abcd"`. Hash keys can be strings; hash dups string keys at insertion to be safe (or freeze them).
- **Truthiness:** only `false` and `nil` are falsy. `0`, `""`, `[]` are truthy.
- **Test corpus:** custom + curated RubySpec slice. Place programs in `lib/ruby/tests/programs/` with `.rb` extension.
## General gotchas (all loops)
- SX `do` = R7RS iteration. Use `begin` for multi-expr sequences.
- `cond`/`when`/`let` clauses evaluate only the last expr.
- `type-of` on user fn returns `"lambda"`.
- Shell heredoc `||` gets eaten — escape or use `case`.
## Style
- No comments in `.sx` unless non-obvious.
- No new planning docs — update `plans/ruby-on-sx.md` inline.
- Short, factual commit messages (`ruby: Fiber.yield + Fiber.resume (+8)`).
- One feature per iteration. Commit. Log. Next.
Go. Read the plan; find first `[ ]`; implement.

View File

@@ -1,77 +0,0 @@
# smalltalk-on-sx loop agent (single agent, queue-driven)
Role: iterates `plans/smalltalk-on-sx.md` forever. Message-passing OO + **blocks with non-local return** on delimited continuations. Non-local return is the headline showcase — every other Smalltalk reinvents it on the host stack; on SX it falls out of the captured method-return continuation.
```
description: smalltalk-on-sx queue loop
subagent_type: general-purpose
run_in_background: true
isolation: worktree
```
## Prompt
You are the sole background agent working `/root/rose-ash/plans/smalltalk-on-sx.md`. Isolated worktree, forever, one commit per feature. Never push.
## Restart baseline — check before iterating
1. Read `plans/smalltalk-on-sx.md` — roadmap + Progress log.
2. `ls lib/smalltalk/` — pick up from the most advanced file.
3. If `lib/smalltalk/tests/*.sx` exist, run them. Green before new work.
4. If `lib/smalltalk/scoreboard.md` exists, that's your baseline.
## The queue
Phase order per `plans/smalltalk-on-sx.md`:
- **Phase 1** — tokenizer + parser (chunk format, identifiers, keywords `foo:`, binary selectors, `#sym`, `#(…)`, `$c`, blocks `[:a | …]`, cascades, message precedence)
- **Phase 2** — object model + sequential eval (class table bootstrap, message dispatch, `super`, `doesNotUnderstand:`, instance variables)
- **Phase 3** — **THE SHOWCASE**: blocks with non-local return via captured method-return continuation. `whileTrue:` / `ifTrue:ifFalse:` as block sends. 5 classic programs (eight-queens, quicksort, mandelbrot, life, fibonacci) green.
- **Phase 4** — reflection + MOP: `perform:`, `respondsTo:`, runtime method addition, `becomeForward:`, `Exception` / `on:do:` / `ensure:` on top of `handler-bind`/`raise`
- **Phase 5** — collections + numeric tower + streams
- **Phase 6** — port SUnit, vendor Pharo Kernel-Tests slice, drive corpus to 200+
- **Phase 7** — speed (optional): inline caching, block intrinsification
Within a phase, pick the checkbox that unlocks the most tests per effort.
Every iteration: implement → test → commit → tick `[ ]` → Progress log → next.
## Ground rules (hard)
- **Scope:** only `lib/smalltalk/**` and `plans/smalltalk-on-sx.md`. Do **not** edit `spec/`, `hosts/`, `shared/`, other `lib/<lang>/` dirs, `lib/stdlib.sx`, or `lib/` root. Smalltalk primitives go in `lib/smalltalk/runtime.sx`.
- **NEVER call `sx_build`.** 600s watchdog. If sx_server binary broken → Blockers entry, stop.
- **Shared-file issues** → plan's Blockers with minimal repro.
- **Delimited continuations** are in `lib/callcc.sx` + `spec/evaluator.sx` Step 5. `sx_summarise` spec/evaluator.sx first — 2300+ lines.
- **SX files:** `sx-tree` MCP tools ONLY. `sx_validate` after edits.
- **Worktree:** commit locally. Never push. Never touch `main`.
- **Commit granularity:** one feature per commit.
- **Plan file:** update Progress log + tick boxes every commit.
## Smalltalk-specific gotchas
- **Method invocation captures `^k`** — the return continuation. Bind it as the block's escape token. `^expr` from inside any nested block invokes that captured `^k`. Escape past method return raises `BlockContext>>cannotReturn:`.
- **Blocks are lambdas + escape token**, not bare lambdas. `value`/`value:`/… invoke the lambda; `^` invokes the escape.
- **`ifTrue:` / `ifFalse:` / `whileTrue:` are ordinary block sends** — no special form. The runtime intrinsifies them in the JIT path (Tier 1 of bytecode expansion already covers this pattern).
- **Cascade** `r m1; m2; m3` desugars to `(let ((tmp r)) (st-send tmp 'm1 ()) (st-send tmp 'm2 ()) (st-send tmp 'm3 ()))`. Result is the cascade's last send (or first, depending on parser variant — pick one and document).
- **`super` send** looks up starting from the *defining* class's superclass, not the receiver class. Stash the defining class on the method record.
- **Selectors are interned symbols.** Use SX symbols.
- **Receiver dispatch:** tagged ints / floats / strings / symbols / `nil` / `true` / `false` aren't boxed. Their classes (`SmallInteger`, `Float`, `String`, `Symbol`, `UndefinedObject`, `True`, `False`) are looked up by SX type-of, not by an `:class` field.
- **Method precedence:** unary > binary > keyword. `3 + 4 factorial` is `3 + (4 factorial)`. `a foo: b bar` is `a foo: (b bar)` (keyword absorbs trailing unary).
- **Image / fileIn / become: between sessions** = out of scope. One-way `becomeForward:` only.
- **Test corpus:** ~200 hand-written + a slice of Pharo Kernel-Tests. Place programs in `lib/smalltalk/tests/programs/`.
## General gotchas (all loops)
- SX `do` = R7RS iteration. Use `begin` for multi-expr sequences.
- `cond`/`when`/`let` clauses evaluate only the last expr.
- `type-of` on user fn returns `"lambda"`.
- Shell heredoc `||` gets eaten — escape or use `case`.
## Style
- No comments in `.sx` unless non-obvious.
- No new planning docs — update `plans/smalltalk-on-sx.md` inline.
- Short, factual commit messages (`smalltalk: tokenizer + 56 tests`).
- One feature per iteration. Commit. Log. Next.
Go. Read the plan; find first `[ ]`; implement.

View File

@@ -1,83 +0,0 @@
# tcl-on-sx loop agent (single agent, queue-driven)
Role: iterates `plans/tcl-on-sx.md` forever. `uplevel`/`upvar` is the headline showcase — Tcl's superpower for defining your own control structures, requiring deep VM cooperation in any normal host but falling out of SX's first-class env-chain. Plus the Dodekalogue (12 rules), command-substitution everywhere, and "everything is a string" homoiconicity.
```
description: tcl-on-sx queue loop
subagent_type: general-purpose
run_in_background: true
isolation: worktree
```
## Prompt
You are the sole background agent working `/root/rose-ash/plans/tcl-on-sx.md`. Isolated worktree, forever, one commit per feature. Never push.
## Restart baseline — check before iterating
1. Read `plans/tcl-on-sx.md` — roadmap + Progress log.
2. `ls lib/tcl/` — pick up from the most advanced file.
3. If `lib/tcl/tests/*.sx` exist, run them. Green before new work.
4. If `lib/tcl/scoreboard.md` exists, that's your baseline.
## The queue
Phase order per `plans/tcl-on-sx.md`:
- **Phase 1** — tokenizer + parser. The Dodekalogue (12 rules): word-splitting, command sub `[…]`, var sub `$name`/`${name}`/`$arr(idx)`, double-quote vs brace word, backslash, `;`, `#` comments only at command start, single-pass left-to-right substitution
- **Phase 2** — sequential eval + core commands. `set`/`unset`/`incr`/`append`/`lappend`, `puts`/`gets`, `expr` (own mini-language), `if`/`while`/`for`/`foreach`/`switch`, string commands, list commands, dict commands
- **Phase 3** — **THE SHOWCASE**: `proc` + `uplevel` + `upvar`. Frame stack with proc-call push/pop; `uplevel #N script` evaluates in caller's frame; `upvar` aliases names across frames. Classic programs (for-each-line, assert macro, with-temp-var) green
- **Phase 4** — `return -code N`, `catch`, `try`/`trap`/`finally`, `throw`. Control flow as integer codes
- **Phase 5** — namespaces + ensembles. `namespace eval`, qualified names `::ns::cmd`, ensembles, `namespace path`
- **Phase 6** — coroutines (built on fibers, same delcc as Ruby fibers) + system commands + drive corpus to 150+
Within a phase, pick the checkbox that unlocks the most tests per effort.
Every iteration: implement → test → commit → tick `[ ]` → Progress log → next.
## Ground rules (hard)
- **Scope:** only `lib/tcl/**` and `plans/tcl-on-sx.md`. Do **not** edit `spec/`, `hosts/`, `shared/`, other `lib/<lang>/` dirs, `lib/stdlib.sx`, or `lib/` root. Tcl primitives go in `lib/tcl/runtime.sx`.
- **NEVER call `sx_build`.** 600s watchdog. If sx_server binary broken → Blockers entry, stop.
- **Shared-file issues** → plan's Blockers with minimal repro.
- **Delimited continuations** are in `lib/callcc.sx` + `spec/evaluator.sx` Step 5. `sx_summarise` spec/evaluator.sx first — 2300+ lines.
- **SX files:** `sx-tree` MCP tools ONLY. `sx_validate` after edits.
- **Worktree:** commit locally. Never push. Never touch `main`.
- **Commit granularity:** one feature per commit.
- **Plan file:** update Progress log + tick boxes every commit.
## Tcl-specific gotchas
- **Everything is a string.** Internally cache shimmer reps (list, dict, int, double) for performance, but every value must be re-stringifiable. Mutating one rep dirties the cached string and vice versa.
- **The Dodekalogue is strict.** Substitution is **one-pass**, **left-to-right**. The result of a substitution is a value, not a script — it does NOT get re-parsed for further substitutions. This is what makes Tcl safe-by-default. Don't accidentally re-parse.
- **Brace word `{…}`** is the only way to defer evaluation. No substitution inside, just balanced braces. Used for `if {expr}` body, `proc body`, `expr` arguments.
- **Double-quote word `"…"`** is identical to a bare word for substitution purposes — it just allows whitespace in a single word. `\` escapes still apply.
- **Comments are only at command position.** `# this is a comment` after a `;` or newline; *not* inside a command. `set x 1 # not a comment` is a 4-arg `set`.
- **`expr` has its own grammar** — operator precedence, function calls — and does its own substitution. Brace `expr {$x + 1}` to avoid double-substitution and to enable bytecode caching.
- **`if` and `while` re-parse** the condition only if not braced. Always use `if {…}`/`while {…}` form. The unbraced form re-substitutes per iteration.
- **`return` from a `proc`** uses control code 2. `break` is 3, `continue` is 4. `error` is 1. `catch` traps any non-zero code; user can return non-zero with `return -code error -errorcode FOO message`.
- **`uplevel #0 script`** is global frame. `uplevel 1 script` (or just `uplevel script`) is caller's frame. `uplevel #N` is absolute level N (0=global, 1=top-level proc, 2=proc-called-from-top, …). Negative levels are errors.
- **`upvar #N otherVar localVar`** binds `localVar` in the current frame as an *alias* — both names refer to the same storage. Reads and writes go through the alias.
- **`info level`** with no arg returns current level number. `info level N` (positive) returns the command list that invoked level N. `info level -N` returns the command list of the level N relative-up.
- **Variable names with `(…)`** are array elements: `set arr(foo) 1`. Arrays are not first-class values — you can't `set x $arr`. `array get arr` gives a flat list `{key1 val1 key2 val2 …}`.
- **List vs string.** `set l "a b c"` and `set l [list a b c]` look the same when printed but the second has a cached list rep. `lindex` works on both via shimmering. Most user code can't tell the difference.
- **`incr x`** errors if x doesn't exist; pre-set with `set x 0` or use `incr x 0` first if you mean "create-or-increment". Or use `dict incr` for dicts.
- **Coroutines are fibers.** `coroutine name body` starts a coroutine; calling `name` resumes it; `yield value` from inside suspends and returns `value` to the resumer. Same primitive as Ruby fibers — share the implementation under the hood.
- **`switch`** matches first clause whose pattern matches. Default is `default`. Variant matches: glob (default), `-exact`, `-glob`, `-regexp`. Body `-` means "fall through to next clause's body".
- **Test corpus:** custom + slice of Tcl's own tests. Place programs in `lib/tcl/tests/programs/` with `.tcl` extension.
## General gotchas (all loops)
- SX `do` = R7RS iteration. Use `begin` for multi-expr sequences.
- `cond`/`when`/`let` clauses evaluate only the last expr.
- `type-of` on user fn returns `"lambda"`.
- Shell heredoc `||` gets eaten — escape or use `case`.
## Style
- No comments in `.sx` unless non-obvious.
- No new planning docs — update `plans/tcl-on-sx.md` inline.
- Short, factual commit messages (`tcl: uplevel + upvar (+11)`).
- One feature per iteration. Commit. Log. Next.
Go. Read the plan; find first `[ ]`; implement.

View File

@@ -1,115 +0,0 @@
# APL-on-SX: rank-polymorphic primitives + glyph parser
The headline showcase is **rank polymorphism** — a single primitive (`+`, `⌈`, `⊂`, ``) works uniformly on scalars, vectors, matrices, and higher-rank arrays. ~80 glyph primitives + 6 operators bind together with right-to-left evaluation; the entire language is a high-density combinator algebra. The JIT compiler + primitive table pay off massively here because almost every program is `array → array` pure pipelines.
End-state goal: Dyalog-flavoured APL subset, dfns + tradfns, classic programs (game-of-life, mandelbrot, prime-sieve, n-queens, conway), 100+ green tests.
## Scope decisions (defaults — override by editing before we spawn)
- **Syntax:** Dyalog APL surface, Unicode glyphs. `⎕`-quad system functions for I/O. `∇` tradfn header.
- **Conformance:** "Reads like APL, runs like APL." Not byte-compat with Dyalog; we care about right-to-left semantics and rank polymorphism.
- **Test corpus:** custom — APL idioms (Roger Hui style), classic programs, plus ~50 pattern tests for primitives.
- **Out of scope:** ⎕-namespaces beyond a handful, complex numbers, full TAO ordering, `⎕FX` runtime function definition (use static `∇` only), nested-array-of-functions higher orders, the editor.
- **Glyphs:** input via plain Unicode in `.apl` source files. Backtick-prefix shortcuts handled by the user's editor — we don't ship one.
## Ground rules
- **Scope:** only touch `lib/apl/**` and `plans/apl-on-sx.md`. Don't edit `spec/`, `hosts/`, `shared/`, or any other `lib/<lang>/**`. APL primitives go in `lib/apl/runtime.sx`.
- **SX files:** use `sx-tree` MCP tools only.
- **Commits:** one feature per commit. Keep `## Progress log` updated and tick roadmap boxes.
## Architecture sketch
```
APL source (Unicode glyphs)
lib/apl/tokenizer.sx — glyphs, identifiers, numbers (¯ for negative), strings, strands
lib/apl/parser.sx — right-to-left with valence resolution (mon vs dyadic by position)
lib/apl/transpile.sx — AST → SX AST (entry: apl-eval-ast)
lib/apl/runtime.sx — array model, ~80 primitives, 6 operators, dfns/tradfns
```
Core mapping:
- **Array** = SX dict `{:shape (d1 d2 …) :ravel #(v1 v2 …)}`. Scalar is rank-0 (empty shape), vector is rank-1, matrix rank-2, etc. Type uniformity not required (heterogeneous nested arrays via "boxed" elements `⊂x`).
- **Rank polymorphism** — every scalar primitive is broadcast: `1 2 3 + 4 5 6``5 7 9`; `(2 36) + 1` ↦ broadcast scalar to matrix.
- **Conformability** = matching shapes, or one-side scalar, or rank-1 cycling (deferred — keep strict in v1).
- **Valence** = each glyph has a monadic and a dyadic meaning; resolution is purely positional (left-arg present → dyadic).
- **Operator** = takes one or two function operands, returns a derived function (`f¨` = `each f`, `f/` = `reduce f`, `f∘g` = `compose`, `f⍨` = `commute`).
- **Tradfn** `∇R←L F R; locals` = named function with explicit header.
- **Dfn** `{+⍵}` = anonymous, `` = left arg, `⍵` = right arg, `∇` = recurse.
## Roadmap
### Phase 1 — tokenizer + parser
- [ ] Tokenizer: Unicode glyphs (the full APL set: `+ - × ÷ * ⍟ ⌈ ⌊ | ! ? ○ ~ < ≤ = ≥ > ≠ ∊ ∧ ⍱ ⍲ , ⍪ ⌽ ⊖ ⍉ ↑ ↓ ⊂ ⊃ ⊆ ⍸ ⌷ ⍋ ⍒ ⊥ ⊣ ⊢ ⍎ ⍕ ⍝`), operators (`/ \ ¨ ⍨ ∘ . ⍣ ⍤ ⍥ @`), numbers (`¯` for negative, `1E2`, `1J2` complex deferred), characters (`'a'`, `''` escape), strands (juxtaposition of literals: `1 2 3`), names, comments `⍝ …`
- [ ] Parser: right-to-left; classify each token as function, operator, value, or name; resolve valence positionally; dfn `{…}` body, tradfn `∇` header, guards `:`, control words `:If :While :For …` (Dyalog-style)
- [ ] Unit tests in `lib/apl/tests/parse.sx`
### Phase 2 — array model + scalar primitives
- [ ] Array constructor: `make-array shape ravel`, `scalar v`, `vector v…`, `enclose`/`disclose`
- [ ] Shape arithmetic: `` (shape), `,` (ravel), `≢` (tally / first-axis-length), `≡` (depth)
- [ ] Scalar arithmetic primitives broadcast: `+ - × ÷ ⌈ ⌊ * ⍟ | ! ○`
- [ ] Scalar comparison primitives: `< ≤ = ≥ > ≠`
- [ ] Scalar logical: `~ ∧ ⍱ ⍲`
- [ ] Index generator: `n` (vector 1..n or 0..n-1 depending on `⎕IO`)
- [ ] `⎕IO` = 1 default (Dyalog convention)
- [ ] 40+ tests in `lib/apl/tests/scalar.sx`
### Phase 3 — structural primitives + indexing
- [ ] Reshape ``, ravel `,`, transpose `⍉` (full + dyadic axis spec)
- [ ] Take `↑`, drop `↓`, rotate `⌽` (last axis), `⊖` (first axis)
- [ ] Catenate `,` (last axis) and `⍪` (first axis)
- [ ] Index `⌷` (squad), bracket-indexing `A[I]` (sugar for `⌷`)
- [ ] Grade-up `⍋`, grade-down `⍒`
- [ ] Enclose `⊂`, disclose `⊃`, partition (subset deferred)
- [ ] Membership `∊`, find `` (dyadic), without `~` (dyadic), unique `` (deferred to phase 6)
- [ ] 40+ tests in `lib/apl/tests/structural.sx`
### Phase 4 — operators (THE SHOWCASE)
- [ ] Reduce `f/` (last axis), `f⌿` (first axis) — including `∧/`, `/`, `+/`, `×/`, `⌈/`, `⌊/`
- [ ] Scan `f\`, `f⍀`
- [ ] Each `f¨` — applies `f` to each scalar/element
- [ ] Outer product `∘.f``1 2 3 ∘.× 1 2 3` ↦ multiplication table
- [ ] Inner product `f.g``+.×` is matrix multiply
- [ ] Commute `f⍨``f⍨ x``x f x`, `x f⍨ y``y f x`
- [ ] Compose `f∘g` — applies `g` first then `f`
- [ ] Power `f⍣n` — apply f n times; `f⍣≡` until fixed point
- [ ] Rank `f⍤k` — apply f at sub-rank k
- [ ] At `@` — selective replace
- [ ] 40+ tests in `lib/apl/tests/operators.sx`
### Phase 5 — dfns + tradfns + control flow
- [ ] Dfn `{…}` with `` (left arg, may be absent → niladic/monadic), `⍵` (right arg), `∇` (recurse), guards `cond:expr`, default left arg `←default`
- [ ] Local assignment via `←` (lexical inside dfn)
- [ ] Tradfn `∇` header: `R←L F R;l1;l2`, statement-by-statement, branch via `→linenum`
- [ ] Dyalog control words: `:If/:Else/:EndIf`, `:While/:EndWhile`, `:For X :In V :EndFor`, `:Select/:Case/:EndSelect`, `:Trap`/`:EndTrap`
- [ ] Niladic / monadic / dyadic dispatch (function valence at definition time)
- [ ] `lib/apl/conformance.sh` + runner, `scoreboard.json` + `scoreboard.md`
### Phase 6 — classic programs + drive corpus
- [ ] Classic programs in `lib/apl/tests/programs/`:
- [ ] `life.apl` — Conway's Game of Life as a one-liner using `⊂` `⊖` `⌽` `+/`
- [ ] `mandelbrot.apl` — complex iteration with rank-polymorphic `+ × ⌊` (or real-axis subset)
- [ ] `primes.apl``(2=+⌿0=A∘.|A)/A←N` sieve
- [ ] `n-queens.apl` — backtracking via reduce
- [ ] `quicksort.apl` — the classic Roger Hui one-liner
- [ ] System functions: `⎕FMT`, `⎕FR` (float repr), `⎕TS` (timestamp), `⎕IO`, `⎕ML` (migration level — fixed at 1), `⎕←` (print)
- [ ] Drive corpus to 100+ green
- [ ] Idiom corpus — `lib/apl/tests/idioms.sx` covering classic Roger Hui / Phil Last idioms
## Progress log
_Newest first._
- _(none yet)_
## Blockers
- _(none yet)_

View File

@@ -1,124 +0,0 @@
# Common-Lisp-on-SX: conditions + restarts on delimited continuations
The headline showcase is the **condition system**. Restarts are *resumable* exceptions — every other Lisp implementation reinvents this on host-stack unwind tricks. On SX restarts are textbook delimited continuations: `signal` walks the handler chain; `invoke-restart` resumes the captured continuation at the restart point. Same delcc primitive that powers Erlang actors, expressed as a different surface.
End-state goal: ANSI Common Lisp subset with a working condition/restart system, CLOS multimethods (with `:before`/`:after`/`:around`), the LOOP macro, packages, and ~150 hand-written + classic programs.
## Scope decisions (defaults — override by editing before we spawn)
- **Syntax:** ANSI Common Lisp surface. Read tables, dispatch macros (`#'`, `#(`, `#\`, `#:`, `#x`, `#b`, `#o`, ratios `1/3`).
- **Conformance:** ANSI X3.226 *as a target*, not bug-for-bug SBCL/CCL. "Reads like CL, runs like CL."
- **Test corpus:** custom + a curated slice of `ansi-test`. Plus classic programs: condition-system demo, restart-driven debugger, multiple-dispatch geometry, LOOP corpus.
- **Out of scope:** compilation to native, FFI, sockets, threads, MOP class redefinition, full pathname/logical-pathname machinery, structures with `:include` deep customization.
- **Packages:** simple — `defpackage`/`in-package`/`export`/`use-package`/`:cl`/`:cl-user`. No nicknames, no shadowing-import edge cases.
## Ground rules
- **Scope:** only touch `lib/common-lisp/**` and `plans/common-lisp-on-sx.md`. Don't edit `spec/`, `hosts/`, `shared/`, or any other `lib/<lang>/**`. CL primitives go in `lib/common-lisp/runtime.sx`.
- **SX files:** use `sx-tree` MCP tools only.
- **Commits:** one feature per commit. Keep `## Progress log` updated and tick roadmap boxes.
## Architecture sketch
```
Common Lisp source
lib/common-lisp/reader.sx — tokenizer + reader (read macros, dispatch chars)
lib/common-lisp/parser.sx — AST: forms, declarations, lambda lists
lib/common-lisp/transpile.sx — AST → SX AST (entry: cl-eval-ast)
lib/common-lisp/runtime.sx — special forms, condition system, CLOS, packages, BIFs
```
Core mapping:
- **Symbol** = SX symbol with package prefix; package table is a flat dict.
- **Cons cell** = SX pair via `cons`/`car`/`cdr`; lists native.
- **Multiple values** = thread through `values`/`multiple-value-bind`; primary-value default for one-context callers.
- **Block / return-from** = captured continuation; `return-from name v` invokes the block-named `^k`.
- **Tagbody / go** = each tag is a continuation; `go tag` invokes it.
- **Unwind-protect** = scope frame with a cleanup thunk fired on any non-local exit.
- **Conditions / restarts** = layered handler chain on top of `handler-bind` + delcc. `signal` walks handlers; `invoke-restart` resumes a captured continuation.
- **CLOS** = generic functions are dispatch tables on argument-class lists; method combination computed lazily; `call-next-method` is a continuation.
- **Macros** = SX macros (sentinel-body) — defmacro lowers directly.
## Roadmap
### Phase 1 — reader + parser
- [x] Tokenizer: symbols (with package qualification `pkg:sym` / `pkg::sym`), numbers (int, float, ratio `1/3`, `#xFF`, `#b1010`, `#o17`), strings `"…"` with `\` escapes, characters `#\Space` `#\Newline` `#\a`, comments `;`, block comments `#| … |#`
- [x] Reader: list, dotted pair, quote `'`, function `#'`, quasiquote `` ` ``, unquote `,`, splice `,@`, vector `#(…)`, uninterned `#:foo`, nil/t literals
- [x] Parser: lambda lists with `&optional` `&rest` `&key` `&aux` `&allow-other-keys`, defaults, supplied-p variables
- [x] Unit tests in `lib/common-lisp/tests/read.sx`
### Phase 2 — sequential eval + special forms
- [x] `cl-eval-ast`: `quote`, `if`, `progn`, `let`, `let*`, `flet`, `labels`, `setq`, `setf` (subset), `function`, `lambda`, `the`, `locally`, `eval-when`
- [ ] `block` + `return-from` via captured continuation
- [ ] `tagbody` + `go` via per-tag continuations
- [ ] `unwind-protect` cleanup frame
- [ ] `multiple-value-bind`, `multiple-value-call`, `multiple-value-prog1`, `values`, `nth-value`
- [x] `defun`, `defparameter`, `defvar`, `defconstant`, `declaim`, `proclaim` (no-op)
- [ ] Dynamic variables — `defvar`/`defparameter` produce specials; `let` rebinds via parameterize-style scope
- [x] 127 tests in `lib/common-lisp/tests/eval.sx`
### Phase 3 — conditions + restarts (THE SHOWCASE)
- [ ] `define-condition` — class hierarchy rooted at `condition`/`error`/`warning`/`simple-error`/`simple-warning`/`type-error`/`arithmetic-error`/`division-by-zero`
- [ ] `signal`, `error`, `cerror`, `warn` — all walk the handler chain
- [ ] `handler-bind` — non-unwinding handlers, may decline by returning normally
- [ ] `handler-case` — unwinding handlers (delcc abort)
- [ ] `restart-case`, `with-simple-restart`, `restart-bind`
- [ ] `find-restart`, `invoke-restart`, `invoke-restart-interactively`, `compute-restarts`
- [ ] `with-condition-restarts` — associate restarts with a specific condition
- [ ] `*break-on-signals*`, `*debugger-hook*` (basic)
- [ ] Classic programs in `lib/common-lisp/tests/programs/`:
- [ ] `restart-demo.lisp` — division with `:use-zero` and `:retry` restarts
- [ ] `parse-recover.lisp` — parser with skipped-token restart
- [ ] `interactive-debugger.lisp` — ASCII REPL using `:debugger-hook`
- [ ] `lib/common-lisp/conformance.sh` + runner, `scoreboard.json` + `scoreboard.md`
### Phase 4 — CLOS
- [ ] `defclass` with `:initarg`/`:initform`/`:accessor`/`:reader`/`:writer`/`:allocation`
- [ ] `make-instance`, `slot-value`, `(setf slot-value)`, `with-slots`, `with-accessors`
- [ ] `defgeneric` with `:method-combination` (standard, plus `+`, `and`, `or`)
- [ ] `defmethod` with `:before` / `:after` / `:around` qualifiers
- [ ] `call-next-method` (continuation), `next-method-p`
- [ ] `class-of`, `find-class`, `slot-boundp`, `change-class` (basic)
- [ ] Multiple dispatch — method specificity by argument-class precedence list
- [ ] Built-in classes registered for tagged values (`integer`, `float`, `string`, `symbol`, `cons`, `null`, `t`)
- [ ] Classic programs:
- [ ] `geometry.lisp``intersect` generic dispatching on (point line), (line line), (line plane)…
- [ ] `mop-trace.lisp``:before` + `:after` printing call trace
### Phase 5 — macros + LOOP + reader macros
- [ ] `defmacro`, `macrolet`, `symbol-macrolet`, `macroexpand-1`, `macroexpand`
- [ ] `gensym`, `gentemp`
- [ ] `set-macro-character`, `set-dispatch-macro-character`, `get-macro-character`
- [ ] **The LOOP macro** — iteration drivers (`for … in/across/from/upto/downto/by`, `while`, `until`, `repeat`), accumulators (`collect`, `append`, `nconc`, `count`, `sum`, `maximize`, `minimize`), conditional clauses (`if`/`when`/`unless`/`else`), termination (`finally`/`thereis`/`always`/`never`), `named` blocks
- [ ] LOOP test corpus: 30+ tests covering all clause types
### Phase 6 — packages + stdlib drive
- [ ] `defpackage`, `in-package`, `export`, `use-package`, `import`, `find-package`
- [ ] Package qualification at the reader level — `cl:car`, `mypkg::internal`
- [ ] `:common-lisp` (`:cl`) and `:common-lisp-user` (`:cl-user`) packages
- [ ] Sequence functions — `mapcar`, `mapc`, `mapcan`, `reduce`, `find`, `find-if`, `position`, `count`, `every`, `some`, `notany`, `notevery`, `remove`, `remove-if`, `subst`
- [ ] List ops — `assoc`, `getf`, `nth`, `last`, `butlast`, `nthcdr`, `tailp`, `ldiff`
- [ ] String ops — `string=`, `string-upcase`, `string-downcase`, `subseq`, `concatenate`
- [ ] FORMAT — basic directives `~A`, `~S`, `~D`, `~F`, `~%`, `~&`, `~T`, `~{...~}` (iteration), `~[...~]` (conditional), `~^` (escape), `~P` (plural)
- [ ] Drive corpus to 200+ green
## Progress log
_Newest first._
- 2026-04-25: Phase 2 eval — 127 tests, 299 total green. `lib/common-lisp/eval.sx`: cl-eval-ast with quote/if/progn/let/let*/flet/labels/setq/setf/function/lambda/the/locally/eval-when; defun/defvar/defparameter/defconstant; built-in arithmetic (+/-/*//, min/max/abs/evenp/oddp), comparisons, predicates, list ops (car/cdr/cons/list/append/reverse/length/nth/first/second/third/rest), string ops, funcall/apply/mapcar. Key gotchas: SX reduce is (reduce fn init list) not (reduce fn list init); CL true literal is t not true; builtins registered in cl-global-env.fns via wrapper dicts for #' syntax.
- 2026-04-25: Phase 1 lambda-list parser — 31 new tests, 172 total green. `cl-parse-lambda-list` in `parser.sx` + `tests/lambda.sx`. Handles &optional/&rest/&body/&key/&aux/&allow-other-keys, defaults, supplied-p. Key gotchas: `(when (> (len items) 0) ...)` not `(when items ...)` (empty list is truthy); custom `cl-deep=` needed for dict/list structural equality in tests.
- 2026-04-25: Phase 1 reader/parser — 62 new tests, 141 total green. `lib/common-lisp/parser.sx`: cl-read/cl-read-all, lists, dotted pairs, quote/backquote/unquote/splice/#', vectors, #:uninterned, NIL→nil, T→true, reader macro wrappers.
- 2026-04-25: Phase 1 tokenizer — 79 tests green. `lib/common-lisp/reader.sx` + `tests/read.sx` + `test.sh`. Handles symbols (pkg:sym, pkg::sym), integers, floats, ratios, hex/binary/octal, strings, #\ chars, reader macros (#' #( #: ,@), line/block comments. Key gotcha: SX `str` for string concat (not `concat`), substring-based read-while.
## Blockers
- _(none yet)_

View File

@@ -55,33 +55,40 @@ Key mappings:
### Phase 1 — tokenizer + parser + layout rule
- [x] Tokenizer: reserved words, qualified names, operators, numbers (int, float, Rational later), chars/strings, comments (`--` and `{-` nested)
- [ ] Layout algorithm: turn indentation into virtual `{`, `;`, `}` tokens per Haskell 98 §10.3
- [ ] Parser: modules, imports (stub), top-level decls, type sigs, function clauses with patterns + guards + where-clauses, expressions with operator precedence, lambdas, `let`, `if`, `case`, `do`, list comp, sections
- [ ] AST design modelled on GHC's HsSyn at a surface level
- [x] Layout algorithm: turn indentation into virtual `{`, `;`, `}` tokens per Haskell 98 §10.3
- Parser (split into sub-items — implement one per iteration):
- [x] Expressions: atoms, parens, tuples, lists, ranges, application, infix with full Haskell-98 precedence table, unary `-`, backtick operators, lambdas, `if`, `let`
- [x] `case … of` and `do`-notation expressions (plus minimal patterns needed for arms/binds: var, wildcard, literal, 0-arity and applied constructor, tuple, list)
- [x] Patterns — full: `as` patterns, nested, negative literal, `~` lazy, infix constructor (`:` / consym), extend lambdas/let with non-var patterns
- [x] Top-level decls: function clauses (simple — no guards/where yet), pattern bindings, multi-name type signatures, `data` with type vars and recursive constructors, `type` synonyms, `newtype`, fixity (`infix`/`infixl`/`infixr` with optional precedence, comma-separated ops, backtick names). Types: vars / constructors / application / `->` (right-assoc) / tuples / lists. `hk-parse-top` entry.
- [x] `where` clauses + guards (on fun-clauses, case alts, and let/do-let bindings — with the let funclause shorthand `let f x = …` now supported)
- [x] Module header + imports — `module NAME [exports] where …`, qualified/as/hiding/explicit imports, operator exports, `module Foo` exports, dotted names, headerless-with-imports
- [x] List comprehensions + operator sections — `(op)` / `(op e)` / `(e op)` (excluding `-` from right sections), `[e | q1, q2, …]` with `q-gen` / `q-guard` / `q-let` qualifiers
- [x] AST design modelled on GHC's HsSyn at a surface level — keyword-tagged lists cover modules/imports/decls/types/patterns/expressions; see parser.sx docstrings for the full node catalogue
- [x] Unit tests in `lib/haskell/tests/parse.sx` (43 tokenizer tests, all green)
### Phase 2 — desugar + eager-ish eval + ADTs (untyped)
- [ ] Desugar: guards → nested `if`s; `where``let`; list comp → `concatMap`-based; do-notation stays for now (desugared in phase 3)
- [ ] `data` declarations register constructors in runtime
- [ ] Pattern match (tag-based, value-level): atoms, vars, wildcards, constructor patterns, `as` patterns, nested
- [ ] Evaluator (still strict internally — laziness in phase 3): `let`, `lambda`, application, `case`, literals, constructors
- [ ] 30+ eval tests in `lib/haskell/tests/eval.sx`
- [x] Desugar: guards → nested `if`s; `where``let`; list comp → `concatMap`-based; do-notation stays for now (desugared in phase 3)
- [x] `data` declarations register constructors in runtime
- [x] Pattern match (tag-based, value-level): atoms, vars, wildcards, constructor patterns, `as` patterns, nested
- [x] Evaluator (still strict internally — laziness in phase 3): `let`, `lambda`, application, `case`, literals, constructors
- [x] 30+ eval tests in `lib/haskell/tests/eval.sx`
### Phase 3 — laziness + classic programs
- [ ] Transpile to thunk-wrapped SX: every application arg becomes `(make-thunk (lambda () <arg>))`
- [ ] `force` = SX eval-thunk-to-WHNF primitive
- [ ] Pattern match forces scrutinee before matching
- [ ] Infinite structures: `repeat x`, `iterate f x`, `[1..]`, Fibonacci stream, sieve of Eratosthenes
- [ ] `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
- [ ] `sieve.hs` — lazy sieve of Eratosthenes
- [ ] `quicksort.hs` — naive QS
- [ ] `nqueens.hs`
- [ ] `calculator.hs` — parser combinator style expression evaluator
- [ ] `lib/haskell/conformance.sh` + runner; `scoreboard.json` + `scoreboard.md`
- [ ] Target: 5/5 classic programs passing
- [x] Transpile to thunk-wrapped SX: every application arg becomes `(make-thunk (lambda () <arg>))`
- [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`)
- [x] `seq`, `deepseq` from Prelude
- [x] Do-notation for a stub `IO` monad (just threading, no real side effects yet)
- [x] Classic programs in `lib/haskell/tests/programs/`:
- [x] `fib.hs` — infinite Fibonacci stream
- [x] `sieve.hs` — lazy sieve of Eratosthenes
- [x] `quicksort.hs` — naive QS
- [x] `nqueens.hs`
- [x] `calculator.hs` — parser combinator style expression evaluator
- [x] `lib/haskell/conformance.sh` + runner; `scoreboard.json` + `scoreboard.md`
- [x] Target: 5/5 classic programs passing
### Phase 4 — Hindley-Milner inference
- [ ] Algorithm W: unification + type schemes + generalisation + instantiation
@@ -107,6 +114,435 @@ Key mappings:
_Newest first._
- **2026-04-25** — `conformance.sh` runner + `scoreboard.json` + `scoreboard.md`.
Script runs each classic program's test suite, prints per-program pass/fail,
and writes both files. `--check` mode skips writing for CI use.
Initial snapshot: 16/16 tests, 5/5 programs passing. Phase 3 complete.
- **2026-04-25** — Classic program `calculator.hs`: recursive descent
expression evaluator using ADTs for tokens and results.
`data Token = TNum Int | TOp String` + `data Result = R Int [Token]`;
parser threads token lists through `R` constructors enabling nested
constructor pattern matching (`R v (TOp "+":rest)`). Handles two-level
operator precedence (* / tighter than + ) and left-associativity.
5 tests: addition, precedence, left-assoc subtraction, left-assoc
div+mul, single number. All 5 classic programs complete. 402/402 green.
- **2026-04-25** — Classic program `nqueens.hs`: backtracking n-queens via list
comprehension and multi-clause `where`. Three fixes needed: (1) `hk-eval-let`
now delegates to `hk-bind-decls!` so multi-clause `where`/`let` bindings
(e.g., `go 0 = [[]]; go k = [...]`) are grouped as multifuns; (2) added
`concatMap`, `concat`, `abs`, `negate` to `hk-prelude-src` (list comprehensions
desugar to `concatMap`); (3) cached the Prelude env in `hk-env0` so
`hk-eval-expr-source` copies it instead of re-parsing. Tests: `queens 4 = 2`,
`queens 5 = 10`. n=8 (92 solutions) is too slow at ~50s/n — omitted.
397/397 green.
- **2026-04-25** — Classic program `quicksort.hs`: naive functional quicksort.
`qsort (x:xs) = qsort smaller ++ [x] ++ qsort larger where smaller = filter (< x) xs; larger = filter (>= x) xs`.
No new runtime additions needed — right sections, `filter`, `++` all worked out of the box.
5 tests (general sort, empty, singleton, already-sorted, reverse-sorted). 395/395 green.
- **2026-04-25** — Classic program `sieve.hs`: lazy sieve of Eratosthenes.
Added `mod`, `div`, `rem`, `quot` to `hk-binop` (and as first-class
values in `hk-init-env`), enabling backtick operator use. The filter-based
sieve `sieve (p:xs) = p : sieve (filter (\x -> x \`mod\` p /= 0) xs)` works
with the existing lazy cons + Prelude `filter`. 2 new tests in
`lib/haskell/tests/program-sieve.sx` (first 10 primes, 20th prime = 71).
390/390 green.
- **2026-04-25** — First classic program: `fib.hs`. Canonical Haskell
source lives at `lib/haskell/tests/programs/fib.hs` (the
two-cons-cell self-referential fibs definition plus a hand-rolled
`zipPlus`). The runner at `lib/haskell/tests/program-fib.sx`
mirrors the source as an SX string (the OCaml server's
`read-file` lives in the page-helpers env, not the default load
env, so direct file reads from inside `eval` aren't available).
Tests: `take 15 myFibs == [0,1,1,2,3,5,8,13,21,34,55,89,144,233,377]`,
plus a spot-check that the user-defined `zipPlus` is also
reachable. Found and fixed an ordering bug in `hk-bind-decls!`:
pass 3 (0-arity body evaluation) iterated `(keys groups)` whose
order is implementation-defined, so a top-down program where
`result = take 15 myFibs` came after `myFibs = …` could see
`myFibs` still bound to its `nil` placeholder. Now group names
are tracked in source order via a parallel list and pass 3 walks
that. 388/388 green.
- **2026-04-25** — Phase 3 do-notation + stub IO monad. Added a
`hk-desugar-do` pass that follows Haskell 98 §3.14 verbatim:
`do { e } = e`, `do { e ; ss } = e >> do { ss }`,
`do { p <- e ; ss } = e >>= \p -> do { ss }`, and
`do { let ds ; ss } = let ds in do { ss }`. The desugarer's
`:do` branch now invokes this pass directly so the surface
AST forms (`:do-expr`, `:do-bind`, `:do-let`) never reach the
evaluator. IO is represented as a tagged value
`("IO" payload)` — `return` (lazy builtin) wraps; `>>=` (lazy
builtin) forces the action, unwraps, and calls the bound
function on the payload; `>>` (lazy builtin) forces the
action and returns the second one. All three are non-strict
in their action arguments so deeply nested do-blocks don't
walk the whole chain at construction time. 14 new tests in
`lib/haskell/tests/do-io.sx` cover single-stmt do, single
and multi-bind, `>>` sequencing (last action wins), do-let
(single, multi, interleaved with bind), bind-to-`Just`,
bind-to-tuple, do inside a top-level fun, nested do, and
using `(>>=)`/`(>>)` directly as functions. 382/382 green.
- **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:
1. Op-form `:` is now non-strict in both args — `hk-eval-op`
special-cases it before the eager force-and-binop path, so a
cons-cell holds two thunks. This is what makes `repeat x =
x : repeat x`, `iterate f x = x : iterate f (f x)`, and the
classic `fibs = 0 : 1 : zipWith plus fibs (tail fibs)`
terminate when only a finite prefix is consumed.
2. Operators are now first-class values via a small
`hk-make-binop-builtin` helper, so `(+)`, `(*)`, `(==)` etc.
can be passed to `zipWith` and `map`.
Added range support across parser + evaluator: `[from..to]` and
`[from,next..to]` evaluate eagerly via `hk-build-range` (handles
step direction); `[from..]` parses to a new `:range-from` node
that the evaluator desugars to `iterate (+ 1) from`. New
`hk-load-into!` runs the regular pipeline (parse → desugar →
register data → bind decls) on a source string, and `hk-init-env`
preloads `hk-prelude-src` with the Phase-3 Prelude:
`head`, `tail`, `fst`, `snd`, `take`, `drop`, `repeat`, `iterate`,
`length`, `map`, `filter`, `zipWith`, plus `fibs` and `plus`.
25 new tests in `lib/haskell/tests/infinite.sx`, including
`take 10 fibs == [0,1,1,2,3,5,8,13,21,34]`,
`head (drop 99 [1..])`, `iterate (\x -> x * 2) 1` powers of two,
user-defined `ones = 1 : ones`, `naturalsFrom`, range edge cases,
composed `map`/`filter`, and a custom `mySum`. 359/359 green.
Sieve of Eratosthenes is deferred — it needs lazy `++` plus a
`mod` primitive — and lives under `Classic programs` anyway.
- **2026-04-24** — Phase 3 laziness foundation. Added a thunk type to
`lib/haskell/eval.sx` (`hk-mk-thunk` / `hk-is-thunk?`) backed by a
one-shot memoizing `hk-force` that evaluates the deferred AST, then
flips a `forced` flag and caches the value on the thunk dict; the
shared `hk-deep-force` walks the result tree at the test/output
boundary. Three single-line wiring changes in the evaluator make
every application argument lazy: `:app` now wraps its argument in
`hk-mk-thunk` rather than evaluating it. To preserve correctness
where values must be inspected, `hk-apply`, `hk-eval-op`,
`hk-eval-if`, `hk-eval-case`, and `hk-eval` for `:neg` now force
their operand. `hk-apply-builtin` forces every collected arg
before invoking the underlying SX fn so built-ins (`error`, `not`,
`id`) stay strict. The pattern matcher in `match.sx` now forces
the scrutinee just-in-time only for patterns that need to inspect
shape — `p-wild`, `p-var`, `p-as`, and `p-lazy` are no-force
paths, so the value flows through as a thunk and binding
preserves laziness. `hk-match-list-pat` forces at every cons-spine
step. 6 new lazy-specific tests in `lib/haskell/tests/eval.sx`
verify that `(\x y -> x) 1 (error …)` and `(\x y -> y) (error …) 99`
return without diverging, that `case Just (error …) of Just _ -> 7`
short-circuits, that `const` drops its second arg, that
`myHead (1 : error … : [])` returns 1 without touching the tail,
and that `Just (error …)` survives a wildcard-arm `case`. 333/333
green, all prior eval tests preserved by deep-forcing the result
in `hk-eval-expr-source` and `hk-prog-val`.
- **2026-04-24** — Phase 2 evaluator (`lib/haskell/eval.sx`) — ties
the whole pipeline together. Strict semantics throughout (laziness
is Phase 3). Function values are tagged dicts: `closure`,
`multi`(fun), `con-partial`, `builtin`. `hk-apply` unifies dispatch
across all four; closures and multifuns curry one argument at a
time, multifuns trying each clause's pat-list in order once arity
is reached. Top-level `hk-bind-decls!` is three-pass —
collect groups + pre-seed names → install multifuns (so closures
observe later names) → eval 0-arity bodies and pat-binds — making
forward and mutually recursive references work. `hk-eval-let` does
the same trick with a mutable child env. Built-ins:
`error`/`not`/`id`, plus `otherwise = True`. Operators wired:
arithmetic, comparison (returning Bool conses), `&&`, `||`, `:`,
`++`. Sections evaluate the captured operand once and return a
closure synthesized via the existing AST. `hk-eval-program`
registers data decls then binds, returning the env; `hk-run`
fetches `main` if present. Also extended `runtime.sx` to
pre-register the standard Prelude conses (`Maybe`, `Either`,
`Ordering`) so expression-level eval doesn't need a leading
`data` decl. 48 new tests in `lib/haskell/tests/eval.sx` cover
literals, arithmetic precedence, comparison/Bool, `if`, `let`
(incl. recursive factorial), lambdas (incl. constructor pattern
args), constructors, `case` (Just/Nothing/literal/tuple/wildcard),
list literals + cons + `++`, tuples, sections, multi-clause
top-level (factorial, list length via cons pattern, Maybe handler
with default), user-defined `data` with case-style matching, a
binary-tree height program, currying, higher-order (`twice`),
short-circuit `error` via `if`, and the three built-ins. 329/329
green. Phase 2 is now complete; Phase 3 (laziness) is next.
- **2026-04-24** — Phase 2: value-level pattern matcher
(`lib/haskell/match.sx`). Core entry `hk-match pat val env` returns
an extended env dict on success or `nil` on failure (uses `assoc`
rather than `dict-set!` so failed branches never pollute the
caller's env). Constructor values are tagged lists with the
constructor name as the first element; tuples use the tag `"Tuple"`,
lists are chained `(":" h t)` cons cells terminated by `("[]")`.
Value builders `hk-mk-con` / `hk-mk-tuple` / `hk-mk-nil` /
`hk-mk-cons` / `hk-mk-list` keep tests readable. The matcher
handles every pattern node the parser emits:
- `:p-wild` (always matches), `:p-var` (binds), `:p-int` /
`:p-float` / `:p-string` / `:p-char` (literal equality)
- `:p-as` (sub-match then bind whole), `:p-lazy` (eager for now;
laziness wired in phase 3)
- `:p-con` with arity check + recursive arg matching, including
deeply nested patterns and infix `:` cons (uses the same
code path as named constructors)
- `:p-tuple` against `"Tuple"` values, `:p-list` against an
exact-length cons spine.
Helper `hk-parse-pat-source` lifts a real Haskell pattern out of
`case _ of <pat> -> 0`, letting tests drive against parser output.
31 new tests in `lib/haskell/tests/match.sx` cover atomic
patterns, success/failure for each con/tuple/list shape, nested
`Just (Just x)`, cons-vs-empty, `as` over con / wildcard /
failing-sub, `~` lazy, plus four parser-driven cases (`Just x`,
`x : xs`, `(a, b)`, `n@(Just x)`). 281/281 green.
- **2026-04-24** — Phase 2: runtime constructor registry
(`lib/haskell/runtime.sx`). A mutable dict `hk-constructors` keyed
by constructor name, each entry carrying arity and owning type.
`hk-register-data!` walks a `:data` AST and registers every
`:con-def` with its arity (= number of field types) and the type
name; `hk-register-newtype!` does the one-constructor variant;
`hk-register-decls!` / `hk-register-program!` filter a decls list
(or a `:program` / `:module` AST) and call the appropriate
registrar. `hk-load-source!` composes it with `hk-core`
(tokenize → layout → parse → desugar → register). Pre-registers
five built-ins tied to Haskell syntactic forms: `True` / `False`
(Bool), `[]` and `:` (List), `()` (Unit) — everything else comes
from user declarations or the eventual Prelude. Query helpers:
`hk-is-con?`, `hk-con-arity`, `hk-con-type`, `hk-con-names`. 24
new tests in `lib/haskell/tests/runtime.sx` cover each built-in
(arity + type), unknown-name probes, registration of `MyBool` /
`Maybe` / `Either` / recursive `Tree` / `newtype Age`, multi-data
programs, a module-header body, ignoring non-data decls, and
last-wins re-registration. 250/250 green.
- **2026-04-24** — Phase 2 kicks off with `lib/haskell/desugar.sx` — a
tree-walking rewriter that eliminates the three surface-only forms
produced by the parser, leaving a smaller core AST for the evaluator:
- `:where BODY DECLS` → `:let DECLS BODY`
- `:guarded ((:guard C1 E1) (:guard C2 E2) …)` → right-folded
`(:if C1 E1 (:if C2 E2 … (:app (:var "error") (:string "…"))))`
- `:list-comp E QUALS` → Haskell 98 §3.11 translation:
empty quals → `(:list (E))`, `:q-guard` → `(:if … (:list (E)) (:list ()))`,
`:q-gen PAT SRC` → `(concatMap (\PAT -> …) SRC)`, `:q-let BINDS` →
`(:let BINDS …)`. Nested generators compile to nested concatMap.
Every other expression, decl, pattern, and type node is recursed
into and passed through unchanged. Public entries `hk-desugar`,
`hk-core` (tokenize → layout → parse → desugar on a module), and
`hk-core-expr` (the same for an expression). 15 new tests in
`lib/haskell/tests/desugar.sx` cover two- and three-way guards,
case-alt guards, single/multi-binding `where`, guards + `where`
combined, the four list-comprehension cases (single-gen, gen +
filter, gen + let, nested gens), and pass-through for literals,
lambdas, simple fun-clauses, `data` decls, and a module header
wrapping a guarded function. 226/226 green.
- **2026-04-24** — Phase 1 parser is now complete. This iteration adds
operator sections and list comprehensions, the two remaining
aexp-level forms, plus ticks the “AST design” item (the keyword-
tagged list shape has accumulated a full HsSyn-level surface).
Changes:
- `hk-parse-infix` now bails on `op )` without consuming the op, so
the paren parser can claim it as a left section.
- `hk-parse-parens` rewritten to recognise five new forms:
`()` (unit), `(op)` → `(:var OP)`, `(op e)` → `(:sect-right OP E)`
(excluded for `-` so that `(- 5)` stays `(:neg 5)`), `(e op)` →
`(:sect-left OP E)`, plus regular parens and tuples. Works for
varsym, consym, reservedop `:`, and backtick-quoted varids.
- `hk-section-op-info` inspects the current token and returns a
`{:name :len}` dict, so the same logic handles 1-token ops and
3-token backtick ops uniformly.
- `hk-parse-list-lit` now recognises a `|` after the first element
and dispatches to `hk-parse-qual` per qualifier (comma-separated),
producing `(:list-comp EXPR QUALS)`. Qualifiers are:
`(:q-gen PAT EXPR)` when a paren-balanced lookahead
(`hk-comp-qual-is-gen?`) finds `<-` before the next `,`/`]`,
`(:q-let BINDS)` for `let …`, and `(:q-guard EXPR)` otherwise.
- `hk-parse-comp-let` accepts `]` or `,` as an implicit block close
(single-line comprehensions never see layout's vrbrace before the
qualifier terminator arrives); explicit `{ }` still closes
strictly.
22 new tests in `lib/haskell/tests/parser-sect-comp.sx` cover
op-references (inc. `(-)`, `(:)`, backtick), right sections (inc.
backtick), left sections, the `(- 5)` → `:neg` corner, plain parens
and tuples, six comprehension shapes (simple, filter, let,
nested-generators, constructor pattern bind, tuple pattern bind,
and a three-qualifier mix). 211/211 green.
- **2026-04-24** — Phase 1: module header + imports. Added
`hk-parse-module-header`, `hk-parse-import`, plus shared helpers for
import/export entity lists (`hk-parse-ent`, `hk-parse-ent-member`,
`hk-parse-ent-list`). New AST:
- `(:module NAME EXPORTS IMPORTS DECLS)` — NAME `nil` means no header,
EXPORTS `nil` means no export list (distinct from empty `()`)
- `(:import QUALIFIED NAME AS SPEC)` — QUALIFIED bool, AS alias or nil,
SPEC nil / `(:spec-items ENTS)` / `(:spec-hiding ENTS)`
- Entity refs: `:ent-var`, `:ent-all` (`Tycon(..)`), `:ent-with`
(`Tycon(m1, m2, …)`), `:ent-module` (exports only).
`hk-parse-program` now dispatches on the leading token: `module`
keyword → full header-plus-body parse (consuming the `where` layout
brace around the module body); otherwise collect any leading
`import` decls and then remaining decls with the existing logic.
The outer shell is `(:module …)` as soon as any header or import is
present, and stays as `(:program DECLS)` otherwise — preserving every
previous test expectation untouched. Handles operator exports `((+:))`,
dotted module names (`Data.Map`), and the Haskell-98 context-sensitive
keywords `qualified`/`as`/`hiding` (all lexed as ordinary varids and
matched only in import position). 16 new tests in
`lib/haskell/tests/parser-module.sx` covering simple/exports/empty
headers, dotted names, operator exports, `module Foo` exports,
qualified/aliased/items/hiding imports, and a headerless-with-imports
file. 189/189 green.
- **2026-04-24** — Phase 1: guards + where clauses. Factored a single
`hk-parse-rhs sep` that all body-producing sites now share: it reads
a plain `sep expr` body or a chain of `| cond sep expr` guards, then
— regardless of which form — looks for an optional `where` block and
wraps accordingly. AST additions:
- `:guarded GUARDS` where each GUARD is `:guard COND EXPR`
- `:where BODY DECLS` where BODY is a plain expr or a `:guarded`
Both can nest (guards inside where). `hk-parse-alt` now routes through
`hk-parse-rhs "->"`, `hk-parse-fun-clause` and `hk-parse-bind` through
`hk-parse-rhs "="`. `hk-parse-where-decls` reuses `hk-parse-decl` so
where-blocks accept any decl form (signatures, fixity, nested funs).
As a side effect, `hk-parse-bind` now also picks up the Haskell-native
`let f x = …` funclause shorthand: a varid followed by one or more
apats produces `(:fun-clause NAME APATS BODY)` instead of a
`(:bind (:p-var …) …)` — keeping the simple `let x = e` shape
unchanged for existing tests. 11 new tests in
`lib/haskell/tests/parser-guards-where.sx` cover two- and three-way
guards, mixed guarded + equality clauses, single- and multi-binding
where blocks, guards plus where, case-alt guards, case-alt where,
let with funclause shorthand, let with guards, and a where containing
a type signature alongside a fun-clause. 173/173 green.
- **2026-04-24** — Phase 1: top-level decls. Refactored `hk-parse-expr` into a
`hk-parser tokens mode` with `:expr` / `:module` dispatch so the big lexical
state is shared (peek/advance/pat/expr helpers all reachable); added public
wrappers `hk-parse-expr`, `hk-parse-module`, and source-level entry
`hk-parse-top`. New type parser (`hk-parse-type` / `hk-parse-btype` /
`hk-parse-atype`): type variables (`:t-var`), type constructors (`:t-con`),
type application (`:t-app`, left-assoc), right-associative function arrow
(`:t-fun`), unit/tuples (`:t-tuple`), and lists (`:t-list`). New decl parser
(`hk-parse-decl` / `hk-parse-program`) producing a `(:program DECLS)` shell:
- `:type-sig NAMES TYPE` — comma-separated multi-name support
- `:fun-clause NAME APATS BODY` — patterns for args, body via existing expr
- `:pat-bind PAT BODY` — top-level pattern bindings like `(a, b) = pair`
- `:data NAME TVARS CONS` with `:con-def CNAME FIELDS` for nullary and
multi-arg constructors, including recursive references
- `:type-syn NAME TVARS TYPE`, `:newtype NAME TVARS CNAME FIELD`
- `:fixity ASSOC PREC OPS` — assoc one of `"l"`/`"r"`/`"n"`, default prec 9,
comma-separated operator names, including backtick-quoted varids.
Sig vs fun-clause disambiguated by a paren-balanced top-level scan for
`::` before the next `;`/`}` (`hk-has-top-dcolon?`). 24 new tests in
`lib/haskell/tests/parser-decls.sx` cover all decl forms, signatures with
application / tuples / lists / right-assoc arrows, nullary and recursive
data types, multi-clause functions, and a mixed program with data + type-
synonym + signature + two function clauses. Not yet: guards, where
clauses, module header, imports, deriving, contexts, GADTs. 162/162 green.
- **2026-04-24** — Phase 1: full patterns. Added `as` patterns
(`name@apat` → `(:p-as NAME PAT)`), lazy patterns (`~apat` →
`(:p-lazy PAT)`), negative literal patterns (`-N` / `-F` resolving
eagerly in the parser so downstream passes see a plain `(:p-int -1)`),
and infix constructor patterns via a right-associative single-band
layer on top of `hk-parse-pat-lhs` for any `consym` or reservedop `:`
(so `x : xs` parses as `(:p-con ":" [x, xs])`, `a :+: b` likewise).
Extended `hk-apat-start?` with `-` and `~` so the pattern-argument
loops in lambdas and constructor applications pick these up.
Lambdas now parse apat parameters instead of bare varids — so the
`:lambda` AST is `(:lambda APATS BODY)` with apats as pattern nodes.
`hk-parse-bind` became a plain `pat = expr` form, so `:bind` now has
a pattern LHS throughout (simple `x = 1` → `(:bind (:p-var "x") …)`);
this picks up `let (x, y) = pair in …` and `let Just x = m in x`
automatically, and flows through `do`-notation lets. Eight existing
tests updated to the pattern-flavoured AST. Also fixed a pragmatic
layout issue that surfaced in multi-line `let`s: when a layout-indent
would emit a spurious `;` just before an `in` token (because the
let block had already been closed by dedent), `hk-peek-next-reserved`
now lets the layout pass skip that indent and leave closing to the
existing `in` handler. 18 new tests in
`lib/haskell/tests/parser-patterns.sx` cover every pattern variant,
lambda with mixed apats, let pattern-bindings (tuple / constructor /
cons), and do-bind with a tuple pattern. 138/138 green.
- **2026-04-24** — Phase 1: `case … of` and `do`-notation parsers. Added `hk-parse-case`
/ `hk-parse-alt`, `hk-parse-do` / `hk-parse-do-stmt` / `hk-parse-do-let`, plus the
minimal pattern language needed to make arms and binds meaningful:
`hk-parse-apat` (var, wildcard `_`, int/float/string/char literal, 0-arity
conid/qconid, paren+tuple, list) and `hk-parse-pat` (conid applied to
apats greedily). AST nodes: `:case SCRUT ALTS`, `:alt PAT BODY`, `:do STMTS`
with stmts `:do-expr E` / `:do-bind PAT E` / `:do-let BINDS`, and pattern
tags `:p-wild` / `:p-int` / `:p-float` / `:p-string` / `:p-char` / `:p-var`
/ `:p-con NAME ARGS` / `:p-tuple` / `:p-list`. `do`-stmts disambiguate
`pat <- e` vs bare expression with a forward paren/bracket/brace-balanced
scan for `<-` before the next `;`/`}` — no backtracking, no AST rewrite.
`case` and `do` accept both implicit (`vlbrace`/`vsemi`/`vrbrace`) and
explicit braces. Added to `hk-parse-lexp` so they participate fully in
operator-precedence expressions. 19 new tests in
`lib/haskell/tests/parser-case-do.sx` cover every pattern variant,
explicit-brace `case`, expression scrutinees, do with bind/let/expr,
multi-binding `let` in `do`, constructor patterns in binds, and
`case`/`do` nested inside `let` and lambda. The full pattern item (as
patterns, negative literals, `~` lazy, lambda/let pattern extension)
remains a separate sub-item. 119/119 green.
- **2026-04-24** — Phase 1: expression parser (`lib/haskell/parser.sx`, ~380 lines).
Pratt-style precedence climbing against a Haskell-98-default op table (24
operators across precedence 09, left/right/non assoc, default infixl 9 for
anything unlisted). Supports literals (int/float/string/char), varid/conid
(qualified variants folded into `:var` / `:con`), parens / unit / tuples,
list literals, ranges `[a..b]` and `[a,b..c]`, left-associative application,
unary `-`, backtick operators (`x \`mod\` 3`), lambdas, `if-then-else`, and
`let … in` consuming both virtual and explicit braces. AST uses keyword
tags (`:var`, `:op`, `:lambda`, `:let`, `:bind`, `:tuple`, `:range`,
`:range-step`, `:app`, `:neg`, `:if`, `:list`, `:int`, `:float`, `:string`,
`:char`, `:con`). The parser skips a leading `vlbrace` / `lbrace` so it can
be called on full post-layout output, and uses a `raise`-based error channel
with location-lite messages. 42 new tests in `lib/haskell/tests/parser-expr.sx`
cover literals, identifiers, parens/tuple/unit, list + range, app associativity,
operator precedence (mul over add, cons right-assoc, function-composition
right-assoc, `$` lowest), backtick ops, unary `-`, lambda multi-param,
`if` with infix condition, single- and multi-binding `let` (both implicit
and explicit braces), plus a few mixed nestings. 100/100 green.
- **2026-04-24** — Phase 1: layout algorithm (`lib/haskell/layout.sx`, ~260 lines)
implementing Haskell 98 §10.3. Two-pass design: a pre-pass augments the raw
token stream with explicit `layout-open` / `layout-indent` markers (suppressing
`<n>` when `{n}` already applies, per note 3), then an L pass consumes the
augmented stream against a stack of implicit/explicit layout contexts and
emits `vlbrace` / `vsemi` / `vrbrace` tokens; newlines are dropped. Supports
the initial module-level implicit open (skipped when the first token is
`module` or `{`), the four layout keywords (`let`/`where`/`do`/`of`), explicit
braces disabling layout, dedent closing nested implicit blocks while also
emitting `vsemi` at the enclosing level, and the pragmatic single-line
`let … in` rule (emit `}` when `in` meets an implicit let). 15 new tests
in `lib/haskell/tests/layout.sx` cover module-start, do/let/where/case/of,
explicit braces, multi-level dedent, line continuation, and EOF close-down.
Shared test helpers moved to `lib/haskell/testlib.sx` so both test files
can share one `hk-test`. `test.sh` preloads tokenizer + layout + testlib.
58/58 green.
- **2026-04-24** — Phase 1: Haskell 98 tokenizer (`lib/haskell/tokenizer.sx`, 490 lines)
covering idents (lower/upper/qvarid/qconid), 23 reserved words, 11 reserved ops,
varsym/consym operator chains, integer/hex/octal/float literals incl. exponent

View File

@@ -1,96 +0,0 @@
# HS conformance — blockers drain
Goal: take hyperscript conformance from **1277/1496 (85.4%)** to **1496/1496 (100%)** by clearing the blocked clusters and the design-done Bucket E subsystems.
This plan exists because the per-iteration `loops/hs` agent can't fit these into its 30-min budget — they need dedicated multi-commit sit-downs. Track progress here; refer to `plans/hs-conformance-to-100.md` for the canonical cluster ledger.
## Current state (2026-04-25)
- Loop running in `/root/rose-ash-loops/hs` (branch `loops/hs`)
- sx-tree MCP **fixed** (was a session-stale binary issue — restart of claude in the tmux window picked it up). Loop hinted to retry **#32**, **#29** first.
- Recent loop progress: ~1 commit/6h — easy wins drained, what's left needs focused attention.
## Remaining work
### Bucket-A/B/C blockers (small, in-place fixes)
| # | Cluster | Tests | Effort | Blocker | Fix sketch |
|---|---------|------:|--------|---------|------------|
| **17** | `tell` semantics | +3 | ~1h | Implicit-default-target ambiguity. `bare add .bar` inside `tell X` should target `X` but explicit `to me` must reach the original element. | Add `beingTold` symbol distinct from `me`; bare commands compile to `beingTold-or-me`; explicit `me` always the original. |
| **22** | window global fn fallback | +2-4 | ~1h | `foo()` where `foo` isn't SX-defined needs to fall back to `(host-global "foo")`. Three attempts failed: guard (host-level error not catchable), `env-has?` (not in HS kernel), `hs-win-call` (NativeFn not callable from CALL). | Add `symbol-bound?` predicate to HS kernel **OR** a host-call-fn primitive with arity-agnostic dispatch. |
| **29** | `hyperscript:before:init` / `:after:init` / `:parse-error` events | +4-6 | ~30m (post sx-tree fix) | Was sx-tree MCP outage. Now unblocked — loop should retry. 4 of 6 tests need stricter parser error-rejection (out of scope; mark partial). | Edit `integration.sx` to fire DOM events at activation boundaries. |
### Bucket D — medium features
| # | Cluster | Tests | Effort | Status |
|---|---------|------:|--------|--------|
| **31** | runtime null-safety error reporting | **+15-18** | **2-4h** | **THIS SESSION'S TARGET.** Plan node fully spec'd: 5 pieces of work. |
| **32** | MutationObserver mock + `on mutation` | +10-15 | ~2h | Was sx-tree-blocked. Now unblocked — loop hinted to retry. Multi-file: parser, compiler, runtime, runner mock, generator skip-list. |
| **33** | cookie API | +2 (remaining) | ~30m | Partial done (+3). Remaining 2 need `hs-method-call` runtime fallback for unknown methods + `hs-for-each` recognising host-array/proxy collections. |
| 34 | event modifier DSL | +6-8 | ~1-2h | `elsewhere`, `every`, count filters (`once`/`twice`/`3 times`/ranges), `from elsewhere`. Pending. |
| 35 | namespaced `def` | +3 | ~30m | Pending. |
### Bucket E — subsystems (design docs landed, multi-commit each)
Each has a design doc with a step-by-step checklist. These are 1-2 days of focused work each, not loop-fits.
| # | Subsystem | Tests | Design doc | Branch |
|---|-----------|------:|------------|--------|
| 36 | WebSocket + `socket` + RPC Proxy | +12-16 | `plans/designs/e36-websocket.md` | `worktree-agent-a9daf73703f520257` |
| 37 | Tokenizer-as-API | +16-17 | `plans/designs/e37-tokenizer-api.md` | `worktree-agent-a6bb61d59cc0be8b4` |
| 38 | SourceInfo API | +4 | `plans/designs/e38-sourceinfo.md` | `agent-e38-sourceinfo` |
| 39 | WebWorker plugin (parser-only stub) | +1 | `plans/designs/e39-webworker.md` | `hs-design-e39-webworker` |
| 40 | Real Fetch / non-2xx / before-fetch | +7 | `plans/designs/e40-real-fetch.md` | `worktree-agent-a94612a4283eaa5e0` |
### Bucket F — generator translation gaps
~25 tests SKIP'd because `tests/playwright/generate-sx-tests.py` bails with `return None`. Single dedicated generator-repair sit-down once Bucket D is drained. ~half-day.
## Order of attack
In approximate cost-per-test order:
1. **Loop self-heal** (no human work) — wait for #29, #32 to land via the running loop ⏱️ ~next 1-2 hours
2. **#31 null-safety** — biggest scoped single win, dedicated worktree agent (this session)
3. **#33 cookie API remainder** — quick partial completion
4. **#17 / #22 / #34 / #35** — small fiddly fixes, one sit-down each
5. **Bucket E** — pick one subsystem at a time. **#39 (WebWorker stub) first** — single commit, smallest. Then **#38 (SourceInfo)** — 4 commits. Then the bigger three (#36, #37, #40).
6. **Bucket F** — generator repair sweep at the end.
Estimated total to 100%: ~10-15 days of focused work, parallelisable across branches.
## Cluster #31 spec (full detail)
The plan note from `hs-conformance-to-100.md`:
> 18 tests in `runtimeErrors`. When accessing `.foo` on nil, emit a structured error with position info. One coordinated fix in the compiler emit paths for property access, function calls, set/put.
**Required pieces:**
1. **Generator-side `eval-hs-error` helper + recognizer** for `expect(await error("HS")).toBe("MSG")` blocks. In `tests/playwright/generate-sx-tests.py`.
2. **Runtime helpers** in `lib/hyperscript/runtime.sx`:
- `hs-null-error!` raising `'<sel>' is null`
- `hs-named-target` — wraps a query result with the original selector source
- `hs-named-target-list` — same for list results
3. **Compiler patches at every target-position `(query SEL)` emit** — wrap in named-target carrying the original selector source. ~17 command emit paths in `lib/hyperscript/compiler.sx`:
add, remove, hide, show, measure, settle, trigger, send, set, default, increment, decrement, put, toggle, transition, append, take.
4. **Function-call null-check** at bare `(name)`, `hs-method-call`, and `host-get` chains, deriving the leftmost-uncalled-name (`'x'` / `'x.y'`) from the parse tree.
5. **Possessive-base null-check** (`set x's y to true``'x' is null`).
**Files in scope:**
- `lib/hyperscript/runtime.sx` (new helpers)
- `lib/hyperscript/compiler.sx` (~17 emit-path edits)
- `tests/playwright/generate-sx-tests.py` (test recognizer)
- `tests/hs-run-filtered.js` (if mock helpers needed)
- `shared/static/wasm/sx/hs-runtime.sx` + `hs-compiler.sx` (WASM staging copies)
**Approach:** target-named pieces incrementally — runtime helpers first (no compiler change), then compiler emit paths in batches (group similar commands), then function-call/possessive at the end. Each batch is one commit if it lands +N tests; mark partial if it only unlocks part.
**Watch for:** smoke-range regressions (tests flipping pass→fail). Each commit: rerun smoke 0-195 and the `runtimeErrors` suite.
## Notes for future sessions
- `plans/hs-conformance-to-100.md` is the canonical cluster ledger — update it on every commit.
- `plans/hs-conformance-scoreboard.md` is the live tally — bump `Merged:` and the bucket roll-up.
- Loop has scope rule "never edit `spec/evaluator.sx` or broader SX kernel" — most fixes here stay in `lib/hyperscript/**`, `tests/`, generator. If a fix needs kernel work, surface to the user; don't merge silently.
- Cluster #22's `symbol-bound?` predicate would be a kernel addition — that's a real cross-boundary scope expansion.

View File

@@ -1,124 +0,0 @@
# Ruby-on-SX: fibers + blocks + open classes on delimited continuations
The headline showcase is **fibers** — Ruby's `Fiber.new { … Fiber.yield v … }` / `Fiber.resume` are textbook delimited continuations with sugar. MRI implements them by swapping C stacks; on SX they fall out of the existing `perform`/`cek-resume` machinery for free. Plus blocks/yield (lexical escape continuations, same shape as Smalltalk's non-local return), method_missing, and singleton classes.
End-state goal: Ruby 2.7-flavoured subset, Enumerable mixin, fibers + threads-via-fibers (no real OS threads), method_missing-driven DSLs, ~150 hand-written + classic programs.
## Scope decisions (defaults — override by editing before we spawn)
- **Syntax:** Ruby 2.7. No 3.x pattern matching, no rightward assignment, no endless methods. We pick 2.7 because it's the biggest semantic surface that still parses cleanly.
- **Conformance:** "Reads like Ruby, runs like Ruby." Slice of RubySpec (Core + Library subset), not full RubySpec.
- **Test corpus:** custom + curated RubySpec slice. Plus classic programs: fiber-based generator, internal DSL with method_missing, mixin-based Enumerable on a custom class.
- **Out of scope:** real threads, GIL, refinements, `binding_of_caller` from non-Ruby contexts, Encoding object beyond UTF-8/ASCII-8BIT, RubyVM::* introspection beyond bytecode-disassembly placeholder, IO subsystem beyond `puts`/`gets`/`File.read`.
- **Symbols:** SX symbols. Strings are mutable copies; symbols are interned.
## Ground rules
- **Scope:** only touch `lib/ruby/**` and `plans/ruby-on-sx.md`. Don't edit `spec/`, `hosts/`, `shared/`, or any other `lib/<lang>/**`. Ruby primitives go in `lib/ruby/runtime.sx`.
- **SX files:** use `sx-tree` MCP tools only.
- **Commits:** one feature per commit. Keep `## Progress log` updated and tick roadmap boxes.
## Architecture sketch
```
Ruby source
lib/ruby/tokenizer.sx — keywords, ops, %w[], %i[], heredocs (deferred), regex (deferred)
lib/ruby/parser.sx — AST: classes, modules, methods, blocks, calls
lib/ruby/transpile.sx — AST → SX AST (entry: rb-eval-ast)
lib/ruby/runtime.sx — class table, MOP, dispatch, fibers, primitives
```
Core mapping:
- **Object** = SX dict `{:class :ivars :singleton-class?}`. Instance variables live in `ivars` keyed by symbol.
- **Class** = SX dict `{:name :superclass :methods :class-methods :metaclass :includes :prepends}`. Class table is flat.
- **Method dispatch** = lookup walks ancestor chain (prepended → class → included modules → superclass → …). Falls back to `method_missing` with a `Symbol`+args.
- **Block** = lambda + escape continuation. `yield` invokes the block in current context. `return` from within a block invokes the enclosing-method's escape continuation.
- **Proc** = lambda without strict arity. `Proc.new` + `proc {}`.
- **Lambda** = lambda with strict arity + `return`-returns-from-lambda semantics.
- **Fiber** = pair of continuations (resume-k, yield-k) wrapped in a record. `Fiber.new { … }` builds it; `Fiber.resume` invokes the resume-k; `Fiber.yield` invokes the yield-k. Built directly on `perform`/`cek-resume`.
- **Module** = class without instance allocation. `include` puts it in the chain; `prepend` puts it earlier; `extend` puts it on the singleton.
- **Singleton class** = lazily allocated per-object class for `def obj.foo` definitions.
- **Symbol** = interned SX symbol. `:foo` reads as `(quote foo)` flavour.
## Roadmap
### Phase 1 — tokenizer + parser
- [ ] Tokenizer: keywords (`def end class module if unless while until do return yield begin rescue ensure case when then else elsif`), identifiers (lowercase = local/method, `@` = ivar, `@@` = cvar, `$` = global, uppercase = constant), numbers (int, float, `0x` `0o` `0b`, `_` separators), strings (`"…"` interpolation, `'…'` literal, `%w[a b c]`, `%i[a b c]`), symbols `:foo` `:"…"`, operators (`+ - * / % ** == != < > <= >= <=> === =~ !~ << >> & | ^ ~ ! && || and or not`), `:: . , ; ( ) [ ] { } -> => |`, comments `#`
- [ ] Parser: program is sequence of statements separated by newlines or `;`; method def `def name(args) … end`; class `class Foo < Bar … end`; module `module M … end`; block `do |a, b| … end` and `{ |a, b| … }`; call sugar (no parens), `obj.method`, `Mod::Const`; arg shapes (positional, default, splat `*args`, double-splat `**opts`, block `&blk`)
- [ ] If/while/case expressions (return values), `unless`/`until`, postfix modifiers
- [ ] Begin/rescue/ensure/retry, raise, raise with class+message
- [ ] Unit tests in `lib/ruby/tests/parse.sx`
### Phase 2 — object model + sequential eval
- [ ] Class table bootstrap: `BasicObject`, `Object`, `Kernel`, `Module`, `Class`, `Numeric`, `Integer`, `Float`, `String`, `Symbol`, `Array`, `Hash`, `Range`, `NilClass`, `TrueClass`, `FalseClass`, `Proc`, `Method`
- [ ] `rb-eval-ast`: literals, variables (local, ivar, cvar, gvar, constant), assignment (single and parallel `a, b = 1, 2`, splat receive), method call, message dispatch
- [ ] Method lookup walks ancestor chain; cache hit-class per `(class, selector)`
- [ ] `method_missing` fallback constructing args list
- [ ] `super` and `super(args)` — lookup in defining class's superclass
- [ ] Singleton class allocation on first `def obj.foo` or `class << obj`
- [ ] `nil`, `true`, `false` are singletons of their classes; tagged values aren't boxed
- [ ] Constant lookup (lexical-then-inheritance) with `Module.nesting`
- [ ] 60+ tests in `lib/ruby/tests/eval.sx`
### Phase 3 — blocks + procs + lambdas
- [ ] Method invocation captures escape continuation `^k` for `return`; binds it as block's escape
- [ ] `yield` invokes implicit block
- [ ] `block_given?`, `&blk` parameter, `&proc` arg unpacking
- [ ] `Proc.new`, `proc { }`, `lambda { }` (or `->(x) { x }`)
- [ ] Lambda strict arity + lambda-local `return` semantics
- [ ] Proc lax arity (`a, b, c` unpacks Array; missing args nil)
- [ ] `break`, `next`, `redo``break` is escape-from-loop-or-block; `next` is escape-from-block-iteration; `redo` re-runs current iteration
- [ ] 30+ tests in `lib/ruby/tests/blocks.sx`
### Phase 4 — fibers (THE SHOWCASE)
- [ ] `Fiber.new { |arg| … Fiber.yield v … }` allocates a fiber record with paired continuations
- [ ] `Fiber.resume(args…)` resumes the fiber, returning the value passed to `Fiber.yield`
- [ ] `Fiber.yield(v)` from inside the fiber suspends and returns control to the resumer
- [ ] `Fiber.current` from inside the fiber
- [ ] `Fiber#alive?`, `Fiber#raise` (deferred)
- [ ] `Fiber.transfer` — symmetric coroutines (resume from any side)
- [ ] Classic programs in `lib/ruby/tests/programs/`:
- [ ] `generator.rb` — pull-style infinite enumerator built on fibers
- [ ] `producer-consumer.rb` — bounded buffer with `Fiber.transfer`
- [ ] `tree-walk.rb` — recursive tree walker that yields each node, driven by `Fiber.resume`
- [ ] `lib/ruby/conformance.sh` + runner, `scoreboard.json` + `scoreboard.md`
### Phase 5 — modules + mixins + metaprogramming
- [ ] `include M` — appends M's methods after class methods in chain
- [ ] `prepend M` — prepends M before class methods
- [ ] `extend M` — adds M to singleton class
- [ ] `Module#ancestors`, `Module#included_modules`
- [ ] `define_method`, `class_eval`, `instance_eval`, `module_eval`
- [ ] `respond_to?`, `respond_to_missing?`, `method_missing`
- [ ] `Object#send`, `Object#public_send`, `Object#__send__`
- [ ] `Module#method_added`, `singleton_method_added` hooks
- [ ] Hooks: `included`, `extended`, `inherited`, `prepended`
- [ ] Internal-DSL classic program: `lib/ruby/tests/programs/dsl.rb`
### Phase 6 — stdlib drive
- [ ] `Enumerable` mixin: `each` (abstract), `map`, `select`/`filter`, `reject`, `reduce`/`inject`, `each_with_index`, `each_with_object`, `take`, `drop`, `take_while`, `drop_while`, `find`/`detect`, `find_index`, `any?`, `all?`, `none?`, `one?`, `count`, `min`, `max`, `min_by`, `max_by`, `sort`, `sort_by`, `group_by`, `partition`, `chunk`, `each_cons`, `each_slice`, `flat_map`, `lazy`
- [ ] `Comparable` mixin: `<=>`, `<`, `<=`, `>`, `>=`, `==`, `between?`, `clamp`
- [ ] `Array`: indexing, slicing, `push`/`pop`/`shift`/`unshift`, `concat`, `flatten`, `compact`, `uniq`, `sort`, `reverse`, `zip`, `dig`, `pack`/`unpack` (deferred)
- [ ] `Hash`: `[]`, `[]=`, `delete`, `merge`, `each_pair`, `keys`, `values`, `to_a`, `dig`, `fetch`, default values, default proc
- [ ] `Range`: `each`, `step`, `cover?`, `include?`, `size`, `min`, `max`
- [ ] `String`: indexing, slicing, `split`, `gsub` (string-arg version, regex deferred), `sub`, `upcase`, `downcase`, `strip`, `chomp`, `chars`, `bytes`, `to_i`, `to_f`, `to_sym`, `*`, `+`, `<<`, format with `%`
- [ ] `Integer`: `times`, `upto`, `downto`, `step`, `digits`, `gcd`, `lcm`
- [ ] Drive corpus to 200+ green
## Progress log
_Newest first._
- _(none yet)_
## Blockers
- _(none yet)_

View File

@@ -1,116 +0,0 @@
# Smalltalk-on-SX: blocks with non-local return on delimited continuations
The headline showcase is **blocks** — Smalltalk's closures with non-local return (`^expr` aborts the enclosing *method*, not the block). Every other Smalltalk on top of a host VM (RSqueak on PyPy, GemStone on C, Maxine on Java) reinvents non-local return on whatever stack discipline the host gives them. On SX it's a one-liner: a block holds a captured continuation; `^` just invokes it. Message-passing OO falls out cheaply on top of the existing component / dispatch machinery.
End-state goal: ANSI-ish Smalltalk-80 subset, SUnit working, ~200 hand-written tests + a vendored slice of the Pharo kernel tests, classic corpus (eight queens, quicksort, mandelbrot, Conway's Life).
## Scope decisions (defaults — override by editing before we spawn)
- **Syntax:** Pharo / Squeak chunk format (`!` separators, `Object subclass: #Foo …`). No fileIn/fileOut images — text source only.
- **Conformance:** ANSI X3J20 *as a target*, not bug-for-bug Squeak. "Reads like Smalltalk, runs like Smalltalk."
- **Test corpus:** SUnit ported to SX-Smalltalk + custom programs + a curated slice of Pharo `Kernel-Tests` / `Collections-Tests`.
- **Image:** out of scope. Source-only. No `become:` between sessions, no snapshotting.
- **Reflection:** `class`, `respondsTo:`, `perform:`, `doesNotUnderstand:` in. `become:` (object-identity swap) **in** — it's a good CEK exercise. Method modification at runtime in.
- **GUI / Morphic / threads:** out entirely.
## Ground rules
- **Scope:** only touch `lib/smalltalk/**` and `plans/smalltalk-on-sx.md`. Don't edit `spec/`, `hosts/`, `shared/`, or any other `lib/<lang>/**`. Smalltalk primitives go in `lib/smalltalk/runtime.sx`.
- **SX files:** use `sx-tree` MCP tools only.
- **Commits:** one feature per commit. Keep `## Progress log` updated and tick roadmap boxes.
## Architecture sketch
```
Smalltalk source
lib/smalltalk/tokenizer.sx — selectors, keywords, literals, $c, #sym, #(…), $'…'
lib/smalltalk/parser.sx — AST: classes, methods, blocks, cascades, sends
lib/smalltalk/transpile.sx — AST → SX AST (entry: smalltalk-eval-ast)
lib/smalltalk/runtime.sx — class table, MOP, dispatch, primitives
```
Core mapping:
- **Class** = SX dict `{:name :superclass :ivars :methods :class-methods :metaclass}`. Class table is a flat dict keyed by class name.
- **Object** = SX dict `{:class :ivars}``ivars` keyed by symbol. Tagged ints / floats / strings / symbols are not boxed; their class is looked up by SX type.
- **Method** = SX lambda closing over a `self` binding + temps. Body wrapped in a delimited continuation so `^` can escape.
- **Message send** = `(st-send receiver selector args)` — does class-table lookup, walks superclass chain, falls back to `doesNotUnderstand:` with a `Message` object.
- **Block** `[:x | … ^v … ]` = lambda + captured `^k` (the method-return continuation). Invoking `^` calls `k`; outer block invocation past method return raises `BlockContext>>cannotReturn:`.
- **Cascade** `r m1; m2; m3` = `(let ((tmp r)) (st-send tmp 'm1 ()) (st-send tmp 'm2 ()) (st-send tmp 'm3 ()))`.
- **`ifTrue:ifFalse:` / `whileTrue:`** = ordinary block sends; the runtime intrinsifies them in the JIT path so they compile to native branches (Tier 1 of bytecode expansion already covers this pattern).
- **`become:`** = swap two object identities everywhere — in SX this is a heap walk, but we restrict to `oneWayBecome:` (cheap: rewrite class field) by default.
## Roadmap
### Phase 1 — tokenizer + parser
- [ ] Tokenizer: identifiers, keywords (`foo:`), binary selectors (`+`, `==`, `,`, `->`, `~=` etc.), numbers (radix `16r1F`, scaled `1.5s2`), strings `'…''…'`, characters `$c`, symbols `#foo` `#'foo bar'` `#+`, byte arrays `#[1 2 3]`, literal arrays `#(1 #foo 'x')`, comments `"…"`
- [ ] Parser: chunk format (`! !` separators), class definitions (`Object subclass: #X instanceVariableNames: '…' classVariableNames: '…' …`), method definitions (`extend: #Foo with: 'bar ^self'`), pragmas `<primitive: 1>`, blocks `[:a :b | | t1 t2 | …]`, cascades, message precedence (unary > binary > keyword)
- [ ] Unit tests in `lib/smalltalk/tests/parse.sx`
### Phase 2 — object model + sequential eval
- [ ] Class table + bootstrap: `Object`, `Behavior`, `Class`, `Metaclass`, `UndefinedObject`, `Boolean`/`True`/`False`, `Number`/`Integer`/`Float`, `String`, `Symbol`, `Array`, `Block`
- [ ] `smalltalk-eval-ast`: literals, variable reference, assignment, message send, cascade, sequence, return
- [ ] Method lookup: walk class → superclass; cache hit-class on `(class, selector)`
- [ ] `doesNotUnderstand:` fallback constructing `Message` object
- [ ] `super` send (lookup starts at superclass of *defining* class, not receiver class)
- [ ] 30+ tests in `lib/smalltalk/tests/eval.sx`
### Phase 3 — blocks + non-local return (THE SHOWCASE)
- [ ] Method invocation captures a `^k` (the return continuation) and binds it as the block's escape
- [ ] `^expr` from inside a block invokes that captured `^k`
- [ ] `BlockContext>>value`, `value:`, `value:value:`, …, `valueWithArguments:`
- [ ] `whileTrue:` / `whileTrue` / `whileFalse:` / `whileFalse` as ordinary block sends — runtime intrinsifies the loop in the bytecode JIT
- [ ] `ifTrue:` / `ifFalse:` / `ifTrue:ifFalse:` as block sends, similarly intrinsified
- [ ] Escape past returned-from method raises `BlockContext>>cannotReturn:`
- [ ] Classic programs in `lib/smalltalk/tests/programs/`:
- [ ] `eight-queens.st`
- [ ] `quicksort.st`
- [ ] `mandelbrot.st`
- [ ] `life.st` (Conway's Life, glider gun)
- [ ] `fibonacci.st` (recursive + memoised)
- [ ] `lib/smalltalk/conformance.sh` + runner, `scoreboard.json` + `scoreboard.md`
### Phase 4 — reflection + MOP
- [ ] `Object>>class`, `class>>name`, `class>>superclass`, `class>>methodDict`, `class>>selectors`
- [ ] `Object>>perform:` / `perform:with:` / `perform:withArguments:`
- [ ] `Object>>respondsTo:`, `Object>>isKindOf:`, `Object>>isMemberOf:`
- [ ] `Behavior>>compile:` — runtime method addition
- [ ] `Object>>becomeForward:` (one-way become; rewrites the class field of `aReceiver`)
- [ ] Exceptions: `Exception`, `Error`, `signal`, `signal:`, `on:do:`, `ensure:`, `ifCurtailed:` — built on top of SX `handler-bind`/`raise`
### Phase 5 — collections + numeric tower
- [ ] `SequenceableCollection`/`OrderedCollection`/`Array`/`String`/`Symbol`
- [ ] `HashedCollection`/`Set`/`Dictionary`/`IdentityDictionary`
- [ ] `Stream` hierarchy: `ReadStream`/`WriteStream`/`ReadWriteStream`
- [ ] `Number` tower: `SmallInteger`/`LargePositiveInteger`/`Float`/`Fraction`
- [ ] `String>>format:`, `printOn:` for everything
### Phase 6 — SUnit + corpus to 200+
- [ ] Port SUnit (TestCase, TestSuite, TestResult) — written in SX-Smalltalk, runs in itself
- [ ] Vendor a slice of Pharo `Kernel-Tests` and `Collections-Tests`
- [ ] Drive the scoreboard up: aim for 200+ green tests
- [ ] Stretch: ANSI Smalltalk validator subset
### Phase 7 — speed (optional)
- [ ] Method-dictionary inline caching (already in CEK as a primitive; just wire selector cache)
- [ ] Block intrinsification beyond `whileTrue:` / `ifTrue:`
- [ ] Compare against GNU Smalltalk on the corpus
## Progress log
_Newest first. Agent appends on every commit._
- _(none yet)_
## Blockers
_Shared-file issues that need someone else to fix. Minimal repro only._
- _(none yet)_

View File

@@ -1,127 +0,0 @@
# Tcl-on-SX: uplevel/upvar = stack-walking delcc, everything-is-a-string
The headline showcase is **uplevel/upvar** — Tcl's superpower for defining your own control structures. `uplevel` evaluates a script in the *caller's* stack frame; `upvar` aliases a variable in the caller. On a normal language host this requires deep VM cooperation; on SX it falls out of the env-chain made first-class via captured continuations. Plus the *Dodekalogue* (12 rules), command-substitution everywhere, and "everything is a string" homoiconicity.
End-state goal: Tcl 8.6-flavoured subset, the Dodekalogue parser, namespaces, `try`/`catch`/`return -code`, `coroutine` (built on fibers), classic programs that show off uplevel-driven DSLs, ~150 hand-written tests.
## Scope decisions (defaults — override by editing before we spawn)
- **Syntax:** Tcl 8.6 surface. The 12-rule Dodekalogue. Brace-quoted scripts deferred-evaluate; double-quoted ones substitute.
- **Conformance:** "Reads like Tcl, runs like Tcl." Slice of Tcl's own test suite, not full TCT.
- **Test corpus:** custom + curated `tcl-tests/` slice. Plus classic programs: define-your-own `for-each-line`, expression-language compiler-in-Tcl, fiber-based event loop.
- **Out of scope:** Tk, sockets beyond a stub, threads (mapped to `coroutine` only), `package require` of binary loadables, `dde`/`registry` Windows shims, full `clock format` locale support.
- **Channels:** `puts` and `gets` on `stdout`/`stdin`/`stderr`; `open` on regular files; no async I/O beyond what `coroutine` gives.
## Ground rules
- **Scope:** only touch `lib/tcl/**` and `plans/tcl-on-sx.md`. Don't edit `spec/`, `hosts/`, `shared/`, or any other `lib/<lang>/**`. Tcl primitives go in `lib/tcl/runtime.sx`.
- **SX files:** use `sx-tree` MCP tools only.
- **Commits:** one feature per commit. Keep `## Progress log` updated and tick roadmap boxes.
## Architecture sketch
```
Tcl source
lib/tcl/tokenizer.sx — the Dodekalogue: words, [..], ${..}, "..", {..}, ;, \n, \, #
lib/tcl/parser.sx — list-of-words AST (script = list of commands; command = list of words)
lib/tcl/transpile.sx — AST → SX AST (entry: tcl-eval-script)
lib/tcl/runtime.sx — env stack, command table, uplevel/upvar, coroutines, BIFs
```
Core mapping:
- **Value** = string. Internally we cache a "shimmer" representation (list, dict, integer, double) for performance, but every value can be re-stringified.
- **Variable** = entry in current frame's env. Frames form a stack; level-0 is the global frame.
- **Command** = entry in command table; first word of any list dispatches into it. User-defined via `proc`. Built-ins are SX functions registered in the table.
- **Frame** = `{:locals (dict) :level n :parent frame}`. Each `proc` call pushes a frame; commands run in current frame.
- **`uplevel #N script`** = walk frame chain to absolute level N (or relative if no `#`); evaluate script in that frame's env.
- **`upvar [#N] varname localname`** = bind `localname` in the current frame as an alias to `varname` in the level-N frame (env-chain delegate).
- **`return -code N`** = control flow as integers: 0=ok, 1=error, 2=return, 3=break, 4=continue. `catch` traps any non-zero; `try` adds named handlers.
- **`coroutine`** = fiber on top of `perform`/`cek-resume`. `yield`/`yieldto` suspend; calling the coroutine command resumes.
- **List / dict** = list-shaped string ("element1 element2 …") with a cached parsed form. Modifications dirty the string cache.
## Roadmap
### Phase 1 — tokenizer + parser (the Dodekalogue)
- [ ] Tokenizer applying the 12 rules:
1. Commands separated by `;` or newlines
2. Words separated by whitespace within a command
3. Double-quoted words: `\` escapes + `[…]` + `${…}` + `$var` substitution
4. Brace-quoted words: literal, no substitution; brace count must balance
5. Argument expansion: `{*}list`
6. Command substitution: `[script]` evaluates script, takes its return value
7. Variable substitution: `$name`, `${name}`, `$arr(idx)`, `$arr($i)`
8. Backslash substitution: `\n`, `\t`, `\\`, `\xNN`, `\uNNNN`, `\<newline>` continues
9. Comments: `#` only at the start of a command
10. Order of substitution is left-to-right, single-pass
11. Substitutions don't recurse — substituted text is not re-parsed
12. The result of any substitution is the value, not a new script
- [ ] Parser: script = list of commands; command = list of words; word = literal string + list of substitutions
- [ ] Unit tests in `lib/tcl/tests/parse.sx`
### Phase 2 — sequential eval + core commands
- [ ] `tcl-eval-script`: walk command list, dispatch each first-word into command table
- [ ] Core commands: `set`, `unset`, `incr`, `append`, `lappend`, `puts`, `gets`, `expr`, `if`, `while`, `for`, `foreach`, `switch`, `break`, `continue`, `return`, `error`, `eval`, `subst`, `format`, `scan`
- [ ] `expr` is its own mini-language — operator precedence, function calls (`sin`, `sqrt`, `pow`, `abs`, `int`, `double`), variable substitution, command substitution
- [ ] String commands: `string length`, `string index`, `string range`, `string compare`, `string match`, `string toupper`, `string tolower`, `string trim`, `string map`, `string repeat`, `string first`, `string last`, `string is`, `string cat`
- [ ] List commands: `list`, `lindex`, `lrange`, `llength`, `lreverse`, `lsearch`, `lsort`, `lsort -integer/-real/-dictionary`, `lreplace`, `linsert`, `concat`, `split`, `join`
- [ ] Dict commands: `dict create`, `dict get`, `dict set`, `dict unset`, `dict exists`, `dict keys`, `dict values`, `dict size`, `dict for`, `dict update`, `dict merge`
- [ ] 60+ tests in `lib/tcl/tests/eval.sx`
### Phase 3 — proc + uplevel + upvar (THE SHOWCASE)
- [ ] `proc name args body` — register user-defined command; args supports defaults `{name default}` and rest `args`
- [ ] Frame stack: each proc call pushes a frame with locals dict; pop on return
- [ ] `uplevel ?level? script` — evaluate `script` in level-N frame's env; default level is 1 (caller). `#0` is global, `#1` is relative-1
- [ ] `upvar ?level? otherVar localVar ?…?` — alias localVar to a variable in level-N frame; reads/writes go through the alias
- [ ] `info level`, `info level N`, `info frame`, `info vars`, `info locals`, `info globals`, `info commands`, `info procs`, `info args`, `info body`
- [ ] `global var ?…?` — alias to global frame (sugar for `upvar #0 var var`)
- [ ] `variable name ?value?` — namespace-scoped global
- [ ] Classic programs in `lib/tcl/tests/programs/`:
- [ ] `for-each-line.tcl` — define your own loop construct using `uplevel`
- [ ] `assert.tcl` — assertion macro that reports caller's line
- [ ] `with-temp-var.tcl` — scoped variable rebind via `upvar`
- [ ] `lib/tcl/conformance.sh` + runner, `scoreboard.json` + `scoreboard.md`
### Phase 4 — control flow + error handling
- [ ] `return -code (ok|error|return|break|continue|N) -errorinfo … -errorcode … -level N value`
- [ ] `catch script ?resultVar? ?optionsVar?` — runs script, returns code; sets resultVar to return value/message; optionsVar to the dict
- [ ] `try script ?on code var body ...? ?trap pattern var body...? ?finally body?`
- [ ] `throw type message`
- [ ] `error message ?info? ?code?`
- [ ] Stack-trace with `errorInfo` / `errorCode`
- [ ] 30+ tests in `lib/tcl/tests/error.sx`
### Phase 5 — namespaces + ensembles
- [ ] `namespace eval ns body`, `namespace current`, `namespace which`, `namespace import`, `namespace export`, `namespace forget`, `namespace delete`
- [ ] Qualified names: `::ns::cmd`, `::ns::var`
- [ ] Ensembles: `namespace ensemble create -map { sub1 cmd1 sub2 cmd2 }`
- [ ] `namespace path` for resolution chain
- [ ] `proc` and `variable` work inside namespaces
### Phase 6 — coroutines + drive corpus
- [ ] `coroutine name cmd ?args…?` — start a coroutine; future calls to `name` resume it
- [ ] `yield ?value?` — suspend, return value to resumer
- [ ] `yieldto cmd ?args…?` — symmetric transfer
- [ ] `coroutine` semantics built on fibers (same delcc primitive as Ruby fibers)
- [ ] Classic programs: `event-loop.tcl` — cooperative scheduler with multiple coroutines
- [ ] System: `clock seconds`, `clock format`, `clock scan` (subset)
- [ ] File I/O: `open`, `close`, `read`, `gets`, `puts -nonewline`, `flush`, `eof`, `seek`, `tell`
- [ ] Drive corpus to 150+ green
- [ ] Idiom corpus — `lib/tcl/tests/idioms.sx` covering classic Welch/Jones idioms
## Progress log
_Newest first._
- _(none yet)_
## Blockers
- _(none yet)_

View File

@@ -30,7 +30,7 @@ fi
if [ "$CLEAN" = "1" ]; then
cd "$(dirname "$0")/.."
for lang in lua prolog forth erlang haskell js hs smalltalk common-lisp apl ruby tcl; do
for lang in lua prolog forth erlang haskell js hs; do
wt="$WORKTREE_BASE/$lang"
if [ -d "$wt" ]; then
git worktree remove --force "$wt" 2>/dev/null || rm -rf "$wt"
@@ -39,5 +39,5 @@ if [ "$CLEAN" = "1" ]; then
done
git worktree prune
echo "Worktree branches (loops/<lang>) are preserved. Delete manually if desired:"
echo " git branch -D loops/lua loops/prolog loops/forth loops/erlang loops/haskell loops/js loops/hs loops/smalltalk loops/common-lisp loops/apl loops/ruby loops/tcl"
echo " git branch -D loops/lua loops/prolog loops/forth loops/erlang loops/haskell loops/js loops/hs"
fi

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Spawn 12 claude sessions in tmux, one per language loop.
# Spawn 7 claude sessions in tmux, one per language loop.
# Each runs in its own git worktree rooted at /root/rose-ash-loops/<lang>,
# on branch loops/<lang>. No two loops share a working tree, so there's
# zero risk of file collisions between languages.
@@ -9,7 +9,7 @@
#
# After the script prints done:
# tmux a -t sx-loops
# Ctrl-B + <window-number> to switch (0=lua ... 11=tcl)
# Ctrl-B + <window-number> to switch (0=lua ... 6=hs)
# Ctrl-B + d to detach (loops keep running, SSH-safe)
#
# Stop: ./scripts/sx-loops-down.sh
@@ -38,13 +38,8 @@ declare -A BRIEFING=(
[haskell]=haskell-loop.md
[js]=loop.md
[hs]=hs-loop.md
[smalltalk]=smalltalk-loop.md
[common-lisp]=common-lisp-loop.md
[apl]=apl-loop.md
[ruby]=ruby-loop.md
[tcl]=tcl-loop.md
)
ORDER=(lua prolog forth erlang haskell js hs smalltalk common-lisp apl ruby tcl)
ORDER=(lua prolog forth erlang haskell js hs)
mkdir -p "$WORKTREE_BASE"
@@ -65,13 +60,13 @@ for lang in "${ORDER[@]}"; do
fi
done
# Create tmux session with one window per language, each cwd in its worktree
# Create tmux session with 7 windows, each cwd in its worktree
tmux new-session -d -s "$SESSION" -n "${ORDER[0]}" -c "$WORKTREE_BASE/${ORDER[0]}"
for lang in "${ORDER[@]:1}"; do
tmux new-window -t "$SESSION" -n "$lang" -c "$WORKTREE_BASE/$lang"
done
echo "Starting ${#ORDER[@]} claude sessions..."
echo "Starting 7 claude sessions..."
for lang in "${ORDER[@]}"; do
tmux send-keys -t "$SESSION:$lang" "claude" C-m
done
@@ -94,10 +89,10 @@ for lang in "${ORDER[@]}"; do
done
echo ""
echo "Done. ${#ORDER[@]} loops started in tmux session '$SESSION', each in its own worktree."
echo "Done. 7 loops started in tmux session '$SESSION', each in its own worktree."
echo ""
echo " Attach: tmux a -t $SESSION"
echo " Switch: Ctrl-B <0..11> (0=lua 1=prolog 2=forth 3=erlang 4=haskell 5=js 6=hs 7=smalltalk 8=common-lisp 9=apl 10=ruby 11=tcl)"
echo " Switch: Ctrl-B <0..6> (0=lua 1=prolog 2=forth 3=erlang 4=haskell 5=js 6=hs)"
echo " List: Ctrl-B w"
echo " Detach: Ctrl-B d"
echo " Stop: ./scripts/sx-loops-down.sh"

View File

@@ -88,27 +88,6 @@
(raise _e))))
(handler me-val))))))
;; Evaluate a hyperscript expression, catch the first error raised, and
;; return its message string. Used by runtimeErrors tests.
;; Returns nil if no error is raised (test would then fail equality).
(define eval-hs-error
(fn (src)
(let ((sx (hs-to-sx (hs-compile src))))
(let ((handler (eval-expr-cek
(list (quote fn) (list (quote me))
(list (quote let) (list (list (quote it) nil) (list (quote event) nil)) sx)))))
(guard
(_e
(true
(if
(string? _e)
_e
(if
(and (list? _e) (= (first _e) "hs-return"))
nil
(str _e)))))
(begin (handler nil) nil))))))
;; ── add (19 tests) ──
(defsuite "hs-upstream-add"
(deftest "can add a value to a set"
@@ -2174,75 +2153,41 @@
;; ── core/runtimeErrors (18 tests) ──
(defsuite "hs-upstream-core/runtimeErrors"
(deftest "reports basic function invocation null errors properly"
(assert= (eval-hs-error "x()") "'x' is null")
(assert= (eval-hs-error "x.y()") "'x' is null")
(assert= (eval-hs-error "x.y.z()") "'x.y' is null")
)
(error "SKIP (untranslated): reports basic function invocation null errors properly"))
(deftest "reports basic function invocation null errors properly w/ of"
(assert= (eval-hs-error "z() of y of x") "'z' is null")
)
(error "SKIP (untranslated): reports basic function invocation null errors properly w/ of"))
(deftest "reports basic function invocation null errors properly w/ possessives"
(assert= (eval-hs-error "x's y()") "'x' is null")
(assert= (eval-hs-error "x's y's z()") "'x's y' is null")
)
(error "SKIP (untranslated): reports basic function invocation null errors properly w/ possessives"))
(deftest "reports null errors on add command properly"
(assert= (eval-hs-error "add .foo to #doesntExist") "'#doesntExist' is null")
(assert= (eval-hs-error "add @foo to #doesntExist") "'#doesntExist' is null")
(assert= (eval-hs-error "add {display:none} to #doesntExist") "'#doesntExist' is null")
)
(error "SKIP (untranslated): reports null errors on add command properly"))
(deftest "reports null errors on decrement command properly"
(assert= (eval-hs-error "decrement #doesntExist's innerHTML") "'#doesntExist' is null")
)
(error "SKIP (untranslated): reports null errors on decrement command properly"))
(deftest "reports null errors on default command properly"
(assert= (eval-hs-error "default #doesntExist's innerHTML to 'foo'") "'#doesntExist' is null")
)
(error "SKIP (untranslated): reports null errors on default command properly"))
(deftest "reports null errors on hide command properly"
(assert= (eval-hs-error "hide #doesntExist") "'#doesntExist' is null")
)
(error "SKIP (untranslated): reports null errors on hide command properly"))
(deftest "reports null errors on increment command properly"
(assert= (eval-hs-error "increment #doesntExist's innerHTML") "'#doesntExist' is null")
)
(error "SKIP (untranslated): reports null errors on increment command properly"))
(deftest "reports null errors on measure command properly"
(assert= (eval-hs-error "measure #doesntExist") "'#doesntExist' is null")
)
(error "SKIP (untranslated): reports null errors on measure command properly"))
(deftest "reports null errors on put command properly"
(assert= (eval-hs-error "put 'foo' into #doesntExist") "'#doesntExist' is null")
(assert= (eval-hs-error "put 'foo' into #doesntExist's innerHTML") "'#doesntExist' is null")
(assert= (eval-hs-error "put 'foo' into #doesntExist.innerHTML") "'#doesntExist' is null")
(assert= (eval-hs-error "put 'foo' before #doesntExist") "'#doesntExist' is null")
(assert= (eval-hs-error "put 'foo' after #doesntExist") "'#doesntExist' is null")
(assert= (eval-hs-error "put 'foo' at the start of #doesntExist") "'#doesntExist' is null")
(assert= (eval-hs-error "put 'foo' at the end of #doesntExist") "'#doesntExist' is null")
)
(error "SKIP (untranslated): reports null errors on put command properly"))
(deftest "reports null errors on remove command properly"
(assert= (eval-hs-error "remove .foo from #doesntExist") "'#doesntExist' is null")
(assert= (eval-hs-error "remove @foo from #doesntExist") "'#doesntExist' is null")
(assert= (eval-hs-error "remove #doesntExist from #doesntExist") "'#doesntExist' is null")
)
(error "SKIP (untranslated): reports null errors on remove command properly"))
(deftest "reports null errors on send command properly"
(assert= (eval-hs-error "send 'foo' to #doesntExist") "'#doesntExist' is null")
)
(error "SKIP (untranslated): reports null errors on send command properly"))
(deftest "reports null errors on sets properly"
(assert= (eval-hs-error "set x's y to true") "'x' is null")
(assert= (eval-hs-error "set x's @y to true") "'x' is null")
)
(error "SKIP (untranslated): reports null errors on sets properly"))
(deftest "reports null errors on settle command properly"
(assert= (eval-hs-error "settle #doesntExist") "'#doesntExist' is null")
)
(error "SKIP (untranslated): reports null errors on settle command properly"))
(deftest "reports null errors on show command properly"
(assert= (eval-hs-error "show #doesntExist") "'#doesntExist' is null")
)
(error "SKIP (untranslated): reports null errors on show command properly"))
(deftest "reports null errors on toggle command properly"
(assert= (eval-hs-error "toggle .foo on #doesntExist") "'#doesntExist' is null")
(assert= (eval-hs-error "toggle between .foo and .bar on #doesntExist") "'#doesntExist' is null")
(assert= (eval-hs-error "toggle @foo on #doesntExist") "'#doesntExist' is null")
)
(error "SKIP (untranslated): reports null errors on toggle command properly"))
(deftest "reports null errors on transition command properly"
(assert= (eval-hs-error "transition #doesntExist's *visibility to 0") "'#doesntExist' is null")
)
(error "SKIP (untranslated): reports null errors on transition command properly"))
(deftest "reports null errors on trigger command properly"
(assert= (eval-hs-error "trigger 'foo' on #doesntExist") "'#doesntExist' is null")
)
(error "SKIP (untranslated): reports null errors on trigger command properly"))
)
;; ── core/scoping (20 tests) ──

View File

@@ -2333,25 +2333,6 @@ def generate_eval_only_test(test, idx):
hs_expr = extract_hs_expr(m.group(2))
assertions.append(f' (assert-throws (eval-hs "{hs_expr}"))')
# Pattern 4: eval-hs-error — expect(await error("expr")).toBe("msg")
# These test that running HS raises an error with a specific message string.
for m in re.finditer(
r'(?:const\s+\w+\s*=\s*)?(?:await\s+)?error\((["\x27`])(.+?)\1\)'
r'(?:[^;]|\n)*?(?:expect\([^)]*\)\.toBe\(([^)]+)\)|\.toBe\(([^)]+)\))',
body, re.DOTALL
):
hs_expr = extract_hs_expr(m.group(2))
expected_raw = (m.group(3) or m.group(4) or '').strip()
# Strip only the outermost JS string delimiter (double or single quote)
# without touching inner quotes inside the string value.
if len(expected_raw) >= 2 and expected_raw[0] == expected_raw[-1] and expected_raw[0] in ('"', "'"):
inner = expected_raw[1:-1]
expected_sx = '"' + inner.replace('\\', '\\\\').replace('"', '\\"') + '"'
else:
expected_sx = js_val_to_sx(expected_raw)
hs_escaped = hs_expr.replace('\\', '\\\\').replace('"', '\\"')
assertions.append(f' (assert= (eval-hs-error "{hs_escaped}") {expected_sx})')
if not assertions:
return None # Can't convert this body pattern
@@ -2711,27 +2692,6 @@ output.append(' (nth _e 1)')
output.append(' (raise _e))))')
output.append(' (handler me-val))))))')
output.append('')
output.append(';; Evaluate a hyperscript expression, catch the first error raised, and')
output.append(';; return its message string. Used by runtimeErrors tests.')
output.append(';; Returns nil if no error is raised (test would then fail equality).')
output.append('(define eval-hs-error')
output.append(' (fn (src)')
output.append(' (let ((sx (hs-to-sx (hs-compile src))))')
output.append(' (let ((handler (eval-expr-cek')
output.append(' (list (quote fn) (list (quote me))')
output.append(' (list (quote let) (list (list (quote it) nil) (list (quote event) nil)) sx)))))')
output.append(' (guard')
output.append(' (_e')
output.append(' (true')
output.append(' (if')
output.append(' (string? _e)')
output.append(' _e')
output.append(' (if')
output.append(' (and (list? _e) (= (first _e) "hs-return"))')
output.append(' nil')
output.append(' (str _e)))))')
output.append(' (begin (handler nil) nil))))))')
output.append('')
# Group by category
categories = OrderedDict()