perf(generic-scan): precompute call-paren table for ellipsis lookup

ober

ff1593ab9cd4b646cacc4940bd04d99ccfea805d

diff --git a/lib/semgrep/engine/generic-scan.sls b/lib/semgrep/engine/generic-scan.sls
index 7d6e4a2..dbd2593 100644
--- 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)
diff --git a/src/semgrep/engine/generic-scan.ss b/src/semgrep/engine/generic-scan.ss
index f8a558c..cdcf341 100644
--- 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)])