Files
rose-ash/spec/tests/test-tco.sx
giles ebb3445667 Cross-host test suite: JS 870/870, Python 679/679 (100% both)
New test files:
- test-collections.sx (79): list/dict edge cases, interop, equality
- test-scope.sx (48): let/define/set!/closure/letrec/env isolation

Python test runner (hosts/python/tests/run_tests.py):
- Runs all spec tests against bootstrapped sx_ref.py
- Tree-walk evaluator with full primitive env
- Skips CEK/types/strict/continuations without --full

Cross-host fixes (tests now host-neutral):
- cons onto nil: platform-defined (JS: pair, Python: single)
- = on lists: test identity only (JS: shallow, Python: deep)
- str(true): accept "true" or "True"
- (+ "a" 1): platform-defined (JS: coerces, Python: throws)
- min/max: test with two args (Python single-arg expects iterable)
- TCO depth: lowered to 500 (works on both hosts)
- Strict mode tests moved to test-strict.sx (skipped on Python)

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

192 lines
5.8 KiB
Plaintext

;; ==========================================================================
;; test-tco.sx — Tests for tail-call optimization and set! mutation
;;
;; Requires: test-framework.sx loaded first.
;; Modules tested: eval.sx (trampoline, thunk, set!)
;;
;; TCO note: tail-recursive calls in SX produce thunks that are resolved
;; by the trampoline. Deep recursion that would overflow a native call
;; stack must complete in O(1) stack space via this mechanism.
;; ==========================================================================
;; --------------------------------------------------------------------------
;; Tail-call optimization — basic deep recursion
;; --------------------------------------------------------------------------
(defsuite "tco-basic"
(deftest "tail-recursive sum completes without stack overflow"
;; sum-iter is tail-recursive: the recursive call is the final value.
;; n=500 would blow the call stack without TCO.
;; (Depth limited by Python's default recursion limit)
(define sum-iter
(fn (n acc)
(if (<= n 0)
acc
(sum-iter (- n 1) (+ acc n)))))
(assert-equal 125250 (sum-iter 500 0)))
(deftest "tail-recursive factorial"
(define fact-iter
(fn (n acc)
(if (<= n 1)
acc
(fact-iter (- n 1) (* acc n)))))
(assert-equal 120 (fact-iter 5 1))
(assert-equal 3628800 (fact-iter 10 1)))
(deftest "mutual tail recursion via define"
;; even? and odd? call each other in tail position.
;; With TCO both directions must trampoline correctly.
(define my-even?
(fn (n)
(if (= n 0)
true
(my-odd? (- n 1)))))
(define my-odd?
(fn (n)
(if (= n 0)
false
(my-even? (- n 1)))))
(assert-true (my-even? 100))
(assert-false (my-odd? 100))
(assert-false (my-even? 99))
(assert-true (my-odd? 99)))
(deftest "non-tail recursion at moderate depth"
;; Classic non-tail factorial: O(n) stack frames.
;; n=100 is deep enough to exercise recursion without relying on TCO.
(define factorial
(fn (n)
(if (<= n 1)
1
(* n (factorial (- n 1))))))
(assert-equal 1 (factorial 1))
(assert-equal 24 (factorial 4))
;; Use a boolean check so we don't need big-integer support
(assert-true (> (factorial 20) 1000000))))
;; --------------------------------------------------------------------------
;; set! mutation
;; --------------------------------------------------------------------------
(defsuite "set-mutation"
(deftest "set! changes binding value"
(define x 1)
(set! x 2)
(assert-equal 2 x))
(deftest "set! in let body"
(let ((y 10))
(set! y 20)
(assert-equal 20 y)))
(deftest "set! visible to subsequent expressions in do block"
(let ((counter 0))
(do
(set! counter (+ counter 1))
(set! counter (+ counter 1))
(set! counter (+ counter 1)))
(assert-equal 3 counter)))
(deftest "set! counter pattern"
;; Simulate an imperative loop via set! + tail recursion.
(let ((total 0))
(define loop
(fn (i)
(when (< i 5)
(set! total (+ total i))
(loop (+ i 1)))))
(loop 0)
;; 0+1+2+3+4 = 10
(assert-equal 10 total)))
(deftest "multiple set! to same variable"
(define v 0)
(set! v 1)
(set! v 2)
(set! v 3)
(assert-equal 3 v)))
;; --------------------------------------------------------------------------
;; TCO in various tail positions
;; --------------------------------------------------------------------------
(defsuite "tco-patterns"
(deftest "accumulator pattern"
;; Classic FP accumulator — build result in extra param so the
;; recursive call stays in tail position.
(define reverse-iter
(fn (lst acc)
(if (empty? lst)
acc
(reverse-iter (rest lst) (cons (first lst) acc)))))
(assert-equal (list 3 2 1) (reverse-iter (list 1 2 3) (list)))
(assert-equal (list) (reverse-iter (list) (list))))
(deftest "loop via tail recursion until condition"
;; count-down reaches zero via tail calls only.
(define count-down
(fn (n)
(if (= n 0)
"done"
(count-down (- n 1)))))
(assert-equal "done" (count-down 500)))
(deftest "tail position in if then-branch"
(define f
(fn (n)
(if (> n 0)
(f (- n 1)) ;; tail call in then-branch
"zero")))
(assert-equal "zero" (f 500)))
(deftest "tail position in if else-branch"
(define g
(fn (n)
(if (= n 0)
"done"
(g (- n 1))))) ;; tail call in else-branch
(assert-equal "done" (g 500)))
(deftest "tail position in cond"
(define classify
(fn (n)
(cond (< n 0) "negative"
(= n 0) "zero"
:else "positive")))
(assert-equal "negative" (classify -5))
(assert-equal "zero" (classify 0))
(assert-equal "positive" (classify 7)))
(deftest "tail position in cond recursive clause"
(define count-up
(fn (n limit)
(cond (= n limit) n
:else (count-up (+ n 1) limit))))
(assert-equal 200 (count-up 0 200)))
(deftest "tail position in let body"
;; The body expression of a let is in tail position.
(define h
(fn (n)
(let ((m (- n 1)))
(if (<= m 0)
m
(h m)))))
(assert-equal 0 (h 500)))
(deftest "tail position in when body"
;; The last expression of a when body is in tail position.
(define scan
(fn (lst acc)
(when (not (empty? lst))
(scan (rest lst) (+ acc (first lst))))))
;; scan returns nil on empty — seed with pre-evaluated sum
(define sum-list
(fn (lst)
(reduce (fn (a x) (+ a x)) 0 lst)))
(assert-equal 15 (sum-list (list 1 2 3 4 5)))))