perf: gsub streams pieces into an output port (O(n) assembly), compiles regex once

ober

a6fbc04d68e5d1e7ff08c7e731c77c7b85332bde

diff --git a/lib/jerboa-awk/builtins/string.sls b/lib/jerboa-awk/builtins/string.sls
index f850ef2..d275296 100644
--- a/lib/jerboa-awk/builtins/string.sls
+++ b/lib/jerboa-awk/builtins/string.sls
@@ -112,22 +112,28 @@
                       (awk-expr-regex-pattern ere)
                       (awk->string ere)))
            (target-str (awk->string target-expr))
+           (re (env-regex env pattern))
+           (tlen (string-length target-str))
+           (out (open-output-string))
            (count 0))
-      (let loop ((pos 0) (result '()))
-        (let ((m (pregexp-match-positions (env-regex env pattern) target-str pos)))
+      ;; Compile the regex once and stream the gap/replacement pieces into an
+      ;; output port (O(n) assembly) instead of re-consing a piece list and
+      ;; apply-string-appending it at the end.
+      (let loop ((pos 0))
+        (let ((m (pregexp-match-positions re target-str pos)))
           (if (not m)
-            (let ((final (string-append (apply string-append (reverse result))
-                                        (substring target-str pos (string-length target-str)))))
-              (set-target! (make-awk-string final))
+            (begin
+              (put-string out (substring target-str pos tlen))
+              (set-target! (make-awk-string (get-output-string out)))
               (make-awk-number count))
             (let* ((mpos (car m)) ;; full match (start . end)
                    (start (car mpos))
                    (end (cdr mpos))
                    (replacement (build-replacement repl-str mpos target-str)))
               (set! count (+ count 1))
-              (loop (if (= start end) (+ end 1) end)
-                    (cons replacement
-                          (cons (substring target-str pos start) result)))))))))
+              (put-string out (substring target-str pos start))
+              (put-string out replacement)
+              (loop (if (= start end) (+ end 1) end))))))))
 
   (define (build-replacement repl-str match-pair target-str)
     "Process replacement string: & = matched text, \\ = literal backslash"