diff --git a/lib/js/parser.sx b/lib/js/parser.sx index 972ba0aa..bdcdd4fe 100644 --- a/lib/js/parser.sx +++ b/lib/js/parser.sx @@ -165,7 +165,7 @@ (let ((params (jp-parse-param-list st))) (let - ((body (jp-parse-block st))) + ((body (jp-parse-fn-body st))) (list (quote js-funcexpr) nm params body)))))) ((and (= (get t :type) "keyword") (= (get t :value) "true")) (do (jp-advance! st) (list (quote js-bool) true))) @@ -237,7 +237,7 @@ (let ((params (jp-parse-param-list st))) (let - ((body (jp-parse-block st))) + ((body (jp-parse-fn-body st))) (list (quote js-funcexpr-async) nm params body)))))) ((= (get t :type) "ident") (do @@ -389,7 +389,7 @@ (let ((params (jp-parse-param-list st))) (let - ((body (jp-parse-block st))) + ((body (jp-parse-fn-body st))) (list (quote js-funcexpr) nm params body)))))) ((= (get t :type) "ident") (do @@ -1153,6 +1153,18 @@ (list (quote js-if) c t (jp-parse-stmt st))) (list (quote js-if) c t nil)))))))) +(define + jp-bump! + (fn + (st key) + (dict-set! st key (+ (get st key) 1)))) + +(define + jp-decr! + (fn + (st key) + (dict-set! st key (- (get st key) 1)))) + (define jp-parse-while-stmt (fn @@ -1164,7 +1176,10 @@ ((c (jp-parse-assignment st))) (do (jp-expect! st "punct" ")") - (let ((body (jp-parse-stmt st))) (list (quote js-while) c body))))))) + (jp-bump! st :loop-depth) + (let ((body (jp-parse-stmt st))) + (jp-decr! st :loop-depth) + (list (quote js-while) c body))))))) (define jp-parse-do-while-stmt @@ -1172,8 +1187,10 @@ (st) (do (jp-advance! st) + (jp-bump! st :loop-depth) (let ((body (jp-parse-stmt st))) + (jp-decr! st :loop-depth) (do (if (jp-at? st "keyword" "while") @@ -1218,8 +1235,10 @@ (let ((iter (jp-parse-assignment st))) (jp-expect! st "punct" ")") + (jp-bump! st :loop-depth) (let ((body (jp-parse-stmt st))) + (jp-decr! st :loop-depth) (list (quote js-for-of-in) iter-kind ident iter body))))))) (else (let @@ -1230,8 +1249,10 @@ (let ((step (if (jp-at? st "punct" ")") nil (jp-parse-assignment st)))) (jp-expect! st "punct" ")") + (jp-bump! st :loop-depth) (let ((body (jp-parse-stmt st))) + (jp-decr! st :loop-depth) (list (quote js-for) init cond-ast step body))))))))))) (define @@ -1254,6 +1275,9 @@ (st) (do (jp-advance! st) + (when + (= (get st :fn-depth) 0) + (error "SyntaxError: Illegal return statement")) (if (or (jp-at? st "punct" ";") @@ -1281,7 +1305,7 @@ (let ((params (jp-parse-param-list st))) (let - ((body (jp-parse-block st))) + ((body (jp-parse-fn-body st))) (list (quote js-funcdecl) nm params body)))))))) (define @@ -1300,7 +1324,7 @@ (let ((params (jp-parse-param-list st))) (let - ((body (jp-parse-block st))) + ((body (jp-parse-fn-body st))) (list (quote js-funcdecl-async) nm params body)))))))) (define @@ -1349,7 +1373,7 @@ (let ((params (jp-parse-param-list st))) (let - ((body (jp-parse-block st))) + ((body (jp-parse-fn-body st))) (list (quote js-method) (if static? "static" "instance") @@ -1377,9 +1401,11 @@ ((disc (jp-parse-assignment st))) (jp-expect! st "punct" ")") (jp-expect! st "punct" "{") + (jp-bump! st :switch-depth) (let ((cases (list))) (jp-parse-switch-cases st cases) + (jp-decr! st :switch-depth) (jp-expect! st "punct" "}") (list (quote js-switch) disc cases))))) @@ -1460,14 +1486,26 @@ (cond ((= (get (jp-peek st) :type) "ident") (do (jp-advance! st) (jp-eat-semi st) (list (quote js-break)))) - (else (do (jp-eat-semi st) (list (quote js-break))))))) + (else + (do + (when + (and (= (get st :loop-depth) 0) (= (get st :switch-depth) 0)) + (error "SyntaxError: Illegal break statement")) + (jp-eat-semi st) + (list (quote js-break))))))) ((jp-at? st "keyword" "continue") (do (jp-advance! st) (cond ((= (get (jp-peek st) :type) "ident") (do (jp-advance! st) (jp-eat-semi st) (list (quote js-continue)))) - (else (do (jp-eat-semi st) (list (quote js-continue))))))) + (else + (do + (when + (= (get st :loop-depth) 0) + (error "SyntaxError: Illegal continue statement")) + (jp-eat-semi st) + (list (quote js-continue))))))) ((and (= (get (jp-peek st) :type) "ident") (= (get (jp-peek-at st 1) :type) "punct") @@ -1511,10 +1549,33 @@ jp-parse-arrow-body (fn (st) - (if - (jp-at? st "punct" "{") - (jp-parse-block st) - (jp-parse-assignment st)))) + (jp-bump! st :fn-depth) + (let + ((saved-loop (get st :loop-depth)) (saved-switch (get st :switch-depth))) + (dict-set! st :loop-depth 0) + (dict-set! st :switch-depth 0) + (let + ((body (if (jp-at? st "punct" "{") (jp-parse-block st) (jp-parse-assignment st)))) + (jp-decr! st :fn-depth) + (dict-set! st :loop-depth saved-loop) + (dict-set! st :switch-depth saved-switch) + body)))) + +(define + jp-parse-fn-body + (fn + (st) + (jp-bump! st :fn-depth) + (let + ((saved-loop (get st :loop-depth)) (saved-switch (get st :switch-depth))) + (dict-set! st :loop-depth 0) + (dict-set! st :switch-depth 0) + (let + ((body (jp-parse-block st))) + (jp-decr! st :fn-depth) + (dict-set! st :loop-depth saved-loop) + (dict-set! st :switch-depth saved-switch) + body)))) (define js-parse @@ -1525,7 +1586,7 @@ (= (len tokens) 0) (and (= (len tokens) 1) (= (get (nth tokens 0) :type) "eof"))) (list (quote js-program) (list)) - (let ((st {:idx 0 :tokens tokens :arrow-candidate true})) (jp-parse-program st))))) + (let ((st {:idx 0 :tokens tokens :arrow-candidate true :loop-depth 0 :switch-depth 0 :fn-depth 0})) (jp-parse-program st))))) (define js-parse-expr @@ -1538,4 +1599,4 @@ (= (len tokens) 0) (and (= (len tokens) 1) (= (get (nth tokens 0) :type) "eof"))) (list) - (let ((st {:idx 0 :tokens tokens :arrow-candidate true})) (jp-parse-assignment st)))))) + (let ((st {:idx 0 :tokens tokens :arrow-candidate true :loop-depth 0 :switch-depth 0 :fn-depth 0})) (jp-parse-assignment st)))))) diff --git a/lib/js/transpile.sx b/lib/js/transpile.sx index 17693c1a..106d109a 100644 --- a/lib/js/transpile.sx +++ b/lib/js/transpile.sx @@ -251,7 +251,8 @@ ((= op "!") (list (js-sym "js-not") a)) ((= op "~") (list (js-sym "js-bitnot") a)) ((= op "typeof") (list (js-sym "js-typeof") a)) - ((= op "void") (list (js-sym "quote") :js-undefined)) + ((= op "void") + (list (js-sym "begin") a (list (js-sym "quote") :js-undefined))) (else (error (str "js-transpile-unop: unsupported op: " op))))))))) ;; ── Array literal ───────────────────────────────────────────────── diff --git a/plans/js-on-sx.md b/plans/js-on-sx.md index c91fba08..be2ceb41 100644 --- a/plans/js-on-sx.md +++ b/plans/js-on-sx.md @@ -158,6 +158,8 @@ Each item: implement → tests → update progress. Mark `[x]` when tests green. Append-only record of completed iterations. Loop writes one line per iteration: date, what was done, test count delta. +- 2026-05-10 — **Parse-time SyntaxError for `break`/`continue` outside loops/switches and `return` outside functions; `void ` evaluates `` for side effects.** Parser tracks `:loop-depth`, `:switch-depth`, and `:fn-depth` on the state dict (initialized to 0). `jp-parse-while-stmt`, `jp-parse-do-while-stmt`, `jp-parse-for-stmt` (both for-of/in and C-for) bump `:loop-depth` around body parsing; `jp-parse-switch-stmt` bumps `:switch-depth`; new `jp-parse-fn-body` and `jp-parse-arrow-body` save+reset loop/switch depth and bump `:fn-depth` (so `break` inside an outer loop's nested function is rejected). Bare `break` requires `loop-depth > 0 OR switch-depth > 0`; bare `continue` requires `loop-depth > 0`; `return` requires `fn-depth > 0`. Separately, `void ` was compiling to just `:js-undefined` (dropping the expression entirely); now `(begin :js-undefined)` so side effects fire. Result: language/statements/return 4/15 → 14/15 (+10). statements/break 9/20 → 12/20. statements/continue 12/24 → 15/24. expressions/void 7/9 → 8/9. conformance.sh: 148/148. + - 2026-05-10 — **`Math.hypot` and `Math.cbrt` honour spec edges for NaN, ±Infinity, and ±0.** `Math.hypot(NaN, Infinity)` was returning NaN instead of +Infinity (spec: any ±Infinity arg dominates NaN). Rewrote `js-math-hypot` to scan args once tracking inf/nan flags, return +Infinity if any arg is ±Infinity, else NaN if any was NaN, else `sqrt(sum of squares)`. `Math.cbrt(NaN)` was 0 (because `pow(NaN, 1/3)` produced 0 in our path); also `Math.cbrt(-0)` returned +0 instead of -0. Added explicit short-circuits: NaN→NaN, ±Infinity→arg, ±0→arg, plus changed `(/ 1 3)` (rational) to `(/ 1.0 3.0)` (inexact) to avoid rational fractional-power oddities. Result: built-ins/Math/hypot 9/11 → 10/11. Math/cbrt 3/4 → 4/4. conformance.sh: 148/148. - 2026-05-10 — **`globalThis.globalThis === globalThis`; `Number.prototype.toFixed` honours digit-range and ≥1e21 fallback.** (1) `globalThis` was bound to `nil` in the global object literal (originally to dodge an inspect-cycle hang) — added `(dict-set! js-global "globalThis" js-global)` after the literal so `globalThis.globalThis === globalThis` per spec. (2) `Number.prototype.toFixed` rewrites: RangeError when fractionDigits is NaN or outside `[0,100]` (was silently producing garbage), and for `|x| >= 1e21` returns `js-number-to-string` (the value's own ToString) per spec step 9. conformance.sh: 148/148.