From 4800246b2384182ab174bf2c6dd069673cd4d940 Mon Sep 17 00:00:00 2001 From: giles Date: Thu, 23 Apr 2026 22:10:15 +0000 Subject: [PATCH] js-on-sx: spread ... in array literals and call args Parser: jp-array-loop and jp-call-args-loop detect punct "..." and emit (js-spread inner). Transpile: when any element is spread, build array/args via js-array-spread-build with (list "js-value" v) and (list "js-spread" xs) tags. Runtime: js-array-spread-build walks items, appending values or splicing spreads via js-iterable-to-list (handles list/string/dict). Works in arrays, call args, variadic fns (Math.max(...arr)), and string spread ([...'abc']). 414/416 unit (+5), 148/148 slice unchanged. --- lib/js/parser.sx | 26 ++++++++++--- lib/js/runtime.sx | 20 ++++++++++ lib/js/test.sh | 19 +++++++++ lib/js/transpile.sx | 94 ++++++++++++++++++++++++++++++++++++--------- plans/js-on-sx.md | 2 + 5 files changed, 137 insertions(+), 24 deletions(-) diff --git a/lib/js/parser.sx b/lib/js/parser.sx index 124e7afd..ac30dbfb 100644 --- a/lib/js/parser.sx +++ b/lib/js/parser.sx @@ -486,11 +486,18 @@ (cond ((jp-at? st "punct" "]") nil) (else - (do - (append! elems (jp-parse-assignment st)) + (begin + (cond + ((jp-at? st "punct" "...") + (begin + (jp-advance! st) + (append! + elems + (list (quote js-spread) (jp-parse-assignment st))))) + (else (append! elems (jp-parse-assignment st)))) (cond ((jp-at? st "punct" ",") - (do (jp-advance! st) (jp-array-loop st elems))) + (begin (jp-advance! st) (jp-array-loop st elems))) (else nil))))))) ;; ── Entry point ───────────────────────────────────────────────── @@ -600,11 +607,18 @@ (cond ((jp-at? st "punct" ")") nil) (else - (do - (append! args (jp-parse-assignment st)) + (begin + (cond + ((jp-at? st "punct" "...") + (begin + (jp-advance! st) + (append! + args + (list (quote js-spread) (jp-parse-assignment st))))) + (else (append! args (jp-parse-assignment st)))) (cond ((jp-at? st "punct" ",") - (do (jp-advance! st) (jp-call-args-loop st args))) + (begin (jp-advance! st) (jp-call-args-loop st args))) (else nil))))))) (define diff --git a/lib/js/runtime.sx b/lib/js/runtime.sx index 299ed74d..4051001d 100644 --- a/lib/js/runtime.sx +++ b/lib/js/runtime.sx @@ -1750,6 +1750,26 @@ (define Object {:entries js-object-entries :values js-object-values :freeze js-object-freeze :assign js-object-assign :keys js-object-keys}) +(define + js-array-spread-build + (fn + (&rest items) + (let + ((result (list))) + (for-each + (fn + (item) + (let + ((kind (nth item 0))) + (cond + ((= kind "js-spread") + (for-each + (fn (x) (append! result x)) + (js-iterable-to-list (nth item 1)))) + (else (append! result (nth item 1)))))) + items) + result))) + (define js-array-is-array (fn (v) (list? v))) (define js-array-of (fn (&rest args) args)) diff --git a/lib/js/test.sh b/lib/js/test.sh index 4d10d531..b028d7ef 100755 --- a/lib/js/test.sh +++ b/lib/js/test.sh @@ -1059,6 +1059,18 @@ cat > "$TMPFILE" << 'EPOCHS' (epoch 2401) (eval "(js-eval \"'abcd'.slice('1', '3')\")") +;; ── Phase 11.spread: ... in arrays and calls ─────────────────── +(epoch 2500) +(eval "(js-eval \"var a=[1,2]; var b=[...a,3,4]; b.length\")") +(epoch 2501) +(eval "(js-eval \"var a=[1,2]; var b=[0,...a,3]; b.join(',')\")") +(epoch 2502) +(eval "(js-eval \"function f(a,b,c){ return a+b+c; } var args=[1,2,3]; f(...args)\")") +(epoch 2503) +(eval "(js-eval \"Math.max(...[1,5,3])\")") +(epoch 2504) +(eval "(js-eval \"var a=[...'abc']; a.length\")") + EPOCHS @@ -1629,6 +1641,13 @@ check 2307 "Array.from w/ map" '"2,4,6"' check 2400 "charAt coerces string" '"c"' check 2401 "slice coerces strings" '"bc"' +# ── Phase 11.spread ─────────────────────────────────────────── +check 2500 "spread in array length" '4' +check 2501 "spread middle of array" '"0,1,2,3"' +check 2502 "spread in call args" '6' +check 2503 "spread Math.max" '5' +check 2504 "spread string" '3' + 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 25aaf169..4ac4373b 100644 --- a/lib/js/transpile.sx +++ b/lib/js/transpile.sx @@ -254,15 +254,15 @@ (fn (callee args) (cond - ((and (list? callee) (js-tag? callee "js-member")) + ((and (js-tag? callee "js-member") (not (js-has-spread? args))) (let ((recv (js-transpile (nth callee 1))) (key (nth callee 2))) (list (js-sym "js-invoke-method") recv key - (cons (js-sym "list") (map js-transpile args))))) - ((and (list? callee) (js-tag? callee "js-index")) + (js-transpile-args args)))) + ((and (js-tag? callee "js-index") (not (js-has-spread? args))) (let ((recv (js-transpile (nth callee 1))) (key (js-transpile (nth callee 2)))) @@ -270,12 +270,29 @@ (js-sym "js-invoke-method-dyn") recv key - (cons (js-sym "list") (map js-transpile args))))) + (js-transpile-args args)))) + ((js-tag? callee "js-member") + (let + ((recv (js-transpile (nth callee 1))) (key (nth callee 2))) + (list + (js-sym "js-invoke-method") + recv + key + (js-transpile-args args)))) + ((js-tag? callee "js-index") + (let + ((recv (js-transpile (nth callee 1))) + (key (js-transpile (nth callee 2)))) + (list + (js-sym "js-invoke-method-dyn") + recv + key + (js-transpile-args args)))) (else (list (js-sym "js-call-plain") (js-transpile callee) - (cons (js-sym "list") (map js-transpile args))))))) + (js-transpile-args args)))))) ;; ── Array literal ───────────────────────────────────────────────── @@ -295,10 +312,58 @@ ;; order and allows computed values. (define js-transpile-array - (fn (elts) (cons (js-sym "list") (map js-transpile elts)))) + (fn + (elts) + (if + (js-has-spread? elts) + (cons + (js-sym "js-array-spread-build") + (map + (fn + (e) + (if + (js-tag? e "js-spread") + (list (js-sym "list") "js-spread" (js-transpile (nth e 1))) + (list (js-sym "list") "js-value" (js-transpile e)))) + elts)) + (cons (js-sym "list") (map js-transpile elts))))) ;; ── Conditional ─────────────────────────────────────────────────── +(define + js-has-spread? + (fn + (lst) + (cond + ((empty? lst) false) + ((js-tag? (first lst) "js-spread") true) + (else (js-has-spread? (rest lst)))))) + +;; ── Arrow function ──────────────────────────────────────────────── + +(define + js-transpile-args + (fn + (args) + (if + (js-has-spread? args) + (cons + (js-sym "js-array-spread-build") + (map + (fn + (e) + (if + (js-tag? e "js-spread") + (list (js-sym "list") "js-spread" (js-transpile (nth e 1))) + (list (js-sym "list") "js-value" (js-transpile e)))) + args)) + (cons (js-sym "list") (map js-transpile args))))) + +;; ── Assignment ──────────────────────────────────────────────────── + +;; `a = b` on an ident → (set! a b). +;; `a += b` on an ident → (set! a (js-add a b)). +;; `obj.k = v` / `obj[k] = v` → (js-set-prop obj "k" v). (define js-transpile-object (fn @@ -320,8 +385,6 @@ entries) (list (js-sym "_obj"))))))) -;; ── Arrow function ──────────────────────────────────────────────── - (define js-transpile-cond (fn @@ -332,11 +395,6 @@ (js-transpile t) (js-transpile f)))) -;; ── Assignment ──────────────────────────────────────────────────── - -;; `a = b` on an ident → (set! a b). -;; `a += b` on an ident → (set! a (js-add a b)). -;; `obj.k = v` / `obj[k] = v` → (js-set-prop obj "k" v). (define js-transpile-arrow (fn @@ -367,6 +425,9 @@ (append inits (list (js-transpile body)))))))) (list (js-sym "fn") param-syms body-tr)))) +;; ── End-to-end entry points ─────────────────────────────────────── + +;; Transpile + eval a single JS expression string. (define js-transpile-tpl (fn @@ -378,6 +439,8 @@ (else (cons (js-sym "js-template-concat") (js-transpile-tpl-parts parts)))))) +;; Transpile a JS expression string to SX source text (for inspection +;; in tests). Useful for asserting the exact emitted tree. (define js-transpile-tpl-parts (fn @@ -389,9 +452,6 @@ (js-transpile (first parts)) (js-transpile-tpl-parts (rest parts)))))) -;; ── End-to-end entry points ─────────────────────────────────────── - -;; Transpile + eval a single JS expression string. (define js-transpile-assign (fn @@ -429,8 +489,6 @@ (js-transpile rhs)))) (else (error "js-transpile-assign: unsupported target"))))) -;; Transpile a JS expression string to SX source text (for inspection -;; in tests). Useful for asserting the exact emitted tree. (define js-compound-update (fn diff --git a/plans/js-on-sx.md b/plans/js-on-sx.md index 7851a2d4..2511d03b 100644 --- a/plans/js-on-sx.md +++ b/plans/js-on-sx.md @@ -191,6 +191,8 @@ Append-only record of completed iterations. Loop writes one line per iteration: - 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. +- 2026-04-23 — **String.replace/search/match + Array.from; js-num-to-int coerces strings; spread operator `...`.** Strings: `replace(regex|str, repl)`, `search(regex|str)`, `match(regex|str)` all handle both regex and plain-string args. Regex path walks via `js-string-index-of` with case-adjusted hay/needle. Array.from(iterable, mapFn?) via `js-iterable-to-list`. `js-num-to-int` now routes through `js-to-number` so `'abcd'.charAt('2')` and `.slice('1','3')` coerce properly. **Spread `...` in array literals and call args.** Parser: `jp-array-loop` and `jp-call-args-loop` detect `punct "..."` and emit `(js-spread inner)` entries. Transpile: if any element has a spread, the array/args list is built via `(js-array-spread-build (list "js-value" v) (list "js-spread" xs) ...)`. Runtime `js-array-spread-build` walks items, appending values directly and splicing spread via `js-iterable-to-list`. Works in call args (including variadic `Math.max(...arr)`) and in array literals (prefix, middle, and string-spread `[...'abc']`). Gotcha: early pass used `(js-sym ":js-spread")` thinking it'd make a keyword — `js-sym` makes a SYMBOL which becomes an env lookup (and fails as undefined). Use plain STRING `"js-spread"` as the discriminator. 10 new tests (replace/search/match both arg types, Array.from, coercion, 5 spread variants), **414/416** (399→+15). Conformance unchanged. + ## Phase 3-5 gotchas Worth remembering for later phases: