;; 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 = "") 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 "" :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))))