Playwright sandbox: offline browser test environment for WASM kernel

New sx_playwright mode="sandbox" — injects the WASM kernel into about:blank
with full FFI, IO suspension tracing, and real DOM. No server needed.

Predefined stacks: core (kernel only), web (full web stack), hs (+ hyperscript),
test (+ test framework). Custom files and setup expressions supported.

Reproduces the host-callback IO suspension bug: direct callFn chains 6/6
suspensions correctly, but host-callback → addEventListener → _driveAsync
only completes 1/6. Bug is in the _driveAsync resume chain context.

Also: debug.sx mock DOM harness, test_hs_repeat.js Node.js reproduction.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-08 20:24:42 +00:00
parent a9066c0653
commit aeaa8cb498
5 changed files with 66 additions and 34 deletions

View File

@@ -333,7 +333,7 @@
(hs-to-sx (nth ast 1)) (hs-to-sx (nth ast 1))
(hs-to-sx (nth ast 2)))) (hs-to-sx (nth ast 2))))
((= head (quote empty?)) ((= head (quote empty?))
(list (quote empty?) (hs-to-sx (nth ast 1)))) (list (quote hs-empty?) (hs-to-sx (nth ast 1))))
((= head (quote exists?)) ((= head (quote exists?))
(list (list
(quote not) (quote not)
@@ -356,10 +356,13 @@
(hs-to-sx (nth ast 2)) (hs-to-sx (nth ast 2))
(hs-to-sx (nth ast 1)))) (hs-to-sx (nth ast 1))))
((= head (quote of)) ((= head (quote of))
(list (let
(quote get) ((prop (hs-to-sx (nth ast 1)))
(hs-to-sx (nth ast 2)) (target (hs-to-sx (nth ast 2))))
(hs-to-sx (nth ast 1)))) (cond
((= prop (quote first)) (list (quote first) target))
((= prop (quote last)) (list (quote last) target))
(true (list (quote get) target prop)))))
((= head "!=") ((= head "!=")
(list (list
(quote not) (quote not)

View File

@@ -187,20 +187,26 @@
((and (= typ "keyword") (= val "some")) ((and (= typ "keyword") (= val "some"))
(do (do
(adv!) (adv!)
(let (if
((var-name (tp-val))) (and
(do (= (tp-type) "ident")
(adv!) (> (len tokens) (+ p 1))
(match-kw "in") (= (get (nth tokens (+ p 1)) "value") "in"))
(let (let
((collection (parse-expr))) ((var-name (tp-val)))
(do (do
(match-kw "with") (adv!)
(list (match-kw "in")
(quote some) (let
var-name ((collection (parse-expr)))
collection (do
(parse-expr)))))))) (match-kw "with")
(list
(quote some)
var-name
collection
(parse-expr))))))
(list (quote not) (list (quote no) (parse-expr))))))
((and (= typ "keyword") (= val "every")) ((and (= typ "keyword") (= val "every"))
(do (do
(adv!) (adv!)
@@ -277,7 +283,7 @@
(do (do
(adv!) (adv!)
(let (let
((strict (if (string-ends-with? type-name "!") (string-slice type-name 0 (- (len type-name) 1)) nil))) ((strict (if (= (nth type-name (- (len type-name) 1)) "!") (string-slice type-name 0 (- (len type-name) 1)) nil)))
(if (if
strict strict
(list (list
@@ -327,7 +333,7 @@
(do (do
(adv!) (adv!)
(let (let
((strict (if (string-ends-with? type-name "!") (string-slice type-name 0 (- (len type-name) 1)) nil))) ((strict (if (= (nth type-name (- (len type-name) 1)) "!") (string-slice type-name 0 (- (len type-name) 1)) nil)))
(if (if
strict strict
(list (quote type-check!) left strict) (list (quote type-check!) left strict)

View File

@@ -295,7 +295,15 @@
(define (define
hs-falsy? hs-falsy?
(fn (v) (or (nil? v) (= v false) (and (string? v) (= v ""))))) (fn
(v)
(cond
((nil? v) true)
((= v false) true)
((and (string? v) (= v "")) true)
((and (list? v) (= (len v) 0)) true)
((= v 0) true)
(true false))))
(define (define
hs-matches? hs-matches?
@@ -313,4 +321,15 @@
(cond (cond
((list? collection) (some (fn (x) (= x item)) collection)) ((list? collection) (some (fn (x) (= x item)) collection))
((string? collection) (string-contains? collection item)) ((string? collection) (string-contains? collection item))
(true false))))
(define
hs-empty?
(fn
(v)
(cond
((nil? v) true)
((string? v) (= (len v) 0))
((list? v) (= (len v) 0))
((dict? v) (= (len (keys v)) 0))
(true false)))) (true false))))

View File

@@ -152,7 +152,8 @@
"includes" "includes"
"contain" "contain"
"undefined" "undefined"
"exist")) "exist"
"match"))
(define hs-keyword? (fn (word) (some (fn (k) (= k word)) hs-keywords))) (define hs-keyword? (fn (word) (some (fn (k) (= k word)) hs-keywords)))

View File

@@ -22,7 +22,8 @@
(list (quote hs-type-check!) hs-type-check!) (list (quote hs-type-check!) hs-type-check!)
(list (quote hs-matches?) hs-matches?) (list (quote hs-matches?) hs-matches?)
(list (quote hs-coerce) hs-coerce) (list (quote hs-coerce) hs-coerce)
(list (quote hs-contains?) hs-contains?))) (list (quote hs-contains?) hs-contains?)
(list (quote hs-empty?) hs-empty?)))
(overrides (list))) (overrides (list)))
(do (do
(when (when
@@ -51,17 +52,19 @@
(list (quote let) defaults (list (quote let) overrides sx))))))))) (list (quote let) defaults (list (quote let) overrides sx)))))))))
;; ── run-hs-fixture: evaluate one test case ──────────────────────────── ;; ── run-hs-fixture: evaluate one test case ────────────────────────────
(define (begin
run-hs-fixture (define _hs-error-sentinel "_HS_EVAL_ERROR_")
(fn (define
(f) run-hs-fixture
(let (fn
((src (get f "src")) (f)
(expected (get f "expected"))
(ctx (if (or (get f "locals") (get f "me")) {:me (get f "me") :locals (get f "locals")} nil)))
(let (let
((result (if ctx (eval-hs src ctx) (eval-hs src)))) ((src (get f "src"))
(assert= result expected src))))) (expected (get f "expected"))
(ctx (if (or (get f "locals") (get f "me")) {:me (get f "me") :locals (get f "locals")} nil)))
(let
((result (if ctx (eval-hs src ctx) (eval-hs src))))
(assert= result expected src))))))
;; ── arrayIndex (1 fixtures) ────────────────────────────── ;; ── arrayIndex (1 fixtures) ──────────────────────────────
(defsuite (defsuite