Async error handler: dispatch Eval_error to VM handler_stack in resume_vm

When an error occurs during resumed VM execution (after perform/hs-wait),
resume_vm now checks the VM's handler_stack. If a handler exists (from a
compiled guard form's OP_PUSH_HANDLER), it unwinds frames and jumps to
the catch block — exactly like OP_RAISE. This enables try/catch across
async perform/resume boundaries.

The guard form compiles to OP_PUSH_HANDLER which lives on the vm struct
and survives across setTimeout-based async resume. Previously, errors
during resume escaped to the JS console as unhandled exceptions.

Also restored guard in the test runner (was cek-try which doesn't survive
async) and restored error-throwing assertions in run-action.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-17 22:54:37 +00:00
parent ac193e8839
commit 0410812420
7 changed files with 75 additions and 39 deletions

View File

@@ -852,10 +852,26 @@ let resume_vm vm result =
| None ->
push vm result);
(try run vm
with VmSuspended _ as e ->
(* Re-suspension during resume: the VM hit another perform.
The new VmSuspended carries the current VM state. *)
raise e);
with
| VmSuspended _ as e ->
(* Re-suspension during resume: the VM hit another perform. *)
raise e
| Eval_error msg ->
(* Error during resumed execution. If the VM has a handler on its
handler_stack, dispatch to it (same as OP_RAISE). This enables
try/catch across async perform/resume boundaries — the handler
was pushed before the perform and survives on the vm struct. *)
(match vm.handler_stack with
| entry :: rest ->
vm.handler_stack <- rest;
while List.length vm.frames > entry.h_frame_depth do
match vm.frames with _ :: fs -> vm.frames <- fs | [] -> ()
done;
vm.sp <- entry.h_sp;
entry.h_frame.ip <- entry.h_catch_ip;
push vm (String msg);
run vm
| [] -> raise (Eval_error msg)));
(* Clear reuse_stack — any entries here are stale from the original
suspension and don't apply to the current state. The VM just
completed its execution successfully. *)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1792,7 +1792,7 @@
blake2_js_for_wasm_create: blake2_js_for_wasm_create};
}
(globalThis))
({"link":[["runtime-0db9b496",0],["prelude-d7e4b000",0],["stdlib-23ce0836",[]],["re-9a0de245",[2]],["sx-267801d6",[2,3]],["jsoo_runtime-f96b44a8",[2]],["js_of_ocaml-651f6707",[2,5]],["dune__exe__Sx_browser-b285d4f3",[2,4,6]],["std_exit-10fb8830",[2]],["start-f808dbe1",0]],"generated":(b=>{var
({"link":[["runtime-0db9b496",0],["prelude-d7e4b000",0],["stdlib-23ce0836",[]],["re-9a0de245",[2]],["sx-00b17a73",[2,3]],["jsoo_runtime-f96b44a8",[2]],["js_of_ocaml-651f6707",[2,5]],["dune__exe__Sx_browser-b285d4f3",[2,4,6]],["std_exit-10fb8830",[2]],["start-f808dbe1",0]],"generated":(b=>{var
c=b,a=b?.module?.export||b;return{"env":{"caml_ba_kind_of_typed_array":()=>{throw new
Error("caml_ba_kind_of_typed_array not implemented")},"caml_exn_with_js_backtrace":()=>{throw new
Error("caml_exn_with_js_backtrace not implemented")},"caml_int64_create_lo_mi_hi":()=>{throw new

View File

@@ -55,16 +55,12 @@
(= type :click)
(let
((el (host-call doc "querySelector" (nth action 1))))
(when
(nil? el)
(error (str "Not found: " (nth action 1))))
(when (nil? el) (error (str "Not found: " (nth action 1))))
(host-call el "click"))
(= type :fill)
(let
((el (host-call doc "querySelector" (nth action 1))))
(when
(nil? el)
(error (str "Not found: " (nth action 1))))
(when (nil? el) (error (str "Not found: " (nth action 1))))
(host-call el "focus")
(dom-set-prop el "value" (nth action 2))
(dom-dispatch el "input" nil)
@@ -74,22 +70,14 @@
(= type :assert-text)
(let
((el (host-call doc "querySelector" (nth action 1))))
(when
(nil? el)
(error (str "Not found: " (nth action 1))))
(when (nil? el) (error (str "Not found: " (nth action 1))))
(let
((txt (host-get el "textContent"))
((txt (dom-text-content el))
(kw (nth action 2))
(expected (nth action 3)))
(when
(and (= kw :contains) (not (contains? txt expected)))
(error
(str
"Expected '"
expected
"' in '"
(slice txt 0 60)
"'")))
(error (str "Expected '" expected "' in '" (slice txt 0 60) "'")))
(when
(and (= kw :not-contains) (contains? txt expected))
(error (str "Unexpected '" expected "'")))))
@@ -102,7 +90,8 @@
(expected (nth action 3)))
(when
(and (= kw :gte) (< count expected))
(error (str "Expected >=" expected " got " count)))))
(str "Expected >=" expected " got " count)
nil)))
true
nil))))
(run-all
@@ -119,13 +108,27 @@
(console-log (str "[test] === " name " ==="))
(reset! current (str "Running: " name))
(reset! results (assoc (deref results) name "running"))
(console-log "[test] calling reload-frame")
(console-log "[test] reload-frame")
(reload-frame)
(console-log "[test] reload-frame done, running actions")
(let
((test-ok (cek-try (fn () (let ((actions (get test :actions))) (when (not (empty? actions)) (let ((first-sel (nth (first actions) 1))) (when (string? first-sel) (console-log (str "[test] wait-for-el: " first-sel)) (let ((found (wait-for-el first-sel 15))) (when (nil? found) (error (str "Timeout waiting for: " first-sel))) (console-log (str "[test] found element: " first-sel)))))) (for-each run-action actions)) (console-log (str "[test] actions done for " name)) true) (fn (e) (do (reset! results (assoc (deref results) name "fail")) (console-log (str "[test] FAIL " name ": " e)) false)))))
(when
test-ok
(console-log "[test] running actions")
(guard
(e
(true
(reset! results (assoc (deref results) name "fail"))
(console-log (str "[test] FAIL " name ": " e))))
(let
((actions (get test :actions)))
(when
(not (empty? actions))
(let
((first-sel (nth (first actions) 1)))
(when
(string? first-sel)
(console-log (str "[test] wait-for: " first-sel))
(let ((found (wait-for-el first-sel 15)))
(when (nil? found)
(error (str "Timeout waiting for: " first-sel)))))))
(for-each run-action actions)
(reset! results (assoc (deref results) name "pass"))
(console-log (str "[test] PASS " name))))))
tests)