haskell: expression parser + precedence climbing (+42 tests, 100/100)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
This commit is contained in:
511
lib/haskell/parser.sx
Normal file
511
lib/haskell/parser.sx
Normal file
@@ -0,0 +1,511 @@
|
||||
;; Haskell 98 expression parser.
|
||||
;;
|
||||
;; Input: the post-layout token list from (hk-layout (hk-tokenize src)).
|
||||
;; Output: an AST. Nodes are plain lists tagged by a keyword head
|
||||
;; (keywords evaluate to their string name, so `(list :var "x")` is
|
||||
;; indistinguishable from `(list "var" "x")` at runtime — this lets
|
||||
;; tests literally write `(list :var "x")` on both sides).
|
||||
;;
|
||||
;; Scope (this iteration — expressions only):
|
||||
;; atoms int/float/string/char/var/con, parens, tuple, list, range
|
||||
;; application left-associative, f x y z
|
||||
;; prefix - unary negation on an lexp
|
||||
;; infix ops precedence-climbing, full Haskell 98 default table
|
||||
;; lambda \x y -> body
|
||||
;; if if c then t else e
|
||||
;; let let { x = e ; y = e } in body (uses layout braces)
|
||||
;;
|
||||
;; AST shapes:
|
||||
;; (:int N)
|
||||
;; (:float F)
|
||||
;; (:string S)
|
||||
;; (:char C)
|
||||
;; (:var NAME)
|
||||
;; (:con NAME)
|
||||
;; (:app FN ARG) — binary, chain for multi-arg
|
||||
;; (:op OP LHS RHS) — binary infix
|
||||
;; (:neg E)
|
||||
;; (:tuple ITEMS) — ITEMS is a list of AST nodes
|
||||
;; (:list ITEMS) — enumerated list
|
||||
;; (:range FROM TO) — [from..to]
|
||||
;; (:range-step FROM NEXT TO) — [from,next..to]
|
||||
;; (:if C T E)
|
||||
;; (:lambda PARAMS BODY) — PARAMS is list of varids
|
||||
;; (:let BINDS BODY) — BINDS is list of (:bind NAME EXPR)
|
||||
|
||||
;; ── Operator precedence table (Haskell 98 defaults) ──────────────
|
||||
(define
|
||||
hk-op-prec-table
|
||||
(let
|
||||
((t (dict)))
|
||||
(dict-set! t "!!" {:prec 9 :assoc "left"})
|
||||
(dict-set! t "." {:prec 9 :assoc "right"})
|
||||
(dict-set! t "^" {:prec 8 :assoc "right"})
|
||||
(dict-set! t "^^" {:prec 8 :assoc "right"})
|
||||
(dict-set! t "**" {:prec 8 :assoc "right"})
|
||||
(dict-set! t "*" {:prec 7 :assoc "left"})
|
||||
(dict-set! t "/" {:prec 7 :assoc "left"})
|
||||
(dict-set! t "+" {:prec 6 :assoc "left"})
|
||||
(dict-set! t "-" {:prec 6 :assoc "left"})
|
||||
(dict-set! t ":" {:prec 5 :assoc "right"})
|
||||
(dict-set! t "++" {:prec 5 :assoc "right"})
|
||||
(dict-set! t "==" {:prec 4 :assoc "non"})
|
||||
(dict-set! t "/=" {:prec 4 :assoc "non"})
|
||||
(dict-set! t "<" {:prec 4 :assoc "non"})
|
||||
(dict-set! t "<=" {:prec 4 :assoc "non"})
|
||||
(dict-set! t ">" {:prec 4 :assoc "non"})
|
||||
(dict-set! t ">=" {:prec 4 :assoc "non"})
|
||||
(dict-set! t "&&" {:prec 3 :assoc "right"})
|
||||
(dict-set! t "||" {:prec 2 :assoc "right"})
|
||||
(dict-set! t ">>" {:prec 1 :assoc "left"})
|
||||
(dict-set! t ">>=" {:prec 1 :assoc "left"})
|
||||
(dict-set! t "=<<" {:prec 1 :assoc "right"})
|
||||
(dict-set! t "$" {:prec 0 :assoc "right"})
|
||||
(dict-set! t "$!" {:prec 0 :assoc "right"})
|
||||
t))
|
||||
|
||||
(define
|
||||
hk-op-info
|
||||
(fn
|
||||
(op)
|
||||
(if
|
||||
(has-key? hk-op-prec-table op)
|
||||
(get hk-op-prec-table op)
|
||||
{:prec 9 :assoc "left"})))
|
||||
|
||||
;; ── Atom-start predicate ─────────────────────────────────────────
|
||||
(define
|
||||
hk-atom-start?
|
||||
(fn
|
||||
(tok)
|
||||
(if
|
||||
(nil? tok)
|
||||
false
|
||||
(let
|
||||
((ty (get tok "type")))
|
||||
(or
|
||||
(= ty "integer")
|
||||
(= ty "float")
|
||||
(= ty "string")
|
||||
(= ty "char")
|
||||
(= ty "varid")
|
||||
(= ty "conid")
|
||||
(= ty "qvarid")
|
||||
(= ty "qconid")
|
||||
(= ty "lparen")
|
||||
(= ty "lbracket"))))))
|
||||
|
||||
;; ── Main entry ───────────────────────────────────────────────────
|
||||
(define
|
||||
hk-parse-expr
|
||||
(fn
|
||||
(tokens)
|
||||
(let
|
||||
((toks tokens) (pos 0) (n (len tokens)))
|
||||
|
||||
(define hk-peek (fn () (if (< pos n) (nth toks pos) nil)))
|
||||
(define
|
||||
hk-peek-at
|
||||
(fn
|
||||
(offset)
|
||||
(if (< (+ pos offset) n) (nth toks (+ pos offset)) nil)))
|
||||
(define
|
||||
hk-advance!
|
||||
(fn () (let ((t (hk-peek))) (set! pos (+ pos 1)) t)))
|
||||
(define
|
||||
hk-peek-type
|
||||
(fn () (let ((t (hk-peek))) (if (nil? t) "<eof>" (get t "type")))))
|
||||
(define
|
||||
hk-peek-value
|
||||
(fn () (let ((t (hk-peek))) (if (nil? t) nil (get t "value")))))
|
||||
(define
|
||||
hk-match?
|
||||
(fn
|
||||
(ty v)
|
||||
(let
|
||||
((t (hk-peek)))
|
||||
(and
|
||||
(not (nil? t))
|
||||
(= (get t "type") ty)
|
||||
(or (nil? v) (= (get t "value") v))))))
|
||||
(define
|
||||
hk-err
|
||||
(fn
|
||||
(msg)
|
||||
(raise
|
||||
(str
|
||||
"parse error: "
|
||||
msg
|
||||
" (at "
|
||||
(hk-peek-type)
|
||||
(if (nil? (hk-peek-value)) "" (str " " (hk-peek-value)))
|
||||
")"))))
|
||||
(define
|
||||
hk-expect!
|
||||
(fn
|
||||
(ty v)
|
||||
(if
|
||||
(hk-match? ty v)
|
||||
(hk-advance!)
|
||||
(hk-err
|
||||
(str "expected " ty (if (nil? v) "" (str " '" v "'")))))))
|
||||
|
||||
;; ── Atoms ────────────────────────────────────────────────
|
||||
(define
|
||||
hk-parse-aexp
|
||||
(fn
|
||||
()
|
||||
(let
|
||||
((t (hk-peek)))
|
||||
(cond
|
||||
((nil? t) (hk-err "unexpected end of input"))
|
||||
((= (get t "type") "integer")
|
||||
(do (hk-advance!) (list :int (get t "value"))))
|
||||
((= (get t "type") "float")
|
||||
(do (hk-advance!) (list :float (get t "value"))))
|
||||
((= (get t "type") "string")
|
||||
(do (hk-advance!) (list :string (get t "value"))))
|
||||
((= (get t "type") "char")
|
||||
(do (hk-advance!) (list :char (get t "value"))))
|
||||
((= (get t "type") "varid")
|
||||
(do (hk-advance!) (list :var (get t "value"))))
|
||||
((= (get t "type") "conid")
|
||||
(do (hk-advance!) (list :con (get t "value"))))
|
||||
((= (get t "type") "qvarid")
|
||||
(do (hk-advance!) (list :var (get t "value"))))
|
||||
((= (get t "type") "qconid")
|
||||
(do (hk-advance!) (list :con (get t "value"))))
|
||||
((= (get t "type") "lparen") (hk-parse-parens))
|
||||
((= (get t "type") "lbracket") (hk-parse-list-lit))
|
||||
(:else (hk-err "unexpected token in expression"))))))
|
||||
|
||||
;; ── Parens / tuple / unit ────────────────────────────────
|
||||
(define
|
||||
hk-parse-parens
|
||||
(fn
|
||||
()
|
||||
(hk-expect! "lparen" nil)
|
||||
(cond
|
||||
((hk-match? "rparen" nil)
|
||||
(do (hk-advance!) (list :con "()")))
|
||||
(:else
|
||||
(let
|
||||
((first-e (hk-parse-expr-inner))
|
||||
(items (list))
|
||||
(is-tuple false))
|
||||
(append! items first-e)
|
||||
(define
|
||||
hk-tup-loop
|
||||
(fn
|
||||
()
|
||||
(when
|
||||
(hk-match? "comma" nil)
|
||||
(do
|
||||
(hk-advance!)
|
||||
(set! is-tuple true)
|
||||
(append! items (hk-parse-expr-inner))
|
||||
(hk-tup-loop)))))
|
||||
(hk-tup-loop)
|
||||
(hk-expect! "rparen" nil)
|
||||
(if is-tuple (list :tuple items) first-e))))))
|
||||
|
||||
;; ── List literal / range ─────────────────────────────────
|
||||
(define
|
||||
hk-parse-list-lit
|
||||
(fn
|
||||
()
|
||||
(hk-expect! "lbracket" nil)
|
||||
(cond
|
||||
((hk-match? "rbracket" nil)
|
||||
(do (hk-advance!) (list :list (list))))
|
||||
(:else
|
||||
(let
|
||||
((first-e (hk-parse-expr-inner)))
|
||||
(cond
|
||||
((hk-match? "reservedop" "..")
|
||||
(do
|
||||
(hk-advance!)
|
||||
(let
|
||||
((end-e (hk-parse-expr-inner)))
|
||||
(hk-expect! "rbracket" nil)
|
||||
(list :range first-e end-e))))
|
||||
((hk-match? "comma" nil)
|
||||
(do
|
||||
(hk-advance!)
|
||||
(let
|
||||
((second-e (hk-parse-expr-inner)))
|
||||
(cond
|
||||
((hk-match? "reservedop" "..")
|
||||
(do
|
||||
(hk-advance!)
|
||||
(let
|
||||
((end-e (hk-parse-expr-inner)))
|
||||
(hk-expect! "rbracket" nil)
|
||||
(list
|
||||
:range-step
|
||||
first-e
|
||||
second-e
|
||||
end-e))))
|
||||
(:else
|
||||
(let
|
||||
((items (list)))
|
||||
(append! items first-e)
|
||||
(append! items second-e)
|
||||
(define
|
||||
hk-list-loop
|
||||
(fn
|
||||
()
|
||||
(when
|
||||
(hk-match? "comma" nil)
|
||||
(do
|
||||
(hk-advance!)
|
||||
(append!
|
||||
items
|
||||
(hk-parse-expr-inner))
|
||||
(hk-list-loop)))))
|
||||
(hk-list-loop)
|
||||
(hk-expect! "rbracket" nil)
|
||||
(list :list items)))))))
|
||||
(:else
|
||||
(do
|
||||
(hk-expect! "rbracket" nil)
|
||||
(list :list (list first-e))))))))))
|
||||
|
||||
;; ── Application: left-assoc aexp chain ───────────────────
|
||||
(define
|
||||
hk-parse-fexp
|
||||
(fn
|
||||
()
|
||||
(let
|
||||
((fn-e (hk-parse-aexp)))
|
||||
(define
|
||||
hk-app-loop
|
||||
(fn
|
||||
()
|
||||
(when
|
||||
(hk-atom-start? (hk-peek))
|
||||
(let
|
||||
((arg (hk-parse-aexp)))
|
||||
(set! fn-e (list :app fn-e arg))
|
||||
(hk-app-loop)))))
|
||||
(hk-app-loop)
|
||||
fn-e)))
|
||||
|
||||
;; ── Lambda: \ p1 p2 ... pn -> body ───────────────────────
|
||||
(define
|
||||
hk-parse-lambda
|
||||
(fn
|
||||
()
|
||||
(hk-expect! "reservedop" "\\")
|
||||
(let
|
||||
((params (list)))
|
||||
(when
|
||||
(not (hk-match? "varid" nil))
|
||||
(hk-err "lambda parameter must be a variable"))
|
||||
(define
|
||||
hk-lam-loop
|
||||
(fn
|
||||
()
|
||||
(when
|
||||
(hk-match? "varid" nil)
|
||||
(do
|
||||
(append! params (get (hk-advance!) "value"))
|
||||
(hk-lam-loop)))))
|
||||
(hk-lam-loop)
|
||||
(hk-expect! "reservedop" "->")
|
||||
(list :lambda params (hk-parse-expr-inner)))))
|
||||
|
||||
;; ── if-then-else ────────────────────────────────────────
|
||||
(define
|
||||
hk-parse-if
|
||||
(fn
|
||||
()
|
||||
(hk-expect! "reserved" "if")
|
||||
(let
|
||||
((c (hk-parse-expr-inner)))
|
||||
(hk-expect! "reserved" "then")
|
||||
(let
|
||||
((th (hk-parse-expr-inner)))
|
||||
(hk-expect! "reserved" "else")
|
||||
(let
|
||||
((el (hk-parse-expr-inner)))
|
||||
(list :if c th el))))))
|
||||
|
||||
;; ── Let expression ──────────────────────────────────────
|
||||
(define
|
||||
hk-parse-let
|
||||
(fn
|
||||
()
|
||||
(hk-expect! "reserved" "let")
|
||||
(let ((explicit (hk-match? "lbrace" nil)))
|
||||
(if
|
||||
explicit
|
||||
(hk-advance!)
|
||||
(hk-expect! "vlbrace" nil))
|
||||
(let
|
||||
((binds (list)))
|
||||
(when
|
||||
(not
|
||||
(if
|
||||
explicit
|
||||
(hk-match? "rbrace" nil)
|
||||
(hk-match? "vrbrace" nil)))
|
||||
(do
|
||||
(append! binds (hk-parse-bind))
|
||||
(define
|
||||
hk-let-loop
|
||||
(fn
|
||||
()
|
||||
(when
|
||||
(or
|
||||
(hk-match? "semi" nil)
|
||||
(hk-match? "vsemi" nil))
|
||||
(do
|
||||
(hk-advance!)
|
||||
(when
|
||||
(not
|
||||
(if
|
||||
explicit
|
||||
(hk-match? "rbrace" nil)
|
||||
(hk-match? "vrbrace" nil)))
|
||||
(append! binds (hk-parse-bind)))
|
||||
(hk-let-loop)))))
|
||||
(hk-let-loop)))
|
||||
(if
|
||||
explicit
|
||||
(hk-expect! "rbrace" nil)
|
||||
(hk-expect! "vrbrace" nil))
|
||||
(hk-expect! "reserved" "in")
|
||||
(list :let binds (hk-parse-expr-inner))))))
|
||||
|
||||
(define
|
||||
hk-parse-bind
|
||||
(fn
|
||||
()
|
||||
(when
|
||||
(not (hk-match? "varid" nil))
|
||||
(hk-err "binding must start with a variable"))
|
||||
(let
|
||||
((name (get (hk-advance!) "value")))
|
||||
(hk-expect! "reservedop" "=")
|
||||
(list :bind name (hk-parse-expr-inner)))))
|
||||
|
||||
;; ── lexp: lambda | if | let | fexp ──────────────────────
|
||||
(define
|
||||
hk-parse-lexp
|
||||
(fn
|
||||
()
|
||||
(cond
|
||||
((hk-match? "reservedop" "\\") (hk-parse-lambda))
|
||||
((hk-match? "reserved" "if") (hk-parse-if))
|
||||
((hk-match? "reserved" "let") (hk-parse-let))
|
||||
(:else (hk-parse-fexp)))))
|
||||
|
||||
;; ── Prefix: unary - ─────────────────────────────────────
|
||||
(define
|
||||
hk-parse-prefix
|
||||
(fn
|
||||
()
|
||||
(cond
|
||||
((and (hk-match? "varsym" "-"))
|
||||
(do (hk-advance!) (list :neg (hk-parse-lexp))))
|
||||
(:else (hk-parse-lexp)))))
|
||||
|
||||
;; ── Infix: precedence climbing ──────────────────────────
|
||||
(define
|
||||
hk-is-infix-op?
|
||||
(fn
|
||||
(tok)
|
||||
(if
|
||||
(nil? tok)
|
||||
false
|
||||
(or
|
||||
(= (get tok "type") "varsym")
|
||||
(= (get tok "type") "consym")
|
||||
(and
|
||||
(= (get tok "type") "reservedop")
|
||||
(= (get tok "value") ":"))
|
||||
(= (get tok "type") "backtick")))))
|
||||
|
||||
(define
|
||||
hk-consume-op!
|
||||
(fn
|
||||
()
|
||||
(let
|
||||
((t (hk-peek)))
|
||||
(cond
|
||||
((= (get t "type") "backtick")
|
||||
(do
|
||||
(hk-advance!)
|
||||
(let
|
||||
((v (hk-expect! "varid" nil)))
|
||||
(hk-expect! "backtick" nil)
|
||||
(get v "value"))))
|
||||
(:else (do (hk-advance!) (get t "value")))))))
|
||||
|
||||
(define
|
||||
hk-parse-infix
|
||||
(fn
|
||||
(min-prec)
|
||||
(let
|
||||
((left (hk-parse-prefix)))
|
||||
(define
|
||||
hk-inf-loop
|
||||
(fn
|
||||
()
|
||||
(when
|
||||
(hk-is-infix-op? (hk-peek))
|
||||
(let
|
||||
((op-tok (hk-peek)))
|
||||
(let
|
||||
((op-name
|
||||
(if
|
||||
(= (get op-tok "type") "backtick")
|
||||
(get (hk-peek-at 1) "value")
|
||||
(get op-tok "value"))))
|
||||
(let
|
||||
((info (hk-op-info op-name)))
|
||||
(when
|
||||
(>= (get info "prec") min-prec)
|
||||
(do
|
||||
(hk-consume-op!)
|
||||
(let
|
||||
((next-min
|
||||
(cond
|
||||
((= (get info "assoc") "left")
|
||||
(+ (get info "prec") 1))
|
||||
((= (get info "assoc") "right")
|
||||
(get info "prec"))
|
||||
(:else (+ (get info "prec") 1)))))
|
||||
(let
|
||||
((right (hk-parse-infix next-min)))
|
||||
(set!
|
||||
left
|
||||
(list :op op-name left right))
|
||||
(hk-inf-loop)))))))))))
|
||||
(hk-inf-loop)
|
||||
left)))
|
||||
|
||||
(define hk-parse-expr-inner (fn () (hk-parse-infix 0)))
|
||||
|
||||
;; ── Top-level: strip leading/trailing module-level braces ─
|
||||
(let
|
||||
((start-brace
|
||||
(or
|
||||
(hk-match? "vlbrace" nil)
|
||||
(hk-match? "lbrace" nil))))
|
||||
(when start-brace (hk-advance!))
|
||||
(let
|
||||
((result (hk-parse-expr-inner)))
|
||||
(when start-brace
|
||||
(when
|
||||
(or
|
||||
(hk-match? "vrbrace" nil)
|
||||
(hk-match? "rbrace" nil))
|
||||
(hk-advance!)))
|
||||
result)))))
|
||||
|
||||
;; ── Convenience: tokenize + layout + parse ───────────────────────
|
||||
(define
|
||||
hk-parse
|
||||
(fn (src) (hk-parse-expr (hk-layout (hk-tokenize src)))))
|
||||
@@ -47,6 +47,7 @@ for FILE in "${FILES[@]}"; do
|
||||
(epoch 1)
|
||||
(load "lib/haskell/tokenizer.sx")
|
||||
(load "lib/haskell/layout.sx")
|
||||
(load "lib/haskell/parser.sx")
|
||||
(load "lib/haskell/testlib.sx")
|
||||
(epoch 2)
|
||||
(load "$FILE")
|
||||
@@ -84,6 +85,7 @@ EPOCHS
|
||||
(epoch 1)
|
||||
(load "lib/haskell/tokenizer.sx")
|
||||
(load "lib/haskell/layout.sx")
|
||||
(load "lib/haskell/parser.sx")
|
||||
(load "lib/haskell/testlib.sx")
|
||||
(epoch 2)
|
||||
(load "$FILE")
|
||||
|
||||
258
lib/haskell/tests/parser-expr.sx
Normal file
258
lib/haskell/tests/parser-expr.sx
Normal 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 "x") (list :var "x")))
|
||||
(hk-test
|
||||
"lambda multi-param"
|
||||
(hk-parse "\\x y -> x + y")
|
||||
(list
|
||||
:lambda
|
||||
(list "x" "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 "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 "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 "x" (list :int 1))
|
||||
(list :bind "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 "x" (list :int 1))
|
||||
(list :bind "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 "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 "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}
|
||||
@@ -56,7 +56,14 @@ 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)
|
||||
- [x] 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
|
||||
- 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`
|
||||
- [ ] `case … of` and `do`-notation expressions
|
||||
- [ ] Patterns (var, wildcard, literal, constructor, as, nested) — consumed by lambdas, let, case, and function clauses
|
||||
- [ ] Top-level decls: function clauses, type signatures, `data`, `type`, `newtype`, fixity decls
|
||||
- [ ] `where` clauses + guards
|
||||
- [ ] Module header + imports (stub)
|
||||
- [ ] List comprehensions + operator sections
|
||||
- [ ] AST design modelled on GHC's HsSyn at a surface level
|
||||
- [x] Unit tests in `lib/haskell/tests/parse.sx` (43 tokenizer tests, all green)
|
||||
|
||||
@@ -107,6 +114,25 @@ Key mappings:
|
||||
|
||||
_Newest first._
|
||||
|
||||
- **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 0–9, 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
|
||||
|
||||
Reference in New Issue
Block a user