From 15c310cdc16e0e95782b9f18d74107e39a18c127 Mon Sep 17 00:00:00 2001 From: giles Date: Thu, 23 Apr 2026 22:32:24 +0000 Subject: [PATCH] js-on-sx: object + array destructuring Parser: jp-parse-vardecl handles {a, b} obj pattern and [a, , c] arr pattern (with hole support) in addition to plain idents. Emits (js-vardecl-obj names rhs) and (js-vardecl-arr names rhs). Transpile: js-vardecl-forms dispatches on tag. Destructures emit (define __destruct__ rhs) then (define name (js-get-prop __destruct__ key-or-index)) for each pattern element. Array holes (nil) are skipped. 418/420 unit (+4), 148/148 slice unchanged. --- lib/js/parser.sx | 108 ++++++++++++++++++++++++++++++++++++++------ lib/js/test.sh | 16 +++++++ lib/js/transpile.sx | 68 +++++++++++++++++++++++++--- plans/js-on-sx.md | 2 + 4 files changed, 175 insertions(+), 19 deletions(-) diff --git a/lib/js/parser.sx b/lib/js/parser.sx index ac30dbfb..cee89e46 100644 --- a/lib/js/parser.sx +++ b/lib/js/parser.sx @@ -781,20 +781,102 @@ jp-parse-vardecl (fn (st) - (let - ((nm (get (jp-peek st) :value))) - (do - (if - (= (get (jp-peek st) :type) "ident") - (jp-advance! st) - (error - (str "Expected ident in var decl, got " (get (jp-peek st) :type)))) - (if - (jp-at? st "op" "=") - (do + (cond + ((jp-at? st "punct" "{") + (let + ((pattern (jp-parse-obj-pattern st))) + (jp-expect! st "op" "=") + (list (quote js-vardecl-obj) pattern (jp-parse-assignment st)))) + ((jp-at? st "punct" "[") + (let + ((pattern (jp-parse-arr-pattern st))) + (jp-expect! st "op" "=") + (list (quote js-vardecl-arr) pattern (jp-parse-assignment st)))) + (else + (let + ((nm (get (jp-peek st) :value))) + (if + (= (get (jp-peek st) :type) "ident") (jp-advance! st) - (list (quote js-vardecl) nm (jp-parse-assignment st))) - (list (quote js-vardecl) nm (list (quote js-undef)))))))) + (error + (str + "Expected ident in var decl, got " + (get (jp-peek st) :type)))) + (if + (jp-at? st "op" "=") + (begin + (jp-advance! st) + (list (quote js-vardecl) nm (jp-parse-assignment st))) + (list (quote js-vardecl) nm (list (quote js-undef))))))))) + +(define + jp-parse-obj-pattern + (fn + (st) + (jp-advance! st) + (let + ((names (list))) + (jp-parse-obj-pattern-loop st names) + (jp-expect! st "punct" "}") + names))) + +(define + jp-parse-obj-pattern-loop + (fn + (st names) + (cond + ((jp-at? st "punct" "}") nil) + (else + (begin + (let + ((nm (get (jp-peek st) :value))) + (if + (or + (= (get (jp-peek st) :type) "ident") + (= (get (jp-peek st) :type) "string")) + (jp-advance! st) + (error "expected ident in obj pattern")) + (append! names nm)) + (cond + ((jp-at? st "punct" ",") + (begin (jp-advance! st) (jp-parse-obj-pattern-loop st names))) + (else nil))))))) + +(define + jp-parse-arr-pattern + (fn + (st) + (jp-advance! st) + (let + ((names (list))) + (jp-parse-arr-pattern-loop st names) + (jp-expect! st "punct" "]") + names))) + +(define + jp-parse-arr-pattern-loop + (fn + (st names) + (cond + ((jp-at? st "punct" "]") nil) + ((jp-at? st "punct" ",") + (begin + (append! names nil) + (jp-advance! st) + (jp-parse-arr-pattern-loop st names))) + (else + (begin + (let + ((nm (get (jp-peek st) :value))) + (if + (= (get (jp-peek st) :type) "ident") + (jp-advance! st) + (error "expected ident in arr pattern")) + (append! names nm)) + (cond + ((jp-at? st "punct" ",") + (begin (jp-advance! st) (jp-parse-arr-pattern-loop st names))) + (else nil))))))) (define jp-parse-var-stmt diff --git a/lib/js/test.sh b/lib/js/test.sh index b028d7ef..7163b7bd 100755 --- a/lib/js/test.sh +++ b/lib/js/test.sh @@ -1071,6 +1071,16 @@ cat > "$TMPFILE" << 'EPOCHS' (epoch 2504) (eval "(js-eval \"var a=[...'abc']; a.length\")") +;; ── Phase 11.destruct: object and array destructuring ────────── +(epoch 2600) +(eval "(js-eval \"var {aa, bb} = {aa:1, bb:2}; aa+bb\")") +(epoch 2601) +(eval "(js-eval \"var [ax, bx, cx] = [1, 2, 3]; ax+bx+cx\")") +(epoch 2602) +(eval "(js-eval \"var [fst, , trd] = [1, 2, 3]; fst+trd\")") +(epoch 2603) +(eval "(js-eval \"var pt = {px: 100}; var {px} = pt; px + 1\")") + EPOCHS @@ -1648,6 +1658,12 @@ check 2502 "spread in call args" '6' check 2503 "spread Math.max" '5' check 2504 "spread string" '3' +# ── Phase 11.destruct ───────────────────────────────────────── +check 2600 "obj destructure" '3' +check 2601 "arr destructure" '6' +check 2602 "arr destructure skip" '4' +check 2603 "obj partial+add" '101' + 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 4ac4373b..25e40650 100644 --- a/lib/js/transpile.sx +++ b/lib/js/transpile.sx @@ -845,12 +845,68 @@ (else (let ((d (first decls))) - (cons - (list - (js-sym "define") - (js-sym (nth d 1)) - (js-transpile (nth d 2))) - (js-vardecl-forms (rest decls)))))))) + (cond + ((js-tag? d "js-vardecl") + (cons + (list + (js-sym "define") + (js-sym (nth d 1)) + (js-transpile (nth d 2))) + (js-vardecl-forms (rest decls)))) + ((js-tag? d "js-vardecl-obj") + (let + ((names (nth d 1)) + (rhs (js-transpile (nth d 2))) + (tmp-sym (js-sym "__destruct__"))) + (cons + (list (js-sym "define") tmp-sym rhs) + (js-vardecl-obj-forms + names + tmp-sym + (js-vardecl-forms (rest decls)))))) + ((js-tag? d "js-vardecl-arr") + (let + ((names (nth d 1)) + (rhs (js-transpile (nth d 2))) + (tmp-sym (js-sym "__destruct__"))) + (cons + (list (js-sym "define") tmp-sym rhs) + (js-vardecl-arr-forms + names + tmp-sym + 0 + (js-vardecl-forms (rest decls)))))) + (else (error "js-vardecl-forms: unexpected decl")))))))) + +(define + js-vardecl-obj-forms + (fn + (names tmp-sym tail) + (cond + ((empty? names) tail) + (else + (cons + (list + (js-sym "define") + (js-sym (first names)) + (list (js-sym "js-get-prop") tmp-sym (first names))) + (js-vardecl-obj-forms (rest names) tmp-sym tail)))))) + +(define + js-vardecl-arr-forms + (fn + (names tmp-sym i tail) + (cond + ((empty? names) tail) + ((= (first names) nil) + (js-vardecl-arr-forms (rest names) tmp-sym (+ i 1) tail)) + (else + (cons + (list + (js-sym "define") + (js-sym (first names)) + (list (js-sym "js-get-prop") tmp-sym i)) + (js-vardecl-arr-forms (rest names) tmp-sym (+ i 1) tail)))))) (define js-transpile-if-stmt diff --git a/plans/js-on-sx.md b/plans/js-on-sx.md index 2511d03b..7f2e9bef 100644 --- a/plans/js-on-sx.md +++ b/plans/js-on-sx.md @@ -193,6 +193,8 @@ Append-only record of completed iterations. Loop writes one line per iteration: - 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. +- 2026-04-23 — **Object + array destructuring in var/let/const decls.** Parser: `jp-parse-vardecl` now handles three shapes — plain `ident`, `{a, b, c}` object pattern, `[a, , c]` array pattern with hole support. `jp-parse-obj-pattern` / `jp-parse-arr-pattern` / their loops collect the names. AST: `(js-vardecl-obj (names...) rhs)` and `(js-vardecl-arr (names-with-holes-as-nil...) rhs)`. Transpile: `js-vardecl-forms` dispatches on the three tags. Destructures emit `(define __destruct__ rhs)` then `(define name (js-get-prop __destruct__ key-or-index))` for each pattern element (skips nil holes in array patterns). 4 new unit tests. 418/420 (414→+4). Conformance unchanged. Gotcha: running destructuring tests sequentially — if epoch N defines `a, b` globally and epoch N+1 uses the same names as different types, "Not callable: N" results. Top-level `var` transpiles to `(define name value)`; re-defining a name that's held a value in a prior call to eval carries over. The proper fix would be to use `let` block-scoping; workaround for tests is unique names. + ## Phase 3-5 gotchas Worth remembering for later phases: