test-continuations-advanced.sx (41 tests): multi-shot continuations, composition, provide/context basics, provide across shift, scope/emit basics, scope across shift test-render-advanced.sx (27 tests): nested components, dynamic content, list patterns, component patterns, special elements Bugs found and documented: - case in render context returns DOM object (CEK dispatches case before HTML adapter sees it — use cond instead for render) - context not visible in shift body (correct: shift body runs outside the reset/provide boundary) - Multiple shifts consume reset (correct: each shift needs its own reset) Python runner: skip test-continuations-advanced.sx without --full. JS 815/815 standard, 938/938 full, Python 706/706. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
369 lines
14 KiB
Plaintext
369 lines
14 KiB
Plaintext
;; ==========================================================================
|
|
;; 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))))
|
|
|