diff --git a/lib/js/parser.sx b/lib/js/parser.sx index 734915ef..124e7afd 100644 --- a/lib/js/parser.sx +++ b/lib/js/parser.sx @@ -866,22 +866,48 @@ jp-parse-for-stmt (fn (st) - (do - (jp-advance! st) - (jp-expect! st "punct" "(") + (jp-advance! st) + (jp-expect! st "punct" "(") + (let + ((has-decl false) (decl-kind nil)) + (cond + ((jp-at? st "keyword" "var") + (begin (set! has-decl true) (set! decl-kind "var"))) + ((jp-at? st "keyword" "let") + (begin (set! has-decl true) (set! decl-kind "let"))) + ((jp-at? st "keyword" "const") + (begin (set! has-decl true) (set! decl-kind "const"))) + (else nil)) (let - ((init (jp-parse-for-init st))) - (let - ((cond-ast (if (jp-at? st "punct" ";") nil (jp-parse-assignment st)))) - (do - (jp-expect! st "punct" ";") - (let - ((step (if (jp-at? st "punct" ")") nil (jp-parse-assignment st)))) - (do - (jp-expect! st "punct" ")") + ((ident-off (if has-decl 1 0))) + (cond + ((and (= (get (jp-peek-at st ident-off) :type) "ident") (or (and (= (get (jp-peek-at st (+ ident-off 1)) :type) "keyword") (= (get (jp-peek-at st (+ ident-off 1)) :value) "of")) (and (= (get (jp-peek-at st (+ ident-off 1)) :type) "keyword") (= (get (jp-peek-at st (+ ident-off 1)) :value) "in")))) + (begin + (when has-decl (jp-advance! st)) + (let + ((ident (get (jp-peek st) :value))) + (jp-advance! st) (let - ((body (jp-parse-stmt st))) - (list (quote js-for) init cond-ast step body)))))))))) + ((iter-kind (get (jp-peek st) :value))) + (jp-advance! st) + (let + ((iter (jp-parse-assignment st))) + (jp-expect! st "punct" ")") + (let + ((body (jp-parse-stmt st))) + (list (quote js-for-of-in) iter-kind ident iter body))))))) + (else + (let + ((init (cond (has-decl (jp-parse-var-stmt st decl-kind)) ((jp-at? st "punct" ";") (begin (jp-advance! st) nil)) (else (let ((e (jp-parse-assignment st))) (jp-expect! st "punct" ";") (list (quote js-exprstmt) e)))))) + (let + ((cond-ast (if (jp-at? st "punct" ";") nil (jp-parse-assignment st)))) + (jp-expect! st "punct" ";") + (let + ((step (if (jp-at? st "punct" ")") nil (jp-parse-assignment st)))) + (jp-expect! st "punct" ")") + (let + ((body (jp-parse-stmt st))) + (list (quote js-for) init cond-ast step body))))))))))) (define jp-parse-for-init diff --git a/lib/js/runtime.sx b/lib/js/runtime.sx index 2b50b02a..a78c37b4 100644 --- a/lib/js/runtime.sx +++ b/lib/js/runtime.sx @@ -1585,6 +1585,29 @@ (dict-set! p "value" reason) (js-promise-flush-callbacks! p)))))) +(define + js-iterable-to-list + (fn + (v) + (cond + ((list? v) v) + ((= (type-of v) "string") (js-string-to-list v 0 (list))) + ((dict? v) + (let + ((result (list))) + (for-each (fn (k) (append! result (get v k))) (keys v)) + result)) + (else (list))))) + +(define + js-string-to-list + (fn + (s i acc) + (if + (>= i (len s)) + acc + (begin (append! acc (char-at s i)) (js-string-to-list s (+ i 1) acc))))) + (define js-object-keys (fn diff --git a/lib/js/test.sh b/lib/js/test.sh index 8202e458..a51a3196 100755 --- a/lib/js/test.sh +++ b/lib/js/test.sh @@ -1027,6 +1027,14 @@ cat > "$TMPFILE" << 'EPOCHS' (epoch 2104) (eval "(js-eval \"[1,2,1,2].indexOf(2, 2)\")") +;; ── Phase 11.forofin: for..of / for..in ───────────────────────── +(epoch 2200) +(eval "(js-eval \"var s=0; for (var x of [1,2,3]) s=s+x; s\")") +(epoch 2201) +(eval "(js-eval \"var r=''; for (var k in {a:1, b:2}) r=r+k; r.length\")") +(epoch 2202) +(eval "(js-eval \"var s=''; for (var c of 'abc') s=s+c; s\")") + EPOCHS @@ -1578,6 +1586,11 @@ check 2102 "fill(0)" '"0,0,0"' check 2103 "fill(0,1,3)" '"1,0,0,4"' check 2104 "indexOf with start" '3' +# ── Phase 11.forofin ─────────────────────────────────────────── +check 2200 "for-of array" '6' +check 2201 "for-in object keys count" '2' +check 2202 "for-of string" '"abc"' + 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 e1d0a871..25aaf169 100644 --- a/lib/js/transpile.sx +++ b/lib/js/transpile.sx @@ -92,6 +92,12 @@ (nth ast 2) (nth ast 3) (nth ast 4))) + ((js-tag? ast "js-for-of-in") + (js-transpile-for-of-in + (nth ast 1) + (nth ast 2) + (nth ast 3) + (nth ast 4))) ((js-tag? ast "js-return") (js-transpile-return (nth ast 1))) ((js-tag? ast "js-break") (js-transpile-break)) ((js-tag? ast "js-continue") (js-transpile-continue)) @@ -643,6 +649,42 @@ (for-each (fn (stmt) (append! forms (js-transpile stmt))) body) (cons (js-sym "begin") forms)))) +(define + js-transpile-for-of-in + (fn + (iter-kind ident iter-ast body-ast) + (let + ((ident-sym (js-sym ident)) + (iter-sx (js-transpile iter-ast)) + (body-sx (js-transpile body-ast)) + (items-sym (js-sym "__js_items__"))) + (list + (js-sym "call/cc") + (list + (js-sym "fn") + (list (js-sym "__break__")) + (list + (js-sym "let") + (list + (list + items-sym + (if + (= iter-kind "of") + (list (js-sym "js-iterable-to-list") iter-sx) + (list (js-sym "js-object-keys") iter-sx)))) + (list + (js-sym "for-each") + (list + (js-sym "fn") + (list ident-sym) + (list + (js-sym "call/cc") + (list + (js-sym "fn") + (list (js-sym "__continue__")) + body-sx))) + items-sym))))))) + (define js-param-sym (fn diff --git a/plans/js-on-sx.md b/plans/js-on-sx.md index d576ac8b..7851a2d4 100644 --- a/plans/js-on-sx.md +++ b/plans/js-on-sx.md @@ -189,6 +189,8 @@ Append-only record of completed iterations. Loop writes one line per iteration: - 2026-04-23 — **JSON.stringify + JSON.parse.** Shipped a recursive-descent parser and serializer in SX. `js-json-stringify` dispatches on `type-of` for primitives, lists, dicts. `js-json-parse` uses a state dict `{:s src :i idx}` mutated in-place by helpers (`js-json-skip-ws!`, `js-json-parse-value`, `-string`, `-number`, `-array`, `-object`). String parser handles `\n \t \r \" \\ \/` escapes. Number parser collects digits/signs/e+E/. then delegates to `js-to-number`. Array and object loops recursively call parse-value. JSON wired into `js-global`. 10 new tests (stringify primitives/arrays/objects, parse primitives/string/array/object), **391/393** (381→+10). Conformance unchanged. +- 2026-04-23 — **Array.prototype flat/fill; indexOf start arg; for..of/for..in.** Array: `flat(depth=1)` uses `js-list-flat-loop` (recursive flatten), `fill(value, start?, end?)` mutates in-place then returns self via `js-list-fill-loop`. Fixed `indexOf` to honor the `fromIndex` second argument. Parser: `jp-parse-for-stmt` now does a 2-token lookahead — if it sees `(var? ident (of|in) expr)` it emits `(js-for-of-in kind ident iter body)`, else falls back to classic `for(;;)`. Transpile: `js-transpile-for-of-in` wraps body in `(call/cc (fn (__break__) (let ((__js_items__ )) (for-each (fn (ident) (call/cc (fn (__continue__) body))) items))))`. For `of` it normalizes via `js-iterable-to-list` (list → self, string → char list, dict → values). For `in` it iterates over `js-object-keys`. `break` / `continue` already route to the call/cc bindings. 8 new tests (flat, fill variants, indexOf with start, for-of array/string, for-in dict), **399/401** (391→+8). Conformance unchanged. Gotcha: **SX `cond` clauses evaluate only the last expr of a body. `(cond ((test) (set! a 1) (set! b 2)) …)` silently drops the first set!.** Must wrap multi-stmt clause bodies in `(begin …)`. First pass on the for-stmt rewrite had multi-expr cond clauses that silently did nothing — broke all existing for-loop tests, not just the new ones. + ## Phase 3-5 gotchas Worth remembering for later phases: