Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/eslint-plugin/src/rules/noInvalidAssignment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
},
};
Expand Down
12 changes: 11 additions & 1 deletion packages/eslint-plugin/src/rules/noUnsupportedSyntax.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}'`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ describe('noInvalidAssignment', () => {
},
],
},
{
code: "const fn = (a) => { 'use gpu'; ({ a } = obj); }",
errors: [{ messageId: 'parameterAssignment', data: { snippet: 'a' } }],
},
],
});

Expand Down Expand Up @@ -205,6 +209,10 @@ describe('noInvalidAssignment', () => {
},
],
},
{
code: "let a; const fn = () => { 'use gpu'; ({ a } = obj); }",
errors: [{ messageId: 'jsAssignment', data: { snippet: 'a' } }],
},
],
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
{
Expand Down Expand Up @@ -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',
},
},
],
Expand Down
12 changes: 9 additions & 3 deletions packages/tinyest-for-wgsl/src/parsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
},
Expand Down
21 changes: 12 additions & 9 deletions packages/tinyest-for-wgsl/tests/parsers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]]]"`
);
}),
);
});
13 changes: 12 additions & 1 deletion packages/tinyest/src/nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ export type AssignmentOperator =

export type AssignmentExpression = readonly [
type: NodeTypeCatalog['assignmentExpr'],
lhs: Expression,
lhs: Expression | BindingPattern,
op: AssignmentOperator,
rhs: Expression,
];
Expand Down Expand Up @@ -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;
4 changes: 4 additions & 0 deletions packages/typegpu/src/shared/tseynit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)}`;
}

Expand Down
72 changes: 69 additions & 3 deletions packages/typegpu/src/tgsl/wgslGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -1352,6 +1355,42 @@ 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,
],
{ asValue: true },
);

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: true,
};
}

protected _letStatement(statement: tinyest.Let): ResolvedStatement {
const [_, binding, eqNode] = statement;

Expand Down Expand Up @@ -1430,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) {
Expand Down Expand Up @@ -1484,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';
Expand All @@ -1505,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 = '<deferred>';
varOrigin = 'local-def';
Expand Down Expand Up @@ -1680,6 +1727,17 @@ ${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 = [];

Expand Down Expand Up @@ -1894,6 +1952,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 : '';
Expand Down
83 changes: 83 additions & 0 deletions packages/typegpu/tests/tgsl/wgslGenerator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2155,5 +2155,88 @@ 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:
- <root>
- fn*:fn
- fn*:fn(): '({ a: x } = obj)' cannot be used as an expression.]
`);
});

it('snapshots the source before an alias overwrites it', () => {
const fn = () => {
'use gpu';
let obj = d.vec2f(1, 2);
let x = 0;
({ yx: obj, x } = obj);

const a = obj;
({ yx: obj, x } = a);

return x;
};

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;
}"
`);
});
});
});
5 changes: 4 additions & 1 deletion packages/unplugin-typegpu/src/core/obfuscate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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])];
Expand Down