perf: lazy $0 rebuild via dirty flag (field assigns no longer re-join all NF fields)

ober

92de278a3a65bcd75b0144975e41fc4a29b57cf7

diff --git a/lib/jerboa-awk/runtime.sls b/lib/jerboa-awk/runtime.sls
index ccbf260..b792bd5 100644
--- a/lib/jerboa-awk/runtime.sls
+++ b/lib/jerboa-awk/runtime.sls
@@ -222,6 +222,7 @@
             (mutable rand-initialized? %awk-env-rand-initialized? %awk-env-rand-initialized?-set!)
             (mutable exit-code %awk-env-exit-code %awk-env-exit-code-set!)
             (mutable local-frames %awk-env-local-frames %awk-env-local-frames-set!)
+            (mutable record-dirty? %awk-env-record-dirty? %awk-env-record-dirty?-set!)
             (immutable owner-thread %awk-env-owner-thread)))
 
   ;; Preserve the original public constructor surface. The evaluator-owned
@@ -235,6 +236,7 @@
                    (make-regex-cache) '() '()
                    1 0 #f
                    exit-code local-frames
+                   #f
                    (get-thread-id)))
 
   (define (env-assert-owner! who env)
@@ -404,7 +406,12 @@
     (env-assert-owner! 'env-get-field env)
     (let ((fields (awk-env-fields env)))
       (if (and (>= idx 0) (< idx (vector-length fields)))
-        (vector-ref fields idx)
+        (begin
+          ;; $0 is rebuilt lazily: field assignments only mark the record dirty,
+          ;; so a loop like {for(i=1;i<=NF;i++) $i=...} rebuilds $0 once, on read.
+          (when (and (= idx 0) (%awk-env-record-dirty? env))
+            (env-rebuild-record! env))
+          (vector-ref (awk-env-fields env) idx))
         (make-awk-string ""))))
 
   (define (env-set-field! env idx val)
@@ -422,9 +429,9 @@
           (vector-copy! old-fields 0 new-fields 0 (vector-length old-fields))
           (awk-env-fields-set! env new-fields)))
       (vector-set! (awk-env-fields env) idx v)
-      ;; Rebuild $0
+      ;; Mark $0 stale; it is rebuilt lazily on the next read of field 0.
       (when (> idx 0)
-        (env-rebuild-record! env))))
+        (%awk-env-record-dirty?-set! env #t))))
 
   (define (env-set-record! env str)
     (env-assert-owner! 'env-set-record! env)
@@ -434,6 +441,7 @@
                                      (map make-awk-strnum fields)))))
         (awk-env-fields-set! env vec)
         (awk-env-nf-set! env nf)
+        (%awk-env-record-dirty?-set! env #f)
         (hash-put! (awk-env-globals env) 'NF (make-awk-number nf)))))
 
   (define (env-rebuild-record! env)
@@ -445,7 +453,8 @@
                       (loop (+ i 1)
                             (cons (awk->string (env-get-field env i)) acc))))))
       (vector-set! (awk-env-fields env) 0
-                   (make-awk-string (string-join parts ofs)))))
+                   (make-awk-string (string-join parts ofs)))
+      (%awk-env-record-dirty?-set! env #f)))
 
   (define (env-set-nf! env new-nf)
     (when (> new-nf max-nf)
@@ -455,7 +464,7 @@
         ((= new-nf old-nf) (void))
         ((< new-nf old-nf)
          (awk-env-nf-set! env new-nf)
-         (env-rebuild-record! env))
+         (%awk-env-record-dirty?-set! env #t))
         (else
          ;; Expand
          (let ((new-vec (make-vector (+ new-nf 1) (make-awk-string "")))
@@ -463,7 +472,7 @@
            (vector-copy! old-fields 0 new-vec 0 (vector-length old-fields))
            (awk-env-fields-set! env new-vec)
            (awk-env-nf-set! env new-nf)
-           (env-rebuild-record! env))))))
+           (%awk-env-record-dirty?-set! env #t))))))
 
   ;;; ---- Field splitting ----