perf: rbtree contains? O(log n) via rbtree-ref; size/min/max drop rbtree->list materialization

ober

5334fcb924ba6836f8b208fba8a05d9de56339a1

diff --git a/powers.ss b/powers.ss
index 345db88..1c57103 100644
--- a/powers.ss
+++ b/powers.ss
@@ -77,13 +77,24 @@
 (def rbtree-insert rbtree-put)               ; (rbtree-insert t k v) -> new tree
 (def rbtree-lookup rbtree-get)               ; (rbtree-lookup t k) -> value | #f
 (def rbtree-delete rbtree-remove)
-(def (rbtree-contains? t k)                  ; rbtree->list yields sorted (k . v)
-  (let loop ((l (rbtree->list t)))
-    (and (pair? l) (or (equal? (caar l) k) (loop (cdr l))))))
-(def (rbtree-size t) (length (rbtree->list t)))
+;; contains? walks the tree via rbtree-ref (O(log n)) instead of scanning
+;; rbtree->list (O(n)); a sentinel distinguishes an absent key from a #f value.
+(def +rbtree-absent+ (list 'rbtree-absent))
+(def (rbtree-contains? t k)
+  (not (eq? (rbtree-ref t k +rbtree-absent+) +rbtree-absent+)))
+;; size/min/max avoid materializing rbtree->list: a single allocation-free
+;; for-each pass (for-each visits keys in ascending order, so the first key seen
+;; is min and the last is max). Gerbil's rbtree is opaque (no root/node access,
+;; no size field), so these stay O(n) time but no longer allocate the full list.
+(def (rbtree-size t)
+  (let (n 0) (rbtree-for-each (lambda (k v) (set! n (+ n 1))) t) n))
 (def (rbtree-min t)
-  (let (l (rbtree->list t)) (and (pair? l) (car l))))
+  (let ((k* #f) (v* #f) (any? #f))
+    (rbtree-for-each (lambda (k v)
+                       (unless any? (set! k* k) (set! v* v) (set! any? #t)))
+                     t)
+    (and any? (cons k* v*))))
 (def (rbtree-max t)
-  (let (l (rbtree->list t))
-    (and (pair? l)
-         (let loop ((l l)) (if (null? (cdr l)) (car l) (loop (cdr l)))))))
+  (let ((k* #f) (v* #f) (any? #f))
+    (rbtree-for-each (lambda (k v) (set! k* k) (set! v* v) (set! any? #t)) t)
+    (and any? (cons k* v*))))