diff --git a/packages/eslint-plugin/src/rules/noUnsupportedSyntax.ts b/packages/eslint-plugin/src/rules/noUnsupportedSyntax.ts index be092ff776..e3d9bb33b2 100644 --- a/packages/eslint-plugin/src/rules/noUnsupportedSyntax.ts +++ b/packages/eslint-plugin/src/rules/noUnsupportedSyntax.ts @@ -29,8 +29,30 @@ export const noUnsupportedSyntax = createRule({ }); } + function validateFunctionParameters( + node: + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionExpression + | TSESTree.FunctionDeclaration, + ) { + if (directives.getEnclosingTypegpuFunction() !== node) { + return; + } + + for (const parameter of node.params) { + if ( + parameter.type !== 'Identifier' && + parameter.type !== 'AssignmentPattern' && + parameter.type !== 'ObjectPattern' + ) { + report(parameter, 'unsupported function parameter binding pattern'); + } + } + } + return { ArrowFunctionExpression(node) { + validateFunctionParameters(node); if (directives.getDirectiveStack().at(-2)?.directives.includes('use gpu')) { report(node, 'arrow function'); } @@ -40,6 +62,12 @@ export const noUnsupportedSyntax = createRule({ if (!directives.getEnclosingTypegpuFunction()) { return; } + + if (node.left.type === 'ArrayPattern') { + report(node.left, 'destructuring assignment'); + return; + } + if (unsupportedAssignmentOps.includes(node.operator)) { report(node, `assignment expression '${node.operator}'`); } @@ -104,12 +132,14 @@ export const noUnsupportedSyntax = createRule({ }, FunctionDeclaration(node) { + validateFunctionParameters(node); if (directives.getDirectiveStack().at(-2)?.directives.includes('use gpu')) { report(node, 'function declaration'); } }, FunctionExpression(node) { + validateFunctionParameters(node); if (directives.getDirectiveStack().at(-2)?.directives.includes('use gpu')) { report(node, 'function expression'); } @@ -209,8 +239,9 @@ export const noUnsupportedSyntax = createRule({ if (!directives.getEnclosingTypegpuFunction()) { return; } - if (node.id.type !== 'Identifier') { - report(node, 'variable declaration using destructuring'); + + if (node.id.type !== 'Identifier' && node.id.type !== 'ObjectPattern') { + report(node, 'unsupported variable binding pattern'); } }, @@ -220,6 +251,31 @@ export const noUnsupportedSyntax = createRule({ } report(node, 'yield expression'); }, + + ObjectPattern(node) { + const gpuFunction = directives.getEnclosingTypegpuFunction(); + if (!gpuFunction) { + return; + } + + const parent = node.parent; + // The outer pattern reports unsupported nested destructuring + if (parent.type === 'Property' && parent.parent.type === 'ObjectPattern') { + return; + } + + const isBlockDeclaration = + parent.type === 'VariableDeclarator' && + parent.parent.type === 'VariableDeclaration' && + parent.parent.parent.type === 'BlockStatement'; + const isFunctionParameter = parent === gpuFunction; + + if ((isBlockDeclaration || isFunctionParameter) && isSupportedObjectBindingPattern(node)) { + return; + } + + report(node, 'object destructuring'); + }, }; }), }); @@ -227,3 +283,13 @@ export const noUnsupportedSyntax = createRule({ const unsupportedAssignmentOps = ['&&=', '**=', '||=', '??=']; const unsupportedBinaryOps = ['==', '!=', 'in', 'instanceof', '|>']; const unsupportedUnaryOps = ['+', 'typeof', 'void', 'delete']; + +function isSupportedObjectBindingPattern(pattern: TSESTree.ObjectPattern): boolean { + return pattern.properties.every( + (prop) => + prop.type === 'Property' && + !prop.computed && + prop.key.type === 'Identifier' && + prop.value.type === 'Identifier', + ); +} diff --git a/packages/eslint-plugin/tests/rules/noUnsupportedSyntax.test.ts b/packages/eslint-plugin/tests/rules/noUnsupportedSyntax.test.ts index 1e18b8139e..bb4fee650b 100644 --- a/packages/eslint-plugin/tests/rules/noUnsupportedSyntax.test.ts +++ b/packages/eslint-plugin/tests/rules/noUnsupportedSyntax.test.ts @@ -11,6 +11,15 @@ describe('noUnsupportedSyntax', () => { "const cls = new (class { #priv = 1; fn = () => { 'use gpu'; const a = this.#priv; } } )()", "const fn = () => { 'use gpu'; switch (x) { case 1: break; } }", "const fn = () => { 'use gpu'; const obj = { [key]: 1 }; }", + "const fn = () => { 'use gpu'; const { a } = obj; }", + "const fn = () => { 'use gpu'; const { a, b: renamed } = obj; }", + "const fn = () => { 'use gpu'; let { a } = obj; }", + "const fn = () => { 'use gpu'; { const { a } = obj; } }", + "const fn = ({ a, b: renamed }) => { 'use gpu'; }", + "const fn = function({ a, b: renamed }) { 'use gpu'; }", + "function fn({ a, b: renamed }) { 'use gpu'; }", + 'const fn = () => { const { nested: { a } } = obj; }', + 'const fn = () => { let a = 0; for ({ a } of source) {} }', ], invalid: [ { @@ -290,11 +299,153 @@ describe('noUnsupportedSyntax', () => { ], }, { - code: "const fn = () => { 'use gpu'; const { a } = obj; }", + code: "const fn = () => { 'use gpu'; const { nested: { a } } = obj; }", errors: [ { messageId: 'unexpected', - data: { snippet: '{ a } = obj', syntax: 'variable declaration using destructuring' }, + data: { + snippet: '{ nested: { a } }', + syntax: 'object destructuring', + }, + }, + ], + }, + { + code: "const fn = () => { 'use gpu'; const { a = 1 } = obj; }", + errors: [ + { + messageId: 'unexpected', + data: { + snippet: '{ a = 1 }', + syntax: 'object destructuring', + }, + }, + { + messageId: 'unexpected', + data: { + snippet: 'a = 1', + syntax: 'assignment pattern (default parameter)', + }, + }, + ], + }, + { + code: "const fn = () => { 'use gpu'; const { ...rest } = obj; }", + errors: [ + { + messageId: 'unexpected', + data: { + snippet: '{ ...rest }', + syntax: 'object destructuring', + }, + }, + ], + }, + { + code: "const fn = () => { 'use gpu'; const { [key]: a } = obj; }", + errors: [ + { + messageId: 'unexpected', + data: { + snippet: '{ [key]: a }', + syntax: 'object destructuring', + }, + }, + ], + }, + { + code: "const fn = () => { 'use gpu'; for (const { value } = source; value < 10;) {} }", + errors: [ + { + messageId: 'unexpected', + data: { + snippet: '{ value }', + syntax: 'object destructuring', + }, + }, + ], + }, + { + code: "const fn = () => { 'use gpu'; for (const { value } of source) {} }", + errors: [ + { + messageId: 'unexpected', + data: { + snippet: '{ value }', + syntax: 'object destructuring', + }, + }, + ], + }, + { + code: "const fn = () => { 'use gpu'; let a = 0; for ({ a } of source) {} }", + errors: [ + { + messageId: 'unexpected', + data: { + snippet: '{ a }', + syntax: 'object destructuring', + }, + }, + ], + }, + { + code: "const fn = ([a]) => { 'use gpu'; }", + errors: [ + { + messageId: 'unexpected', + data: { + snippet: '[a]', + syntax: 'unsupported function parameter binding pattern', + }, + }, + ], + }, + { + code: "function fn({ nested: { a } }) { 'use gpu'; }", + errors: [ + { + messageId: 'unexpected', + data: { + snippet: '{ nested: { a } }', + syntax: 'object destructuring', + }, + }, + ], + }, + { + code: "const fn = () => { 'use gpu'; let a = 0; ({ a } = obj); }", + errors: [ + { + messageId: 'unexpected', + data: { + snippet: '{ a }', + syntax: 'object destructuring', + }, + }, + ], + }, + { + code: "const fn = () => { 'use gpu'; let a = 0; return ({ nested: { a } } = obj); }", + errors: [ + { + messageId: 'unexpected', + data: { + snippet: '{ nested: { a } }', + syntax: 'object destructuring', + }, + }, + ], + }, + { + code: "const fn = function(...args) { 'use gpu'; }", + errors: [ + { + messageId: 'unexpected', + data: { + snippet: '...args', + syntax: 'unsupported function parameter binding pattern', + }, }, ], }, @@ -303,7 +454,7 @@ describe('noUnsupportedSyntax', () => { errors: [ { messageId: 'unexpected', - data: { snippet: '[a] = arr', syntax: 'variable declaration using destructuring' }, + data: { snippet: '[a] = arr', syntax: 'unsupported variable binding pattern' }, }, ], }, diff --git a/packages/tinyest-for-wgsl/src/bindingPatterns.ts b/packages/tinyest-for-wgsl/src/bindingPatterns.ts new file mode 100644 index 0000000000..7ff8fd7de6 --- /dev/null +++ b/packages/tinyest-for-wgsl/src/bindingPatterns.ts @@ -0,0 +1,29 @@ +import type * as babel from '@babel/types'; +import type * as acorn from 'acorn'; +import { NodeTypeCatalog, type BindingPattern } from 'tinyest'; + +export function parseBindingPattern(node: babel.LVal | acorn.Pattern): BindingPattern { + if (node.type === 'Identifier') { + return node.name; + } + + if (node.type !== 'ObjectPattern') { + throw new Error(`Unsupported binding pattern: ${node.type}`); + } + + return [ + NodeTypeCatalog.objectPattern, + node.properties.map((prop): [name: string, alias: string] => { + if ( + (prop.type !== 'Property' && prop.type !== 'ObjectProperty') || + prop.computed || + prop.key.type !== 'Identifier' || + prop.value.type !== 'Identifier' + ) { + throw new Error('Only simple object destructuring is currently supported.'); + } + + return [prop.key.name, prop.value.name]; + }), + ]; +} diff --git a/packages/tinyest-for-wgsl/src/functionParts.ts b/packages/tinyest-for-wgsl/src/functionParts.ts index 4fad4b9330..54d6f1db1e 100644 --- a/packages/tinyest-for-wgsl/src/functionParts.ts +++ b/packages/tinyest-for-wgsl/src/functionParts.ts @@ -2,6 +2,7 @@ import type * as babel from '@babel/types'; import type * as acorn from 'acorn'; import * as tinyest from 'tinyest'; import type { JsNode } from './types.ts'; +import { parseBindingPattern } from './bindingPatterns.ts'; type FunctionNode = | acorn.ArrowFunctionExpression @@ -80,7 +81,7 @@ function validateFunction(functionNode: FunctionNode): void { } } -function parseParams(functionNode: FunctionNode): tinyest.FuncParameter[] { +function parseParams(functionNode: FunctionNode): tinyest.BindingPattern[] { return ( functionNode.params as ( | babel.Identifier @@ -88,27 +89,11 @@ function parseParams(functionNode: FunctionNode): tinyest.FuncParameter[] { | babel.ObjectPattern | acorn.ObjectPattern )[] - ).map((param) => - param.type === 'ObjectPattern' - ? { - type: tinyest.FuncParameterType.destructuredObject, - props: param.properties.flatMap((prop) => - (prop.type === /* acorn */ 'Property' || prop.type === /* babel */ 'ObjectProperty') && - prop.key.type === 'Identifier' && - prop.value.type === 'Identifier' - ? [{ name: prop.key.name, alias: prop.value.name }] - : [], - ), - } - : { - type: tinyest.FuncParameterType.identifier, - name: param.name, - }, - ); + ).map(parseBindingPattern); } export function extractFunctionParts(rootNode: JsNode): { - params: tinyest.FuncParameter[]; + params: tinyest.BindingPattern[]; body: acorn.BlockStatement | acorn.Expression | babel.BlockStatement | babel.Expression; } { const functionNode = unwrapToFunction(rootNode); diff --git a/packages/tinyest-for-wgsl/src/parsers.ts b/packages/tinyest-for-wgsl/src/parsers.ts index 9d13f948d7..2db3cdaa0c 100644 --- a/packages/tinyest-for-wgsl/src/parsers.ts +++ b/packages/tinyest-for-wgsl/src/parsers.ts @@ -20,7 +20,7 @@ import { extractFunctionParts } from './functionParts.ts'; const { NodeTypeCatalog: NODE } = tinyest; -function createContext(params: tinyest.FuncParameter[], opts: TranspilationOptions): Context { +function createContext(params: tinyest.BindingPattern[], opts: TranspilationOptions): Context { return { externalNames: new Map(), ignoreExternalDepth: 0, @@ -28,9 +28,9 @@ function createContext(params: tinyest.FuncParameter[], opts: TranspilationOptio stack: [ { declaredNames: params.flatMap((param) => - param.type === tinyest.FuncParameterType.identifier - ? param.name - : param.props.map((prop) => prop.alias), + tinyest.isObjectPattern(param) + ? param[1].map(([, alias]) => alias) + : tinyest.extractIdentifierString(param), ), }, ], diff --git a/packages/tinyest-for-wgsl/src/transpilers.ts b/packages/tinyest-for-wgsl/src/transpilers.ts index db0de4bd9e..fccd9faafc 100644 --- a/packages/tinyest-for-wgsl/src/transpilers.ts +++ b/packages/tinyest-for-wgsl/src/transpilers.ts @@ -2,6 +2,7 @@ import type * as acorn from 'acorn'; import type * as babel from '@babel/types'; import * as tinyest from 'tinyest'; import type { Context, JsNode, Transpile, Transpilers } from './types.ts'; +import { parseBindingPattern } from './bindingPatterns.ts'; const { NodeTypeCatalog: NODE } = tinyest; @@ -71,6 +72,10 @@ export const baseTranspilers = { }, AssignmentExpression(ctx, node, transpile) { + if (node.left.type === 'ObjectPattern' || node.left.type === 'ArrayPattern') { + throw new Error('Destructuring assignments are not supported.'); + } + const left = transpile(ctx, node.left) as tinyest.Expression; const right = transpile(ctx, node.right) as tinyest.Expression; @@ -145,11 +150,21 @@ export const baseTranspilers = { } const decl = node.declarations[0]; - ctx.ignoreExternalDepth++; - const id = transpile(ctx, decl.id) as tinyest.Identifier; - ctx.ignoreExternalDepth--; + if (decl.id.type === 'VoidPattern') { + throw new Error('Void patterns are not supported.'); + } - ctx.stack[ctx.stack.length - 1]?.declaredNames.push(extractId(id)); + const pattern = parseBindingPattern(decl.id); + let binding: tinyest.BindingPattern; + if (!tinyest.isObjectPattern(pattern)) { + ctx.ignoreExternalDepth++; + binding = transpile(ctx, decl.id) as tinyest.Identifier; + ctx.ignoreExternalDepth--; + ctx.stack[ctx.stack.length - 1]?.declaredNames.push(extractId(binding)); + } else { + binding = pattern; + ctx.stack[ctx.stack.length - 1]?.declaredNames.push(...pattern[1].map(([, alias]) => alias)); + } const init = decl.init ? (transpile(ctx, decl.init) as tinyest.Expression) : undefined; @@ -158,10 +173,10 @@ export const baseTranspilers = { } if (node.kind === 'const') { - return init !== undefined ? [NODE.const, id, init] : [NODE.const, id]; + return init !== undefined ? [NODE.const, binding, init] : [NODE.const, binding]; } - return init !== undefined ? [NODE.let, id, init] : [NODE.let, id]; + return init !== undefined ? [NODE.let, binding, init] : [NODE.let, binding]; }, IfStatement(ctx, node, transpile) { @@ -175,6 +190,13 @@ export const baseTranspilers = { }, ForStatement(ctx, node, transpile) { + if ( + node.init?.type === 'VariableDeclaration' && + node.init.declarations.some((declaration) => declaration.id.type === 'ObjectPattern') + ) { + throw new Error('Object destructuring in for loop initializers is not supported.'); + } + ctx.stack.push({ declaredNames: [] }); const init = node.init ? (transpile(ctx, node.init) as tinyest.Statement) : null; @@ -195,6 +217,13 @@ export const baseTranspilers = { }, ForOfStatement(ctx, node, transpile) { + if ( + node.left.type === 'VariableDeclaration' && + node.left.declarations.some((declaration) => declaration.id.type === 'ObjectPattern') + ) { + throw new Error('Object destructuring in for...of loops is not supported.'); + } + ctx.stack.push({ declaredNames: [] }); const loopVar = transpile(ctx, node.left) as tinyest.Const | tinyest.Let; diff --git a/packages/tinyest-for-wgsl/src/types.ts b/packages/tinyest-for-wgsl/src/types.ts index 3cce4d86a7..f891a85c23 100644 --- a/packages/tinyest-for-wgsl/src/types.ts +++ b/packages/tinyest-for-wgsl/src/types.ts @@ -27,7 +27,7 @@ export type Context = { }; export type TranspilationResult = { - params: tinyest.FuncParameter[]; + params: tinyest.BindingPattern[]; body: tinyest.Block; /** * All identifiers found in the function code that are not declared in the function itself. diff --git a/packages/tinyest-for-wgsl/tests/parsers.test.ts b/packages/tinyest-for-wgsl/tests/parsers.test.ts index 8eca3814d1..238a69ced4 100644 --- a/packages/tinyest-for-wgsl/tests/parsers.test.ts +++ b/packages/tinyest-for-wgsl/tests/parsers.test.ts @@ -1,6 +1,7 @@ import type { ClassDeclaration, ClassProperty, Expression } from '@babel/types'; import * as acorn from 'acorn'; import { describe, expect, it } from 'vitest'; +import { NodeTypeCatalog } from 'tinyest'; import { transpileBabelFn, transpileFn } from 'tinyest-for-wgsl'; import { dualTest, parseBabel, parseRollup } from './helpers.ts'; @@ -80,10 +81,7 @@ describe('transpileBabelFn and transpileAcornFn', () => { dualTest((p, transpileFn) => { const { params, body, externalNames } = transpileFn(p('(a, b) => a + b - c')); - expect(params).toStrictEqual([ - { type: 'i', name: 'a' }, - { type: 'i', name: 'b' }, - ]); + expect(params).toStrictEqual(['a', 'b']); expect(JSON.stringify(body)).toMatchInlineSnapshot( `"[0,[[10,[1,[1,"a","+","b"],"-","c"]]]]"`, ); @@ -169,19 +167,13 @@ describe('transpileBabelFn and transpileAcornFn', () => { ); expect(params).toStrictEqual([ - { - type: 'd', - props: [ - { - alias: 'pos', - name: 'pos', - }, - { - alias: 'b', - name: 'a', - }, + [ + NodeTypeCatalog.objectPattern, + [ + ['pos', 'pos'], + ['a', 'b'], ], - }, + ], ]); expect(externalNames).toMatchInlineSnapshot(`Map {}`); @@ -198,36 +190,21 @@ describe('transpileBabelFn and transpileAcornFn', () => { ); expect(params).toStrictEqual([ - { - type: 'i', - name: 'y', - }, - { - type: 'd', - props: [ - { - alias: 'pos', - name: 'pos', - }, - { - alias: 'b', - name: 'a', - }, + 'y', + [ + NodeTypeCatalog.objectPattern, + [ + ['pos', 'pos'], + ['a', 'b'], ], - }, - { - type: 'd', - props: [ - { - alias: 'c', - name: 'c', - }, - { - alias: 'd', - name: 'd', - }, + ], + [ + NodeTypeCatalog.objectPattern, + [ + ['c', 'c'], + ['d', 'd'], ], - }, + ], ]); expect(externalNames).toMatchInlineSnapshot(`Map {}`); @@ -525,6 +502,93 @@ describe('transpileBabelFn and transpileAcornFn', () => { `); }), ); + it( + 'parses destructured variable declarations', + dualTest((parse, transpileFn) => { + const { body, externalNames } = transpileFn( + parse(`() => { + const { x, position: pos } = source; + return x + pos; + }`), + ); + + expect(externalNames).toStrictEqual(new Map([['source', 'source']])); + expect(JSON.stringify(body)).toMatchInlineSnapshot( + `"[0,[[13,[108,[["x","x"],["position","pos"]]],"source"],[10,[1,"x","+","pos"]]]]"`, + ); + }), + ); + + it( + 'rejects unsupported object destructuring patterns', + dualTest((parse, transpileFn) => { + const patterns = [ + '{ nested: { value } }', + '{ value = 1 }', + '{ ...rest }', + '{ [key]: value }', + '{ "value": alias }', + ]; + + for (const pattern of patterns) { + expect(() => + transpileFn( + parse(`() => { + const ${pattern} = source; + }`), + ), + ).toThrow('Only simple object destructuring is currently supported.'); + } + }), + ); + + it( + 'rejects array destructuring', + dualTest((parse, transpileFn) => { + expect(() => + transpileFn( + parse(`() => { + const [value] = source; + }`), + ), + ).toThrow('Unsupported binding pattern: ArrayPattern'); + }), + ); + + it( + 'rejects object destructuring in loop headers', + dualTest((parse, transpileFn) => { + expect(() => + transpileFn( + parse(`() => { + for (const { value } = source; value < 10;) {} + }`), + ), + ).toThrow('Object destructuring in for loop initializers is not supported.'); + + expect(() => + transpileFn( + parse(`() => { + for (const { value } of source) {} + }`), + ), + ).toThrow('Object destructuring in for...of loops is not supported.'); + }), + ); + + it( + 'rejects destructuring assignments', + dualTest((parse, transpileFn) => { + expect(() => + transpileFn( + parse(`() => { + let a = 0; + ({ a } = source); + }`), + ), + ).toThrow('Destructuring assignments are not supported.'); + }), + ); }); describe('legacy transpileFn', () => { diff --git a/packages/tinyest-for-wgsl/tests/verboseNodes.test.ts b/packages/tinyest-for-wgsl/tests/verboseNodes.test.ts index c48b00bb83..55fb78a394 100644 --- a/packages/tinyest-for-wgsl/tests/verboseNodes.test.ts +++ b/packages/tinyest-for-wgsl/tests/verboseNodes.test.ts @@ -15,18 +15,9 @@ describe('verbose nodes', () => { expect(params).toMatchInlineSnapshot(` [ - { - "name": "a", - "type": "i", - }, - { - "name": "b", - "type": "i", - }, - { - "name": "c", - "type": "i", - }, + "a", + "b", + "c", ] `); expect(JSON.stringify(body)).toMatchInlineSnapshot( diff --git a/packages/tinyest/src/nodes.ts b/packages/tinyest/src/nodes.ts index 3d57bb0658..00d4c003ba 100644 --- a/packages/tinyest/src/nodes.ts +++ b/packages/tinyest/src/nodes.ts @@ -36,12 +36,15 @@ export const NodeTypeCatalog = { conditionalExpr: 105, nullLiteral: 106, booleanLiteral: 107, + objectPattern: 108, } as const; export type NodeTypeCatalog = typeof NodeTypeCatalog; /** Identifier can either be encoded as a node, or as a plain string */ export type Identifier = string | readonly [type: NodeTypeCatalog['identifier'], id: string]; +export const extractIdentifierString = (ident: Identifier): string => + typeof ident === 'string' ? ident : ident[1]; /** * Represents a return statement @@ -66,15 +69,15 @@ export type Block = readonly [type: NodeTypeCatalog['block'], Statement[]]; * Represents a let statement */ export type Let = - | readonly [type: NodeTypeCatalog['let'], identifier: Identifier] - | readonly [type: NodeTypeCatalog['let'], identifier: Identifier, value: Expression]; + | readonly [type: NodeTypeCatalog['let'], binding: BindingPattern] + | readonly [type: NodeTypeCatalog['let'], binding: BindingPattern, value: Expression]; /** * Represents a const statement */ export type Const = - | readonly [type: NodeTypeCatalog['const'], identifier: Identifier] - | readonly [type: NodeTypeCatalog['const'], identifier: Identifier, value: Expression]; + | readonly [type: NodeTypeCatalog['const'], binding: BindingPattern] + | readonly [type: NodeTypeCatalog['const'], binding: BindingPattern, value: Expression]; export type For = readonly [ type: NodeTypeCatalog['for'], @@ -281,12 +284,14 @@ export type Expression = export type AnyNode = Statement | Expression; +/** @deprecated Use {@link BindingPattern} instead */ export const FuncParameterType = { identifier: 'i', destructuredObject: 'd', } as const; -export type FuncParameter = +/** @deprecated Use {@link BindingPattern} instead */ +export type LegacyFuncParameter = | { type: typeof FuncParameterType.identifier; name: string; @@ -299,6 +304,21 @@ export type FuncParameter = }[]; }; +export type ObjectPattern = readonly [ + type: NodeTypeCatalog['objectPattern'], + props: readonly [name: string, alias: string][], +]; +export type BindingPattern = Identifier | ObjectPattern; +export const isObjectPattern = (binding: BindingPattern): binding is ObjectPattern => + typeof binding !== 'string' && binding[0] === NodeTypeCatalog.objectPattern; + +/** + * Transitional type + * Remove this alias along with {@link LegacyFuncParameter} after the migration period, + * when the metadata version is bumped and legacy FuncParameter support is dropped. + */ +export type FuncParameter = LegacyFuncParameter | BindingPattern; + /** * Map from an array/node to its source. * diff --git a/packages/typegpu/src/core/function/fnCore.ts b/packages/typegpu/src/core/function/fnCore.ts index 020207d21d..6b3c67f719 100644 --- a/packages/typegpu/src/core/function/fnCore.ts +++ b/packages/typegpu/src/core/function/fnCore.ts @@ -1,3 +1,4 @@ +import { extractIdentifierString, isObjectPattern } from 'tinyest'; import { getAttributesString } from '../../data/attributes.ts'; import { undecorate } from '../../data/dataTypes.ts'; import { type ResolvedSnippet, snip } from '../../data/snippet.ts'; @@ -225,10 +226,10 @@ export function createFnCore( // If an entrypoint implementation has a second argument, it represents the output schema. // We look at the identifier chosen by the user and add it to externals. const maybeSecondArg = ast.params[1]; - if (maybeSecondArg && maybeSecondArg.type === 'i' && functionType !== 'normal') { + if (maybeSecondArg && !isObjectPattern(maybeSecondArg) && functionType !== 'normal') { this.setExternals('out', { // oxlint-disable-next-line typescript/no-non-null-assertion -- entry functions cannot be shellless - [maybeSecondArg.name]: undecorate(returnType!), + [extractIdentifierString(maybeSecondArg)]: undecorate(returnType!), }); } diff --git a/packages/typegpu/src/resolutionCtx.ts b/packages/typegpu/src/resolutionCtx.ts index c183d04cdd..42d4bfcc70 100644 --- a/packages/typegpu/src/resolutionCtx.ts +++ b/packages/typegpu/src/resolutionCtx.ts @@ -58,7 +58,7 @@ import type { import { CodegenState, isSelfResolvable, NormalState, type FunctionArgument } from './types.ts'; import type { WgslEnableExtension } from './wgslExtensions.ts'; import { getName, hasTinyestMetadata, isNamable, setName } from './shared/meta.ts'; -import { FuncParameterType } from 'tinyest'; +import { extractIdentifierString, isObjectPattern } from 'tinyest'; import { accessProp } from './tgsl/accessProp.ts'; import { createIoSchema } from './core/function/ioSchema.ts'; import { isShelllessImpl } from './core/function/shelllessImpl.ts'; @@ -640,9 +640,9 @@ export class ResolutionCtxImpl implements ResolutionCtx { args.push(structArg); } - if (firstParam?.type === FuncParameterType.destructuredObject) { + if (firstParam !== undefined && isObjectPattern(firstParam)) { // Route each destructured prop to a positional arg or struct field. - for (const { name, alias } of firstParam.props) { + for (const [name, alias] of firstParam[1]) { const argInfo = positionalArgs.find((a) => a.schemaKey === name); if (argInfo) { const arg = createArgument(this.makeUniqueIdentifier(alias, 'block'), argInfo.type); @@ -652,7 +652,7 @@ export class ResolutionCtxImpl implements ResolutionCtx { scope.argAccess[alias] = createArgumentPropAccess(structArg.access, name); } } - } else if (firstParam?.type === FuncParameterType.identifier) { + } else if (firstParam !== undefined) { // Create named arg snippets, then a proxy for property access routing. const proxyEntries: Array<{ schemaKey: string; arg: FunctionArgumentAccess }> = []; for (const a of positionalArgs) { @@ -662,7 +662,8 @@ export class ResolutionCtxImpl implements ResolutionCtx { proxyEntries.push({ schemaKey: a.schemaKey, arg: arg.access }); } const router = new EntryInputRouter(structArg?.access, proxyEntries); - scope.argAccess[firstParam.name] = () => snip('N/A', router, 'argument'); + scope.argAccess[extractIdentifierString(firstParam)] = () => + snip('N/A', router, 'argument'); } else { // No first param: push positional args with schema key names. for (const a of positionalArgs) { @@ -690,46 +691,41 @@ export class ResolutionCtxImpl implements ResolutionCtx { : argType.addressSpace : 'argument'; - switch (astParam?.type) { - case FuncParameterType.identifier: { - const arg = createArgument( - this.makeUniqueIdentifier(astParam.name, 'block'), - argType, - origin, - ); - args.push(arg); - scope.argAccess[astParam.name] = arg.access; - break; + if (astParam === undefined) { + // Only push the argument if it's not an auto-struct. + // If we're not using an auto-struct, it's not going to + // have any properties anyway. + if (!(argType instanceof AutoStruct)) { + args.push({ + name: this.makeUniqueIdentifier(`_arg_${i}`, 'block'), + access: () => { + throw new Error( + `Unreachable: Accessing an argument that wasn't named in the function signature`, + ); + }, + decoratedType: argType, + used: false, + }); } - case FuncParameterType.destructuredObject: { - const objArg = createArgument( - this.makeUniqueIdentifier(`_arg_${i}`, 'block'), - argType, - origin, - ); - args.push(objArg); - for (const { name, alias } of astParam.props) { - scope.argAccess[alias] = createArgumentPropAccess(objArg.access, name); - } - break; - } - case undefined: { - // Only push the argument if it's not an auto-struct. - // If we're not using an auto-struct, it's not going to - // have any properties anyway. - if (!(argType instanceof AutoStruct)) { - args.push({ - name: this.makeUniqueIdentifier(`_arg_${i}`, 'block'), - access: () => { - throw new Error( - `Unreachable: Accessing an argument that wasn't named in the function signature`, - ); - }, - decoratedType: argType, - used: false, - }); - } + } else if (isObjectPattern(astParam)) { + const objArg = createArgument( + this.makeUniqueIdentifier(`_arg_${i}`, 'block'), + argType, + origin, + ); + args.push(objArg); + for (const [name, alias] of astParam[1]) { + scope.argAccess[alias] = createArgumentPropAccess(objArg.access, name); } + } else { + const paramName = extractIdentifierString(astParam); + const arg = createArgument( + this.makeUniqueIdentifier(paramName, 'block'), + argType, + origin, + ); + args.push(arg); + scope.argAccess[paramName] = arg.access; } } } diff --git a/packages/typegpu/src/shared/normalizeMetadata.ts b/packages/typegpu/src/shared/normalizeMetadata.ts index 0d821064f6..43a5ab1db6 100644 --- a/packages/typegpu/src/shared/normalizeMetadata.ts +++ b/packages/typegpu/src/shared/normalizeMetadata.ts @@ -1,10 +1,11 @@ -import type { Block, FuncParameter } from 'tinyest'; +import type { BindingPattern, Block, FuncParameter, LegacyFuncParameter } from 'tinyest'; +import { NodeTypeCatalog } from 'tinyest'; import { safeStringify } from './stringify.ts'; export interface RawMetadataV1 { v: 1; name: string; - ast: { params: FuncParameter[]; body: Block; externalNames: string[] }; + ast: { params: LegacyFuncParameter[]; body: Block; externalNames: string[] }; externals: // Passing a record happens prior to version 0.9.0 Record | (() => Record); @@ -26,7 +27,7 @@ export type RawMetadata = RawMetadataV1 | RawMetadataV2; * Holds normalized function metadata required for WGSL generation */ export interface Metadata { - ast: { params: FuncParameter[]; body: Block }; + ast: { params: BindingPattern[]; body: Block }; externals: () => Record; } @@ -50,13 +51,37 @@ export function normalizeMetadata(meta: RawMetadata): Metadata { if (meta.v === 1) { const rawExternals = meta.externals; const externals = typeof rawExternals === 'function' ? rawExternals : () => rawExternals; - return { ...meta, externals }; + return { + ...meta, + ast: { ...meta.ast, params: meta.ast.params.map(normalizeParameter) }, + externals, + }; } if (meta.v === 2) { - const externals = normalizeExternalsV2(meta?.externals); - return { ...meta, externals: () => externals }; + const externals = normalizeExternalsV2(meta.externals); + return { + ...meta, + ast: { ...meta.ast, params: meta.ast.params.map(normalizeParameter) }, + externals: () => externals, + }; } throw new Error(`Unrecognized TypeGPU metadata format: ${safeStringify(meta)}`); } + +function normalizeParameter(param: FuncParameter): BindingPattern { + // Only legacy parameters have a `type` property. + if (typeof param === 'string' || !('type' in param)) { + return param; + } + + if (param.type === 'i') { + return param.name; + } + + return [ + NodeTypeCatalog.objectPattern, + param.props.map(({ name, alias }): [string, string] => [name, alias]), + ]; +} diff --git a/packages/typegpu/src/shared/tseynit.ts b/packages/typegpu/src/shared/tseynit.ts index c9180215d0..617804322a 100644 --- a/packages/typegpu/src/shared/tseynit.ts +++ b/packages/typegpu/src/shared/tseynit.ts @@ -10,6 +10,16 @@ export function stringifyNode(node: tinyest.AnyNode): string { return stringifyStatement(node, ''); } +function stringifyBindingPattern(binding: tinyest.BindingPattern): string { + if (!tinyest.isObjectPattern(binding)) { + return stringifyExpression(binding); + } + + const props = binding[1].map(([name, alias]) => (name === alias ? name : `${name}: ${alias}`)); + + return `{ ${props.join(', ')} }`; +} + function stringifyStatement(node: tinyest.Statement, indent: string): string { if (isExpression(node)) { return `${indent}${stringifyExpression(node)};`; @@ -36,7 +46,7 @@ function stringifyStatement(node: tinyest.Statement, indent: string): string { } if (node[0] === NODE.let) { - const decl = `${indent}let ${stringifyExpression(node[1])}`; + const decl = `${indent}let ${stringifyBindingPattern(node[1])}`; if (node[2] !== undefined) { return `${decl} = ${stringifyExpression(node[2])};`; } @@ -44,7 +54,7 @@ function stringifyStatement(node: tinyest.Statement, indent: string): string { } if (node[0] === NODE.const) { - const decl = `${indent}const ${stringifyExpression(node[1])}`; + const decl = `${indent}const ${stringifyBindingPattern(node[1])}`; if (node[2] !== undefined) { return `${decl} = ${stringifyExpression(node[2])};`; } @@ -75,7 +85,7 @@ function stringifyStatement(node: tinyest.Statement, indent: string): string { if (node[0] === NODE.forOf) { const leftKind = node[1][0] === NODE.const ? 'const' : 'let'; - const leftName = stringifyExpression(node[1][1]); + const leftName = stringifyBindingPattern(node[1][1]); const right = stringifyExpression(node[2]); const body = stringifyStatement(node[3], indent); return `${indent}for (${leftKind} ${leftName} of ${right}) ${body}`; diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index 26f66067e4..2dea04c9d7 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -506,7 +506,7 @@ export class WgslGenerator implements ShaderGenerator { private _expressionImpl(expression: tinyest.Expression): Snippet { if (isId(expression)) { - return this._identifier(extractId(expression)); + return this._identifier(tinyest.extractIdentifierString(expression)); } if (isBool(expression)) { @@ -773,7 +773,7 @@ export class WgslGenerator implements ShaderGenerator { // Member Access const [_, targetNode, propertyNode] = expression; const target = this._expression(targetNode); - const property = extractId(propertyNode); + const property = tinyest.extractIdentifierString(propertyNode); const accessed = accessProp(target, property); if (!accessed) { @@ -1474,9 +1474,43 @@ Try 'return ${typeStr}(${str});' instead. return `${this.ctx.pre}return;`; } + protected _objectDestructuringStatement( + kind: 'let' | 'const', + props: readonly [name: string, alias: string][], + eqNode: tinyest.Expression, + ): ResolvedStatement { + let sourceNode: tinyest.Expression = eqNode; + let temporaryDeclaration: ResolvedStatement | undefined; + + if (!isId(eqNode)) { + const temporaryId = `#destructured`; + temporaryDeclaration = this._constStatement([NODE.const, temporaryId, eqNode]); + sourceNode = temporaryId; + } else { + sourceNode = '#destructured_source'; + this.ctx.setBlockExternals({ [sourceNode]: this._expression(eqNode) }); + } + + const propertyDeclaration = props.map(([name, alias]) => { + const propertyAccess: tinyest.MemberAccess = [NODE.memberAccess, sourceNode, name]; + + return kind === 'const' + ? this._constStatement([NODE.const, alias, propertyAccess]) + : this._letStatement([NODE.let, alias, propertyAccess]); + }); + + const declarations = temporaryDeclaration + ? [temporaryDeclaration, ...propertyDeclaration] + : propertyDeclaration; + + return { + code: declarations.map((declaration) => declaration.code).join('\n'), + definesInNearestScope: declarations.some((declaration) => declaration.code.length > 0), + }; + } + protected _letStatement(statement: tinyest.Let): ResolvedStatement { - const [_, rawIdNode, eqNode] = statement; - const rawId = extractId(rawIdNode); + const [_, binding, eqNode] = statement; if (eqNode === undefined) { throw new Error( @@ -1484,6 +1518,11 @@ Try 'return ${typeStr}(${str});' instead. ); } + if (!isId(binding)) { + return this._objectDestructuringStatement('let', binding[1], eqNode); + } + const rawId = tinyest.extractIdentifierString(binding); + const eq = this._expression(eqNode); if (eq.value instanceof RefOperator) { @@ -1547,8 +1586,7 @@ Try 'return ${typeStr}(${str});' instead. } protected _constStatement(statement: tinyest.Const): ResolvedStatement { - const [_, rawIdNode, eqNode] = statement; - const rawId = extractId(rawIdNode); + const [_, binding, eqNode] = statement; if (eqNode === undefined) { throw new Error( @@ -1556,6 +1594,11 @@ Try 'return ${typeStr}(${str});' instead. ); } + if (!isId(binding)) { + return this._objectDestructuringStatement('const', binding[1], eqNode); + } + const rawId = tinyest.extractIdentifierString(binding); + const eq = this._expression(eqNode); if (eq.value instanceof RefOperator) { @@ -1705,7 +1748,7 @@ Try 'return ${typeStr}(${str});' instead. protected _statement(statement: tinyest.Statement): ResolvedStatement { // TODO(#3078): Remove this check. if (isId(statement)) { - const item = this.ctx.getById(extractId(statement)); + const item = this.ctx.getById(tinyest.extractIdentifierString(statement)); if (item?.value instanceof TgpuDeclareImpl) { this.ctx.resolveSnippet(item); return { code: '', definesInNearestScope: false }; @@ -1783,6 +1826,14 @@ ${this.ctx.pre}else ${alternate}`, if (statement[0] === NODE.for) { const [_, init, condition, update, body] = statement; + + if ( + Array.isArray(init) && + (init[0] === NODE.let || init[0] === NODE.const) && + !isId(init[1]) + ) { + throw new WgslTypeError('Object destructuring in for loop initializers is not supported.'); + } const prevUnrollingChain = this.#unrollingChain; this.#unrollingChain = []; @@ -1846,7 +1897,12 @@ ${this.ctx.pre}else ${alternate}`, const shouldUnroll = iterableExpr.value instanceof UnrollableIterable; const iterableSnippet = shouldUnroll ? iterableExpr.value.snippet : iterableExpr; const range = forOfUtils.getRangeSnippets(this.ctx, iterableSnippet, shouldUnroll); - const originalLoopVarName = extractId(loopVar[1]); + const loopBinding = loopVar[1]; + if (!isId(loopBinding)) { + throw new WgslTypeError('Destructuring in for...of loops is not supported yet.'); + } + + const originalLoopVarName = tinyest.extractIdentifierString(loopBinding); const blockified = blockifySingleStatement(body); if (shouldUnroll) { @@ -2248,7 +2304,7 @@ function extractObject(expr: tinyest.Expression): string | undefined { object = object[1]; } if (isId(object)) { - return extractId(object); + return tinyest.extractIdentifierString(object); } } @@ -2256,13 +2312,6 @@ function isId(expr: unknown): expr is tinyest.Identifier { return typeof expr === 'string' || (Array.isArray(expr) && expr[0] === NODE.identifier); } -function extractId(ident: tinyest.Identifier): string { - if (typeof ident === 'string') { - return ident; - } - return ident[1]; -} - function isBool(expr: unknown): expr is tinyest.Bool { return typeof expr === 'boolean' || (Array.isArray(expr) && expr[0] === NODE.booleanLiteral); } diff --git a/packages/typegpu/src/types.ts b/packages/typegpu/src/types.ts index 3c66e1031f..9ccd04f2da 100644 --- a/packages/typegpu/src/types.ts +++ b/packages/typegpu/src/types.ts @@ -1,4 +1,4 @@ -import type { Block, FuncParameter } from 'tinyest'; +import type { BindingPattern, Block } from 'tinyest'; import type { IndexFlag, TgpuBuffer, UniformFlag, VertexFlag } from './core/buffer/buffer.ts'; import type { TgpuConst } from './core/constant/tgpuConstant.ts'; import type { TgpuDeclare } from './core/declare/tgpuDeclare.ts'; @@ -91,7 +91,7 @@ export interface ResolveFunctionOptions { */ returnType: BaseData | undefined; body: Block; - params: FuncParameter[]; + params: BindingPattern[]; externalMap: Record; /** * For entry functions: positional args and optional data struct. diff --git a/packages/typegpu/tests/internal/metadata.test.ts b/packages/typegpu/tests/internal/metadata.test.ts index 6cbd5f9885..bdca13dc5a 100644 --- a/packages/typegpu/tests/internal/metadata.test.ts +++ b/packages/typegpu/tests/internal/metadata.test.ts @@ -4,10 +4,11 @@ import * as tinyest from 'tinyest'; import { it } from 'typegpu-testing-utility'; import { getName, setName, type INTERNAL_GlobalExt } from '../../src/shared/meta.ts'; import { tgpu } from '../../src/index.js'; -import type { - RawMetadata, - RawMetadataV1, - RawMetadataV2, +import { + normalizeMetadata, + type RawMetadata, + type RawMetadataV1, + type RawMetadataV2, } from '../../src/shared/normalizeMetadata.ts'; // functions in this file intentionally have no 'use gpu' directive @@ -110,6 +111,129 @@ describe('meta', () => { `); }); + describe.each([1, 2] as const)('legacy v%i parameters', (version) => { + function legacyMetadata( + params: tinyest.LegacyFuncParameter[], + body: tinyest.Block, + ): RawMetadata { + return version === 1 + ? { v: 1, name: 'fn', externals: {}, ast: { params, body, externalNames: [] } } + : { v: 2, name: 'fn', externals: {}, ast: { params, body } }; + } + + it('normalizes parameters without changing the input metadata or body', () => { + const params: tinyest.LegacyFuncParameter[] = [ + { type: 'i', name: 'offset' }, + { + type: 'd', + props: [ + { name: 'x', alias: 'first' }, + { name: 'y', alias: 'y' }, + ], + }, + { type: 'd', props: [] }, + ]; + const body: tinyest.Block = [NODE.block, [[NODE.return, 'offset']]]; + const meta = legacyMetadata(params, body); + const original = structuredClone(meta); + + const normalized = normalizeMetadata(meta); + + expect(normalized.ast.params).toStrictEqual([ + 'offset', + [ + NODE.objectPattern, + [ + ['x', 'first'], + ['y', 'y'], + ], + ], + [NODE.objectPattern, []], + ]); + expect(normalized.ast.body).toBe(body); + expect(meta).toStrictEqual(original); + expect(meta.ast.params).toBe(params); + }); + + it('resolves named parameters and destructuring aliases to WGSL', () => { + const implementation = (offset: number, { x: first, y }: d.v2f) => offset + first + y; + assignMetadata( + implementation, + legacyMetadata( + [ + { type: 'i', name: 'offset' }, + { + type: 'd', + props: [ + { name: 'x', alias: 'first' }, + { name: 'y', alias: 'y' }, + ], + }, + ], + [ + NODE.block, + [ + [ + NODE.return, + [NODE.binaryExpr, [NODE.binaryExpr, 'offset', '+', 'first'], '+', 'y'], + ], + ], + ], + ), + ); + const fn = tgpu.fn([d.f32, d.vec2f], d.f32)(implementation).$name('sum'); + + expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` + "fn sum(offset: f32, _arg_1: vec2f) -> f32 { + return ((offset + _arg_1.x) + _arg_1.y); + }" + `); + }); + }); + + it('preserves current v2 parameters while normalizing legacy parameters in the same list', () => { + const identifier: tinyest.Identifier = [NODE.identifier, 'mapped']; + const pattern: tinyest.ObjectPattern = [NODE.objectPattern, [['position', 'pos']]]; + const emptyPattern: tinyest.ObjectPattern = [NODE.objectPattern, []]; + const body: tinyest.Block = [ + NODE.block, + [[NODE.const, [NODE.identifier, 'local'], [NODE.numericLiteral, '1']]], + ]; + const meta: RawMetadataV2 = { + v: 2, + name: 'fn', + externals: {}, + ast: { + params: [ + 'plain', + { type: 'i', name: 'legacy' }, + identifier, + { type: 'd', props: [{ name: 'x', alias: 'first' }] }, + pattern, + emptyPattern, + ], + body, + }, + }; + const original = structuredClone(meta); + + const normalized = normalizeMetadata(meta); + + expect(normalized.ast.params).toStrictEqual([ + 'plain', + 'legacy', + identifier, + [NODE.objectPattern, [['x', 'first']]], + pattern, + emptyPattern, + ]); + expect(normalized.ast.params[2]).toBe(identifier); + expect(normalized.ast.params[4]).toBe(pattern); + expect(normalized.ast.params[5]).toBe(emptyPattern); + expect(normalized.ast.body).toBe(body); + expect(meta).toStrictEqual(original); + }); + it('throws a readable error when metadata version is not recognized', () => { const fn = () => {}; assignMetadata(fn, {} as RawMetadata); diff --git a/packages/typegpu/tests/internal/tseynit.test.ts b/packages/typegpu/tests/internal/tseynit.test.ts index bf6f17dc8b..b7a0b9c591 100644 --- a/packages/typegpu/tests/internal/tseynit.test.ts +++ b/packages/typegpu/tests/internal/tseynit.test.ts @@ -487,5 +487,21 @@ describe('ast to JS transformation', () => { expect(stringifyNode(ast)).toMatchInlineSnapshot(`"ident || true"`); }); + + it('handles object destructuring declarations', () => { + const node: tinyest.Const = [ + N.const, + [ + N.objectPattern, + [ + ['position', 'position'], + ['velocity', 'vel'], + ], + ], + 'particle', + ]; + + expect(stringifyNode(node)).toBe('const { position, velocity: vel } = particle;'); + }); }); }); diff --git a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts index aa4afd180e..69b4b397c6 100644 --- a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts +++ b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts @@ -2733,4 +2733,272 @@ describe('WgslGenerator', () => { `); }); }); + + describe('object destructuring', () => { + it('inlines blocks with empty destructuring of an identifier', () => { + const fn = () => { + 'use gpu'; + const source = d.vec2i(2, 3); + let result = 0; + { + // oxlint-disable-next-line no-empty-pattern -- Regression for empty destructuring. + const {} = source; + result += source.x; + } + return result; + }; + + expect(fn()).toBe(2); + expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` + "fn fn_1() -> i32 { + let source = vec2i(2, 3); + var result = 0; + result += source.x; + return result; + }" + `); + }); + + it('retains evaluation and scope for empty destructuring of a function call', () => { + const createSource = () => { + 'use gpu'; + return d.vec2i(2, 3); + }; + + const fn = () => { + 'use gpu'; + let result = 0; + { + // oxlint-disable-next-line no-empty-pattern -- Regression for empty destructuring. + const {} = createSource(); + result += 1; + } + return result; + }; + + expect(fn()).toBe(1); + expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` + "fn createSource() -> vec2i { + return vec2i(2, 3); + } + + fn fn_1() -> i32 { + var result = 0; + { + let destructured = createSource(); + result += 1i; + } + return result; + }" + `); + }); + + it('reads an external object getter once per destructuring declaration', () => { + let reads = 0; + const source = { + get value() { + reads++; + return { x: reads, y: reads }; + }, + }; + + const fn = () => { + 'use gpu'; + const { x, y } = source.value; + const { x: a, y: b } = source.value; + return x + y + a + b; + }; + + expect(fn()).toBe(6); + expect(reads).toBe(2); + reads = 0; + + expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` + "fn fn_1() -> i32 { + const x = 1; + const y = 1; + const a = 2; + const b = 2; + return (((x + y) + a) + b); + }" + `); + expect(reads).toBe(2); + }); + + it('destructures an identifier without a temporary', () => { + const Pair = d.struct({ + a: d.i32, + b: d.i32, + }); + + const fn = () => { + 'use gpu'; + const { a, b: c } = Pair({ a: 2, b: 3 }); + return a + c; + }; + + expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` + "struct Pair { + a: i32, + b: i32, + } + + fn fn_1() -> i32 { + let destructured = Pair(2i, 3i); + let a = destructured.a; + let c = destructured.b; + return (a + c); + }" + `); + }); + + it('evaluates an expression only once', () => { + const Pair = d.struct({ + a: d.i32, + b: d.i32, + }); + + const createPair = () => { + 'use gpu'; + return Pair({ a: 2, b: 3 }); + }; + + const fn = () => { + 'use gpu'; + const { a, b: renamed } = createPair(); + return a + renamed; + }; + + expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` + "struct Pair { + a: i32, + b: i32, + } + + fn createPair() -> Pair { + return Pair(2i, 3i); + } + + fn fn_1() -> i32 { + let destructured = createPair(); + let a = destructured.a; + let renamed = destructured.b; + return (a + renamed); + }" + `); + }); + + it('does not cause name collisions', () => { + const Schema = d.struct({ prop: d.vec3f }); + + const fn = () => { + 'use gpu'; + const destructured = 1; + const { prop: a } = Schema(); + const { prop: b } = Schema(); + }; + + expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` + "struct Schema { + prop: vec3f, + } + + fn fn_1() { + const destructured = 1; + var destructured_1 = Schema(); + let a = (&destructured_1.prop); + var destructured_2 = Schema(); + let b = (&destructured_2.prop); + }" + `); + }); + + it('does not conflict with externals or user identifiers named destructured', () => { + const Pair = d.struct({ + a: d.i32, + b: d.i32, + }); + const destructured = () => { + 'use gpu'; + return Pair({ a: 2, b: 3 }); + }; + + const fn = () => { + 'use gpu'; + const { a: x, b: y } = destructured(); + const destructured_1 = 1; + return x + y + destructured_1; + }; + + expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` + "struct Pair { + a: i32, + b: i32, + } + + fn destructured() -> Pair { + return Pair(2i, 3i); + } + + fn fn_1() -> i32 { + let destructured_1 = destructured(); + let x = destructured_1.a; + let y = destructured_1.b; + const destructured_1_1 = 1; + return ((x + y) + destructured_1_1); + }" + `); + }); + + it('allows mutable destructured variables', () => { + const Pair = d.struct({ + a: d.i32, + b: d.i32, + }); + + const fn = () => { + 'use gpu'; + + let { a, b: renamed } = Pair({ a: 2, b: 3 }); + a += renamed; + + return a; + }; + + expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` + "struct Pair { + a: i32, + b: i32, + } + + fn fn_1() -> i32 { + let destructured = Pair(2i, 3i); + var a = destructured.a; + let renamed = destructured.b; + a += renamed; + return a; + }" + `); + }); + + it('throws a descriptive error when destructuring a reference to let', () => { + const Schema = d.struct({ prop: d.vec3f }); + const testFn = () => { + 'use gpu'; + const struct = Schema(); + let { prop } = struct; + }; + + expect(() => tgpu.resolve([testFn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:testFn + - fn*:testFn(): 'let prop = #destructured_source.prop' is invalid, because references cannot be assigned to 'let' variable declarations. + ----- + - Try 'let prop = vec3f(#destructured_source.prop)' if you need to reassign 'prop' later + - Try 'const prop = #destructured_source.prop' if you won't reassign 'prop' later. + -----] + `); + }); + }); }); diff --git a/packages/unplugin-typegpu/src/core/obfuscate.ts b/packages/unplugin-typegpu/src/core/obfuscate.ts index 462dd5233d..6a7ea50d44 100644 --- a/packages/unplugin-typegpu/src/core/obfuscate.ts +++ b/packages/unplugin-typegpu/src/core/obfuscate.ts @@ -60,19 +60,28 @@ class Context { } } +function obfuscateBindingPattern( + ctx: Context, + binding: tinyest.BindingPattern, +): tinyest.BindingPattern { + if (!tinyest.isObjectPattern(binding)) { + return obf(ctx, binding); + } + + // We cannot obfuscate destructured names, because WGSL generation relies on these names (e.g. `$instanceIndex`). + return [ + tinyest.NodeTypeCatalog.objectPattern, + binding[1].map(([name, alias]): [name: string, alias: string] => [ + name, + ctx.obfuscator.obfuscate(alias), + ]), + ]; +} + export function obfuscate(fn: TranspilationResult): TranspilationResult { const ctx = new Context(); - const params = fn.params.map((param) => { - if (param.type === 'i') { - return { ...param, name: ctx.obfuscator.obfuscate(param.name) }; - } - // We cannot obfuscate destructured names, because WGSL generation relies on these names (e.g. `$instanceIndex`). - return { - ...param, - props: param.props.map((prop) => ({ ...prop, alias: ctx.obfuscator.obfuscate(prop.alias) })), - }; - }); + const params = fn.params.map((param) => obfuscateBindingPattern(ctx, param)); const body = obf(ctx, fn.body); @@ -123,13 +132,13 @@ const visitors = { }, let(ctx: Context, node: tinyest.Let) { return node.length === 2 - ? [NODE.let, obf(ctx, node[1])] - : [NODE.let, obf(ctx, node[1]), obf(ctx, node[2])]; + ? [NODE.let, obfuscateBindingPattern(ctx, node[1])] + : [NODE.let, obfuscateBindingPattern(ctx, node[1]), obf(ctx, node[2])]; }, const(ctx: Context, node: tinyest.Const) { return node.length === 2 - ? [NODE.const, obf(ctx, node[1])] - : [NODE.const, obf(ctx, node[1]), obf(ctx, node[2])]; + ? [NODE.const, obfuscateBindingPattern(ctx, node[1])] + : [NODE.const, obfuscateBindingPattern(ctx, node[1]), obf(ctx, node[2])]; }, for(ctx: Context, node: tinyest.For) { return [NODE.for, obf(ctx, node[1]), obf(ctx, node[2]), obf(ctx, node[3]), obf(ctx, node[4])]; @@ -202,6 +211,9 @@ const visitors = { } return [NODE.identifier, obf(ctx, node[1])]; }, + objectPattern(_ctx: Context, _node: tinyest.ObjectPattern) { + throw new Error('Object patterns are obfuscated through declaration bindings.'); + }, } as const satisfies { [N in keyof typeof NODE]: ( ctx: Context, diff --git a/packages/unplugin-typegpu/test/obfuscation.test.ts b/packages/unplugin-typegpu/test/obfuscation.test.ts index aa69082b59..79073ce442 100644 --- a/packages/unplugin-typegpu/test/obfuscation.test.ts +++ b/packages/unplugin-typegpu/test/obfuscation.test.ts @@ -36,10 +36,7 @@ describe('plugin obfuscation', () => { v: 2, name: "fn", ast: { - params: [{ - type: "i", - name: "a" - }], + params: ["a"], body: [0, [[13, "b", [5, "3"]], [10, [1, [1, "c", "+", "a"], "+", "b"]]]] }, externals: { @@ -62,7 +59,7 @@ describe('plugin obfuscation', () => { }), { v: 2, name: "fn", - ast: {"params":[{"type":"i","name":"a"}],"body":[0,[[13,"b",[5,"3"]],[10,[1,[1,"c","+","a"],"+","b"]]]]}, + ast: {"params":["a"],"body":[0,[[13,"b",[5,"3"]],[10,[1,[1,"c","+","a"],"+","b"]]]]}, externals: {"c":() => external.n} }) && $.f)({})); @@ -294,14 +291,8 @@ describe('obfuscate', () => { expect(params).toMatchInlineSnapshot(` [ - { - "name": "a", - "type": "i", - }, - { - "name": "b", - "type": "i", - }, + "a", + "b", ] `); expect(stringifyNode(body)).toMatchInlineSnapshot(` @@ -320,19 +311,16 @@ describe('obfuscate', () => { expect(params).toMatchInlineSnapshot(` [ - { - "name": "a", - "type": "i", - }, - { - "props": [ - { - "alias": "b", - "name": "prop", - }, + "a", + [ + 108, + [ + [ + "prop", + "b", + ], ], - "type": "d", - }, + ], ] `); expect(stringifyNode(body)).toMatchInlineSnapshot(` @@ -351,23 +339,20 @@ describe('obfuscate', () => { expect(params).toMatchInlineSnapshot(` [ - { - "name": "a", - "type": "i", - }, - { - "props": [ - { - "alias": "b", - "name": "prop", - }, - { - "alias": "c", - "name": "other", - }, + "a", + [ + 108, + [ + [ + "prop", + "b", + ], + [ + "other", + "c", + ], ], - "type": "d", - }, + ], ] `); expect(stringifyNode(body)).toMatchInlineSnapshot(` @@ -386,10 +371,7 @@ describe('obfuscate', () => { expect(params).toMatchInlineSnapshot(` [ - { - "name": "a", - "type": "i", - }, + "a", ] `); expect(stringifyNode(body)).toMatchInlineSnapshot(` @@ -409,10 +391,7 @@ describe('obfuscate', () => { expect(params).toMatchInlineSnapshot(` [ - { - "name": "a", - "type": "i", - }, + "a", ] `); expect(stringifyNode(body)).toMatchInlineSnapshot(` @@ -569,10 +548,7 @@ describe('obfuscate', () => { expect(params).toMatchInlineSnapshot(` [ - { - "name": "a", - "type": "i", - }, + "a", ] `); expect(stringifyNode(body)).toMatchInlineSnapshot(` @@ -655,10 +631,7 @@ describe('obfuscate', () => { expect(params).toMatchInlineSnapshot(` [ - { - "name": "a", - "type": "i", - }, + "a", ] `); expect(stringifyNode(body)).toMatchInlineSnapshot(` @@ -704,10 +677,7 @@ describe('obfuscate', () => { expect(params).toMatchInlineSnapshot(` [ - { - "name": "a", - "type": "i", - }, + "a", ] `); expect(stringifyNode(body)).toMatchInlineSnapshot(` @@ -717,4 +687,29 @@ describe('obfuscate', () => { `); expect(externalNames).toMatchInlineSnapshot(`Map {}`); }); + + it('obfuscates destructured variable declarations', () => { + const code = `(source) => { + const { position, velocity: localVelocity } = source; + return position + localVelocity; + }`; + + const transpiled = transpileBabelFn(parse(code)); + const { params, body, externalNames } = obfuscate(transpiled); + + expect(params).toMatchInlineSnapshot(` + [ + "a", + ] + `); + + expect(stringifyNode(body)).toMatchInlineSnapshot(` + "{ + const { position: b, velocity: c } = a; + return b + c; + }" + `); + + expect(externalNames).toMatchInlineSnapshot(`Map {}`); + }); }); diff --git a/packages/unplugin-typegpu/test/tgsl-transpiling.test.ts b/packages/unplugin-typegpu/test/tgsl-transpiling.test.ts index ab3f9b116c..2785422976 100644 --- a/packages/unplugin-typegpu/test/tgsl-transpiling.test.ts +++ b/packages/unplugin-typegpu/test/tgsl-transpiling.test.ts @@ -40,10 +40,7 @@ describe('[BABEL] plugin for transpiling tgsl functions to tinyest', () => { v: 2, name: undefined, ast: { - params: [{ - type: "i", - name: "input" - }], + params: ["input"], body: [0, [[13, "tmp", [7, [7, "counter", "$"], "x"]], [2, [7, [7, "counter", "$"], "x"], "=", [7, [7, "counter", "$"], "y"]], [2, [7, [7, "counter", "$"], "y"], "+=", "tmp"], [2, [7, [7, "counter", "$"], "z"], "+=", [6, "d.f32", [[7, [7, "input", "num"], "x"]]]]]] }, externals: { @@ -82,10 +79,7 @@ describe('[BABEL] plugin for transpiling tgsl functions to tinyest', () => { v: 2, name: undefined, ast: { - params: [{ - type: "i", - name: "input" - }], + params: ["input"], body: [0, [[13, "x", true]]] }, externals: {} @@ -159,10 +153,7 @@ describe('[BABEL] plugin for transpiling tgsl functions to tinyest', () => { v: 2, name: undefined, ast: { - params: [{ - type: "i", - name: "input" - }], + params: ["input"], body: [0, [[13, "x", true]]] }, externals: {} @@ -175,10 +166,7 @@ describe('[BABEL] plugin for transpiling tgsl functions to tinyest', () => { v: 2, name: undefined, ast: { - params: [{ - type: "i", - name: "input" - }], + params: ["input"], body: [0, [[13, "x", true]]] }, externals: {} @@ -191,10 +179,7 @@ describe('[BABEL] plugin for transpiling tgsl functions to tinyest', () => { v: 2, name: undefined, ast: { - params: [{ - type: "i", - name: "input" - }], + params: ["input"], body: [0, [[13, "x", true]]] }, externals: {} @@ -284,7 +269,7 @@ describe('[ROLLUP] plugin for transpiling tgsl functions to tinyest', () => { }), { v: 2, name: undefined, - ast: {"params":[{"type":"i","name":"input"}],"body":[0,[[13,"tmp",[7,[7,"counter","$"],"x"]],[2,[7,[7,"counter","$"],"x"],"=",[7,[7,"counter","$"],"y"]],[2,[7,[7,"counter","$"],"y"],"+=","tmp"],[2,[7,[7,"counter","$"],"z"],"+=",[6,"d.f32",[[7,[7,"input","num"],"x"]]]]]]}, + ast: {"params":["input"],"body":[0,[[13,"tmp",[7,[7,"counter","$"],"x"]],[2,[7,[7,"counter","$"],"x"],"=",[7,[7,"counter","$"],"y"]],[2,[7,[7,"counter","$"],"y"],"+=","tmp"],[2,[7,[7,"counter","$"],"z"],"+=",[6,"d.f32",[[7,[7,"input","num"],"x"]]]]]]}, externals: {"counter":() => counter,"d.f32":() => d.f32} }) && $.f)({}))); " @@ -316,7 +301,7 @@ describe('[ROLLUP] plugin for transpiling tgsl functions to tinyest', () => { }), { v: 2, name: undefined, - ast: {"params":[{"type":"i","name":"input"}],"body":[0,[[13,"x",true]]]}, + ast: {"params":["input"],"body":[0,[[13,"x",true]]]}, externals: {} }) && $.f)({}))); diff --git a/packages/unplugin-typegpu/test/typescript-syntax.test.ts b/packages/unplugin-typegpu/test/typescript-syntax.test.ts index feb0c8a873..c314c91e4c 100644 --- a/packages/unplugin-typegpu/test/typescript-syntax.test.ts +++ b/packages/unplugin-typegpu/test/typescript-syntax.test.ts @@ -21,13 +21,7 @@ describe('as type', () => { v: 2, name: "hello", ast: { - params: [{ - type: "i", - name: "a" - }, { - type: "i", - name: "b" - }], + params: ["a", "b"], body: [0, [[10, [1, "a", "+", "b"]]]] }, externals: {} diff --git a/packages/unplugin-typegpu/test/use-gpu-directive.test.ts b/packages/unplugin-typegpu/test/use-gpu-directive.test.ts index f2b411f7bc..2cf6a1609d 100644 --- a/packages/unplugin-typegpu/test/use-gpu-directive.test.ts +++ b/packages/unplugin-typegpu/test/use-gpu-directive.test.ts @@ -175,13 +175,7 @@ describe('"use gpu" marked arrow function, assigned to a const', () => { v: 2, name: "addGPU", ast: { - params: [{ - type: "i", - name: "a" - }, { - type: "i", - name: "b" - }], + params: ["a", "b"], body: [0, [[10, [1, "a", "+", "b"]]]] }, externals: {} @@ -203,7 +197,7 @@ describe('"use gpu" marked arrow function, assigned to a const', () => { }), { v: 2, name: "addGPU", - ast: {"params":[{"type":"i","name":"a"},{"type":"i","name":"b"}],"body":[0,[[10,[1,"a","+","b"]]]]}, + ast: {"params":["a","b"],"body":[0,[[10,[1,"a","+","b"]]]]}, externals: {} }) && $.f)({})); @@ -245,13 +239,7 @@ describe('marked arrow functions passed to shells', () => { v: 2, name: undefined, ast: { - params: [{ - type: "i", - name: "a" - }, { - type: "i", - name: "b" - }], + params: ["a", "b"], body: [0, [[10, [1, "a", "+", "b"]]]] }, externals: {} @@ -274,7 +262,7 @@ describe('marked arrow functions passed to shells', () => { }), { v: 2, name: undefined, - ast: {"params":[{"type":"i","name":"a"},{"type":"i","name":"b"}],"body":[0,[[10,[1,"a","+","b"]]]]}, + ast: {"params":["a","b"],"body":[0,[[10,[1,"a","+","b"]]]]}, externals: {} }) && $.f)({}))); @@ -314,13 +302,7 @@ describe('marked anonymous function expressions passed to shells', () => { v: 2, name: undefined, ast: { - params: [{ - type: "i", - name: "a" - }, { - type: "i", - name: "b" - }], + params: ["a", "b"], body: [0, [[10, [1, "a", "+", "b"]]]] }, externals: {} @@ -343,7 +325,7 @@ describe('marked anonymous function expressions passed to shells', () => { }), { v: 2, name: undefined, - ast: {"params":[{"type":"i","name":"a"},{"type":"i","name":"b"}],"body":[0,[[10,[1,"a","+","b"]]]]}, + ast: {"params":["a","b"],"body":[0,[[10,[1,"a","+","b"]]]]}, externals: {} }) && $.f)({}))); @@ -383,13 +365,7 @@ describe('marked named function expressions passed to shells', () => { v: 2, name: "addGPU", ast: { - params: [{ - type: "i", - name: "a" - }, { - type: "i", - name: "b" - }], + params: ["a", "b"], body: [0, [[10, [1, "a", "+", "b"]]]] }, externals: {} @@ -412,7 +388,7 @@ describe('marked named function expressions passed to shells', () => { }), { v: 2, name: "addGPU", - ast: {"params":[{"type":"i","name":"a"},{"type":"i","name":"b"}],"body":[0,[[10,[1,"a","+","b"]]]]}, + ast: {"params":["a","b"],"body":[0,[[10,[1,"a","+","b"]]]]}, externals: {} }) && $.f)({}))); @@ -507,13 +483,7 @@ describe('marked function statements', () => { v: 2, name: "addGPU", ast: { - params: [{ - type: "i", - name: "a" - }, { - type: "i", - name: "b" - }], + params: ["a", "b"], body: [0, [[10, [1, "a", "+", "b"]]]] }, externals: {} @@ -535,7 +505,7 @@ describe('marked function statements', () => { }), { v: 2, name: "addGPU", - ast: {"params":[{"type":"i","name":"a"},{"type":"i","name":"b"}],"body":[0,[[10,[1,"a","+","b"]]]]}, + ast: {"params":["a","b"],"body":[0,[[10,[1,"a","+","b"]]]]}, externals: {} }) && $.f)({})); @@ -592,13 +562,7 @@ describe('marked object methods', () => { v: 2, name: "mod", ast: { - params: [{ - type: "i", - name: "a" - }, { - type: "i", - name: "b" - }], + params: ["a", "b"], body: [0, [[10, [1, "a", "%", "b"]]]] }, externals: {} @@ -622,10 +586,7 @@ describe('marked object methods', () => { v: 2, name: "isPrime", ast: { - params: [{ - type: "i", - name: "n" - }], + params: ["n"], body: [0, [[11, [1, "n", "<=", [5, "1"]], [0, [[10, false]]]], [14, [12, "i", [5, "2"]], [1, "i", "<", "n"], [102, "++", "i"], [0, [[11, [1, [6, "obj.mod", ["n", "i"]], "===", [5, "0"]], [0, [[10, false]]]]]]], [10, true]]] }, externals: { @@ -646,7 +607,7 @@ describe('marked object methods', () => { }), { v: 2, name: "mod", - ast: {"params":[{"type":"i","name":"a"},{"type":"i","name":"b"}],"body":[0,[[10,[1,"a","%","b"]]]]}, + ast: {"params":["a","b"],"body":[0,[[10,[1,"a","%","b"]]]]}, externals: {} }) && $.f)({})) }; @@ -667,7 +628,7 @@ describe('marked object methods', () => { }), { v: 2, name: "isPrime", - ast: {"params":[{"type":"i","name":"n"}],"body":[0,[[11,[1,"n","<=",[5,"1"]],[0,[[10,false]]]],[14,[12,"i",[5,"2"]],[1,"i","<","n"],[102,"++","i"],[0,[[11,[1,[6,"obj.mod",["n","i"]],"===",[5,"0"]],[0,[[10,false]]]]]]],[10,true]]]}, + ast: {"params":["n"],"body":[0,[[11,[1,"n","<=",[5,"1"]],[0,[[10,false]]]],[14,[12,"i",[5,"2"]],[1,"i","<","n"],[102,"++","i"],[0,[[11,[1,[6,"obj.mod",["n","i"]],"===",[5,"0"]],[0,[[10,false]]]]]]],[10,true]]]}, externals: {"obj.mod":() => obj.mod} }) && $.f)({})); @@ -714,13 +675,7 @@ describe('transforms numeric operations', () => { v: 2, name: "main", ast: { - params: [{ - type: "i", - name: "a" - }, { - type: "i", - name: "b" - }], + params: ["a", "b"], body: [0, [[12, "c", [1, [1, "a", "+", "b"], "+", [5, "2"]]], [2, "c", "+=", [1, [5, "2"], "*", "b"]], [2, [7, "countMutable", "$"], "+=", [5, "3"]]]] }, externals: { @@ -748,7 +703,7 @@ describe('transforms numeric operations', () => { }), { v: 2, name: "main", - ast: {"params":[{"type":"i","name":"a"},{"type":"i","name":"b"}],"body":[0,[[12,"c",[1,[1,"a","+","b"],"+",[5,"2"]]],[2,"c","+=",[1,[5,"2"],"*","b"]],[2,[7,"countMutable","$"],"+=",[5,"3"]]]]}, + ast: {"params":["a","b"],"body":[0,[[12,"c",[1,[1,"a","+","b"],"+",[5,"2"]]],[2,"c","+=",[1,[5,"2"],"*","b"]],[2,[7,"countMutable","$"],"+=",[5,"3"]]]]}, externals: {"countMutable":() => countMutable} }) && $.f)({})); @@ -790,13 +745,7 @@ describe('hoists global function statements marked with "use gpu"', () => { v: 2, name: "mul", ast: { - params: [{ - type: "i", - name: "a" - }, { - type: "i", - name: "b" - }], + params: ["a", "b"], body: [0, [[10, [1, "a", "*", "b"]]]] }, externals: {} @@ -811,13 +760,7 @@ describe('hoists global function statements marked with "use gpu"', () => { v: 2, name: "add", ast: { - params: [{ - type: "i", - name: "a" - }, { - type: "i", - name: "b" - }], + params: ["a", "b"], body: [0, [[10, [1, "a", "+", "b"]]]] }, externals: {} @@ -836,7 +779,7 @@ describe('hoists global function statements marked with "use gpu"', () => { }), { v: 2, name: "mul", - ast: {"params":[{"type":"i","name":"a"},{"type":"i","name":"b"}],"body":[0,[[10,[1,"a","*","b"]]]]}, + ast: {"params":["a","b"],"body":[0,[[10,[1,"a","*","b"]]]]}, externals: {} }) && $.f)({})); @@ -848,7 +791,7 @@ describe('hoists global function statements marked with "use gpu"', () => { }), { v: 2, name: "add", - ast: {"params":[{"type":"i","name":"a"},{"type":"i","name":"b"}],"body":[0,[[10,[1,"a","+","b"]]]]}, + ast: {"params":["a","b"],"body":[0,[[10,[1,"a","+","b"]]]]}, externals: {} }) && $.f)({})); @@ -880,10 +823,7 @@ describe('hoists function statements marked with "use gpu" that shadow their own v: 2, name: "miter", ast: { - params: [{ - type: "i", - name: "a" - }], + params: ["a"], body: [0, [[13, "miter", [1, "a", "*", [5, "2"]]], [10, "miter"]]] }, externals: {} @@ -901,7 +841,7 @@ describe('hoists function statements marked with "use gpu" that shadow their own }), { v: 2, name: "miter", - ast: {"params":[{"type":"i","name":"a"}],"body":[0,[[13,"miter",[1,"a","*",[5,"2"]]],[10,"miter"]]]}, + ast: {"params":["a"],"body":[0,[[13,"miter",[1,"a","*",[5,"2"]]],[10,"miter"]]]}, externals: {} }) && $.f)({})); @@ -945,13 +885,7 @@ describe('hoists function statements marked with "use gpu", scoped inside anothe v: 2, name: "mul", ast: { - params: [{ - type: "i", - name: "a" - }, { - type: "i", - name: "b" - }], + params: ["a", "b"], body: [0, [[10, [1, "a", "*", "b"]]]] }, externals: {} @@ -966,13 +900,7 @@ describe('hoists function statements marked with "use gpu", scoped inside anothe v: 2, name: "add", ast: { - params: [{ - type: "i", - name: "a" - }, { - type: "i", - name: "b" - }], + params: ["a", "b"], body: [0, [[10, [1, "a", "+", "b"]]]] }, externals: {} @@ -993,7 +921,7 @@ describe('hoists function statements marked with "use gpu", scoped inside anothe }), { v: 2, name: "mul", - ast: {"params":[{"type":"i","name":"a"},{"type":"i","name":"b"}],"body":[0,[[10,[1,"a","*","b"]]]]}, + ast: {"params":["a","b"],"body":[0,[[10,[1,"a","*","b"]]]]}, externals: {} }) && $.f)({})); @@ -1005,7 +933,7 @@ describe('hoists function statements marked with "use gpu", scoped inside anothe }), { v: 2, name: "add", - ast: {"params":[{"type":"i","name":"a"},{"type":"i","name":"b"}],"body":[0,[[10,[1,"a","+","b"]]]]}, + ast: {"params":["a","b"],"body":[0,[[10,[1,"a","+","b"]]]]}, externals: {} }) && $.f)({})); @@ -1060,13 +988,7 @@ describe('hoists function statements marked with "use gpu", scoped inside an arr v: 2, name: "mul", ast: { - params: [{ - type: "i", - name: "a" - }, { - type: "i", - name: "b" - }], + params: ["a", "b"], body: [0, [[10, [1, "a", "*", "b"]]]] }, externals: {} @@ -1081,13 +1003,7 @@ describe('hoists function statements marked with "use gpu", scoped inside an arr v: 2, name: "add", ast: { - params: [{ - type: "i", - name: "a" - }, { - type: "i", - name: "b" - }], + params: ["a", "b"], body: [0, [[10, [1, "a", "+", "b"]]]] }, externals: {} @@ -1108,7 +1024,7 @@ describe('hoists function statements marked with "use gpu", scoped inside an arr }), { v: 2, name: "mul", - ast: {"params":[{"type":"i","name":"a"},{"type":"i","name":"b"}],"body":[0,[[10,[1,"a","*","b"]]]]}, + ast: {"params":["a","b"],"body":[0,[[10,[1,"a","*","b"]]]]}, externals: {} }) && $.f)({})); @@ -1120,7 +1036,7 @@ describe('hoists function statements marked with "use gpu", scoped inside an arr }), { v: 2, name: "add", - ast: {"params":[{"type":"i","name":"a"},{"type":"i","name":"b"}],"body":[0,[[10,[1,"a","+","b"]]]]}, + ast: {"params":["a","b"],"body":[0,[[10,[1,"a","+","b"]]]]}, externals: {} }) && $.f)({})); @@ -1177,13 +1093,7 @@ describe('hoists function statements marked with "use gpu", scoped inside an if v: 2, name: "mul", ast: { - params: [{ - type: "i", - name: "a" - }, { - type: "i", - name: "b" - }], + params: ["a", "b"], body: [0, [[10, [1, [1, "a", "*", "b"], "*", "c"]]]] }, externals: { @@ -1200,13 +1110,7 @@ describe('hoists function statements marked with "use gpu", scoped inside an if v: 2, name: "add", ast: { - params: [{ - type: "i", - name: "a" - }, { - type: "i", - name: "b" - }], + params: ["a", "b"], body: [0, [[10, [1, [1, "a", "+", "b"], "+", "c"]]]] }, externals: { @@ -1230,7 +1134,7 @@ describe('hoists function statements marked with "use gpu", scoped inside an if }), { v: 2, name: "mul", - ast: {"params":[{"type":"i","name":"a"},{"type":"i","name":"b"}],"body":[0,[[10,[1,[1,"a","*","b"],"*","c"]]]]}, + ast: {"params":["a","b"],"body":[0,[[10,[1,[1,"a","*","b"],"*","c"]]]]}, externals: {"c":() => c} }) && $.f)({})); @@ -1242,7 +1146,7 @@ describe('hoists function statements marked with "use gpu", scoped inside an if }), { v: 2, name: "add", - ast: {"params":[{"type":"i","name":"a"},{"type":"i","name":"b"}],"body":[0,[[10,[1,[1,"a","+","b"],"+","c"]]]]}, + ast: {"params":["a","b"],"body":[0,[[10,[1,[1,"a","+","b"],"+","c"]]]]}, externals: {"c":() => c} }) && $.f)({})); @@ -1303,13 +1207,7 @@ describe('replaces function statements marked with "use gpu" in place when condi v: 2, name: "add", ast: { - params: [{ - type: "i", - name: "a" - }, { - type: "i", - name: "b" - }], + params: ["a", "b"], body: [0, [[10, [1, [1, "a", "+", "b"], "+", "c"]]]] }, externals: { @@ -1328,13 +1226,7 @@ describe('replaces function statements marked with "use gpu" in place when condi v: 2, name: "mul", ast: { - params: [{ - type: "i", - name: "a" - }, { - type: "i", - name: "b" - }], + params: ["a", "b"], body: [0, [[10, [1, [1, "a", "*", "b"], "*", "c"]]]] }, externals: { @@ -1361,7 +1253,7 @@ describe('replaces function statements marked with "use gpu" in place when condi }), { v: 2, name: "add", - ast: {"params":[{"type":"i","name":"a"},{"type":"i","name":"b"}],"body":[0,[[10,[1,[1,"a","+","b"],"+","c"]]]]}, + ast: {"params":["a","b"],"body":[0,[[10,[1,[1,"a","+","b"],"+","c"]]]]}, externals: {"c":() => c} }) && $.f)({})); @@ -1376,7 +1268,7 @@ describe('replaces function statements marked with "use gpu" in place when condi }), { v: 2, name: "mul", - ast: {"params":[{"type":"i","name":"a"},{"type":"i","name":"b"}],"body":[0,[[10,[1,[1,"a","*","b"],"*","c"]]]]}, + ast: {"params":["a","b"],"body":[0,[[10,[1,[1,"a","*","b"],"*","c"]]]]}, externals: {"c":() => c} }) && $.f)({})); @@ -1417,13 +1309,7 @@ describe('hoists exported marked function statements', () => { v: 2, name: "mul", ast: { - params: [{ - type: "i", - name: "a" - }, { - type: "i", - name: "b" - }], + params: ["a", "b"], body: [0, [[10, [1, "a", "*", "b"]]]] }, externals: {} @@ -1437,13 +1323,7 @@ describe('hoists exported marked function statements', () => { v: 2, name: "add", ast: { - params: [{ - type: "i", - name: "a" - }, { - type: "i", - name: "b" - }], + params: ["a", "b"], body: [0, [[10, [1, "a", "+", "b"]]]] }, externals: {} @@ -1464,7 +1344,7 @@ describe('hoists exported marked function statements', () => { }), { v: 2, name: "mul", - ast: {"params":[{"type":"i","name":"a"},{"type":"i","name":"b"}],"body":[0,[[10,[1,"a","*","b"]]]]}, + ast: {"params":["a","b"],"body":[0,[[10,[1,"a","*","b"]]]]}, externals: {} }) && $.f)({})); @@ -1475,7 +1355,7 @@ describe('hoists exported marked function statements', () => { }), { v: 2, name: "add", - ast: {"params":[{"type":"i","name":"a"},{"type":"i","name":"b"}],"body":[0,[[10,[1,"a","+","b"]]]]}, + ast: {"params":["a","b"],"body":[0,[[10,[1,"a","+","b"]]]]}, externals: {} }) && $.f)({})); @@ -1510,13 +1390,7 @@ describe('hoists default exported marked function statement', () => { v: 2, name: "add", ast: { - params: [{ - type: "i", - name: "a" - }, { - type: "i", - name: "b" - }], + params: ["a", "b"], body: [0, [[10, [1, "a", "+", "b"]]]] }, externals: {} @@ -1535,7 +1409,7 @@ describe('hoists default exported marked function statement', () => { }), { v: 2, name: "add", - ast: {"params":[{"type":"i","name":"a"},{"type":"i","name":"b"}],"body":[0,[[10,[1,"a","+","b"]]]]}, + ast: {"params":["a","b"],"body":[0,[[10,[1,"a","+","b"]]]]}, externals: {} }) && $.f)({})); console.log(add); @@ -1628,13 +1502,7 @@ describe('export marked arrow function', () => { v: 2, name: "add", ast: { - params: [{ - type: "i", - name: "a" - }, { - type: "i", - name: "b" - }], + params: ["a", "b"], body: [0, [[10, [1, "a", "+", "b"]]]] }, externals: {} @@ -1647,10 +1515,7 @@ describe('export marked arrow function', () => { v: 2, name: "increment", ast: { - params: [{ - type: "i", - name: "n" - }], + params: ["n"], body: [0, [[10, [1, "n", "+", [5, "1"]]]]] }, externals: {} @@ -1664,13 +1529,7 @@ describe('export marked arrow function', () => { v: 2, name: "mul", ast: { - params: [{ - type: "i", - name: "a" - }, { - type: "i", - name: "b" - }], + params: ["a", "b"], body: [0, [[10, [1, "a", "*", "b"]]]] }, externals: {} @@ -1687,7 +1546,7 @@ describe('export marked arrow function', () => { }), { v: 2, name: "add", - ast: {"params":[{"type":"i","name":"a"},{"type":"i","name":"b"}],"body":[0,[[10,[1,"a","+","b"]]]]}, + ast: {"params":["a","b"],"body":[0,[[10,[1,"a","+","b"]]]]}, externals: {} }) && $.f)({})); @@ -1697,7 +1556,7 @@ describe('export marked arrow function', () => { }), { v: 2, name: "increment", - ast: {"params":[{"type":"i","name":"n"}],"body":[0,[[10,[1,"n","+",[5,"1"]]]]]}, + ast: {"params":["n"],"body":[0,[[10,[1,"n","+",[5,"1"]]]]]}, externals: {} }) && $.f)({})); @@ -1707,7 +1566,7 @@ describe('export marked arrow function', () => { }), { v: 2, name: "mul", - ast: {"params":[{"type":"i","name":"a"},{"type":"i","name":"b"}],"body":[0,[[10,[1,"a","*","b"]]]]}, + ast: {"params":["a","b"],"body":[0,[[10,[1,"a","*","b"]]]]}, externals: {} }) && $.f)({})); @@ -1735,13 +1594,7 @@ describe('anonymous default export marked function statement', () => { v: 2, name: undefined, ast: { - params: [{ - type: "i", - name: "a" - }, { - type: "i", - name: "b" - }], + params: ["a", "b"], body: [0, [[10, [1, "a", "+", "b"]]]] }, externals: {} @@ -1757,7 +1610,7 @@ describe('anonymous default export marked function statement', () => { }), { v: 2, name: undefined, - ast: {"params":[{"type":"i","name":"a"},{"type":"i","name":"b"}],"body":[0,[[10,[1,"a","+","b"]]]]}, + ast: {"params":["a","b"],"body":[0,[[10,[1,"a","+","b"]]]]}, externals: {} }) && $.f)({})); @@ -1785,13 +1638,7 @@ describe('anonymous default export marked arrow function', () => { v: 2, name: undefined, ast: { - params: [{ - type: "i", - name: "a" - }, { - type: "i", - name: "b" - }], + params: ["a", "b"], body: [0, [[10, [1, "a", "+", "b"]]]] }, externals: {} @@ -1807,7 +1654,7 @@ describe('anonymous default export marked arrow function', () => { }), { v: 2, name: undefined, - ast: {"params":[{"type":"i","name":"a"},{"type":"i","name":"b"}],"body":[0,[[10,[1,"a","+","b"]]]]}, + ast: {"params":["a","b"],"body":[0,[[10,[1,"a","+","b"]]]]}, externals: {} }) && $.f)({}));