;; lib/guest/layout.sx — configurable off-side / layout-sensitive lexer. ;; ;; Inserts virtual open / close / separator tokens based on indentation. ;; Configurable enough to encode either the Haskell 98 layout rule (let / ;; where / do / of opens a virtual brace at the next token's column) or ;; a Python-ish indent / dedent rule (a colon at the end of a line opens ;; a block at the next non-blank line's column). ;; ;; Token shape (input + output) ;; ---------------------------- ;; Each token is a dict {:type :value :line :col …}. The kit reads ;; only :type / :value / :line / :col and passes everything else ;; through. The input stream MUST be free of newline filler tokens ;; (preprocess them away with your tokenizer) — line breaks are detected ;; by comparing :line of consecutive tokens. ;; ;; Config ;; ------ ;; :open-keywords list of strings; a token whose :value matches ;; opens a new layout block at the next token's ;; column (Haskell: let/where/do/of). ;; :open-trailing-fn (fn (tok) -> bool) — alternative trigger that ;; fires AFTER the token is emitted. Use for ;; Python-style trailing `:`. ;; :open-token / :close-token / :sep-token ;; templates {:type :value} merged with :line and ;; :col when virtual tokens are emitted. ;; :explicit-open? (fn (tok) -> bool) — if the next token after a ;; trigger satisfies this, suppress virtual layout ;; for that block (Haskell: `{`). ;; :module-prelude? if true, wrap whole input in an implicit block ;; at the first token's column (Haskell yes, ;; Python no). ;; ;; Public entry ;; ------------ ;; (layout-pass cfg tokens) -> tokens with virtual layout inserted. (define layout-mk-virtual (fn (template line col) (assoc (assoc template :line line) :col col))) (define layout-is-open-kw? (fn (tok open-kws) (and (= (get tok :type) "reserved") (some (fn (k) (= k (get tok :value))) open-kws)))) (define layout-pass (fn (cfg tokens) (let ((open-kws (get cfg :open-keywords)) (trailing-fn (get cfg :open-trailing-fn)) (open-tmpl (get cfg :open-token)) (close-tmpl (get cfg :close-token)) (sep-tmpl (get cfg :sep-token)) (mod-prelude? (get cfg :module-prelude?)) (expl?-fn (get cfg :explicit-open?)) (out (list)) (stack (list)) (n (len tokens)) (i 0) (prev-line -1) (pending-open false) (just-opened false)) (define emit-closes-while-greater (fn (col line) (when (and (not (empty? stack)) (> (first stack) col)) (do (append! out (layout-mk-virtual close-tmpl line col)) (set! stack (rest stack)) (emit-closes-while-greater col line))))) (define emit-pending-open (fn (line col) (do (append! out (layout-mk-virtual open-tmpl line col)) (set! stack (cons col stack)) (set! pending-open false) (set! just-opened true)))) (define layout-step (fn () (when (< i n) (let ((tok (nth tokens i))) (let ((line (get tok :line)) (col (get tok :col))) (cond (pending-open (cond ((and (not (= expl?-fn nil)) (expl?-fn tok)) (do (set! pending-open false) (append! out tok) (set! prev-line line) (set! i (+ i 1)) (layout-step))) (:else (do (emit-pending-open line col) (layout-step))))) (:else (let ((on-fresh-line? (and (> prev-line 0) (> line prev-line)))) (do (when on-fresh-line? (let ((stack-before stack)) (begin (emit-closes-while-greater col line) (when (and (not (empty? stack)) (= (first stack) col) (not just-opened) ;; suppress separator if a dedent fired ;; — the dedent is itself the separator (= (len stack) (len stack-before))) (append! out (layout-mk-virtual sep-tmpl line col)))))) (set! just-opened false) (append! out tok) (set! prev-line line) (set! i (+ i 1)) (cond ((layout-is-open-kw? tok open-kws) (set! pending-open true)) ((and (not (= trailing-fn nil)) (trailing-fn tok)) (set! pending-open true))) (layout-step)))))))))) (begin ;; Module prelude: implicit layout block at the first token's column. (when (and mod-prelude? (> n 0)) (let ((tok (nth tokens 0))) (do (append! out (layout-mk-virtual open-tmpl (get tok :line) (get tok :col))) (set! stack (cons (get tok :col) stack)) (set! just-opened true)))) (layout-step) ;; EOF: close every remaining block. (define close-rest (fn () (when (not (empty? stack)) (do (append! out (layout-mk-virtual close-tmpl 0 0)) (set! stack (rest stack)) (close-rest))))) (close-rest) out))))