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.
This commit is contained in:
2026-04-23 22:10:15 +00:00
parent b502b8f58e
commit 4800246b23
5 changed files with 137 additions and 24 deletions

View File

@@ -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

View File

@@ -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))

View File

@@ -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"

View File

@@ -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

View File

@@ -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__ <normalized-source>)) (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: