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 (cond
((jp-at? st "punct" "]") nil) ((jp-at? st "punct" "]") nil)
(else (else
(do (begin
(append! elems (jp-parse-assignment st)) (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 (cond
((jp-at? st "punct" ",") ((jp-at? st "punct" ",")
(do (jp-advance! st) (jp-array-loop st elems))) (begin (jp-advance! st) (jp-array-loop st elems)))
(else nil))))))) (else nil)))))))
;; ── Entry point ───────────────────────────────────────────────── ;; ── Entry point ─────────────────────────────────────────────────
@@ -600,11 +607,18 @@
(cond (cond
((jp-at? st "punct" ")") nil) ((jp-at? st "punct" ")") nil)
(else (else
(do (begin
(append! args (jp-parse-assignment st)) (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 (cond
((jp-at? st "punct" ",") ((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))))))) (else nil)))))))
(define (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 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-is-array (fn (v) (list? v)))
(define js-array-of (fn (&rest args) args)) (define js-array-of (fn (&rest args) args))

View File

@@ -1059,6 +1059,18 @@ cat > "$TMPFILE" << 'EPOCHS'
(epoch 2401) (epoch 2401)
(eval "(js-eval \"'abcd'.slice('1', '3')\")") (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 EPOCHS
@@ -1629,6 +1641,13 @@ check 2307 "Array.from w/ map" '"2,4,6"'
check 2400 "charAt coerces string" '"c"' check 2400 "charAt coerces string" '"c"'
check 2401 "slice coerces strings" '"bc"' 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)) TOTAL=$((PASS + FAIL))
if [ $FAIL -eq 0 ]; then if [ $FAIL -eq 0 ]; then
echo "$PASS/$TOTAL JS-on-SX tests passed" echo "$PASS/$TOTAL JS-on-SX tests passed"

View File

@@ -254,15 +254,15 @@
(fn (fn
(callee args) (callee args)
(cond (cond
((and (list? callee) (js-tag? callee "js-member")) ((and (js-tag? callee "js-member") (not (js-has-spread? args)))
(let (let
((recv (js-transpile (nth callee 1))) (key (nth callee 2))) ((recv (js-transpile (nth callee 1))) (key (nth callee 2)))
(list (list
(js-sym "js-invoke-method") (js-sym "js-invoke-method")
recv recv
key key
(cons (js-sym "list") (map js-transpile args))))) (js-transpile-args args))))
((and (list? callee) (js-tag? callee "js-index")) ((and (js-tag? callee "js-index") (not (js-has-spread? args)))
(let (let
((recv (js-transpile (nth callee 1))) ((recv (js-transpile (nth callee 1)))
(key (js-transpile (nth callee 2)))) (key (js-transpile (nth callee 2))))
@@ -270,12 +270,29 @@
(js-sym "js-invoke-method-dyn") (js-sym "js-invoke-method-dyn")
recv recv
key 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 (else
(list (list
(js-sym "js-call-plain") (js-sym "js-call-plain")
(js-transpile callee) (js-transpile callee)
(cons (js-sym "list") (map js-transpile args))))))) (js-transpile-args args))))))
;; ── Array literal ───────────────────────────────────────────────── ;; ── Array literal ─────────────────────────────────────────────────
@@ -295,10 +312,58 @@
;; order and allows computed values. ;; order and allows computed values.
(define (define
js-transpile-array 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 ─────────────────────────────────────────────────── ;; ── 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 (define
js-transpile-object js-transpile-object
(fn (fn
@@ -320,8 +385,6 @@
entries) entries)
(list (js-sym "_obj"))))))) (list (js-sym "_obj")))))))
;; ── Arrow function ────────────────────────────────────────────────
(define (define
js-transpile-cond js-transpile-cond
(fn (fn
@@ -332,11 +395,6 @@
(js-transpile t) (js-transpile t)
(js-transpile f)))) (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 (define
js-transpile-arrow js-transpile-arrow
(fn (fn
@@ -367,6 +425,9 @@
(append inits (list (js-transpile body)))))))) (append inits (list (js-transpile body))))))))
(list (js-sym "fn") param-syms body-tr)))) (list (js-sym "fn") param-syms body-tr))))
;; ── End-to-end entry points ───────────────────────────────────────
;; Transpile + eval a single JS expression string.
(define (define
js-transpile-tpl js-transpile-tpl
(fn (fn
@@ -378,6 +439,8 @@
(else (else
(cons (js-sym "js-template-concat") (js-transpile-tpl-parts parts)))))) (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 (define
js-transpile-tpl-parts js-transpile-tpl-parts
(fn (fn
@@ -389,9 +452,6 @@
(js-transpile (first parts)) (js-transpile (first parts))
(js-transpile-tpl-parts (rest parts)))))) (js-transpile-tpl-parts (rest parts))))))
;; ── End-to-end entry points ───────────────────────────────────────
;; Transpile + eval a single JS expression string.
(define (define
js-transpile-assign js-transpile-assign
(fn (fn
@@ -429,8 +489,6 @@
(js-transpile rhs)))) (js-transpile rhs))))
(else (error "js-transpile-assign: unsupported target"))))) (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 (define
js-compound-update js-compound-update
(fn (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 — **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 ## Phase 3-5 gotchas
Worth remembering for later phases: Worth remembering for later phases: