compile-let called scope-define-local eagerly as part of the let binding, adding the new local to the scope before compile-expr ran for the init expression. When nested lets rebound the same variable (e.g. the hyperscript parser's 4 chained `parts` bindings), the init expression resolved the name to the new uninitialized slot instead of the outer one — producing nil where it should have read the previous value. Move scope-define-local after compile-expr so init expressions see the outer scope's binding. Fixes all 11 JIT hyperscript parser failures. 3127/3127 JIT + non-JIT, 25/25 standalone hyperscript tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
157 lines
5.5 KiB
Bash
157 lines
5.5 KiB
Bash
#!/usr/bin/env bash
|
|
# Fast hyperscript test runner — pipes directly to sx_server.exe via epoch protocol.
|
|
# No MCP, no Docker, no web server. Runs in <2 seconds.
|
|
#
|
|
# Usage:
|
|
# bash lib/hyperscript/test.sh # run all tests
|
|
# bash lib/hyperscript/test.sh -v # verbose — show actual output
|
|
|
|
set -euo pipefail
|
|
cd "$(git rev-parse --show-toplevel)"
|
|
|
|
SX_SERVER="hosts/ocaml/_build/default/bin/sx_server.exe"
|
|
if [ ! -x "$SX_SERVER" ]; then
|
|
echo "ERROR: $SX_SERVER not found. Run: cd hosts/ocaml && dune build"
|
|
exit 1
|
|
fi
|
|
|
|
VERBOSE="${1:-}"
|
|
PASS=0
|
|
FAIL=0
|
|
ERRORS=""
|
|
TMPFILE=$(mktemp)
|
|
trap "rm -f $TMPFILE" EXIT
|
|
|
|
# ── Write epoch commands to temp file ─────────────────────────────
|
|
cat > "$TMPFILE" << 'EPOCHS'
|
|
(epoch 1)
|
|
(load "lib/r7rs.sx")
|
|
(epoch 2)
|
|
(load "lib/hyperscript/tokenizer.sx")
|
|
(epoch 3)
|
|
(load "lib/hyperscript/parser.sx")
|
|
(epoch 4)
|
|
(load "lib/hyperscript/compiler.sx")
|
|
(epoch 10)
|
|
(eval "(hs-compile \"on click add .red to me\")")
|
|
(epoch 11)
|
|
(eval "(hs-compile \"on click toggle .active on me\")")
|
|
(epoch 12)
|
|
(eval "(hs-compile \"on click add .red to me then remove .blue from me\")")
|
|
(epoch 13)
|
|
(eval "(hs-compile \"on click set my innerHTML to 'hello'\")")
|
|
(epoch 14)
|
|
(eval "(hs-compile \"on click log me\")")
|
|
(epoch 15)
|
|
(eval "(hs-compile \"add .highlight to me\")")
|
|
(epoch 16)
|
|
(eval "(hs-compile \"remove .highlight from me\")")
|
|
(epoch 17)
|
|
(eval "(hs-compile \"toggle .visible on me\")")
|
|
(epoch 18)
|
|
(eval "(hs-compile \"hide me\")")
|
|
(epoch 19)
|
|
(eval "(hs-compile \"show me\")")
|
|
(epoch 20)
|
|
(eval "(hs-compile \"wait 500ms\")")
|
|
(epoch 21)
|
|
(eval "(hs-compile \"wait 2s\")")
|
|
(epoch 22)
|
|
(eval "(hs-compile \"set x to 1 + 2\")")
|
|
(epoch 23)
|
|
(eval "(hs-compile \"set x to 3 * 4\")")
|
|
(epoch 24)
|
|
(eval "(hs-compile \"init add .loaded to me end\")")
|
|
(epoch 25)
|
|
(eval "(hs-compile \"set x to 42\")")
|
|
(epoch 26)
|
|
(eval "(hs-compile \"put 'hello' into me\")")
|
|
(epoch 27)
|
|
(eval "(hs-compile \"increment x\")")
|
|
(epoch 28)
|
|
(eval "(hs-compile \"decrement x\")")
|
|
(epoch 29)
|
|
(eval "(hs-compile \"on every click log me end\")")
|
|
(epoch 30)
|
|
(eval "(hs-compile \"on click from .btn log me end\")")
|
|
(epoch 40)
|
|
(eval "(hs-to-sx-from-source \"on click add .red to me\")")
|
|
(epoch 41)
|
|
(eval "(hs-to-sx-from-source \"on click toggle .active on me\")")
|
|
(epoch 42)
|
|
(eval "(hs-to-sx-from-source \"on click set my innerHTML to 'hello'\")")
|
|
(epoch 43)
|
|
(eval "(hs-to-sx-from-source \"on click add .red to me then remove .blue from me\")")
|
|
EPOCHS
|
|
|
|
# ── Run ───────────────────────────────────────────────────────────
|
|
OUTPUT=$(timeout 30 "$SX_SERVER" < "$TMPFILE" 2>/dev/null)
|
|
|
|
# ── Check function ────────────────────────────────────────────────
|
|
check() {
|
|
local epoch="$1" desc="$2" expected="$3"
|
|
local actual
|
|
actual=$(echo "$OUTPUT" | grep -A1 "^(ok-len $epoch " | tail -1)
|
|
if [ -z "$actual" ]; then
|
|
actual=$(echo "$OUTPUT" | grep "^(ok $epoch " || true)
|
|
fi
|
|
if [ -z "$actual" ]; then
|
|
actual=$(echo "$OUTPUT" | grep "^(error $epoch " || true)
|
|
fi
|
|
if [ -z "$actual" ]; then
|
|
actual="<no output for epoch $epoch>"
|
|
fi
|
|
|
|
if echo "$actual" | grep -qF "$expected"; then
|
|
PASS=$((PASS + 1))
|
|
[ "$VERBOSE" = "-v" ] && echo " ✓ $desc"
|
|
else
|
|
FAIL=$((FAIL + 1))
|
|
ERRORS+=" ✗ $desc (epoch $epoch)
|
|
expected: $expected
|
|
actual: $actual
|
|
"
|
|
fi
|
|
}
|
|
|
|
# ── Parser assertions ─────────────────────────────────────────────
|
|
check 10 "on click basic" '(on "click" (add-class "red" (me)))'
|
|
check 11 "on click toggle" '(on "click" (toggle-class "active" (me)))'
|
|
check 12 "on click chain" '(on "click" (do (add-class "red" (me)) (remove-class "blue" (me))))'
|
|
check 13 "on click set prop" '(on "click" (set!'
|
|
check 14 "on click log" '(on "click" (log (me)))'
|
|
check 15 "add class cmd" '(add-class "highlight" (me))'
|
|
check 16 "remove class cmd" '(remove-class "highlight" (me))'
|
|
check 17 "toggle class cmd" '(toggle-class "visible" (me))'
|
|
check 18 "hide cmd" '(hide (me))'
|
|
check 19 "show cmd" '(show (me))'
|
|
check 20 "wait ms" '(wait 500)'
|
|
check 21 "wait seconds" '(wait 2000)'
|
|
check 22 "arithmetic add" '+'
|
|
check 23 "arithmetic mul" '*'
|
|
check 24 "init feature" '(init'
|
|
check 25 "set variable" '(set! (ref "x") 42)'
|
|
check 26 "put into" '(set! (me) "hello")'
|
|
check 27 "increment" 'increment'
|
|
check 28 "decrement" 'decrement'
|
|
check 29 "on every click" '(on'
|
|
check 30 "on click from" '(on'
|
|
|
|
# ── Compiler assertions ───────────────────────────────────────────
|
|
check 40 "compiled: on click" '(hs-on me "click"'
|
|
check 41 "compiled: toggle" 'hs-toggle-class!'
|
|
check 42 "compiled: set prop" 'dom-set-prop'
|
|
check 43 "compiled: chain" 'dom-remove-class'
|
|
|
|
# ── Report ────────────────────────────────────────────────────────
|
|
TOTAL=$((PASS + FAIL))
|
|
if [ $FAIL -eq 0 ]; then
|
|
echo "✓ $PASS/$TOTAL hyperscript tests passed"
|
|
else
|
|
echo "✗ $PASS/$TOTAL passed, $FAIL failed:"
|
|
echo ""
|
|
echo "$ERRORS"
|
|
fi
|
|
|
|
[ $FAIL -eq 0 ]
|