perf(generic-scan): precompute call-paren table for ellipsis lookup
ober
ff1593ab9cd4b646cacc4940bd04d99ccfea805d
--- a/lib/semgrep/engine/generic-scan.sls +++ b/lib/semgrep/engine/generic-scan.sls @@ -86,20 +86,27 @@ (generic-plain-ellipsis-at? source (generic-skip-whitespace source (+ i 1))))) - (def (generic-ellipsis-inside-call-parens? source i) - (let loop ([j 0] [stack '()]) - (cond - [(>= j i) - (and (pair? stack) - (let ([open (car stack)]) - (and (> open 0) - (identifier-token-char? - (string-ref source (- open 1))))))] - [(char=? (string-ref source j) #\() - (loop (+ j 1) (cons j stack))] - [(char=? (string-ref source j) #\)) - (loop (+ j 1) (if (pair? stack) (cdr stack) stack))] - [else (loop (+ j 1) stack)]))) + (def (generic-call-paren-table source) + (let ([len (string-length source)] [stack '()]) + (let ([table (make-vector (+ len 1) #f)]) + (let loop ([i 0]) + (cond + [(> i len) table] + [else + (vector-set! table i (and (pair? stack) (car stack))) + (when (< i len) + (let ([ch (string-ref source i)]) + (cond + [(char=? ch #\() + (set! stack + (cons + (and (> i 0) + (identifier-token-char? + (string-ref source (- i 1)))) + stack))] + [(char=? ch #\)) + (when (pair? stack) (set! stack (cdr stack)))]))) + (loop (+ i 1))]))))) (def (generic-word-char? ch) (or (char-alphabetic? ch) (char-numeric? ch) @@ -135,7 +142,8 @@ [len (string-length source)] [plain-ellipsis (if (string-contains-char? source #\newline) "(?:.|\\n)*?" - "[^\\n]*?")]) + "[^\\n]*?")] + [call-parens (generic-call-paren-table source)]) (let loop ([i 0] [parts '()] [captures '()]) (cond [(>= i len) @@ -221,7 +229,7 @@ (loop (generic-skip-whitespace source (+ i 3)) (cons - (if (generic-ellipsis-inside-call-parens? source i) + (if (vector-ref call-parens i) "(?:.|\\n)*?" plain-ellipsis) parts) --- a/src/semgrep/engine/generic-scan.ss +++ b/src/semgrep/engine/generic-scan.ss @@ -109,18 +109,29 @@ ;; `sink(...)` matches a call whose arguments span multiple lines. The enclosing ;; `(` must be a call (preceded by an identifier): a bare grouping `(...)` keeps ;; the line-bounded form, so it does not span across whole files/statements. -(def (generic-ellipsis-inside-call-parens? source i) - (let loop ([j 0] [stack '()]) - (cond - [(>= j i) - (and (pair? stack) - (let ([open (car stack)]) - (and (> open 0) - (identifier-token-char? (string-ref source (- open 1))))))] - [(char=? (string-ref source j) #\() (loop (+ j 1) (cons j stack))] - [(char=? (string-ref source j) #\)) - (loop (+ j 1) (if (pair? stack) (cdr stack) stack))] - [else (loop (+ j 1) stack)]))) +;; Build a table once per source so each ellipsis is an O(1) lookup instead of +;; rescanning from offset 0 (which was O(pattern^2) for many ellipses). +(def (generic-call-paren-table source) + (let ([len (string-length source)] + [stack '()]) + (let ([table (make-vector (+ len 1) #f)]) + (let loop ([i 0]) + (cond + [(> i len) table] + [else + (vector-set! table i (and (pair? stack) (car stack))) + (when (< i len) + (let ([ch (string-ref source i)]) + (cond + [(char=? ch #\() + (set! stack + (cons (and (> i 0) + (identifier-token-char? + (string-ref source (- i 1)))) + stack))] + [(char=? ch #\)) + (when (pair? stack) (set! stack (cdr stack)))]))) + (loop (+ i 1))]))))) (def (generic-word-char? ch) (or (char-alphabetic? ch) @@ -156,7 +167,8 @@ [len (string-length source)] [plain-ellipsis (if (string-contains-char? source #\newline) "(?:.|\\n)*?" - "[^\\n]*?")]) + "[^\\n]*?")] + [call-parens (generic-call-paren-table source)]) (let loop ([i 0] [parts '()] [captures '()]) (cond [(>= i len) @@ -231,10 +243,10 @@ (char=? (string-ref source (+ i 1)) #\.) (char=? (string-ref source (+ i 2)) #\.)) (loop (generic-skip-whitespace source (+ i 3)) - (cons (if (generic-ellipsis-inside-call-parens? source i) - "(?:.|\\n)*?" - plain-ellipsis) - parts) + (cons (if (vector-ref call-parens i) + "(?:.|\\n)*?" + plain-ellipsis) + parts) captures)] [(generic-word-char? (string-ref source i)) (let word-loop ([j (+ i 1)])