Files
rose-ash/spec/tests/test-letrec-resume.sx
giles e80e655b51 sx: step 2 — restore frame locals on browser VmSuspension resume
In `resume_vm`'s `restore_reuse`, the saved sp captured by
`call_closure_reuse` was ignored when restoring the caller frame after the
async callback finished. The suspended callee's locals/temps stayed on the
value stack above saved_sp, so subsequent LOCAL_GET/SET in the caller
frame (e.g. letrec sibling bindings waiting on the suspending call) read
stale callee data instead of their own slots. Sibling bindings appeared
nil after a perform/resume cycle on the JIT path used by the WASM
browser kernel.

Fix: after popping the callback result and restoring saved_frames, reset
`vm.sp <- saved_sp` (when sp is above), then push the callback result.
Mirrors the OP_RETURN+sp-reset discipline that sync `call_closure_reuse`
already follows.

New tests in `spec/tests/test-letrec-resume.sx` cover single binding,
sibling bindings, mutual recursion siblings, and nested letrec —
all four pass. Full OCaml run_tests: 4529/5868 (was 4525/5864), zero
regressions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 21:45:44 +00:00

45 lines
2.1 KiB
Plaintext

;; Letrec + perform/resume regression tests — Step 2
;; Verifies sibling bindings survive across an IO suspension when the
;; suspended call goes through call_closure_reuse (JIT path).
;; The browser/WASM kernel reuses the host VM via call_closure_reuse;
;; if restore_reuse drops the caller's saved sp, sibling letrec bindings
;; come back as nil after resume.
(defsuite
"letrec-resume"
(deftest
"single binding survives perform/resume"
(let
((state (cek-step-loop (make-cek-state (quote (letrec ((f (fn () (perform {:op "io"})))) (f))) (make-env) (list)))))
(assert (cek-suspended? state))
(let
((final (cek-resume state 7)))
(assert (cek-terminal? final))
(assert= (cek-value final) 7))))
(deftest
"sibling bindings survive perform/resume"
(let
((state (cek-step-loop (make-cek-state (quote (letrec ((g (fn () 100)) (f (fn () (perform {:op "io"})))) (+ (f) (g)))) (make-env) (list)))))
(assert (cek-suspended? state))
(let
((final (cek-resume state 5)))
(assert (cek-terminal? final))
(assert= (cek-value final) 105))))
(deftest
"mutual recursion sibling preserved across resume"
(let
((state (cek-step-loop (make-cek-state (quote (letrec ((even? (fn (n) (if (= n 0) true (odd? (- n 1))))) (odd? (fn (n) (if (= n 0) false (even? (- n 1))))) (fetch (fn () (perform {:op "io"})))) (let ((x (fetch))) (even? x)))) (make-env) (list)))))
(assert (cek-suspended? state))
(let
((final (cek-resume state 4)))
(assert (cek-terminal? final))
(assert= (cek-value final) true))))
(deftest
"nested letrec — outer sibling survives inner perform"
(let
((state (cek-step-loop (make-cek-state (quote (letrec ((outer-val (fn () 99)) (inner-call (fn () (letrec ((suspend-fn (fn () (perform {:op "io"})))) (suspend-fn))))) (+ (inner-call) (outer-val)))) (make-env) (list)))))
(assert (cek-suspended? state))
(let
((final (cek-resume state 1)))
(assert (cek-terminal? final))
(assert= (cek-value final) 100)))))