Configurable layout pass that inserts virtual open / close / separator
tokens based on indentation. Supports both styles the brief calls out:
• Haskell-flavour: layout opens AFTER a reserved keyword
(let/where/do/of) and resolves to the next token's column. Module
prelude wraps the whole input in an implicit block. Explicit `{`
after the keyword suppresses virtual layout.
• Python-flavour: layout opens via an :open-trailing-fn predicate
fired AFTER the trigger token (e.g. trailing `:`) — and resolves
to the column of the next token, which in real source is on a
fresh line. No module prelude.
Public entry: (layout-pass cfg tokens). Token shape: dict with at
least :type :value :line :col; everything else passes through. Newline
filler tokens are NOT used — line-break detection is via :line.
lib/guest/tests/layout.sx — 6 tests covering both flavours:
haskell-do-block / haskell-explicit-brace / haskell-do-inline /
haskell-module-prelude / python-if-block / python-nested.
Per the brief's gotcha note ("Don't ship lib/guest/layout.sx unless
the haskell scoreboard equals baseline") — haskell/layout.sx is left
UNTOUCHED. The kit isn't yet a drop-in replacement for the full
Haskell 98 algorithm (Note 5, multi-stage pre-pass, etc.) and forcing
a port would risk the 156 currently passing programs. Haskell
scoreboard remains at 156/156 baseline because no haskell file
changed. The synthetic Python-ish fixture is the second consumer per
the brief's wording.
PARTIAL — kit + synthetic fixture shipped; haskell port deferred until
the kit grows the missing Haskell-98 wrinkles.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
146 lines
5.9 KiB
Plaintext
146 lines
5.9 KiB
Plaintext
;; 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))))
|