perf(decoder): linear decoded-variants via hash seen-set

ober

c267e7cd10ac3e72dc2141ea598a5d4f5497d541

diff --git a/gitsafe/decoder.ss b/gitsafe/decoder.ss
index 0615cbd..69b4e4f 100644
--- a/gitsafe/decoder.ss
+++ b/gitsafe/decoder.ss
@@ -168,8 +168,18 @@
           (and (decoded-ok? s token) s)))
       (catch (e) #f)))
 
+  (def (hash-unique strings)
+    (let ([seen (make-hash-table)])
+      (let loop ([xs strings] [acc '()])
+        (cond
+          [(null? xs) (reverse acc)]
+          [(hash-key? seen (car xs)) (loop (cdr xs) acc)]
+          [else
+           (hash-put! seen (car xs) #t)
+           (loop (cdr xs) (cons (car xs) acc))]))))
+
   (def (decode-once content)
-    (unique
+    (hash-unique
       (filter (lambda (s) (and s (string? s) (mostly-printable? s)))
         (append
           (filter-map decode-percent-token (regex-matches *percent-token-re* content))
@@ -178,19 +188,25 @@
           (filter-map decode-base64-token  (regex-matches *base64-token-re* content))))))
 
   (def (decoded-variants content depth)
-    (let loop ([remaining depth]
-               [frontier (list content)]
-               [seen (list content)]
-               [acc '()])
-      (if (<= remaining 0)
-        (reverse acc)
-        (let* ([decoded (unique (apply append (map decode-once frontier)))]
-               [fresh   (filter (lambda (s) (not (member s seen))) decoded)])
-          (if (null? fresh)
-            (reverse acc)
-            (loop (- remaining 1)
-                  fresh
-                  (append fresh seen)
-                  (append fresh acc)))))))
+    (let ([seen (make-hash-table)])
+      (hash-put! seen content #t)
+      (let loop ([remaining depth]
+                 [frontier (list content)]
+                 [acc '()])
+        (if (<= remaining 0)
+          (reverse acc)
+          (let* ([decoded (apply append (map decode-once frontier))]
+                 [fresh   (let fl ([ds decoded] [out '()])
+                            (cond
+                              [(null? ds) (reverse out)]
+                              [(hash-key? seen (car ds)) (fl (cdr ds) out)]
+                              [else
+                               (hash-put! seen (car ds) #t)
+                               (fl (cdr ds) (cons (car ds) out))]))])
+            (if (null? fresh)
+              (reverse acc)
+              (loop (- remaining 1)
+                    fresh
+                    (append fresh acc))))))))
 
 ) ;; end library