;; ========================================================================== ;; test-continuations-advanced.sx — Stress tests for multi-shot continuations ;; and frame-based dynamic scope ;; ;; Requires: test-framework.sx loaded, continuations + scope extensions enabled. ;; ;; Tests the CEK continuation + ProvideFrame/ScopeAccFrame system under: ;; - Multi-shot (k invoked 0, 1, 2, 3+ times) ;; - Continuation composition across nested resets ;; - provide/context: dynamic variable binding via kont walk ;; - provide values preserved across shift/resume ;; - scope/emit!/emitted: accumulator frames in kont ;; - Accumulator frames preserved across shift/resume ;; ========================================================================== ;; -------------------------------------------------------------------------- ;; 1. Multi-shot continuations ;; -------------------------------------------------------------------------- (defsuite "multi-shot-continuations" (deftest "k invoked 3 times returns list of results" ;; Each (k N) resumes (+ 1 N) independently. ;; Shift body collects all three results into a list. (assert-equal (list 11 21 31) (reset (+ 1 (shift k (list (k 10) (k 20) (k 30))))))) (deftest "k invoked via map over input list" ;; map applies k to each element; each resume computes (+ 1 elem). (assert-equal (list 11 21 31) (reset (+ 1 (shift k (map k (list 10 20 30))))))) (deftest "k invoked zero times — abort with plain value" ;; Shift body ignores k and returns 42 directly. ;; The outer (+ 1 ...) hole is never filled. (assert-equal 42 (reset (+ 1 (shift k 42))))) (deftest "k invoked conditionally — true branch calls k" ;; Only the true branch calls k; result is (+ 1 10) = 11. (assert-equal 11 (reset (+ 1 (shift k (if true (k 10) 99)))))) (deftest "k invoked conditionally — false branch skips k" ;; False branch returns 99 directly without invoking k. (assert-equal 99 (reset (+ 1 (shift k (if false (k 10) 99)))))) (deftest "k invoked inside let binding" ;; (k 5) = (+ 1 5) = 6; x is bound to 6; (* x 2) = 12. (assert-equal 12 (reset (+ 1 (shift k (let ((x (k 5))) (* x 2))))))) (deftest "nested shift — inner k2 called by outer k1" ;; k1 = (fn (v) (+ 1 v)), k2 = (fn (v) (+ 2 v)) ;; (k2 3) = 5, (k1 5) = 6 ;; inner reset returns 6 to shift-k1 body; (+ 10 6) = 16 ;; outer reset returns 16 (assert-equal 16 (reset (+ 1 (shift k1 (+ 10 (reset (+ 2 (shift k2 (k1 (k2 3))))))))))) (deftest "k called twice accumulates both results" ;; Two invocations in a list: (k 1) = 2, (k 2) = 3. (assert-equal (list 2 3) (reset (+ 1 (shift k (list (k 1) (k 2))))))) (deftest "multi-shot k is idempotent — same arg gives same result" ;; Calling k with the same argument twice should yield equal values. (let ((results (reset (+ 1 (shift k (list (k 5) (k 5))))))) (assert-equal (nth results 0) (nth results 1))))) ;; -------------------------------------------------------------------------- ;; 2. Continuation composition ;; -------------------------------------------------------------------------- (defsuite "continuation-composition" (deftest "two independent resets have isolated continuations" ;; Each reset is entirely separate — the two k values are unrelated. (let ((r1 (reset (+ 1 (shift k1 (k1 10))))) (r2 (reset (+ 100 (shift k2 (k2 5)))))) (assert-equal 11 r1) (assert-equal 105 r2))) (deftest "continuation passed to helper function and invoked there" ;; apply-k is a plain lambda; it calls the continuation it receives. (let ((apply-k (fn (k v) (k v)))) (assert-equal 15 (reset (+ 5 (shift k (apply-k k 10))))))) (deftest "continuation stored in variable and invoked later" ;; reset returns k itself; we then invoke it outside the reset form. (let ((k (reset (shift k k)))) ;; k = identity continuation for (reset _), so (k v) = v (assert-true (continuation? k)) (assert-equal 42 (k 42)) (assert-equal 7 (k 7)))) (deftest "continuation stored then called with multiple values" ;; k from (+ 1 hole); invoking k with different args gives different results. (let ((k (reset (+ 1 (shift k k))))) (assert-equal 11 (k 10)) (assert-equal 21 (k 20)) (assert-equal 31 (k 30)))) (deftest "continuation as argument to map — applied to a list" ;; k = (fn (v) (+ 10 v)); map applies it to each element. (let ((k (reset (+ 10 (shift k k))))) (assert-equal (list 11 12 13) (map k (list 1 2 3))))) (deftest "compose two continuations from nested resets" ;; k1 = (fn (v) (+ 1 v)), k2 = (fn (v) (+ 10 v)) ;; (k2 0) = 10, (k1 10) = 11; outer reset returns 11. (assert-equal 11 (reset (+ 1 (shift k1 (reset (+ 10 (shift k2 (k1 (k2 0)))))))))) (deftest "continuation predicate holds inside and after capture" ;; k captured inside shift is a continuation; so is one returned by reset. (assert-true (reset (shift k (continuation? k)))) (assert-true (continuation? (reset (shift k k)))))) ;; -------------------------------------------------------------------------- ;; 3. provide / context — basic dynamic scope ;; -------------------------------------------------------------------------- (defsuite "provide-context-basic" (deftest "simple provide and context" ;; (context \"x\") walks the kont and finds the ProvideFrame for \"x\". (assert-equal 42 (provide "x" 42 (context "x")))) (deftest "nested provide — inner shadows outer" ;; The nearest ProvideFrame wins when searching kont. (assert-equal 2 (provide "x" 1 (provide "x" 2 (context "x"))))) (deftest "outer provide visible after inner scope exits" ;; After the inner provide's body finishes, its frame is gone. ;; The next (context \"x\") walks past it to the outer frame. (assert-equal 1 (provide "x" 1 (do (provide "x" 2 (context "x")) (context "x"))))) (deftest "multiple provide names are independent" ;; Each name has its own ProvideFrame; they don't interfere. (assert-equal 3 (provide "a" 1 (provide "b" 2 (+ (context "a") (context "b")))))) (deftest "context with default — provider present returns provided value" ;; Second arg to context is the default; present provider overrides it. (assert-equal 42 (provide "x" 42 (context "x" 0)))) (deftest "context with default — no provider returns default" ;; When no ProvideFrame exists for the name, the default is returned. (assert-equal 0 (provide "y" 99 (context "x" 0)))) (deftest "provide with computed value" ;; The value expression is evaluated before pushing the frame. (assert-equal 6 (provide "n" (* 2 3) (context "n")))) (deftest "provide value is the exact bound value (no double-eval)" ;; Passing a list as the provided value should return that list. (let ((result (provide "items" (list 1 2 3) (context "items")))) (assert-equal (list 1 2 3) result)))) ;; -------------------------------------------------------------------------- ;; 4. provide across shift — scope survives continuation capture/resume ;; -------------------------------------------------------------------------- (defsuite "provide-across-shift" (deftest "provide value preserved across shift and k invocation" ;; The ProvideFrame lives in the kont beyond the ResetFrame. ;; When k resumes, the frame is still there — context finds it. (assert-equal "dark" (reset (provide "theme" "dark" (+ 0 (shift k (k 0))) (context "theme"))))) (deftest "two provides both preserved across shift" ;; Both ProvideFrames must survive the shift/resume round-trip. (assert-equal 3 (reset (provide "a" 1 (provide "b" 2 (+ 0 (shift k (k 0))) (+ (context "a") (context "b"))))))) (deftest "context visible inside provide but not in shift body" ;; shift body runs OUTSIDE the reset boundary — provide is not in scope. ;; But context with a default should return the default. (assert-equal "fallback" (reset (provide "theme" "light" (shift k (context "theme" "fallback")))))) (deftest "context after k invocation restores scope frame" ;; k was captured with the ProvideFrame in its saved kont. ;; After (k v) resumes, context finds the frame again. (let ((result (reset (provide "color" "red" (+ 0 (shift k (k 0))) (context "color"))))) (assert-equal "red" result))) (deftest "multi-shot: each k invocation reinstates captured ProvideFrame" ;; k captures the ProvideFrame for "n" (it's inside the reset delimiter). ;; Invoking k twice: each time (context "n") in the resumed body is valid. ;; The shift body collects (context "n") from each resumed branch. (let ((readings (reset (provide "n" 10 (+ 0 (shift k (list (k 0) (k 0)))) (context "n"))))) ;; Each (k 0) resumes and returns (context "n") = 10. (assert-equal (list 10 10) readings)))) ;; -------------------------------------------------------------------------- ;; 5. scope / emit! / emitted — accumulator frames ;; -------------------------------------------------------------------------- (defsuite "scope-emit-basic" (deftest "simple scope: emit two items and read emitted list" ;; emit! appends to the nearest ScopeAccFrame; emitted returns the list. (assert-equal (list "a" "b") (scope "css" (emit! "css" "a") (emit! "css" "b") (emitted "css")))) (deftest "empty scope returns empty list for emitted" ;; No emit! calls means the accumulator stays empty. (assert-equal (list) (scope "css" (emitted "css")))) (deftest "emit! order is preserved" ;; Items appear in emission order, not reverse. (assert-equal (list 1 2 3 4 5) (scope "nums" (emit! "nums" 1) (emit! "nums" 2) (emit! "nums" 3) (emit! "nums" 4) (emit! "nums" 5) (emitted "nums")))) (deftest "nested scopes: inner does not see outer's emitted" ;; The inner scope has its own ScopeAccFrame; kont-find-scope-acc ;; stops at the first matching name, so inner is fully isolated. (let ((inner-emitted (scope "css" (emit! "css" "outer") (scope "css" (emit! "css" "inner") (emitted "css"))))) (assert-equal (list "inner") inner-emitted))) (deftest "two differently-named scopes are independent" ;; emit! to \"a\" must not appear in emitted \"b\" and vice versa. (let ((result-a nil) (result-b nil)) (scope "a" (scope "b" (emit! "a" "for-a") (emit! "b" "for-b") (set! result-b (emitted "b"))) (set! result-a (emitted "a"))) (assert-equal (list "for-a") result-a) (assert-equal (list "for-b") result-b))) (deftest "scope body returns last expression value" ;; scope itself returns the last body expression, not the emitted list. (assert-equal 42 (scope "x" (emit! "x" "ignored") 42))) (deftest "scope with :value acts as provide for context" ;; When :value is given, the ScopeAccFrame also carries the value. ;; context should be able to read it (if the evaluator searches scope-acc ;; frames the same way as provide frames). ;; NOTE: this tests the :value keyword path in step-sf-scope. ;; If context only walks ProvideFrames, use provide directly instead. ;; We verify at minimum that :value does not crash. (let ((r (try-call (fn () (scope "x" :value 42 (emitted "x")))))) (assert-true (get r "ok"))))) ;; -------------------------------------------------------------------------- ;; 6. scope / emit! across shift — accumulator frames survive continuation ;; -------------------------------------------------------------------------- (defsuite "scope-emit-across-shift" (deftest "emit before and after shift both appear in emitted" ;; The ScopeAccFrame is in the kont beyond the ResetFrame. ;; After k resumes, the frame is still present; the second emit! ;; appends to it. (assert-equal (list "a" "b") (reset (scope "acc" (emit! "acc" "a") (+ 0 (shift k (k 0))) (emit! "acc" "b") (emitted "acc"))))) (deftest "emit only before shift — one item in emitted" ;; emit! before shift commits to the frame; shift/resume preserves it. (assert-equal (list "only") (reset (scope "log" (emit! "log" "only") (+ 0 (shift k (k 0))) (emitted "log"))))) (deftest "emit only after shift — one item in emitted" ;; No emit! before shift; the frame starts empty; post-resume emit! adds one. (assert-equal (list "after") (reset (scope "log" (+ 0 (shift k (k 0))) (emit! "log" "after") (emitted "log"))))) (deftest "emits on both sides of single shift boundary" ;; Single shift/resume; emits before and after are preserved. (assert-equal (list "a" "b") (reset (scope "trace" (emit! "trace" "a") (+ 0 (shift k (k 0))) (emit! "trace" "b") (emitted "trace"))))) (deftest "emitted inside shift body reads current accumulator" ;; kont in the shift body is rest-kont (outer kont beyond the reset). ;; The ScopeAccFrame should be present if it was installed before reset. ;; emit! and emitted inside shift body use that outer frame. (let ((outer-acc nil)) (scope "outer" (reset (shift k (do (emit! "outer" "from-shift") (set! outer-acc (emitted "outer"))))) nil) (assert-equal (list "from-shift") outer-acc))))