diff --git a/lib/compiler.sx b/lib/compiler.sx index f7abdd4d..a8aa666d 100644 --- a/lib/compiler.sx +++ b/lib/compiler.sx @@ -599,11 +599,12 @@ (binding) (let ((name (if (= (type-of (first binding)) "symbol") (symbol-name (first binding)) (first binding))) - (value (nth binding 1)) - (slot (scope-define-local let-scope name))) + (value (nth binding 1))) (compile-expr em value let-scope false) - (emit-op em 17) - (emit-byte em slot))) + (let + ((slot (scope-define-local let-scope name))) + (emit-op em 17) + (emit-byte em slot)))) bindings) (compile-begin em body let-scope tail?))))) (define diff --git a/lib/hyperscript/test-closure.sx b/lib/hyperscript/test-closure.sx new file mode 100644 index 00000000..d63bcdc7 --- /dev/null +++ b/lib/hyperscript/test-closure.sx @@ -0,0 +1,13 @@ +;; Minimal test: define-inside-let pattern (like hs-parse) +(define + test-closure-parse + (fn + (tokens) + (let + ((p 0) (tok-len (len tokens))) + (define get-val (fn () (get (nth tokens p) "value"))) + (define advance! (fn () (set! p (+ p 1)))) + (let + ((first-val (get-val))) + (advance!) + (list "first:" first-val "second:" (get-val) "p:" p))))) \ No newline at end of file diff --git a/lib/hyperscript/test.sh b/lib/hyperscript/test.sh new file mode 100644 index 00000000..00c74b3c --- /dev/null +++ b/lib/hyperscript/test.sh @@ -0,0 +1,156 @@ +#!/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="" + 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 ]