Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 68 additions & 2 deletions racket/prologos/parse-reader.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -1962,8 +1962,17 @@
;; Indent grouping is recovered by treating child-node boundaries as
;; implicit wrapping points when not inside an open bracket.
(define (tree-node->stx-elements node source source-str)
;; Collect tokens with indent-boundary markers
(define items (flatten-with-boundaries node))
;; Collect tokens with indent-boundary markers. For a `spec` form, the
;; type-signature continuation lines are inlined directly (rather than
;; wrapped as sub-lists) so that arrows like `-> Result` placed on their
;; own continuation line remain at the top level of the spec body — where
;; `decompose-spec-type` looks for them. Metadata-style continuation lines
;; (whose first token is a keyword-like symbol such as `:doc`) are still
;; wrapped so the `process-spec` metadata loop recognizes them.
(define items
(if (spec-form-node? node)
(flatten-with-boundaries/spec node)
(flatten-with-boundaries node)))
(define vec (list->vector items))
(define-values (elems _end)
(group-items vec 0 (vector-length vec) #f source source-str))
Expand All @@ -1986,6 +1995,63 @@
(list 'indent-close))]
[else '()]))))

;; Recognize a parse-tree-node whose first token is `spec` or `spec-`. The
;; node's tag may not yet be refined to `'spec` (e.g., the private `spec-`
;; variant is registered as a preparser keyword but has no tag rule), so we
;; look at the first child token directly.
(define (spec-form-node? node)
(and (parse-tree-node? node)
(let ([children (parse-tree-node-children node)])
(and (> (rrb-size children) 0)
(let ([first (rrb-get children 0)])
(and (token-entry? first)
(let ([lex (token-entry-lexeme first)])
(or (equal? lex "spec")
(equal? lex "spec-")))))))))

;; Flatten variant for `spec` forms: top-level child lines are SPLICED into
;; the parent token stream (no `indent-open`/`indent-close` wrapping), so a
;; multi-line type signature like
;; spec foo
;; [List Rat]
;; -> Result
;; flattens to `spec foo [List Rat] -> Result` — keeping `->` at the top
;; level where `split-on-arrow-datum` can find it. Continuation lines whose
;; first token is a keyword-like symbol (e.g. `:doc`) are still wrapped so
;; that the `process-spec` metadata loop, which expects `(:key val ...)`
;; sub-lists, continues to recognize them.
(define (flatten-with-boundaries/spec node)
(define children (parse-tree-node-children node))
(define n (rrb-size children))
(apply append
(for/list ([i (in-range n)])
(define child (rrb-get children i))
(cond
[(token-entry? child) (list child)]
[(parse-tree-node? child)
(define inner (flatten-with-boundaries child))
(if (continuation-starts-with-keyword? inner)
;; Metadata-style continuation: keep wrapped as a sub-list
(append (list 'indent-open) inner (list 'indent-close))
;; Type-signature continuation: splice tokens directly
inner)]
[else '()]))))

;; Does this flattened token list start with a keyword-like token (lexeme
;; beginning with `:`)? Used to distinguish metadata continuations from
;; type-signature continuations in spec forms.
(define (continuation-starts-with-keyword? items)
(let loop ([items items])
(cond
[(null? items) #f]
[(eq? (car items) 'indent-open) (loop (cdr items))]
[(token-entry? (car items))
(let ([lex (token-entry-lexeme (car items))])
(and (string? lex)
(positive? (string-length lex))
(char=? (string-ref lex 0) #\:)))]
[else #f])))

;; Lookahead: check if there's a matching rangle before the current scope closes.
;; Scans forward tracking nesting depth for <> pairs.
(define (has-matching-rangle? vec start end close-type)
Expand Down
195 changes: 195 additions & 0 deletions racket/prologos/tests/test-spec-multiline-ws.rkt
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
#lang racket/base

;;;
;;; Regression tests for multi-line `spec` forms in WS mode.
;;;
;;; Background — the eigentrust pitfalls doc (2026-04-23) #6 reported that
;;; a multi-line spec like
;;;
;;; spec eigentrust-step
;;; [List [List Rat]] ;; matrix C
;;; [List Rat] ;; pre-trust p
;;; Rat ;; damping
;;; [List Rat] ;; current t
;;; -> [List Rat] ;; next t
;;; defn eigentrust-step [c p alpha t] ...
;;;
;;; failed with
;;;
;;; spec: spec type for eigentrust-step has no arrow but defn has 4 params
;;;
;;; Root cause — the WS reader wraps each indent-grouped continuation line
;;; with multiple tokens as a sub-list (via wrap-stx-list). When the user
;;; placed `-> [List Rat]` on its own continuation line, that line wrapped
;;; to `(-> [List Rat])`, hiding the arrow inside a sub-list where
;;; split-on-arrow-datum (which only scans the top level of the spec body
;;; tokens) couldn't see it. Line comments are a red herring — they're
;;; stripped by the tokenizer; the bug is purely about indent wrapping of
;;; multi-token continuation lines in spec form bodies.
;;;

(require rackunit
"../parse-reader.rkt")

;; Read a single WS datum
(define (ws-read s)
(define in (open-input-string s))
(prologos-read in))

;; Read all WS datums until eof
(define (ws-read-all s)
(define in (open-input-string s))
(let loop ([acc '()])
(define d (prologos-read in))
(if (eof-object? d) (reverse acc) (loop (cons d acc)))))

;; ========================================
;; Multi-line spec body tokens stay flat
;; ========================================

(test-case "multi-line spec: arrow on its own continuation line is at top level"
(define d (ws-read "spec foo\n A\n B\n -> C"))
;; Body tokens: (A B -> C). The `->` MUST be at the top level (not buried
;; inside a sub-list) for split-on-arrow-datum to find it.
(check-equal? d '(spec foo A B -> C))
(check-not-false (memq '-> (cddr d))
"arrow must appear at top level of spec body tokens"))

(test-case "multi-line spec: bracket-grouped types preserved as sub-lists"
(define d (ws-read "spec eigentrust-step\n [List [List Rat]]\n [List Rat]\n Rat\n [List Rat]\n -> [List Rat]"))
;; Each [...]-grouped type stays a sub-list (came from explicit brackets,
;; not from indent grouping). The arrow line `-> [List Rat]` is spliced
;; so that `->` is at top level.
(check-equal? d
'(spec eigentrust-step
(List (List Rat))
(List Rat)
Rat
(List Rat)
-> (List Rat)))
(check-not-false (memq '-> (cddr d))
"arrow must appear at top level of spec body tokens"))

(test-case "multi-line spec: line comments between tokens do not break it"
;; The eigentrust pitfalls doc #6 reproducer — with trailing line
;; comments on every continuation line.
(define src
(string-append
"spec eigentrust-step\n"
" [List [List Rat]] ;; matrix C\n"
" [List Rat] ;; pre-trust p\n"
" Rat ;; damping\n"
" [List Rat] ;; current t\n"
" -> [List Rat] ;; next t\n"))
(define d (ws-read src))
(check-equal? d
'(spec eigentrust-step
(List (List Rat))
(List Rat)
Rat
(List Rat)
-> (List Rat))))

(test-case "multi-line spec: still works when `defn` follows on a sibling line"
(define src
(string-append
"spec eigentrust-step\n"
" [List [List Rat]]\n"
" [List Rat]\n"
" Rat\n"
" [List Rat]\n"
" -> [List Rat]\n"
"defn eigentrust-step [c p alpha t]\n"
" c\n"))
(define forms (ws-read-all src))
(check-equal? (length forms) 2 "spec and defn are two separate top-level forms")
(check-equal? (car forms)
'(spec eigentrust-step
(List (List Rat))
(List Rat)
Rat
(List Rat)
-> (List Rat)))
(check-equal? (caar (cdr forms)) 'defn))

;; ========================================
;; Single-line spec is unchanged
;; ========================================

(test-case "single-line spec: unchanged behavior"
(define d (ws-read "spec foo Nat -> Bool"))
(check-equal? d '(spec foo Nat -> Bool)))

(test-case "single-line spec with bracket function-type param: unchanged"
;; [-> Nat Bool] is a legitimate prefix-arrow function type — the
;; sub-list (-> Nat Bool) MUST be preserved (it's bracket-grouped, not
;; indent-grouped).
(define d (ws-read "spec all? [-> Nat Bool] -> [List Nat] -> Bool"))
(check-equal? d '(spec all? (-> Nat Bool) -> (List Nat) -> Bool)))

;; ========================================
;; Metadata continuations stay wrapped
;; ========================================

(test-case "spec with :doc continuation: keyword line stays wrapped as sub-list"
;; The process-spec metadata loop expects (:doc "value") as a sub-list,
;; so metadata-style continuation lines must NOT be spliced.
(define d (ws-read "spec sum [Add A] -> [List A] -> A\n :doc \"Sum a list\""))
(check-equal? d '(spec sum (Add A) -> (List A) -> A (:doc "Sum a list"))))

(test-case "spec with type continuations AND :doc continuation: type lines spliced, :doc kept wrapped"
(define src
(string-append
"spec foo\n"
" A\n"
" -> B\n"
" :doc \"description\"\n"))
(define d (ws-read src))
(check-equal? d '(spec foo A -> B (:doc "description"))))

(test-case "spec with forall (brace-params) + where + :doc, all multi-line"
;; Cover the dependent-type / trait-constraint / docstring keywords
;; together. The forall-style brace binder `{A : Type}` rides along on
;; the spec name's line, type tokens splice across continuation lines,
;; the bare `where` keyword splices flat (along with its trait
;; constraints), and the keyword-like `:doc` continuation is wrapped
;; per the metadata path.
(define src
(string-append
"spec compare {A : Type}\n"
" A\n"
" A\n"
" -> Ord\n"
" where (Eq A)\n"
" :doc \"compare two values\"\n"))
(define d (ws-read src))
(check-equal? d '(spec compare ($brace-params A : Type)
A A -> Ord
where (Eq A)
(:doc "compare two values"))))

(test-case "spec multi-line forall+where+:doc matches one-line shape (modulo :doc wrap)"
;; Same content on a single line produces the same flat token stream up
;; to the `:doc` continuation — single-line `:doc` is bare, multi-line
;; `:doc` becomes `(:doc ...)` wrapped. Both shapes are accepted by
;; `process-spec`'s metadata loop.
(define one-line
"spec compare {A : Type} A A -> Ord where (Eq A) :doc \"compare two values\"")
(check-equal? (ws-read one-line)
'(spec compare ($brace-params A : Type)
A A -> Ord
where (Eq A)
:doc "compare two values")))

;; ========================================
Comment thread
kumavis marked this conversation as resolved.
;; Other forms are unaffected (defn, def, match, etc. still wrap
;; multi-token indent-grouped continuations as sub-lists)
;; ========================================

(test-case "defn body with multi-token continuation line: still wrapped"
(define d (ws-read "defn foo [x]\n do-thing x"))
(check-equal? d '(defn foo (x) (do-thing x))))

(test-case "private spec- form: same multi-line treatment"
(define d (ws-read "spec- foo\n A\n -> B"))
(check-equal? d '(spec- foo A -> B)))
Loading