;; ========================================================================== ;; test-vm-closures.sx — Tests for inner closure recursion patterns ;; ;; Requires: test-framework.sx loaded first. ;; ;; These tests exercise patterns where inner closures recurse deeply ;; while sharing mutable state via upvalues. This is the sx-parse ;; pattern: many inner functions close over a mutable cursor variable. ;; Without proper VM closure support, each recursive call would ;; allocate a fresh VM — blowing the stack or hanging. ;; ========================================================================== ;; -------------------------------------------------------------------------- ;; Inner closure recursion with mutable upvalues ;; -------------------------------------------------------------------------- (defsuite "inner-closure-recursion" (deftest "self-recursive inner closure with set! on captured variable" ;; Pattern: closure mutates captured var on each recursive call. ;; This is the core pattern in skip-ws, read-str-loop, etc. (let ((counter 0)) (define count-up (fn (n) (when (> n 0) (set! counter (+ counter 1)) (count-up (- n 1))))) (count-up 100) (assert-equal 100 counter))) (deftest "deep inner closure recursion (500 iterations)" ;; Stress test: 500 recursive calls through an inner closure ;; mutating a shared upvalue. Would stack-overflow without TCO. (let ((acc 0)) (define sum-up (fn (n) (if (<= n 0) acc (do (set! acc (+ acc n)) (sum-up (- n 1)))))) (assert-equal 125250 (sum-up 500)))) (deftest "inner closure reading captured variable updated by another" ;; Two closures: one writes, one reads, sharing the same binding. (let ((pos 0)) (define advance! (fn () (set! pos (+ pos 1)))) (define current (fn () pos)) (advance!) (advance!) (advance!) (assert-equal 3 (current)))) (deftest "recursive closure with multiple mutable upvalues" ;; Like sx-parse: multiple cursor variables mutated during recursion. (let ((pos 0) (count 0)) (define scan (fn (source) (when (< pos (len source)) (set! count (+ count 1)) (set! pos (+ pos 1)) (scan source)))) (scan "hello world") (assert-equal 11 pos) (assert-equal 11 count)))) ;; -------------------------------------------------------------------------- ;; Mutual recursion between inner closures ;; -------------------------------------------------------------------------- (defsuite "mutual-inner-closures" (deftest "two inner closures calling each other" ;; Pattern: read-expr calls read-list, read-list calls read-expr. (let ((result (list))) (define process-a (fn (items) (when (not (empty? items)) (append! result (str "a:" (first items))) (process-b (rest items))))) (define process-b (fn (items) (when (not (empty? items)) (append! result (str "b:" (first items))) (process-a (rest items))))) (process-a (list 1 2 3 4)) (assert-equal 4 (len result)) (assert-equal "a:1" (nth result 0)) (assert-equal "b:2" (nth result 1)) (assert-equal "a:3" (nth result 2)) (assert-equal "b:4" (nth result 3)))) (deftest "mutual recursion with shared mutable state" ;; Both closures read and write the same captured variable. (let ((pos 0) (source "aAbBcC")) (define skip-lower (fn () (when (and (< pos (len source)) (>= (nth source pos) "a") (<= (nth source pos) "z")) (set! pos (+ pos 1)) (skip-upper)))) (define skip-upper (fn () (when (and (< pos (len source)) (>= (nth source pos) "A") (<= (nth source pos) "Z")) (set! pos (+ pos 1)) (skip-lower)))) (skip-lower) (assert-equal 6 pos))) (deftest "three-way mutual recursion" (let ((n 30) (result nil)) (define step-a (fn (i) (if (>= i n) (set! result "done") (step-b (+ i 1))))) (define step-b (fn (i) (step-c (+ i 1)))) (define step-c (fn (i) (step-a (+ i 1)))) (step-a 0) (assert-equal "done" result)))) ;; -------------------------------------------------------------------------- ;; Parser-like patterns (the sx-parse structure) ;; -------------------------------------------------------------------------- (defsuite "parser-pattern" (deftest "mini-parser: tokenize digits from string" ;; Simplified sx-parse pattern: closure over pos + source, ;; multiple inner functions sharing the mutable cursor. (let ((pos 0) (source "12 34 56") (len-src 8)) (define skip-ws (fn () (when (and (< pos len-src) (= (nth source pos) " ")) (set! pos (+ pos 1)) (skip-ws)))) (define read-digits (fn () (let ((start pos)) (define digit-loop (fn () (when (and (< pos len-src) (>= (nth source pos) "0") (<= (nth source pos) "9")) (set! pos (+ pos 1)) (digit-loop)))) (digit-loop) (slice source start pos)))) (define read-all (fn () (let ((tokens (list))) (define parse-loop (fn () (skip-ws) (when (< pos len-src) (append! tokens (read-digits)) (parse-loop)))) (parse-loop) tokens))) (let ((tokens (read-all))) (assert-equal 3 (len tokens)) (assert-equal "12" (nth tokens 0)) (assert-equal "34" (nth tokens 1)) (assert-equal "56" (nth tokens 2))))) (deftest "nested inner closures with upvalue chain" ;; Inner function defines its own inner function, ;; both closing over the outer mutable variable. (let ((total 0)) (define outer-fn (fn (items) (for-each (fn (item) (let ((sub-total 0)) (define inner-loop (fn (n) (when (> n 0) (set! sub-total (+ sub-total 1)) (set! total (+ total 1)) (inner-loop (- n 1))))) (inner-loop item))) items))) (outer-fn (list 3 2 1)) (assert-equal 6 total))) (deftest "closure returning accumulated list via append!" ;; Pattern from read-list: loop appends to mutable list, returns it. (let ((items (list))) (define collect (fn (source pos) (if (>= pos (len source)) items (do (append! items (nth source pos)) (collect source (+ pos 1)))))) (let ((result (collect (list "a" "b" "c" "d") 0))) (assert-equal 4 (len result)) (assert-equal "a" (first result)) (assert-equal "d" (last result)))))) ;; -------------------------------------------------------------------------- ;; Closures as callbacks to higher-order functions ;; -------------------------------------------------------------------------- (defsuite "closure-ho-callbacks" (deftest "map with closure that mutates captured variable" (let ((running-total 0)) (let ((results (map (fn (x) (set! running-total (+ running-total x)) running-total) (list 1 2 3 4)))) (assert-equal (list 1 3 6 10) results) (assert-equal 10 running-total)))) (deftest "reduce with closure over external state" (let ((call-count 0)) (let ((sum (reduce (fn (acc x) (set! call-count (+ call-count 1)) (+ acc x)) 0 (list 10 20 30)))) (assert-equal 60 sum) (assert-equal 3 call-count)))) (deftest "filter with closure reading shared state" (let ((threshold 3)) (let ((result (filter (fn (x) (> x threshold)) (list 1 2 3 4 5)))) (assert-equal (list 4 5) result)))))