From 0ffe208e311816499f1a22675d4d3a892f41d0ca Mon Sep 17 00:00:00 2001 From: giles Date: Sun, 26 Apr 2026 16:49:22 +0000 Subject: [PATCH] =?UTF-8?q?spec:=20coroutine=20tests=20=E2=80=94=20expand?= =?UTF-8?q?=20to=2027=20(was=2017)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 10 new tests: state field transitions (ready/suspended/dead), yield from nested helper function, initial resume arg ignored by ready coroutine, mutable closure state via dict-set!, complex yield values (list/dict), round-robin scheduling, factory creates independent coroutines, resuming non-coroutine raises error. 27/27 pass on both OCaml and JS. Co-Authored-By: Claude Sonnet 4.6 --- plans/agent-briefings/primitives-loop.md | 6 +- spec/tests/test-coroutines.sx | 105 ++++++++++++++++++++++- 2 files changed, 109 insertions(+), 2 deletions(-) diff --git a/plans/agent-briefings/primitives-loop.md b/plans/agent-briefings/primitives-loop.md index 7f832c4b..708f702f 100644 --- a/plans/agent-briefings/primitives-loop.md +++ b/plans/agent-briefings/primitives-loop.md @@ -137,8 +137,11 @@ using call/cc+perform/resume. All CEK primitives already in sx-browser.js. Fix: pre-load spec/coroutines.sx + spec/signals.sx in run_tests.js so (import (sx coroutines)) resolves without suspension. 17/17 pass in JS. 1965/2500 (+25 vs 1940 baseline). Zero new failures. -- [ ] Tests: 25+ tests — multi-yield, final return, arg passthrough, alive? predicate, +- [x] Tests: 25+ tests — multi-yield, final return, arg passthrough, alive? predicate, nested coroutines, "final return vs yield" distinction (the Lua gotcha). + 27 tests: added 10 new — state field inspection (ready/suspended/dead), yield from + nested helper, initial resume arg ignored, mutable closure state, complex yield values, + round-robin scheduling, factory-shared-no-state, non-coroutine error. 27/27 OCaml+JS. - [ ] Commit: `spec: coroutine primitive (make-coroutine/resume/yield)` --- @@ -671,6 +674,7 @@ Brief each language's loop agent (or do inline) after rebasing their branch onto _Newest first._ +- 2026-04-26: Phase 4 Tests step done — 27 tests total (10 new: state field inspection, yield-from-helper, initial-arg-ignored, mutable-closure, complex-values, round-robin, factory-no-state, non-coroutine-error). 27/27 OCaml+JS. - 2026-04-26: Phase 4 JS step done — all CEK primitives already in sx-browser.js; fix was pre-loading spec/coroutines.sx+spec/signals.sx in run_tests.js so (import (sx coroutines)) resolves synchronously. 17/17 coroutine tests pass JS. 1965/2500 total (+25), zero new failures. - 2026-04-26: Phase 4 OCaml step done — no native SxCoroutine type needed; existing cek-step-loop/cek-resume/perform/make-cek-state primitives in run_tests.ml fully support the spec/coroutines.sx library. 284/284 pass (coroutines+vectors+numeric-tower+dynamic-wind), zero regressions. - 2026-04-26: Phase 4 Spec step done — spec/coroutines.sx define-library with make-coroutine/coroutine-resume/coroutine-yield/coroutine?/coroutine-alive?; make-coroutine stub in evaluator.sx; 17/17 coroutine tests pass (OCaml). Key insight: coroutine body must use (define loop (fn...)) + (loop 0) not named let — named let uses cek_call→cek_run which errors on IO suspension. diff --git a/spec/tests/test-coroutines.sx b/spec/tests/test-coroutines.sx index 1ca47240..a0b0fc17 100644 --- a/spec/tests/test-coroutines.sx +++ b/spec/tests/test-coroutines.sx @@ -199,4 +199,107 @@ (assert= true (get c "done")) (assert= 5 (get c "value")) (assert= true (get d "done")) - (assert= 6 (get d "value"))))))))) + (assert= 6 (get d "value")))))))) + (deftest + "coroutine state field is ready before first resume" + (let + ((co (make-coroutine (fn () (coroutine-yield 1))))) + (assert= "ready" (get co "state")))) + (deftest + "coroutine state field is suspended between yields" + (let + ((co (make-coroutine (fn () (coroutine-yield 1) 2)))) + (coroutine-resume co nil) + (assert= "suspended" (get co "state")))) + (deftest + "coroutine state field is dead after completion" + (let + ((co (make-coroutine (fn () nil)))) + (coroutine-resume co nil) + (assert= "dead" (get co "state")))) + (deftest + "yield works when called from nested helper function" + (let + ((co (make-coroutine (fn () (define helper (fn (x) (coroutine-yield x))) (helper 10) (helper 20))))) + (let + ((r1 (coroutine-resume co nil))) + (let + ((r2 (coroutine-resume co nil))) + (let + ((r3 (coroutine-resume co nil))) + (assert= false (get r1 "done")) + (assert= 10 (get r1 "value")) + (assert= false (get r2 "done")) + (assert= 20 (get r2 "value")) + (assert= true (get r3 "done"))))))) + (deftest + "initial resume argument is ignored by ready coroutine" + (let + ((co (make-coroutine (fn () (coroutine-yield 42))))) + (let + ((r (coroutine-resume co "ignored"))) + (assert= false (get r "done")) + (assert= 42 (get r "value"))))) + (deftest + "coroutine with mutable closure state" + (let + ((counter {:value 0})) + (let + ((co (make-coroutine (fn () (dict-set! counter "value" 1) (coroutine-yield "a") (dict-set! counter "value" 2) (coroutine-yield "b"))))) + (assert= 0 (get counter "value")) + (coroutine-resume co nil) + (assert= 1 (get counter "value")) + (coroutine-resume co nil) + (assert= 2 (get counter "value"))))) + (deftest + "coroutine can yield complex values" + (let + ((co (make-coroutine (fn () (coroutine-yield (list 1 2 3)) (coroutine-yield {:key "val"}))))) + (let + ((r1 (coroutine-resume co nil))) + (let + ((r2 (coroutine-resume co nil))) + (assert= false (get r1 "done")) + (assert= 3 (len (get r1 "value"))) + (assert= false (get r2 "done")) + (assert= "val" (get (get r2 "value") "key")))))) + (deftest + "round-robin scheduling of multiple coroutines" + (let + ((results (list)) + (co1 + (make-coroutine + (fn () (coroutine-yield "a") (coroutine-yield "b")))) + (co2 + (make-coroutine + (fn () (coroutine-yield "c") (coroutine-yield "d"))))) + (append! results (get (coroutine-resume co1 nil) "value")) + (append! results (get (coroutine-resume co2 nil) "value")) + (append! results (get (coroutine-resume co1 nil) "value")) + (append! results (get (coroutine-resume co2 nil) "value")) + (assert= 4 (len results)) + (assert= "a" (nth results 0)) + (assert= "c" (nth results 1)) + (assert= "b" (nth results 2)) + (assert= "d" (nth results 3)))) + (deftest + "coroutines created from same factory share no state" + (let + ((make-counter (fn (start) (make-coroutine (fn () (define loop (fn (n) (coroutine-yield n) (loop (+ n 1)))) (loop start)))))) + (let + ((c1 (make-counter 0)) (c2 (make-counter 100))) + (let + ((a (get (coroutine-resume c1 nil) "value"))) + (let + ((b (get (coroutine-resume c2 nil) "value"))) + (let + ((c (get (coroutine-resume c1 nil) "value"))) + (let + ((d (get (coroutine-resume c2 nil) "value"))) + (assert= 0 a) + (assert= 100 b) + (assert= 1 c) + (assert= 101 d)))))))) + (deftest + "resuming non-coroutine raises error" + (assert-throws (fn () (coroutine-resume "not-a-coroutine" nil)))))