From 24cc6adae34159749ddbfb0fd5f2fa8fb6227199 Mon Sep 17 00:00:00 2001 From: Michal Date: Mon, 31 Aug 2026 13:52:07 +0200 Subject: [PATCH 1/2] destructuring assignment --- .../src/rules/noInvalidAssignment.ts | 8 ++ .../src/rules/noUnsupportedSyntax.ts | 12 ++- .../tests/rules/noInvalidAssignment.test.ts | 8 ++ .../tests/rules/noUnsupportedSyntax.test.ts | 8 +- packages/tinyest-for-wgsl/src/parsers.ts | 12 ++- .../tinyest-for-wgsl/tests/parsers.test.ts | 21 ++-- packages/tinyest/src/nodes.ts | 13 ++- packages/typegpu/src/shared/tseynit.ts | 4 + packages/typegpu/src/tgsl/wgslGenerator.ts | 57 +++++++++++ .../typegpu/tests/tgsl/wgslGenerator.test.ts | 96 +++++++++++++++++++ .../unplugin-typegpu/src/core/obfuscate.ts | 5 +- 11 files changed, 226 insertions(+), 18 deletions(-) diff --git a/packages/eslint-plugin/src/rules/noInvalidAssignment.ts b/packages/eslint-plugin/src/rules/noInvalidAssignment.ts index 80d4cbf278..e4f23f5588 100644 --- a/packages/eslint-plugin/src/rules/noInvalidAssignment.ts +++ b/packages/eslint-plugin/src/rules/noInvalidAssignment.ts @@ -32,6 +32,14 @@ export const noInvalidAssignment = createRule({ AssignmentExpression(node) { const enclosingFn = directives.getEnclosingTypegpuFunction(); + if (node.left.type === 'ObjectPattern') { + for (const prop of node.left.properties) { + if (prop.type === 'Property') { + validateAssignment(context, node, enclosingFn, prop.value); + } + } + return; + } validateAssignment(context, node, enclosingFn, node.left); }, }; diff --git a/packages/eslint-plugin/src/rules/noUnsupportedSyntax.ts b/packages/eslint-plugin/src/rules/noUnsupportedSyntax.ts index 9e0e48f5fd..f68eae0592 100644 --- a/packages/eslint-plugin/src/rules/noUnsupportedSyntax.ts +++ b/packages/eslint-plugin/src/rules/noUnsupportedSyntax.ts @@ -63,11 +63,21 @@ export const noUnsupportedSyntax = createRule({ return; } - if (node.left.type === 'ObjectPattern' || node.left.type === 'ArrayPattern') { + if (node.left.type === 'ArrayPattern') { report(node.left, 'destructuring assignment'); return; } + if (node.left.type === 'ObjectPattern') { + if (!isSupportedObjectBindingPattern(node.left)) { + report(node.left, 'destructuring assignment'); + } + + if (node.parent.type !== 'ExpressionStatement') { + report(node, 'destructuring assignment as expression'); + } + } + if (unsupportedAssignmentOps.includes(node.operator)) { report(node, `assignment expression '${node.operator}'`); } diff --git a/packages/eslint-plugin/tests/rules/noInvalidAssignment.test.ts b/packages/eslint-plugin/tests/rules/noInvalidAssignment.test.ts index b06d16a33a..6c25c0cee3 100644 --- a/packages/eslint-plugin/tests/rules/noInvalidAssignment.test.ts +++ b/packages/eslint-plugin/tests/rules/noInvalidAssignment.test.ts @@ -105,6 +105,10 @@ describe('noInvalidAssignment', () => { }, ], }, + { + code: "const fn = (a) => { 'use gpu'; ({ a } = obj); }", + errors: [{ messageId: 'parameterAssignment', data: { snippet: 'a' } }], + }, ], }); @@ -205,6 +209,10 @@ describe('noInvalidAssignment', () => { }, ], }, + { + code: "let a; const fn = () => { 'use gpu'; ({ a } = obj); }", + errors: [{ messageId: 'jsAssignment', data: { snippet: 'a' } }], + }, ], }); }); diff --git a/packages/eslint-plugin/tests/rules/noUnsupportedSyntax.test.ts b/packages/eslint-plugin/tests/rules/noUnsupportedSyntax.test.ts index bd5d1d3c35..6f02209e50 100644 --- a/packages/eslint-plugin/tests/rules/noUnsupportedSyntax.test.ts +++ b/packages/eslint-plugin/tests/rules/noUnsupportedSyntax.test.ts @@ -11,6 +11,8 @@ describe('noUnsupportedSyntax', () => { "const cls = new (class { #priv = 1; fn = () => { 'use gpu'; const a = this.#priv; } } )()", "const fn = () => { 'use gpu'; const { a } = obj; }", "const fn = () => { 'use gpu'; const { a, b: renamed } = obj; }", + "const fn = () => { 'use gpu'; let a = 0; ({ a } = obj); }", + "const fn = () => { 'use gpu'; let b = 0; ({ a:b } = obj); }", ], invalid: [ { @@ -418,13 +420,13 @@ describe('noUnsupportedSyntax', () => { ], }, { - code: "const fn = () => { 'use gpu'; let a = 0; ({ a } = obj); }", + code: "const fn = () => { 'use gpu'; let a = 0; return ({ a } = obj); }", errors: [ { messageId: 'unexpected', data: { - snippet: '{ a }', - syntax: 'destructuring assignment', + snippet: '({ a } = obj)', + syntax: 'destructuring assignment as expression', }, }, ], diff --git a/packages/tinyest-for-wgsl/src/parsers.ts b/packages/tinyest-for-wgsl/src/parsers.ts index e61fd91674..c8c0291733 100644 --- a/packages/tinyest-for-wgsl/src/parsers.ts +++ b/packages/tinyest-for-wgsl/src/parsers.ts @@ -104,11 +104,17 @@ const Transpilers: Partial<{ }, AssignmentExpression(ctx, node) { - if (node.left.type === 'ObjectPattern' || node.left.type === 'ArrayPattern') { - throw new Error('Destructuring assignments are not supported.'); + if (node.left.type === 'ArrayPattern') { + throw new Error('Destructuring assignments are not supported for arrays yet.'); + } + + let left; + if (node.left.type === 'ObjectPattern') { + left = parseBindingPattern(node.left); + } else { + left = transpile(ctx, node.left) as tinyest.Expression; } - const left = transpile(ctx, node.left) as tinyest.Expression; const right = transpile(ctx, node.right) as tinyest.Expression; return [NODE.assignmentExpr, left, node.operator as tinyest.AssignmentOperator, right]; }, diff --git a/packages/tinyest-for-wgsl/tests/parsers.test.ts b/packages/tinyest-for-wgsl/tests/parsers.test.ts index 3a3d478643..59cdaf8dc4 100644 --- a/packages/tinyest-for-wgsl/tests/parsers.test.ts +++ b/packages/tinyest-for-wgsl/tests/parsers.test.ts @@ -466,16 +466,19 @@ describe('transpileFn', () => { ); it( - 'rejects destructuring assignments', + 'parses destructuring assignments', dualTest((parse) => { - expect(() => - transpileFn( - parse(`() => { - let a = 0; - ({ a } = source); - }`), - ), - ).toThrow('Destructuring assignments are not supported.'); + const { body, externalNames } = transpileFn( + parse(`() => { + let a = 0; + ({ a } = source); + }`), + ); + + expect(externalNames).toStrictEqual(new Map([['source', 'source']])); + expect(JSON.stringify(body)).toMatchInlineSnapshot( + `"[0,[[12,{"type":"i","name":"a"},[5,"0"]],[2,{"type":"d","props":[{"name":"a","alias":"a"}]},"=","source"]]]"` + ); }), ); }); diff --git a/packages/tinyest/src/nodes.ts b/packages/tinyest/src/nodes.ts index 4905f67c45..83b30246f9 100644 --- a/packages/tinyest/src/nodes.ts +++ b/packages/tinyest/src/nodes.ts @@ -165,7 +165,7 @@ export type AssignmentOperator = export type AssignmentExpression = readonly [ type: NodeTypeCatalog['assignmentExpr'], - lhs: Expression, + lhs: Expression | BindingPattern, op: AssignmentOperator, rhs: Expression, ]; @@ -276,5 +276,16 @@ export type BindingPattern = }[]; }; +export function isBindingPattern(node: unknown): node is BindingPattern { + if (typeof node !== 'object' || node === null || Array.isArray(node) || !('type' in node)) { + return false; + } + + return ( + node.type === BindingPatternType.identifier || + node.type === BindingPatternType.destructuredObject + ); +} + export type FuncParameter = BindingPattern; export const FuncParameterType = BindingPatternType; diff --git a/packages/typegpu/src/shared/tseynit.ts b/packages/typegpu/src/shared/tseynit.ts index 87d330bf59..bf04b1306f 100644 --- a/packages/typegpu/src/shared/tseynit.ts +++ b/packages/typegpu/src/shared/tseynit.ts @@ -122,6 +122,10 @@ function stringifyExpression(node: tinyest.Expression, ident: string): string { } if (node[0] === NODE.assignmentExpr) { + if (tinyest.isBindingPattern(node[1])) { + return `(${stringifyBindingPattern(node[1])} ${node[2]} ${stringifyExpression(node[3], ident)})`; + } + return `${stringifyExpression(node[1], ident)} ${node[2]} ${stringifyExpression(node[3], ident)}`; } diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index 2190b589db..6402fc4039 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -497,6 +497,9 @@ export class WgslGenerator implements ShaderGenerator { if (expression[0] === NODE.binaryExpr || expression[0] === NODE.assignmentExpr) { // Binary/Assignment Expression const [exprType, lhs, op, rhs] = expression; + if (tinyest.isBindingPattern(lhs)) { + throw new WgslTypeError(`'${stringifyNode(expression)}' cannot be used as an expression.`); + } const lhsExpr = this._expression(lhs); const rhsExpr = this._expression(rhs); @@ -1352,6 +1355,39 @@ Try 'return ${typeStr}(${str});' instead. }; } + protected _destructuringAssignmentStatement( + props: readonly { name: string; alias: string }[], + eqNode: tinyest.Expression, + ): ResolvedStatement { + /* + * Always utilizing temporary variable + * otherwise aliases can overwrite the source: ({ yx: obj, x } = obj). + */ + + let temporaryDeclaration: ResolvedStatement; + const temporaryId = `#destructured_${this.#destructuringIndex++}`; + temporaryDeclaration = this._constStatement([ + NODE.const, + { + type: tinyest.BindingPatternType.identifier, + name: temporaryId, + }, + eqNode, + ]); + + const propertyAssignment = props.map((prop) => { + const propertyAccess: tinyest.MemberAccess = [NODE.memberAccess, temporaryId, prop.name]; + return this._statement([NODE.assignmentExpr, prop.alias, '=', propertyAccess]); + }); + + const statements = [temporaryDeclaration, ...propertyAssignment]; + + return { + code: statements.map((statement) => statement.code).join('\n'), + definesInNearestScope: !!temporaryDeclaration, + }; + } + protected _letStatement(statement: tinyest.Let): ResolvedStatement { const [_, binding, eqNode] = statement; @@ -1680,6 +1716,19 @@ ${this.ctx.pre}else ${alternate}`, ) { throw new WgslTypeError('Object destructuring in for loop initializers is not supported.'); } + + if ( + (Array.isArray(init) && + init[0] === NODE.assignmentExpr && + tinyest.isBindingPattern(init[1])) || + (Array.isArray(update) && + update[0] === NODE.assignmentExpr && + tinyest.isBindingPattern(update[1])) + ) { + throw new WgslTypeError( + 'Destructuring assignment in for loop headers is not supported.', + ); + } const prevUnrollingChain = this.#unrollingChain; this.#unrollingChain = []; @@ -1894,6 +1943,14 @@ ${this.ctx.pre}else ${alternate}`, }; } + if ( + statement[0] === NODE.assignmentExpr && + tinyest.isBindingPattern(statement[1]) && + statement[1].type === tinyest.BindingPatternType.destructuredObject + ) { + return this._destructuringAssignmentStatement(statement[1].props, statement[3]); + } + const expr = this._expression(statement); const resolved = expr.value !== undefined && expr.value !== null ? this.ctx.resolveSnippet(expr).value : ''; diff --git a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts index e8199dc956..99ad9a0c3c 100644 --- a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts +++ b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts @@ -2155,5 +2155,101 @@ describe('WgslGenerator', () => { }" `); }); + + it('allows destructuring assignment', () => { + const Pair = d.struct({ + a: d.i32, + b: d.i32, + }); + + const fn = () => { + 'use gpu'; + let x = 0; + let y = 0; + + ({ a: x, b: y } = Pair({ a: 2, b: 3 })); + + return x; + }; + + expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` + "struct Pair { + a: i32, + b: i32, + } + + fn fn_1() -> i32 { + var x = 0; + var y = 0; + let destructured_0 = Pair(2i, 3i); + x = destructured_0.a; + y = destructured_0.b; + return x; + }" + `); + }); + + it('rejects destructuring assignment used as an expression', () => { + const Pair = d.struct({ + a: d.i32, + b: d.i32, + }); + + const fn = () => { + 'use gpu'; + let x = 0; + const obj = Pair({ a: 2, b: 3 }); + return ({ a: x } = obj); + }; + + expect(() => tgpu.resolve([fn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:fn + - fn*:fn(): '({ a: x } = obj)' cannot be used as an expression.] + `); + }); + + it('rejects destructuring assignment in for loop initializers', () => { + const Pair = d.struct({ + a: d.i32, + b: d.i32, + }); + + const fn = () => { + 'use gpu'; + let x = 0; + const obj = Pair({ a: 2, b: 3 }); + for (({ a: x } = obj); x < 10; ) {} + }; + + expect(() => tgpu.resolve([fn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:fn + - fn*:fn(): Destructuring assignment in for loop headers is not supported.] + `); + }); + + it('rejects destructuring assignment in for loop updates', () => { + const Pair = d.struct({ + a: d.i32, + b: d.i32, + }); + + const fn = () => { + 'use gpu'; + let x = 0; + const obj = Pair({ a: 2, b: 3 }); + for (; x < 10; ({ a: x } = obj)) {} + }; + + expect(() => tgpu.resolve([fn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:fn + - fn*:fn(): Destructuring assignment in for loop headers is not supported.] + `); + }); }); }); diff --git a/packages/unplugin-typegpu/src/core/obfuscate.ts b/packages/unplugin-typegpu/src/core/obfuscate.ts index 39a7ecea82..af1860d5d0 100644 --- a/packages/unplugin-typegpu/src/core/obfuscate.ts +++ b/packages/unplugin-typegpu/src/core/obfuscate.ts @@ -102,7 +102,10 @@ const visitors = { return [NODE.binaryExpr, obf(ctx, node[1]), node[2], obf(ctx, node[3])]; }, assignmentExpr(ctx: Context, node: tinyest.AssignmentExpression) { - return [NODE.assignmentExpr, obf(ctx, node[1]), node[2], obf(ctx, node[3])]; + const lhs = tinyest.isBindingPattern(node[1]) + ? obfuscateBindingPattern(ctx, node[1]) + : obf(ctx, node[1]); + return [NODE.assignmentExpr, lhs, node[2], obf(ctx, node[3])]; }, logicalExpr(ctx: Context, node: tinyest.LogicalExpression) { return [NODE.logicalExpr, obf(ctx, node[1]), node[2], obf(ctx, node[3])]; From 748240fdb2661e2fe133d82ddf0ad877821ec031 Mon Sep 17 00:00:00 2001 From: Michal Date: Tue, 1 Sep 2026 15:06:58 +0200 Subject: [PATCH 2/2] fixes --- packages/typegpu/src/tgsl/wgslGenerator.ts | 39 ++++++++------ .../typegpu/tests/tgsl/wgslGenerator.test.ts | 51 +++++++------------ 2 files changed, 43 insertions(+), 47 deletions(-) diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index 6402fc4039..db53f3cf31 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -1366,14 +1366,17 @@ Try 'return ${typeStr}(${str});' instead. let temporaryDeclaration: ResolvedStatement; const temporaryId = `#destructured_${this.#destructuringIndex++}`; - temporaryDeclaration = this._constStatement([ - NODE.const, - { - type: tinyest.BindingPatternType.identifier, - name: temporaryId, - }, - eqNode, - ]); + temporaryDeclaration = this._constStatement( + [ + NODE.const, + { + type: tinyest.BindingPatternType.identifier, + name: temporaryId, + }, + eqNode, + ], + { asValue: true }, + ); const propertyAssignment = props.map((prop) => { const propertyAccess: tinyest.MemberAccess = [NODE.memberAccess, temporaryId, prop.name]; @@ -1384,7 +1387,7 @@ Try 'return ${typeStr}(${str});' instead. return { code: statements.map((statement) => statement.code).join('\n'), - definesInNearestScope: !!temporaryDeclaration, + definesInNearestScope: true, }; } @@ -1466,7 +1469,10 @@ Try 'return ${typeStr}(${str});' instead. }; } - protected _constStatement(statement: tinyest.Const): ResolvedStatement { + protected _constStatement( + statement: tinyest.Const, + { asValue = false }: { asValue?: boolean } = {}, + ): ResolvedStatement { const [_, binding, eqNode] = statement; if (eqNode === undefined) { @@ -1520,6 +1526,10 @@ Try 'return ${typeStr}(${str});' instead. ); } + if (asValue) { + definitionDataType = unptr(definitionDataType); + } + if (eq.origin === 'argument') { // Arguments are immutable, so we 'let' them be (kill me) varType = 'let'; @@ -1541,8 +1551,9 @@ Try 'return ${typeStr}(${str});' instead. // This is mostly because we plan to determine this fact later, after all of the // function code has been processed, so at least currently, we lose that info. varOrigin = 'local-def'; - } else if (!isAlias(eq)) { - // Not a reference, but also not naturally ephemeral, so we cannot guarantee it won't be mutated. + } else if (!isAlias(eq) || asValue) { + // Not a reference (or a copy was explicitly requested), but also not + // naturally ephemeral, so we cannot guarantee it won't be mutated. // We defer the decision for now. varType = ''; varOrigin = 'local-def'; @@ -1725,9 +1736,7 @@ ${this.ctx.pre}else ${alternate}`, update[0] === NODE.assignmentExpr && tinyest.isBindingPattern(update[1])) ) { - throw new WgslTypeError( - 'Destructuring assignment in for loop headers is not supported.', - ); + throw new WgslTypeError('Destructuring assignment in for loop headers is not supported.'); } const prevUnrollingChain = this.#unrollingChain; this.#unrollingChain = []; diff --git a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts index 99ad9a0c3c..8d50c7a31f 100644 --- a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts +++ b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts @@ -2210,45 +2210,32 @@ describe('WgslGenerator', () => { `); }); - it('rejects destructuring assignment in for loop initializers', () => { - const Pair = d.struct({ - a: d.i32, - b: d.i32, - }); - + it('snapshots the source before an alias overwrites it', () => { const fn = () => { 'use gpu'; + let obj = d.vec2f(1, 2); let x = 0; - const obj = Pair({ a: 2, b: 3 }); - for (({ a: x } = obj); x < 10; ) {} - }; + ({ yx: obj, x } = obj); - expect(() => tgpu.resolve([fn])).toThrowErrorMatchingInlineSnapshot(` - [Error: Resolution of the following tree failed: - - - - fn*:fn - - fn*:fn(): Destructuring assignment in for loop headers is not supported.] - `); - }); + const a = obj; + ({ yx: obj, x } = a); - it('rejects destructuring assignment in for loop updates', () => { - const Pair = d.struct({ - a: d.i32, - b: d.i32, - }); - - const fn = () => { - 'use gpu'; - let x = 0; - const obj = Pair({ a: 2, b: 3 }); - for (; x < 10; ({ a: x } = obj)) {} + return x; }; - expect(() => tgpu.resolve([fn])).toThrowErrorMatchingInlineSnapshot(` - [Error: Resolution of the following tree failed: - - - - fn*:fn - - fn*:fn(): Destructuring assignment in for loop headers is not supported.] + expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` + "fn fn_1() -> i32 { + var obj = vec2f(1, 2); + var x = 0; + let destructured_0 = obj; + obj = destructured_0.yx; + x = i32(destructured_0.x); + let a = (&obj); + let destructured_1 = (*a); + obj = destructured_1.yx; + x = i32(destructured_1.x); + return x; + }" `); }); });