IO suspension: _cek_io_suspend_hook propagates perform through eval_expr

Root cause: cek_run_iterative (used by eval_expr/trampoline) raised
"IO suspension in non-IO context" when the CEK hit a perform. This
blocked IO suspension from propagating through nested eval_expr calls
(event handler → trampoline → eval_expr → for-each callback → hs-wait).

Fix: added _cek_io_suspend_hook (Sx_types) that converts CEK suspension
to VmSuspended, set by sx_vm.ml at init. cek_run_iterative now calls the
hook instead of erroring. The VmSuspended propagates to the value_to_js
wrapper which has _driveAsync handling.

+42 test passes (3924→3966), zero regressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-16 16:34:56 +00:00
parent 133edd4c5e
commit b86d0b7e15
3 changed files with 26 additions and 1 deletions

View File

@@ -983,7 +983,15 @@ let cek_run_iterative state =
s := cek_step !s
done;
(match cek_suspended_p !s with
| Bool true -> raise (Eval_error "IO suspension in non-IO context")
| Bool true ->
(* Propagate IO suspension so the outer handler
(value_to_js wrapper or _cek_eval_lambda_ref) can drive it.
Without this, perform inside nested eval_expr contexts
(e.g. event handler → trampoline → eval_expr) gets swallowed.
Use _cek_io_suspend_hook if set; otherwise fall back to error. *)
(match !Sx_types._cek_io_suspend_hook with
| Some hook -> hook !s
| None -> raise (Eval_error "IO suspension in non-IO context"))
| _ -> cek_value !s)
with Eval_error msg ->
_last_error_kont_ref := cek_kont !s;