diff --git a/lib/js/parser.sx b/lib/js/parser.sx index a48949aa..734915ef 100644 --- a/lib/js/parser.sx +++ b/lib/js/parser.sx @@ -1015,6 +1015,63 @@ ((e (jp-parse-assignment st))) (do (jp-eat-semi st) (list (quote js-throw) e)))))) +(define + jp-parse-switch-stmt + (fn + (st) + (jp-advance! st) + (jp-expect! st "punct" "(") + (let + ((disc (jp-parse-assignment st))) + (jp-expect! st "punct" ")") + (jp-expect! st "punct" "{") + (let + ((cases (list))) + (jp-parse-switch-cases st cases) + (jp-expect! st "punct" "}") + (list (quote js-switch) disc cases))))) + +(define + jp-parse-switch-cases + (fn + (st cases) + (cond + ((jp-at? st "punct" "}") nil) + ((jp-at? st "keyword" "case") + (do + (jp-advance! st) + (let + ((val (jp-parse-assignment st))) + (jp-expect! st "punct" ":") + (let + ((body (list))) + (jp-parse-switch-body st body) + (append! cases (list "case" val body)) + (jp-parse-switch-cases st cases))))) + ((jp-at? st "keyword" "default") + (do + (jp-advance! st) + (jp-expect! st "punct" ":") + (let + ((body (list))) + (jp-parse-switch-body st body) + (append! cases (list "default" nil body)) + (jp-parse-switch-cases st cases)))) + (else (error "switch: expected case or default"))))) + +(define + jp-parse-switch-body + (fn + (st body) + (cond + ((jp-at? st "punct" "}") nil) + ((jp-at? st "keyword" "case") nil) + ((jp-at? st "keyword" "default") nil) + (else + (begin + (append! body (jp-parse-stmt st)) + (jp-parse-switch-body st body)))))) + (define jp-parse-try-stmt (fn @@ -1055,6 +1112,7 @@ ((and (jp-at? st "keyword" "async") (= (get (jp-peek-at st 1) :type) "keyword") (= (get (jp-peek-at st 1) :value) "function")) (do (jp-advance! st) (jp-parse-async-function-decl st))) ((jp-at? st "keyword" "function") (jp-parse-function-decl st)) + ((jp-at? st "keyword" "switch") (jp-parse-switch-stmt st)) (else (let ((e (jp-parse-assignment st))) diff --git a/lib/js/test.sh b/lib/js/test.sh index 6b2d9b38..18836f67 100755 --- a/lib/js/test.sh +++ b/lib/js/test.sh @@ -937,6 +937,20 @@ cat > "$TMPFILE" << 'EPOCHS' (epoch 1505) (eval "(js-eval \"Array.of(1,2,3).length\")") +;; ── Phase 11.switch: switch/case/default/break ────────────────── +(epoch 1600) +(eval "(js-eval \"var r=0; switch(1){case 1: r=10; break;} r\")") +(epoch 1601) +(eval "(js-eval \"var r=0; switch(2){case 1: r=10; break; case 2: r=20; break;} r\")") +(epoch 1602) +(eval "(js-eval \"var r=0; switch(9){case 1: r=10; break; default: r=99;} r\")") +(epoch 1603) +(eval "(js-eval \"var r=0; switch(1){case 1: case 2: r=12; break;} r\")") +(epoch 1604) +(eval "(js-eval \"var r=''; switch('a'){case 'a': r='yes'; break;} r\")") +(epoch 1605) +(eval "(js-eval \"var r=0; switch(1){case 1: r=10; case 2: r=20; break;} r\")") + EPOCHS OUTPUT=$(timeout 180 "$SX_SERVER" < "$TMPFILE" 2>/dev/null) @@ -1436,6 +1450,14 @@ check 1503 "Array.isArray yes" 'true' check 1504 "Array.isArray no" 'false' check 1505 "Array.of length" '3' +# ── Phase 11.switch ───────────────────────────────────────────── +check 1600 "switch case 1 match" '10' +check 1601 "switch case 2 match" '20' +check 1602 "switch default" '99' +check 1603 "switch fallthrough stops on break" '12' +check 1604 "switch on string" '"yes"' +check 1605 "switch fallthrough chains" '20' + TOTAL=$((PASS + FAIL)) if [ $FAIL -eq 0 ]; then echo "✓ $PASS/$TOTAL JS-on-SX tests passed" diff --git a/lib/js/transpile.sx b/lib/js/transpile.sx index ae7044ed..e1d0a871 100644 --- a/lib/js/transpile.sx +++ b/lib/js/transpile.sx @@ -105,6 +105,8 @@ (js-transpile-postfix (nth ast 1) (nth ast 2))) ((js-tag? ast "js-prefix") (js-transpile-prefix (nth ast 1) (nth ast 2))) + ((js-tag? ast "js-switch") + (js-transpile-switch (nth ast 1) (nth ast 2))) ((js-tag? ast "js-new") (js-transpile-new (nth ast 1) (nth ast 2))) ((js-tag? ast "js-class") @@ -558,6 +560,89 @@ (js-sym "__js_old__")))) (else (error "js-transpile-postfix: unsupported target")))))) +(define + js-transpile-switch + (fn + (discr cases) + (let + ((discr-sym (js-sym "__discr__")) + (matched-sym (js-sym "__matched__")) + (break-sym (js-sym "__break__"))) + (list + (js-sym "call/cc") + (list + (js-sym "fn") + (list break-sym) + (list + (js-sym "let") + (list + (list discr-sym (js-transpile discr)) + (list matched-sym false)) + (js-transpile-switch-clauses cases discr-sym matched-sym))))))) + +(define + js-transpile-switch-clauses + (fn + (cases discr-sym matched-sym) + (let + ((forms (list))) + (for-each + (fn + (c) + (let + ((kind (nth c 0))) + (cond + ((= kind "case") + (let + ((val (nth c 1)) (body (nth c 2))) + (append! + forms + (list + (js-sym "when") + (list + (js-sym "or") + matched-sym + (list + (js-sym "js-strict-eq") + discr-sym + (js-transpile val))) + (js-transpile-switch-body-block matched-sym body))))) + ((= kind "default") + (let + ((body (nth c 2))) + (append! + forms + (list + (js-sym "when") + matched-sym + (js-transpile-switch-body-block matched-sym body)))))))) + cases) + (let + ((def-body nil)) + (for-each + (fn + (c) + (when (= (nth c 0) "default") (set! def-body (nth c 2)))) + cases) + (when + (not (= def-body nil)) + (append! + forms + (list + (js-sym "when") + (list (js-sym "not") matched-sym) + (js-transpile-switch-body-block matched-sym def-body))))) + (cons (js-sym "begin") forms)))) + +(define + js-transpile-switch-body-block + (fn + (matched-sym body) + (let + ((forms (list (list (js-sym "set!") matched-sym true)))) + (for-each (fn (stmt) (append! forms (js-transpile stmt))) body) + (cons (js-sym "begin") forms)))) + (define js-param-sym (fn diff --git a/plans/js-on-sx.md b/plans/js-on-sx.md index e47fc6ac..b7d5f19c 100644 --- a/plans/js-on-sx.md +++ b/plans/js-on-sx.md @@ -181,6 +181,8 @@ Append-only record of completed iterations. Loop writes one line per iteration: - 2026-04-23 — **String.prototype extensions + Object/Array builtins.** Strings: added `includes`, `startsWith`, `endsWith`, `trim`, `trimStart`, `trimEnd`, `repeat`, `padStart`, `padEnd`, `toString`, `valueOf` to `js-string-method` dispatch and corresponding `js-get-prop` string-branch keys. Helpers: `js-string-repeat` (tail-recursive concat), `js-string-pad` + `js-string-pad-build`. Object: `Object.keys / .values / .entries / .assign / .freeze` (freeze is a no-op — we don't track sealed state). Array: `Array.isArray` (backed by `list?`), `Array.of` (varargs → list). Wired into `js-global`. 17 new unit tests, **357/359** (340→+17). Conformance unchanged. Gotcha: SX's `keys` primitive returns most-recently-inserted-first, so `Object.keys({a:1, b:2})` comes back `["b", "a"]`. Test assertion has to check `.length` rather than the literal pair. If spec order matters for a real app, Object.keys would need its own ordered traversal. +- 2026-04-23 — **switch / case / default.** Parser: new `jp-parse-switch-stmt` (expect `switch (expr) { cases }`), `jp-parse-switch-cases` (walks clauses: `case val:`, `default:`), `jp-parse-switch-body` (collects stmts until next `case`/`default`/`}`). AST: `(js-switch discr (("case" val body-stmts) ("default" nil body-stmts) ...))`. Transpile: wraps body in `(call/cc (fn (__break__) (let ((__discr__ …) (__matched__ false)) …)))`. Each case clause becomes `(when (or __matched__ (js-strict-eq __discr__ val)) (set! __matched__ true) body-stmts)` — implements JS fall-through naturally (once a case matches, all following cases' `when` fires via `__matched__`). Default is a separate `(when (not __matched__) default-body)` appended at the end. `break` inside a case body already transpiles to `(__break__ nil)` and jumps out via the `call/cc`. 6 new unit tests (match, no-match default, fall-through stops on break, string discriminant, empty-body fall-through chain), **363/365** (357→+6). Conformance unchanged. + ## Phase 3-5 gotchas Worth remembering for later phases: