perf(std regex): native fast-path re-fold-positions + substring-free re-fold
ober
a942c986357a171ade082d931f176585fe2cbbf5
--- a/lib/std/regex.ss +++ b/lib/std/regex.ss @@ -39,7 +39,7 @@ ;; Replacement re-replace re-replace-all ;; Splitting and folding - re-split re-fold + re-split re-fold re-fold-positions ;; Match object accessors re-match-full re-match-group re-match-groups re-match-start re-match-end re-match-named @@ -95,6 +95,11 @@ (if native-available? (foreign-procedure "jerboa_regex_free" (unsigned-64) int) (lambda args (error 'c-native-free "native backend not available")))) + (def c-native-find-at + (if native-available? + (foreign-procedure "jerboa_regex_find_at" + (unsigned-64 u8* size_t size_t u8* u8*) int) + (lambda args (error 'c-native-find-at "native backend not available")))) ;; ========== Records ========== @@ -341,8 +346,83 @@ (def (re-split pat str) (pregexp-split (re-object-pat-string (re pat)) str)) + ;; ASCII-only check: returns #t iff every char in str has code <= 0x7F. + ;; Used by re-fold-positions to decide whether the native (Rust regex, + ;; UTF-8) backend can be used safely. For non-ASCII strings the byte + ;; offsets returned by native would not equal char offsets (because + ;; latin-1 bytes 0x80..0xFF become 2-byte UTF-8 sequences) and the + ;; caller's coordinate system would break. This scan is O(N) but + ;; trivial per-char; for text files (the common multi-match case) it + ;; runs once and almost always returns #t. + (def (ascii-only-string? str) + (let ([n (string-length str)]) + (let loop ([i 0]) + (cond + [(fx= i n) #t] + [(fx<= (char->integer (string-ref str i)) #x7f) + (loop (fx+ i 1))] + [else #f])))) + + ;; re-fold-positions: fold kons over (start, end) byte-offset pairs of + ;; all matches in str, left-to-right. kons receives (index start end + ;; subject accumulator) and returns the new accumulator. + ;; + ;; When the native (Rust regex) backend is available AND the subject is + ;; pure ASCII, the iteration runs in O(N) total time using + ;; jerboa_regex_find_at — no backtracking, no per-iteration substring + ;; or capture-group allocation. Used by high-throughput callers + ;; (e.g. multi-rule scanners) that only need positions. Falls back to + ;; pregexp when no native handle or when the subject has non-ASCII + ;; chars (where UTF-8 byte offsets would differ from char offsets). + (def (re-fold-positions pat kons knil str) + (let* ([r (re pat)] + [handle (re-object-native-handle r)] + [pat-str (re-object-pat-string r)] + [len (string-length str)]) + (cond + [(and native-available? handle (ascii-only-string? str)) + ;; Native fast path: bytes-based linear scan with find_at. + ;; ASCII-confined: byte offsets == char offsets, so positions + ;; round-trip cleanly back to the caller's coordinate system. + (let* ([bv (string->utf8 str)] + [bl (bytevector-length bv)] + [sbuf (make-bytevector 8)] + [ebuf (make-bytevector 8)]) + (let loop ([pos 0] [i 0] [acc knil]) + (cond + [(> pos bl) acc] + [else + (let ([rc (c-native-find-at handle bv bl pos sbuf ebuf)]) + (cond + [(= rc 1) + (let* ([ms (bytevector-u64-native-ref sbuf 0)] + [me (bytevector-u64-native-ref ebuf 0)] + [next (if (> me ms) me (+ ms 1))]) + (loop next (+ i 1) (kons i ms me str acc)))] + [else acc]))])))] + [else + ;; Fallback: pregexp positions, start/end form (no substring). + (let loop ([pos 0] [i 0] [acc knil]) + (cond + [(> pos len) acc] + [else + (let ([rp (pregexp-match-positions pat-str str pos len)]) + (cond + [(not rp) acc] + [else + (let* ([ms (caar rp)] + [me (cdar rp)] + [next (if (> me ms) me (+ ms 1))]) + (loop next (+ i 1) (kons i ms me str acc)))]))]))]))) + ;; re-fold: fold kons over all matches left-to-right. ;; kons receives (match-index re-match-object subject accumulator). + ;; + ;; Uses pregexp-match-positions's 4-arg form to search a (start, end) + ;; window of the subject string directly — avoids per-iteration + ;; substring allocation that turned re-fold into O(K * N) for a string + ;; with K matches in N chars. Returned positions are already in + ;; subject-absolute coordinates. (def (re-fold pat kons knil str) (let* ([r (re pat)] [pat-str (re-object-pat-string r)] @@ -351,15 +431,10 @@ (let loop ([pos 0] [i 0] [acc knil]) (if (> pos len) acc - (let* ([subject (if (= pos 0) str (substring str pos len))] - [raw-pos (pregexp-match-positions pat-str subject)]) + (let ([raw-pos (pregexp-match-positions pat-str str pos len)]) (if (not raw-pos) acc - (let* ([adj-pos (map (lambda (p) - (and p (cons (+ pos (car p)) - (+ pos (cdr p))))) - raw-pos)] - [m (build-match-object str adj-pos named)] + (let* ([m (build-match-object str raw-pos named)] [mstart (re-match-object-start m)] [mend (re-match-object-end m)] [next (max (+ mstart 1) mend)])