Files
rose-ash/lib/tests/test-vm-closures.sx
giles f3f70cc00b Move stdlib out of spec — clean spec/library boundary
spec/ now contains only the language definition (5 files):
  evaluator.sx, parser.sx, primitives.sx, render.sx, special-forms.sx

lib/ contains code written IN the language (8 files):
  stdlib.sx, types.sx, freeze.sx, content.sx,
  bytecode.sx, compiler.sx, vm.sx, callcc.sx

Test files follow source: spec/tests/ for core language tests,
lib/tests/ for library tests (continuations, freeze, types, vm).

Updated all consumers:
- JS/Python/OCaml bootstrappers: added lib/ to source search paths
- OCaml bridge: spec_dir for parser/render, lib_dir for compiler/freeze
- JS test runner: scans spec/tests/ (always) + lib/tests/ (--full)
- OCaml test runner: scans spec/tests/, lib tests via explicit request
- Docker dev mounts: added ./lib:/app/lib:ro

Tests: 1041 JS standard, 1322 JS full, 1101 OCaml — all pass

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 23:18:30 +00:00

245 lines
8.3 KiB
Plaintext

;; ==========================================================================
;; 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)))))