From 7ff190135fbaecc8856c7c86a35059bd04bfcb3a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 17:56:23 +0000 Subject: [PATCH 1/4] Only start a destructuring pattern at the head of an AssignmentExpression `[a] = b` and `{a} = b` are AssignmentExpressions whose target happens to be written like a literal. They are only reachable through AssignmentExpression : LeftHandSideExpression = AssignmentExpression, so an array or object literal anywhere else in an expression cannot begin one: `0 || [a] = b` has no production, because a LogicalORExpression is not a valid assignment target. js_parse_postfix_expr() decides purely by peeking past the closing bracket for a '=', without knowing where in the expression it is, so it accepts programs with no parse: 0 || [a] = b; // -> 1 0 || {p:a} = b; // -> [object Object] V8 rejects both with "Invalid left-hand side in assignment". Add PF_PATTERN, set by js_parse_assign_expr2() when it starts the ConditionalExpression that may turn out to be an assignment target, and threaded down the unary/binary chain. Every recursive call that descends into a non-leftmost operand clears it, so only the leftmost literal is considered. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014mv33YvfHz7t9mmkituBnn --- quickjs.c | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/quickjs.c b/quickjs.c index bbac00c33..c7396a5a7 100644 --- a/quickjs.c +++ b/quickjs.c @@ -25365,6 +25365,9 @@ static __exception int js_parse_object_literal(JSParseState *s) /* forbid the exponentiation operator in js_parse_unary() */ #define PF_POW_FORBIDDEN (1 << 3) #define PF_AWAIT_USING (1 << 4) +/* an object/array literal may start a destructuring assignment: only set at + the leftmost position of an AssignmentExpression */ +#define PF_PATTERN (1 << 5) static __exception int js_parse_postfix_expr(JSParseState *s, int parse_flags); @@ -27247,7 +27250,8 @@ static __exception int js_parse_postfix_expr(JSParseState *s, int parse_flags) case '[': { int skip_bits; - if (js_parse_skip_parens_token(s, &skip_bits, false) == '=') { + if ((parse_flags & PF_PATTERN) && + js_parse_skip_parens_token(s, &skip_bits, false) == '=') { if (js_parse_destructuring_element(s, 0, false, false, skip_bits & SKIP_HAS_ELLIPSIS, true, false) < 0) return -1; } else { @@ -27912,7 +27916,8 @@ static __exception int js_parse_unary(JSParseState *s, int parse_flags) parse_flags = 0; break; default: - if (js_parse_postfix_expr(s, PF_POSTFIX_CALL)) + if (js_parse_postfix_expr(s, PF_POSTFIX_CALL | + (parse_flags & PF_PATTERN))) return -1; if (!s->got_lf && (s->token.val == TOK_DEC || s->token.val == TOK_INC)) { @@ -27955,7 +27960,7 @@ static __exception int js_parse_expr_binary(JSParseState *s, int level, int op, opcode; if (level == 0) { - return js_parse_unary(s, PF_POW_ALLOWED); + return js_parse_unary(s, PF_POW_ALLOWED | (parse_flags & PF_PATTERN)); } else if (s->token.val == TOK_PRIVATE_NAME && (parse_flags & PF_IN_ACCEPTED) && level == 4 && peek_token(s, false) == TOK_IN) { @@ -27967,7 +27972,7 @@ static __exception int js_parse_expr_binary(JSParseState *s, int level, goto fail_private_in; if (next_token(s)) goto fail_private_in; - if (js_parse_expr_binary(s, level - 1, parse_flags)) { + if (js_parse_expr_binary(s, level - 1, parse_flags & ~PF_PATTERN)) { fail_private_in: JS_FreeAtom(s->ctx, atom); return -1; @@ -28105,7 +28110,7 @@ static __exception int js_parse_expr_binary(JSParseState *s, int level, if (next_token(s)) return -1; emit_source_loc(s); - if (js_parse_expr_binary(s, level - 1, parse_flags)) + if (js_parse_expr_binary(s, level - 1, parse_flags & ~PF_PATTERN)) return -1; emit_op(s, opcode); } @@ -28136,10 +28141,11 @@ static __exception int js_parse_logical_and_or(JSParseState *s, int op, emit_op(s, OP_drop); if (op == TOK_LAND) { - if (js_parse_expr_binary(s, 8, parse_flags)) + if (js_parse_expr_binary(s, 8, parse_flags & ~PF_PATTERN)) return -1; } else { - if (js_parse_logical_and_or(s, TOK_LAND, parse_flags)) + if (js_parse_logical_and_or(s, TOK_LAND, + parse_flags & ~PF_PATTERN)) return -1; } if (s->token.val != op) { @@ -28171,7 +28177,7 @@ static __exception int js_parse_coalesce_expr(JSParseState *s, int parse_flags) emit_goto(s, OP_if_false, label1); emit_op(s, OP_drop); - if (js_parse_expr_binary(s, 8, parse_flags)) + if (js_parse_expr_binary(s, 8, parse_flags & ~PF_PATTERN)) return -1; if (s->token.val != TOK_DOUBLE_QUESTION_MARK) break; @@ -28401,7 +28407,7 @@ static __exception int js_parse_assign_expr2(JSParseState *s, int parse_flags) /* name0 is used to check for OP_set_name pattern, not duplicated */ name0 = s->token.u.ident.atom; } - if (js_parse_cond_expr(s, parse_flags)) + if (js_parse_cond_expr(s, parse_flags | PF_PATTERN)) return -1; op = s->token.val; From 30c7c459fdd60b7fcd1757cbd203bc6e60239b34 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 08:35:05 +0000 Subject: [PATCH 2/4] Add a test for destructuring only at the head of an assignment Covers the positions where an object/array literal must stay a literal (binary, short circuit, unary and private-name-in operands) and the head positions that must keep parsing as a pattern: statement level, both branches of a conditional, comma elements, call arguments, literal elements, initialisers, default parameter values, arrow bodies and the three for loop forms. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01K6eRbuuuCujKgQkrHgvMrc --- tests/destructuring-assignment-head.js | 257 +++++++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 tests/destructuring-assignment-head.js diff --git a/tests/destructuring-assignment-head.js b/tests/destructuring-assignment-head.js new file mode 100644 index 000000000..4b6190f8f --- /dev/null +++ b/tests/destructuring-assignment-head.js @@ -0,0 +1,257 @@ +import { assert } from "./assert.js"; + +/* An ObjectLiteral or ArrayLiteral only turns into a destructuring pattern + when it sits at the head of an AssignmentExpression. Anywhere else it is + just a literal, and a following '=' is an assignment to a non-reference, + i.e. an early SyntaxError. */ + +function syntaxError(src) { + try { + eval(src); + } catch (e) { + return e instanceof SyntaxError; + } + return false; +} + +function evaluate(src) { + return (0, eval)(src); +} + +/* ------------------------------------------------------------------------- + the literal is not at the head: it stays a literal and the '=' is invalid + ------------------------------------------------------------------------- */ +{ + /* binary operators */ + assert(syntaxError("var a; 1 + [a] = [1];"), true, "1 + [a] = [1]"); + assert(syntaxError("var a; 1 + {a} = {a: 1};"), true, "1 + {a} = {}"); + assert(syntaxError("var a; 1 - [a] = [1];"), true, "1 - [a] = [1]"); + assert(syntaxError("var a; 1 * [a] = [1];"), true, "1 * [a] = [1]"); + assert(syntaxError("var a; 2 ** [a] = [1];"), true, "2 ** [a] = [1]"); + assert(syntaxError("var a; 1 | [a] = [1];"), true, "1 | [a] = [1]"); + assert(syntaxError("var a; 1 < [a] = [1];"), true, "1 < [a] = [1]"); + assert(syntaxError("var a; 1 instanceof [a] = [1];"), true, "instanceof"); + assert(syntaxError("var a; 'x' in {a} = {a: 1};"), true, "in"); + + /* short circuit operators: the right hand side is not a head either */ + assert(syntaxError("var a, o = 1; o || [a] = [1];"), true, "|| [a] ="); + assert(syntaxError("var a, o = 1; o && [a] = [1];"), true, "&& [a] ="); + assert(syntaxError("var a, o = 1; o ?? [a] = [1];"), true, "?? [a] ="); + assert(syntaxError("var a, o = 1; o || {a} = {a: 1};"), true, "|| {a} ="); + assert(syntaxError("var a, o = 1; o && {a} = {a: 1};"), true, "&& {a} ="); + + /* unary operators */ + assert(syntaxError("var a; void [a] = [1];"), true, "void"); + assert(syntaxError("var a; typeof {a} = {a: 1};"), true, "typeof"); + assert(syntaxError("var a; !{a} = {a: 1};"), true, "!"); + assert(syntaxError("var a; -[a] = [1];"), true, "unary -"); + assert(syntaxError("var a; +[a] = [1];"), true, "unary +"); + assert(syntaxError("var a; ~[a] = [1];"), true, "~"); + assert(syntaxError("var a; delete [a] = [1];"), true, "delete"); + + /* the comma operator: only the first operand of each element is a head, + the literal here follows a binary operator */ + assert(syntaxError("var a; (0, 1 + [a] = [1]);"), true, "comma element"); + + /* a private name 'in' check is not a head either */ + assert(syntaxError("var a; class C { #p; m(o) { #p in [a] = [1]; } }"), + true, "#p in"); +} + +/* ------------------------------------------------------------------------- + the head positions that must keep working + ------------------------------------------------------------------------- */ +{ + /* plain statement level */ + let a, b; + [a, b] = [1, 2]; + assert(a, 1); + assert(b, 2); + + ({ a, b } = { a: 3, b: 4 }); + assert(a, 3); + assert(b, 4); + + /* holes, defaults, rest and nesting */ + let r; + [a, , b, ...r] = [1, 2, 3, 4, 5]; + assert(a, 1); + assert(b, 3); + assert(r.join(","), "4,5"); + + ({ a = 10, ...r } = { b: 1, c: 2 }); + assert(a, 10); + assert(JSON.stringify(r), '{"b":1,"c":2}'); + + let c; + [a, [b, { c }]] = [1, [2, { c: 3 }]]; + assert(a, 1); + assert(b, 2); + assert(c, 3); + + /* empty patterns */ + [] = []; + ({} = {}); + + /* member expression targets */ + const o = {}; + [o.x] = [7]; + assert(o.x, 7); + ({ y: o.y } = { y: 8 }); + assert(o.y, 8); +} + +{ + /* right hand side of an assignment is a fresh AssignmentExpression */ + let a, b, x; + x = ([a] = [1]); + assert(a, 1); + assert(x.join(","), "1"); + + [a] = [b] = [2]; + assert(a, 2); + assert(b, 2); + + /* compound assignment right hand side */ + let n = 1; + n += ([a] = [4])[0]; + assert(n, 5); +} + +{ + /* both branches of a conditional restart an AssignmentExpression */ + let a, b; + const t = true ? ([a] = [1]) : 0; + assert(a, 1); + assert(t.join(","), "1"); + false ? 0 : ({ b } = { b: 2 }); + assert(b, 2); + + /* without the parentheses too: '?' and ':' each start a new head */ + let c, d; + true ? [c] = [5] : 0; + assert(c, 5); + false ? 0 : [d] = [6]; + assert(d, 6); +} + +{ + /* each element of a comma expression is its own AssignmentExpression */ + let a, b; + (([a] = [1]), ({ b } = { b: 2 })); + assert(a, 1); + assert(b, 2); + + /* ... and so is every argument of a call */ + let c, d; + const seen = ((x, y) => [x, y])(([c] = [3])[0], ({ d } = { d: 4 }).d); + assert(c, 3); + assert(d, 4); + assert(seen.join(","), "3,4"); +} + +{ + /* array/object literal elements and properties are AssignmentExpressions */ + let a, b; + const arr = [[a] = [1], ({ b } = { b: 2 })]; + assert(a, 1); + assert(b, 2); + assert(arr[0].join(","), "1"); + + let c; + const obj = { p: ([c] = [3]) }; + assert(c, 3); + assert(obj.p.join(","), "3"); + + /* computed keys and template substitutions */ + let d, e; + const k = { [([d] = ["k"])[0]]: 1 }; + assert(d, "k"); + assert(k.k, 1); + assert(`${([e] = [9])[0]}`, "9"); +} + +{ + /* initialisers and default parameter values */ + let a; + const init = ([a] = [1]); + assert(a, 1); + assert(init.join(","), "1"); + + let b; + function f(p = ([b] = [2])) { return p; } + assert(f().join(","), "2"); + assert(b, 2); + + /* arrow body */ + let c; + const g = () => ([c] = [3]); + assert(g().join(","), "3"); + assert(c, 3); + + /* return / throw operands */ + let d; + function h() { return [d] = [4]; } + assert(h().join(","), "4"); + assert(d, 4); +} + +{ + /* for-of and for-in bind through the same destructuring path */ + let a, b; + for ([a, b] of [[1, 2]]) ; + assert(a, 1); + assert(b, 2); + + for ({ a, b } of [{ a: 3, b: 4 }]) ; + assert(a, 3); + assert(b, 4); + + let k; + for ([k] in { ab: 1 }) ; + assert(k, "a"); + + /* the head of a for(;;) is an Expression: each element is a head */ + let c = 0, d; + for ([d] = [0]; d < 3; [d] = [d + 1]) c += d; + assert(c, 3); + assert(d, 3); +} + +/* ------------------------------------------------------------------------- + literals in non head positions still evaluate as literals + ------------------------------------------------------------------------- */ +{ + assert(1 + [2], "12"); + assert([1] + [2], "12"); + assert(typeof {}, "object"); + assert((0 || [1, 2]).join(","), "1,2"); + assert((1 && { a: 1 }).a, 1); + assert((null ?? [3]).join(","), "3"); + assert(evaluate("1 + [1] == '11'"), true); + + /* '==' is not '=': the literal stays a literal and nothing throws */ + assert(evaluate("var a = 1; [a] == '1'"), true); + assert(evaluate("var a = 1; ({a}) != null"), true); + + /* an '=' further inside the literal does not make the literal a + pattern: it belongs to the nested AssignmentExpression */ + let a; + const arr = [1, 2] + ([a] = [3]); + assert(a, 3); + assert(arr, "1,23"); +} + +/* ------------------------------------------------------------------------- + destructuring failures still report at runtime, not at parse time + ------------------------------------------------------------------------- */ +{ + let a; + let threw = false; + try { + eval("[a] = null;"); + } catch (e) { + threw = e instanceof TypeError; + } + assert(threw, true, "[a] = null is a runtime TypeError"); +} From f1e14e157af46c06dd59f197f18a6a2b855e9d3a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 10:06:31 +0000 Subject: [PATCH 3/4] Drop the private-field assignment target from the expected failures `#field in {} = 0` must be an early SyntaxError. It was not one because the `{}` was taken for a destructuring pattern on the strength of the `=` that follows, which is exactly what this change stops; test262's private-field-invalid-assignment-target now passes in both modes. Leaving the two entries behind makes the full test262 run exit non-zero for errors that are fixed rather than new. Also covers the construct in the local test, next to the array form already there. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01K6eRbuuuCujKgQkrHgvMrc --- test262_errors.txt | 2 -- tests/destructuring-assignment-head.js | 4 +++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test262_errors.txt b/test262_errors.txt index 082fb016a..8a9165473 100644 --- a/test262_errors.txt +++ b/test262_errors.txt @@ -27,8 +27,6 @@ test262/test/language/expressions/assignment/target-member-computed-reference.js test262/test/language/expressions/assignment/target-member-computed-reference.js:22: strict mode: Test262Error: Expected a DummyError but got a Test262Error test262/test/language/expressions/assignment/target-super-computed-reference.js:20: Test262Error: Expected a DummyError but got a Test262Error test262/test/language/expressions/assignment/target-super-computed-reference.js:20: strict mode: Test262Error: Expected a DummyError but got a Test262Error -test262/test/language/expressions/in/private-field-invalid-assignment-target.js:23: unexpected error type: Test262: This statement should not be evaluated. -test262/test/language/expressions/in/private-field-invalid-assignment-target.js:23: strict mode: unexpected error type: Test262: This statement should not be evaluated. test262/test/language/expressions/object/computed-property-name-topropertykey-before-value-evaluation.js:31: Test262Error: Expected SameValue(«"bad"», «"ok"») to be true test262/test/language/expressions/object/computed-property-name-topropertykey-before-value-evaluation.js:31: strict mode: Test262Error: Expected SameValue(«"bad"», «"ok"») to be true test262/test/language/module-code/ambiguous-export-bindings/import-and-export-propagates-binding.js:75: SyntaxError: export 'foo' in module 'test262/test/language/module-code/ambiguous-export-bindings/imp' is ambiguous diff --git a/tests/destructuring-assignment-head.js b/tests/destructuring-assignment-head.js index 4b6190f8f..a016de00c 100644 --- a/tests/destructuring-assignment-head.js +++ b/tests/destructuring-assignment-head.js @@ -55,7 +55,9 @@ function evaluate(src) { /* a private name 'in' check is not a head either */ assert(syntaxError("var a; class C { #p; m(o) { #p in [a] = [1]; } }"), - true, "#p in"); + true, "#p in [a] ="); + assert(syntaxError("class C { #p; constructor() { #p in {} = 0; } }"), + true, "#p in {} ="); } /* ------------------------------------------------------------------------- From cb47703c3210690921b4aec8188f0495d02008c7 Mon Sep 17 00:00:00 2001 From: Andreas Rosdal Date: Fri, 7 Aug 2026 12:48:38 +0000 Subject: [PATCH 4/4] Cover more of the head/non-head boundary for destructuring The rule is positional, so the test is mostly a list of positions. Added the ones the first round did not name: a NewExpression callee and argument, an update operand, a member access or call on a literal, an optional chain, every compound assignment operator, the head of each statement that takes a full Expression, an argument, array element, property value and spread, the operand of yield and await, a class field initialiser, a default parameter value, a template substitution and the tag of a tagged template. --- tests/destructuring-assignment-head.js | 75 ++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/tests/destructuring-assignment-head.js b/tests/destructuring-assignment-head.js index a016de00c..0a28c3541 100644 --- a/tests/destructuring-assignment-head.js +++ b/tests/destructuring-assignment-head.js @@ -257,3 +257,78 @@ function evaluate(src) { } assert(threw, true, "[a] = null is a runtime TypeError"); } + +/* ------------------------------------------------------------------------- + more non head positions, and the head positions they are easy to confuse + with + ------------------------------------------------------------------------- */ +{ + /* a literal that is a NewExpression's callee, or a member of one */ + assert(syntaxError("var a; new [a] = [1];"), true, "new [a] ="); + assert(syntaxError("var a; new C([a]) = [1];"), true, "new C([a]) ="); + + /* the operand of an update operator, and an update of a literal */ + assert(syntaxError("var a; ++[a] = [1];"), true, "++[a] ="); + assert(syntaxError("var a; [a]++ = [1];"), true, "[a]++ ="); + + /* a literal followed by a member access is a member expression */ + assert(syntaxError("var a; [a].length = 1, [a] = [1];") === false, true, + "[a].length = 1 is fine"); + assert(syntaxError("var a; [a][0] = [1];") === false, true, "[a][0] = 1"); + + /* an optional chain is never a valid assignment target */ + assert(syntaxError("var a, o = {}; o?.[a] = [1];"), true, "o?.[a] ="); + + /* a compound assignment never destructures, whichever operator it is */ + assert(syntaxError("var a; [a] += [1];"), true, "[a] +="); + assert(syntaxError("var a; [a] **= [1];"), true, "[a] **="); + assert(syntaxError("var a; [a] ||= [1];"), true, "[a] ||="); + assert(syntaxError("var a; [a] &&= [1];"), true, "[a] &&="); + assert(syntaxError("var a; [a] ??= [1];"), true, "[a] ??="); + assert(syntaxError("var a; ({a} >>>= {a: 1});"), true, "{a} >>>="); + + /* the head of every statement that takes a full Expression is a head */ + let a, b; + assert(evaluate("var a; for ([a] = [7]; false; ) ; a"), 7, "for init"); + assert(evaluate("var a; while (([a] = [8]) && false) ; a"), 8, "while"); + assert(evaluate("var a; switch ([a] = [9]) { } a"), 9, "switch"); + assert(evaluate("var a; if (([a] = [10])) ; a"), 10, "if"); + assert(evaluate("var a; do ; while (([a] = [11]) && false); a"), 11, "do"); + assert(evaluate("var a; with ({}) { [a] = [12]; } a"), 12, "with body"); + assert(evaluate("var a; ((0, [a] = [13]), a)"), 13, "comma element head"); + assert(evaluate("var a; (true ? ([a] = [14]) : 0, a)"), 14, "conditional"); + assert(evaluate("var a; f(); function f() { [a] = [15]; } a"), 15, "call"); + + /* an argument, an array element and a property value are each their own + AssignmentExpression, so a literal starts one there too */ + [a] = [1]; + assert(((x) => x)([a] = [16])[0], 16, "argument"); + assert([[a] = [17]][0][0], 17, "array element"); + assert(({ p: [a] = [18] }).p[0], 18, "property value"); + assert([...([a] = [19])][0], 19, "spread argument"); + + /* the operand of yield and await is a head as well */ + assert(evaluate(` + var a; + function* g() { yield [a] = [20]; } + g().next(); + a`), 20, "yield operand"); + + /* a class field initialiser and a default parameter value are heads */ + assert(evaluate(` + var a; + class C { f = ([a] = [21]); } + new C(); + a`), 21, "class field initialiser"); + assert(evaluate(` + var a; + function f(p = ([a] = [22])) { return p; } + f(); + a`), 22, "default parameter"); + + /* and a template substitution */ + assert(evaluate("var a; `${[a] = [23]}`; a"), 23, "template substitution"); + + /* the tag of a tagged template is a member expression, not a target */ + assert(syntaxError("var a; [a]`x` = [1];"), true, "tagged template"); +}