From ef81fffb6fd0d1699a8c13d38b3397f9772fbc44 Mon Sep 17 00:00:00 2001 From: giles Date: Fri, 24 Apr 2026 21:16:53 +0000 Subject: [PATCH] =?UTF-8?q?haskell:=20desugar=20guards/where/list-comp=20?= =?UTF-8?q?=E2=86=92=20core=20AST=20(+15=20tests,=20226/226)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/haskell/desugar.sx | 213 ++++++++++++++++++++++++ lib/haskell/test.sh | 2 + lib/haskell/tests/desugar.sx | 305 +++++++++++++++++++++++++++++++++++ plans/haskell-on-sx.md | 23 ++- 4 files changed, 542 insertions(+), 1 deletion(-) create mode 100644 lib/haskell/desugar.sx create mode 100644 lib/haskell/tests/desugar.sx diff --git a/lib/haskell/desugar.sx b/lib/haskell/desugar.sx new file mode 100644 index 00000000..c44fbe89 --- /dev/null +++ b/lib/haskell/desugar.sx @@ -0,0 +1,213 @@ +;; 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)))))))) + +;; 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") + (list :do (map hk-desugar (nth node 1)))) + ((= tag "do-expr") + (list :do-expr (hk-desugar (nth node 1)))) + ((= tag "do-bind") + (list + :do-bind + (nth node 1) + (hk-desugar (nth node 2)))) + ((= tag "do-let") + (list :do-let (map hk-desugar (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)))) diff --git a/lib/haskell/test.sh b/lib/haskell/test.sh index 54a47fa4..031880f8 100755 --- a/lib/haskell/test.sh +++ b/lib/haskell/test.sh @@ -48,6 +48,7 @@ for FILE in "${FILES[@]}"; do (load "lib/haskell/tokenizer.sx") (load "lib/haskell/layout.sx") (load "lib/haskell/parser.sx") +(load "lib/haskell/desugar.sx") (load "lib/haskell/testlib.sx") (epoch 2) (load "$FILE") @@ -86,6 +87,7 @@ EPOCHS (load "lib/haskell/tokenizer.sx") (load "lib/haskell/layout.sx") (load "lib/haskell/parser.sx") +(load "lib/haskell/desugar.sx") (load "lib/haskell/testlib.sx") (epoch 2) (load "$FILE") diff --git a/lib/haskell/tests/desugar.sx b/lib/haskell/tests/desugar.sx new file mode 100644 index 00000000..2487aeb4 --- /dev/null +++ b/lib/haskell/tests/desugar.sx @@ -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} diff --git a/plans/haskell-on-sx.md b/plans/haskell-on-sx.md index 63f88c06..f8f729aa 100644 --- a/plans/haskell-on-sx.md +++ b/plans/haskell-on-sx.md @@ -68,7 +68,7 @@ Key mappings: - [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) +- [x] 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 @@ -114,6 +114,27 @@ Key mappings: _Newest first._ +- **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-