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

This commit is contained in:
2026-04-24 21:16:53 +00:00
parent cab7ca883f
commit ef81fffb6f
4 changed files with 542 additions and 1 deletions

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

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

View File

@@ -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")

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}

View File

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