guard and cek-try both create CEK frames that don't survive async
perform/resume. Instead, run-action returns nil on success and an
error string on failure. The for-each loop checks the return value
and sets fail-msg. No exceptions cross async boundaries.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
Logs at every step: run-all start, test name, reload-frame, wait-for-el,
actions done, PASS/FAIL, run-all complete.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous insert wrapped reload-frame and wait-for-el in a begin
block instead of making them separate letrec bindings. This made
reload-frame invisible to later bindings. Fixed by inserting
wait-for-el at index 3 in the letrec bindings list.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
wait-for-el polls the iframe doc for a CSS selector up to max-tries
times with 200ms intervals. Used before running test actions to ensure
the target elements exist in the iframe after page load.
Also restores reload-frame timing and keeps cek-try error handling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The guard form (call/cc + handler-bind expansion) doesn't survive async
IO suspension — the CEK continuation from guard's call/cc captures frames
that become invalid after the VM resumes from hs-wait. Replacing guard
with cek-try (which compiles to VM-native OP_PUSH_HANDLER/OP_POP_HANDLER)
avoids the CEK boundary crossing.
The test runner now executes: suspends on hs-wait, resumes, runs test
actions, and test assertions fire correctly. The "Not callable: nil"
error is eliminated. Remaining: test assertion errors from iframe content
not loading fast enough (timing issue, not a framework bug).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
runner.sx: Converted define forms inside island body to letrec. Multiple
define forms in a let body cause render-to-dom to fall back to eval-expr
for the whole body, which evaluates (div ...) as a list instead of
rendering it to DOM. letrec keeps the last body expression (div) as the
render target.
sx_browser.ml: js_to_value now stores plain JS functions as host objects
(Dict with __host_handle) instead of wrapping as NativeFn. This preserves
the original JS function identity through the SX→JS round-trip, keeping
_driveAsync wrappers from host-callback intact when passed to
addEventListener via host-call.
Remaining: IO suspension in click handler is caught as "IO suspension in
non-IO context" instead of being driven by _driveAsync. The host-callback
wrapper creates the right JS function, but the event dispatch path doesn't
go through K.callFn.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Test runner island (~test-runner) with 8 test definitions as SX data
- SSR renders test list with expandable deftest source
- Island body has run-all/run-action/reload-frame/wait-boot helpers
- Header: "test" link on every page, derives test URL from current path
- _test added to skip_dirs in sx_server.ml (both load_dir locations)
- Handler names: ex-{slug} convention for dispatch compatibility
- JS fallback runner updated with data-role selectors
Next: wire island hydration so browser re-evaluates the island body
(component bundler needs to include ~test-runner in page scripts)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- htmx-boot-subtree! wired into process-elements for auto-activation
- Fixed cond compilation bug in hx-verb-info (Clojure-style flat cond)
- Platform io-fetch upgraded: method/body/headers support, full response dict
- Replaced perform IO ops with browser primitives (set-timeout, browser-confirm, etc)
- SX→HTML rendering in hx-do-swap with OOB section filtering
- hx-collect-params: collects input name/value for all methods
- Handler naming: ex-{slug} convention, removed perform IO dependencies
- Test runner page at (test.(applications.(htmx))) with iframe-based runner
- Header "test" link on every page linking to test URL
- Page file restructure: 285 files moved to URL-matching paths (a/b/c/index.sx)
- page-functions.sx: ~100 component name references updated
- _test added to skip_dirs, test- file prefix convention for test files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Parser: skip unit suffix when next ident is a comparison keyword
(starts, ends, contains, matches, is, does, in, precedes, follows).
Fixes "123 starts with '12'" returning "123starts" instead of true.
eval-hs: use hs-compile directly instead of hs-to-sx-from-source with
"return " prefix, which was causing the parser to consume the comparison
as a string suffix.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>