From 270aa2d5b244fa0ae21265a2b6b399a88235a89c Mon Sep 17 00:00:00 2001 From: Jack Firth Date: Tue, 21 Jul 2026 17:32:53 -0700 Subject: [PATCH] Stop traversing whole subtrees to decide if syntax is one line or many `oneline-syntax?` and `multiline-syntax?` were implemented by counting every distinct source line among all the atoms in a syntax object, using a `syntax-search` stream fed through a rebellion transducer pipeline. Neither caller needs an exact count, and both build a lazy stream over the entire subtree even when the second line is found immediately. Counting directly with a recursive traversal that stops at two distinct lines cuts analysis time for `xrepl-lib/xrepl/xrepl.rkt` (1877 lines) from ~78 seconds to ~65 seconds. The traversal deliberately mirrors `syntax-search`'s shape, including its handling of improper lists and its treatment of non-list, non-atom syntax (such as vectors) as having no lines at all. Both implementations were run against every subform of several large source files, plus hand-built edge cases, with identical results on all 28,585 syntax objects. Co-Authored-By: Claude Opus 4.8 --- .../private/syntax-lines.rkt | 62 ++++++++++++++----- 1 file changed, 48 insertions(+), 14 deletions(-) diff --git a/default-recommendations/private/syntax-lines.rkt b/default-recommendations/private/syntax-lines.rkt index a3e2da37..fedf45a8 100644 --- a/default-recommendations/private/syntax-lines.rkt +++ b/default-recommendations/private/syntax-lines.rkt @@ -10,9 +10,8 @@ [multiline-syntax? (-> any/c boolean?)])) -(require rebellion/streaming/reducer - rebellion/streaming/transducer - resyntax/private/syntax-traversal) +(require resyntax/private/syntax-traversal + syntax/parse) (module+ test @@ -25,18 +24,40 @@ (define (oneline-syntax? v) - (and (syntax? v) (equal? (syntax-line-count v) 1))) + (and (syntax? v) (equal? (syntax-line-count v #:limit 2) 1))) (define (multiline-syntax? v) - (and (syntax? v) (> (syntax-line-count v) 1))) - - -(define (syntax-line-count stx) - (transduce (syntax-search stx [:atom]) - (mapping syntax-line) - (deduplicating) - #:into into-count)) + (and (syntax? v) (> (syntax-line-count v #:limit 2) 1))) + + +;; Counts how many distinct source lines the atoms within stx occupy, giving up and returning limit +;; as soon as that many distinct lines have been seen. Both predicates above only need to +;; distinguish between zero, one, and multiple lines, and they're checked against enough syntax +;; objects that traversing entire subtrees after the answer is already determined shows up as a +;; large chunk of Resyntax's runtime. +(define (syntax-line-count stx #:limit limit) + (let/ec return + (define lines-seen '()) + (let loop ([stx stx]) + (syntax-parse stx + [:atom + (define line (syntax-line this-syntax)) + (unless (member line lines-seen) + (set! lines-seen (cons line lines-seen)) + (when (>= (length lines-seen) limit) + (return limit)))] + [(part ...) + #:cut + (for ([part-stx (in-list (attribute part))]) + (loop part-stx))] + [(part ...+ . tail-part) + #:cut + (for ([part-stx (in-list (attribute part))]) + (loop part-stx)) + (loop #'tail-part)] + [_ (void)])) + (length lines-seen))) (module+ test @@ -48,7 +69,16 @@ c))) (check-false (oneline-syntax? #'(1 2 - 3)))) + 3))) + (check-true (oneline-syntax? #'(a . b))) + (check-false (oneline-syntax? #'(a . + b))) + (check-false (oneline-syntax? 'not-syntax)) + + ;; Syntax objects containing no atoms at all occupy zero lines, so they're neither one-line nor + ;; multiline. + (check-false (oneline-syntax? #'())) + (check-false (multiline-syntax? #'()))) (test-case (name-string multiline-syntax?) (check-false (multiline-syntax? #'a)) @@ -58,4 +88,8 @@ c))) (check-true (multiline-syntax? #'(1 2 - 3))))) + 3))) + (check-false (multiline-syntax? #'(a . b))) + (check-true (multiline-syntax? #'(a . + b))) + (check-false (multiline-syntax? 'not-syntax))))