From 46c7d7bf722665bd425ba025cc23155e1df3d179 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Thu, 27 Aug 2026 14:49:59 +0200 Subject: [PATCH 1/8] update unsupported syntax in plugin --- .../eslint-plugin/src/rules/noUnsupportedSyntax.ts | 9 --------- .../tests/rules/noUnsupportedSyntax.test.ts | 10 +--------- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/packages/eslint-plugin/src/rules/noUnsupportedSyntax.ts b/packages/eslint-plugin/src/rules/noUnsupportedSyntax.ts index 6bcd642c44..e148027d11 100644 --- a/packages/eslint-plugin/src/rules/noUnsupportedSyntax.ts +++ b/packages/eslint-plugin/src/rules/noUnsupportedSyntax.ts @@ -140,15 +140,6 @@ export const noUnsupportedSyntax = createRule({ report(node, `'new' expression`); }, - Property(node) { - if (!directives.getEnclosingTypegpuFunction()) { - return; - } - if (node.computed) { - report(node, 'computed property key'); - } - }, - SequenceExpression(node) { if (!directives.getEnclosingTypegpuFunction()) { return; diff --git a/packages/eslint-plugin/tests/rules/noUnsupportedSyntax.test.ts b/packages/eslint-plugin/tests/rules/noUnsupportedSyntax.test.ts index 7ee0b6798a..0bc73771de 100644 --- a/packages/eslint-plugin/tests/rules/noUnsupportedSyntax.test.ts +++ b/packages/eslint-plugin/tests/rules/noUnsupportedSyntax.test.ts @@ -9,6 +9,7 @@ describe('noUnsupportedSyntax', () => { "const fn = () => { 'use gpu'; const x = Struct({ prop: 1}); }", "const fn = () => { 'use gpu'; let x = 1; }", "const cls = new (class { #priv = 1; fn = () => { 'use gpu'; const a = this.#priv; } } )()", + "const fn = () => { 'use gpu'; const obj = { [key]: 1 }; }", ], invalid: [ { @@ -202,15 +203,6 @@ describe('noUnsupportedSyntax', () => { }, ], }, - { - code: "const fn = () => { 'use gpu'; const obj = { [key]: 1 }; }", - errors: [ - { - messageId: 'unexpected', - data: { snippet: '[key]: 1', syntax: 'computed property key' }, - }, - ], - }, { code: "const fn = () => { 'use gpu'; (a, b); }", errors: [ From d43aed66301e3d9c35b8aab0bc3ec01039f9d332 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Thu, 27 Aug 2026 16:28:15 +0200 Subject: [PATCH 2/8] tinyest + tinyest for wgsl fix 1 test format --- packages/tinyest-for-wgsl/src/transpilers.ts | 126 ++++++++++++------ .../tinyest-for-wgsl/tests/parsers.test.ts | 57 +++++++- packages/tinyest/src/nodes.ts | 24 +++- 3 files changed, 159 insertions(+), 48 deletions(-) diff --git a/packages/tinyest-for-wgsl/src/transpilers.ts b/packages/tinyest-for-wgsl/src/transpilers.ts index 719684dddc..4e806b60b8 100644 --- a/packages/tinyest-for-wgsl/src/transpilers.ts +++ b/packages/tinyest-for-wgsl/src/transpilers.ts @@ -240,10 +240,28 @@ const acornSpecificTranspilers = { return [NODE.numericLiteral, String(Number(node.value))]; }, - ObjectExpression(ctx, node, transpile) { - const properties: Record = {}; + Property(ctx, node, transpile) { + if (node.computed) { + const key = transpile(ctx, node.key) as tinyest.Expression; + const value = transpile(ctx, node.value) as tinyest.Expression; + + return [NODE.objectProperty, key, value, true] as tinyest.ObjectProperty; + } + + if ( + (node.key.type !== 'Identifier' && node.key.type !== 'Literal') || + (node.key.type === 'Literal' && (node.key.raw === null || node.key.regex)) + ) { + throw new Error(`Unsupported non-computed object property key.`); + } + + const key = node.key.type === 'Identifier' ? node.key.name : String(node.key.value); + const value = transpile(ctx, node.value) as tinyest.Expression; + return [NODE.objectProperty, key, value, false] as tinyest.ObjectProperty; + }, - for (const prop of node.properties) { + ObjectExpression(ctx, node, transpile) { + const objectProperties = node.properties.map((prop) => { // TODO: Handle SpreadElement if (prop.type === 'SpreadElement') { throw new Error('Spread elements are not supported in TGSL.'); @@ -254,24 +272,29 @@ const acornSpecificTranspilers = { throw new Error('Object method elements are not supported in TGSL.'); } - // TODO: Handle computed properties - if (prop.computed) { - throw new Error('Computed object properties are not supported in TGSL.'); - } + return transpile(ctx, prop) as tinyest.ObjectProperty; + }); - if ( - (prop.key.type !== 'Identifier' && prop.key.type !== 'Literal') || - (prop.key.type === 'Literal' && (prop.key.raw === null || prop.key.regex)) - ) { - throw new Error(`Unsupported non-computed object property key.`); - } + if (objectProperties.some((prop) => /* computed */ prop[3])) { + return [ + NODE.objectExprWithComputedProps, + objectProperties, + ] as tinyest.ObjectExpressionWithComputedProps; + } - const key = prop.key.type === 'Identifier' ? prop.key.name : String(prop.key.value); - const value = transpile(ctx, prop.value) as tinyest.Expression; - properties[key] = value; + const obj: Record = {}; + const seenKeys = new Set(); + + for (const prop of objectProperties) { + const key = prop[1] as string; + if (seenKeys.has(key)) { + throw new Error(`Duplicate object property key: '${key}'.`); + } + seenKeys.add(key); + obj[key] = /* value */ prop[2]; } - return [NODE.objectExpr, properties]; + return [NODE.objectExpr, obj] as tinyest.ObjectExpression; }, } satisfies Transpilers; @@ -310,47 +333,66 @@ const babelSpecificTranspilers = { return [NODE.nullLiteral]; }, - ObjectExpression(ctx, node, transpile) { - const properties: Record = {}; + ObjectProperty(ctx, node, transpile) { + if (node.computed) { + const key = transpile(ctx, node.key) as tinyest.Expression; + const value = transpile(ctx, node.value) as tinyest.Expression; + + return [NODE.objectProperty, key, value, true] as tinyest.ObjectProperty; + } + + let key: string; + switch (node.key.type) { + case 'Identifier': + key = node.key.name; + break; + case 'StringLiteral': + case 'NumericLiteral': + case 'BigIntLiteral': + key = String(node.key.value); + break; + default: + throw new Error(`Unsupported non-computed object property key.`); + } + + const value = transpile(ctx, node.value) as tinyest.Expression; + return [NODE.objectProperty, key, value, false] as tinyest.ObjectProperty; + }, - for (const prop of node.properties) { + ObjectExpression(ctx, node, transpile) { + const objectProperties = node.properties.map((prop) => { // TODO: Handle SpreadElement if (prop.type === 'SpreadElement') { throw new Error('Spread elements are not supported in TGSL.'); } - // TODO: Handle Object method if (prop.type === 'ObjectMethod') { throw new Error('Object method elements are not supported in TGSL.'); } - // TODO: Handle computed properties - if (prop.computed) { - throw new Error('Computed object properties are not supported in TGSL.'); - } - - let key: string; + return transpile(ctx, prop) as tinyest.ObjectProperty; + }); - switch (prop.key.type) { - case 'Identifier': - key = prop.key.name; - break; + if (objectProperties.some((prop) => /* computed */ prop[3])) { + return [ + NODE.objectExprWithComputedProps, + objectProperties, + ] as tinyest.ObjectExpressionWithComputedProps; + } - case 'StringLiteral': - case 'NumericLiteral': - case 'BigIntLiteral': - key = String(prop.key.value); - break; + const obj: Record = {}; + const seenKeys = new Set(); - default: - throw new Error(`Unsupported non-computed object property key.`); + for (const prop of objectProperties) { + const key = prop[1] as string; + if (seenKeys.has(key)) { + throw new Error(`Duplicate object property key: '${key}'.`); } - - const value = transpile(ctx, prop.value) as tinyest.Expression; - properties[key] = value; + seenKeys.add(key); + obj[key] = /* value */ prop[2]; } - return [NODE.objectExpr, properties]; + return [NODE.objectExpr, obj] as tinyest.ObjectExpression; }, TSAsExpression: tsFallthrough, diff --git a/packages/tinyest-for-wgsl/tests/parsers.test.ts b/packages/tinyest-for-wgsl/tests/parsers.test.ts index 9b0ee0944e..c7c56e0295 100644 --- a/packages/tinyest-for-wgsl/tests/parsers.test.ts +++ b/packages/tinyest-for-wgsl/tests/parsers.test.ts @@ -409,18 +409,65 @@ describe('transpileFnBabel and transpileFnAcorn', () => { it( 'parses binary bigints', dualTest((p, transpileFn) => { - expect(JSON.stringify(transpileFn(p('() => 0b101n')).body)).toMatchInlineSnapshot( - `"[0,[[10,[5,"5"]]]]"`, + const { body, externalNames } = transpileFn(p('() => 0b101n')); + + expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[[10,[5,"5"]]]]"`); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); + }), + ); + + it( + 'parses identifier, string, numeric, and bigint object keys', + dualTest((p, transpileFn) => { + const { body, externalNames } = transpileFn( + p(`() => ({ + identifier: 1, + 'string-key': 2, + 1: 3, + 2n: 4, + })`), + ); + + expect(JSON.stringify(body)).toMatchInlineSnapshot( + `"[0,[[10,[104,{"1":[5,"3"],"2":[5,"4"],"identifier":[5,"1"],"string-key":[5,"2"]}]]]]"`, ); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); + }), + ); + + it( + 'rejects duplicate non-computed object keys', + dualTest((p, transpileFn) => { + expect(() => + transpileFn( + p(`() => ({ + field: 1, + field: 2, + })`), + ), + ).toThrowErrorMatchingInlineSnapshot(`[Error: Duplicate object property key: 'field'.]`); }), ); it( - 'rejects computed object properties', + 'parses computed object keys', dualTest((p, transpileFn) => { - expect(() => transpileFn(p('() => ({ [k]: 1 })'))).toThrowErrorMatchingInlineSnapshot( - `[Error: Computed object properties are not supported in TGSL.]`, + const { body, externalNames } = transpileFn( + p(`() => ({ + [id]: 1, + [getId()]: 2, + })`), ); + + expect(JSON.stringify(body)).toMatchInlineSnapshot( + `"[0,[[10,[108,[[107,"id",[5,"1"],true],[107,[6,"getId",[]],[5,"2"],true]]]]]]"`, + ); + expect(externalNames).toMatchInlineSnapshot(` + Map { + "id" => "id", + "getId" => "getId", + } + `); }), ); }); diff --git a/packages/tinyest/src/nodes.ts b/packages/tinyest/src/nodes.ts index 942afe4d78..4ef34e09c5 100644 --- a/packages/tinyest/src/nodes.ts +++ b/packages/tinyest/src/nodes.ts @@ -33,6 +33,8 @@ export const NodeTypeCatalog = { objectExpr: 104, conditionalExpr: 105, nullLiteral: 106, + objectProperty: 107, + objectExprWithComputedProps: 108, } as const; export type NodeTypeCatalog = typeof NodeTypeCatalog; @@ -193,6 +195,25 @@ export type ObjectExpression = readonly [ Record, ]; +export type ObjectProperty = + | readonly [ + type: NodeTypeCatalog['objectProperty'], + key: string, + value: Expression, + computed: false, + ] + | readonly [ + type: NodeTypeCatalog['objectProperty'], + key: Expression, + value: Expression, + computed: true, + ]; + +export type ObjectExpressionWithComputedProps = readonly [ + type: NodeTypeCatalog['objectExprWithComputedProps'], + ObjectProperty[], +]; + export type ArrayExpression = readonly [type: NodeTypeCatalog['arrayExpr'], values: Expression[]]; export type ConditionalExpression = readonly [ @@ -251,6 +272,7 @@ export type Expression = | LogicalExpression | UnaryExpression | ObjectExpression + | ObjectExpressionWithComputedProps | MemberAccess | IndexAccess | ArrayExpression @@ -260,7 +282,7 @@ export type Expression = | Call | Literal; -export type AnyNode = Statement | Expression; +export type AnyNode = Statement | Expression | ObjectProperty; export const FuncParameterType = { identifier: 'i', From 529c714334c7ea9224f8a1413f6a6bf2a09a7487 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Fri, 28 Aug 2026 11:16:11 +0200 Subject: [PATCH 3/8] obfuscation + tseynit --- packages/typegpu/src/shared/tseynit.ts | 28 +++++++++++++++++++ .../typegpu/tests/internal/tseynit.test.ts | 16 +++++++++++ .../unplugin-typegpu/src/core/obfuscate.ts | 9 ++++++ .../unplugin-typegpu/test/obfuscation.test.ts | 22 ++++++++++++++- 4 files changed, 74 insertions(+), 1 deletion(-) diff --git a/packages/typegpu/src/shared/tseynit.ts b/packages/typegpu/src/shared/tseynit.ts index ef7a8479df..0e6aca8f53 100644 --- a/packages/typegpu/src/shared/tseynit.ts +++ b/packages/typegpu/src/shared/tseynit.ts @@ -6,6 +6,11 @@ export function stringifyNode(node: tinyest.AnyNode): string { if (isExpression(node)) { return stringifyExpression(node, ''); } + + if (isObjectProperty(node)) { + return stringifyObjectProperty(node); + } + return stringifyStatement(node, ''); } @@ -153,6 +158,11 @@ function stringifyExpression(node: tinyest.Expression, ident: string): string { return `{ ${entries.join(', ')} }`; } + if (node[0] === NODE.objectExprWithComputedProps) { + const entries = node[1].map((prop) => stringifyObjectProperty(prop)); + return `{ ${entries.join(', ')} }`; + } + if (node[0] === NODE.conditionalExpr) { return `${wrapIfComplex(node[1], ident)} ? ${wrapIfComplex(node[2], ident)} : ${wrapIfComplex(node[3], ident)}`; } @@ -164,6 +174,13 @@ function stringifyExpression(node: tinyest.Expression, ident: string): string { assertExhaustive(node); } +function stringifyObjectProperty(node: tinyest.ObjectProperty): string { + const computed = node[3]; + const key = computed ? `[${stringifyExpression(node[1], '')}]` : stringifyExpression(node[1], ''); + const value = stringifyExpression(node[2], ''); + return `${key}: ${value}`; +} + function assertExhaustive(value: never): never { throw new Error(`'${JSON.stringify(value)}' was not handled by the stringify function.`); } @@ -185,6 +202,7 @@ function isExpression(node: tinyest.AnyNode): node is tinyest.Expression { node[0] === NODE.preUpdate || node[0] === NODE.postUpdate || node[0] === NODE.objectExpr || + node[0] === NODE.objectExprWithComputedProps || node[0] === NODE.conditionalExpr || node[0] === NODE.nullLiteral ) { @@ -195,6 +213,16 @@ function isExpression(node: tinyest.AnyNode): node is tinyest.Expression { return false; } +function isObjectProperty(node: tinyest.AnyNode): node is tinyest.ObjectProperty { + if (typeof node !== 'string' && typeof node !== 'boolean' && node[0] === NODE.objectProperty) { + node satisfies tinyest.ObjectProperty; + return true; + } + + node satisfies Exclude; + return false; +} + const SIMPLE_NODES: number[] = [ NODE.memberAccess, // highest precedence NODE.indexAccess, // highest precedence diff --git a/packages/typegpu/tests/internal/tseynit.test.ts b/packages/typegpu/tests/internal/tseynit.test.ts index 96cccef9b0..2e900f58d7 100644 --- a/packages/typegpu/tests/internal/tseynit.test.ts +++ b/packages/typegpu/tests/internal/tseynit.test.ts @@ -153,6 +153,22 @@ describe('ast to JS transformation', () => { expect(stringifyNode(node)).toBe('{ a: 1, b: x }'); }); + it('handles object expressions with computed keys', () => { + const node: tinyest.ObjectExpressionWithComputedProps = [ + N.objectExprWithComputedProps, + [ + [N.objectProperty, 'a', 'x', false], + [N.objectProperty, 'b', 'y', false], + [N.objectProperty, 'externalKey', 'z', true], + [N.objectProperty, [N.call, 'getKey', []], 'w', true], + [N.objectProperty, [N.stringLiteral, 'key'], 'v', true], + ], + ]; + expect(stringifyNode(node)).toBe( + '{ a: x, b: y, [externalKey]: z, [getKey()]: w, ["key"]: v }', + ); + }); + it('handles conditional expressions', () => { const node: tinyest.ConditionalExpression = [ N.conditionalExpr, diff --git a/packages/unplugin-typegpu/src/core/obfuscate.ts b/packages/unplugin-typegpu/src/core/obfuscate.ts index 00629bb377..bf6f0786ad 100644 --- a/packages/unplugin-typegpu/src/core/obfuscate.ts +++ b/packages/unplugin-typegpu/src/core/obfuscate.ts @@ -171,6 +171,15 @@ const visitors = { ), ]; }, + objectProperty(ctx: Context, node: tinyest.ObjectProperty) { + const computed = node[3]; + return computed + ? [NODE.objectProperty, obf(ctx, node[1]), obf(ctx, node[2]), computed] + : [NODE.objectProperty, node[1], obf(ctx, node[2]), computed]; + }, + objectExprWithComputedProps(ctx: Context, node: tinyest.ObjectExpressionWithComputedProps) { + return [NODE.objectExprWithComputedProps, node[1].map((prop) => obf(ctx, prop))]; + }, conditionalExpr(ctx: Context, node: tinyest.ConditionalExpression) { return [NODE.conditionalExpr, obf(ctx, node[1]), obf(ctx, node[2]), obf(ctx, node[3])]; }, diff --git a/packages/unplugin-typegpu/test/obfuscation.test.ts b/packages/unplugin-typegpu/test/obfuscation.test.ts index 0718d2c933..7e4bf32ee3 100644 --- a/packages/unplugin-typegpu/test/obfuscation.test.ts +++ b/packages/unplugin-typegpu/test/obfuscation.test.ts @@ -401,7 +401,7 @@ describe('obfuscate', () => { expect(externalNames).toMatchInlineSnapshot(`Map {}`); }); - it('does not obfuscate struct keys', () => { + it('does not obfuscate non-computed struct keys', () => { const code = `(param) => { let struct = { field: 1 }; return struct.field; }`; const transpiled = transpileFnBabel(parse(code)); @@ -424,6 +424,26 @@ describe('obfuscate', () => { expect(externalNames).toMatchInlineSnapshot(`Map {}`); }); + it('obfuscates computed struct keys', () => { + const code = `() => { const prop = 'field'; const struct = { [prop]: 1, [getProp()]: 2, ['prop']: 3 }; return struct[prop] + struct[getProp()] + struct['prop']; }`; + const transpiled = transpileFn(parse(code)); + + const { body, externalNames } = obfuscate(transpiled); + + expect(stringifyNode(body)).toMatchInlineSnapshot(` + "{ + const a = "field"; + const b = { [a]: 1, [c()]: 2, ["prop"]: 3 }; + return (b[a] + b[c()]) + b["prop"]; + }" + `); + expect(externalNames).toMatchInlineSnapshot(` + Map { + "c" => "getProp", + } + `); + }); + it("obfuscates 'this'", () => { const code = `() => { return this.prop1.prop2; }`; const transpiled = transpileFnBabel(parse(code)); From f382f91d25eff626fb5068133112d78e0629572c Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Fri, 28 Aug 2026 12:51:26 +0200 Subject: [PATCH 4/8] wgslgenerator --- packages/typegpu/src/tgsl/wgslGenerator.ts | 129 +++++--- .../typegpu/tests/tgsl/wgslGenerator.test.ts | 300 +++++++++++++++++- 2 files changed, 392 insertions(+), 37 deletions(-) diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index 33d64ec786..1e5b6e26c5 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -887,35 +887,56 @@ export class WgslGenerator implements ShaderGenerator { ); } - if (expression[0] === NODE.objectExpr) { - // Object Literal - const obj = expression[1]; + if (expression[0] === NODE.objectExpr || expression[0] === NODE.objectExprWithComputedProps) { + // Normalize to `objectProperty[]` + const properties = + expression[0] === NODE.objectExprWithComputedProps + ? expression[1] + : Object.entries(expression[1]).map( + ([key, value]) => + [NODE.objectProperty, key, value, false] satisfies tinyest.ObjectProperty, + ); + + const seenKeys = new Map(); + const resolveUniqueKey = (prop: tinyest.ObjectProperty): string => { + const key = this._resolveObjectPropertyKey(prop); + const dupProp = seenKeys.get(key); + if (dupProp) { + throw new WgslTypeError( + `Duplicate object property key found: '${stringifyNode(dupProp)}' and '${stringifyNode(prop)}'.`, + ); + } + seenKeys.set(key, prop); + return key; + }; + const structType = this.ctx.expectedType; if (structType instanceof AutoStruct) { - const entries = Object.fromEntries( - Object.entries(obj).map(([key, value]) => { - let accessed = structType.accessProp(key); - let expr: Snippet; - if (accessed) { - // Generating the expression expecting a specific type - expr = this._typedExpression(value, accessed.type); - } else { - // Generating the expression and inferring the type instead - expr = this._expression(value); - if (expr.dataType === UnknownData) { - throw new WgslTypeError( - stitch`Property ${key} in object literal has a value of unknown type: '${expr}'`, - ); - } - // Taking care of abstract numerics and implicit pointers - accessed = structType.provideProp(key, unptr(concretize(expr.dataType))); + const keySnippetPairs = properties.map((prop) => { + const key = resolveUniqueKey(prop); + const value = prop[2]; + + let accessed = structType.accessProp(key); + let expr: Snippet; + if (accessed) { + // Generating the expression expecting a specific type + expr = this._typedExpression(value, accessed.type); + } else { + // Generating the expression and inferring the type instead + expr = this._expression(value); + if (expr.dataType === UnknownData) { + throw new WgslTypeError( + stitch`Property ${key} in object literal has a value of unknown type: '${expr}'`, + ); } + // Taking care of abstract numerics and implicit pointers + accessed = structType.provideProp(key, unptr(concretize(expr.dataType))); + } + return [accessed.prop, expr]; + }); - return [accessed.prop, expr]; - }), - ); - + const entries = Object.fromEntries(keySnippetPairs); const completeStruct = structType.completeStruct; const convertedSnippets = convertStructValues(this.ctx, completeStruct, entries); @@ -927,18 +948,30 @@ export class WgslGenerator implements ShaderGenerator { } if (wgsl.isWgslStruct(structType)) { - const entries = Object.fromEntries( - Object.entries(structType.propTypes).map(([key, value]) => { - const val = obj[key]; - if (val === undefined) { - throw new WgslTypeError( - `Missing property ${key} in object literal for struct ${structType}`, - ); - } - const result = this._typedExpression(val, value); - return [key, result]; - }), - ); + const entries: Record = {}; + + for (const prop of properties) { + const key = resolveUniqueKey(prop); + const value = prop[2]; + const propType = structType.propTypes[key]; + + if (propType === undefined) { + // Evaluate every field even if it gets stripped by the struct schema + void this._expression(value); + continue; + } + + const expr = this._typedExpression(value, propType); + entries[key] = expr; + } + + for (const key of Object.keys(structType.propTypes)) { + if (entries[key] === undefined) { + throw new WgslTypeError( + `Missing property ${key} in object literal for struct ${structType}`, + ); + } + } const convertedSnippets = convertStructValues(this.ctx, structType, entries); @@ -1836,6 +1869,30 @@ ${this.ctx.pre}else ${alternate}`, return { code: resolved ? `${this.ctx.pre}${resolved};` : '', definesInNearestScope: false }; } + /** + * Resolves the key of an object property. Handles both computed and non-computed keys. + */ + protected _resolveObjectPropertyKey(property: tinyest.ObjectProperty) { + const computed = property[3]; + if (!computed) { + return property[1]; + } + + const key = this._expression(property[1]); + + if (!isKnownAtComptime(key)) { + throw new WgslTypeError( + `Computed object property key '${stringifyNode(property)}' must be known at comptime.`, + ); + } + + if (typeof key.value !== 'string') { + throw new WgslTypeError('Object property keys must be strings in TypeGPU functions.'); + } + + return key.value; + } + /** * Attempts a member access lookup to mark a variable as modified. * @example diff --git a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts index 883cb66b88..67a546c480 100644 --- a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts +++ b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect } from 'vitest'; +import { beforeEach, describe, expect, vi } from 'vitest'; import { CAPTURE, captureSnippets, it } from 'typegpu-testing-utility'; import { expectDataTypeOf, extractSnippetFromFn } from '../utils/parseResolved.ts'; import { tgpu, d, std } from 'typegpu'; @@ -2022,4 +2022,302 @@ describe('WgslGenerator', () => { expect(snippets[1]?.origin).toBe('constant'); expect(snippets[2]?.origin).toBe('runtime'); }); + + it('evaluates object properties in the order they are written', () => { + using consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + + const fieldX = tgpu.comptime(() => { + console.log('fieldX'); + return 6; + }); + const fieldY = tgpu.comptime(() => { + console.log('fieldY'); + return 7; + }); + + const f = tgpu.fn( + [], + d.struct({ x: d.u32, y: d.u32 }), + )(() => { + 'use gpu'; + return { + y: fieldY(), + x: fieldX(), + }; + }); + + void tgpu.resolve([f]); + + expect(consoleLogSpy.mock.calls).toEqual([['fieldY'], ['fieldX']]); + }); + + describe('computed object properties', () => { + const Struct = d.struct({ + field: d.u32, + }); + + it('resolves inline string', () => { + const f = () => { + 'use gpu'; + return Struct({ ['field']: 1 }); + }; + + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "struct Struct { + field: u32, + } + + fn f() -> Struct { + return Struct(1u); + }" + `); + }); + + it('resolves external string', () => { + const key = 'field'; + + const f = () => { + 'use gpu'; + return Struct({ [key]: 1 }); + }; + + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "struct Struct { + field: u32, + } + + fn f() -> Struct { + return Struct(1u); + }" + `); + }); + + it('resolves comptime function call', () => { + const getKey = tgpu.comptime(() => 'field' as const); + + const f = () => { + 'use gpu'; + return Struct({ [getKey()]: 1 }); + }; + + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "struct Struct { + field: u32, + } + + fn f() -> Struct { + return Struct(1u); + }" + `); + }); + + it('resolves builtin and inferred AutoStruct keys', ({ root }) => { + const positionKey = '$position'; + const varyingKey = 'uv'; + + const pipeline = root.createRenderPipeline({ + vertex: () => { + 'use gpu'; + return { + [positionKey]: d.vec4f(), + [varyingKey]: d.vec2f(), + }; + }, + fragment: ({ uv }) => { + 'use gpu'; + return d.vec4f(uv, 0, 1); + }, + }); + + expect(tgpu.resolve([pipeline])).toMatchInlineSnapshot(` + "struct VertexOut { + @builtin(position) position: vec4f, + @location(0) uv: vec2f, + } + + @vertex fn vertex() -> VertexOut { + return VertexOut(vec4f(), vec2f()); + } + + struct FragmentIn { + @location(0) uv: vec2f, + } + + @fragment fn fragment(_arg_0: FragmentIn) -> @location(0) vec4f { + return vec4f(_arg_0.uv, 0f, 1f); + }" + `); + }); + + it('preserves JS evaluation order', () => { + using consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + + const key1 = tgpu.comptime(() => { + console.log('key1'); + return 'x' as const; + }); + const key2 = tgpu.comptime(() => { + console.log('key2'); + return 'y' as const; + }); + const field1 = tgpu.comptime(() => { + console.log('field1'); + return 6; + }); + const field2 = tgpu.comptime(() => { + console.log('field2'); + return 7; + }); + + const f = tgpu.fn( + [], + d.struct({ x: d.u32, y: d.u32 }), + )(() => { + 'use gpu'; + return { + [key1()]: field1(), + [key2()]: field2(), + }; + }); + + void tgpu.resolve([f]); + + expect(consoleLogSpy.mock.calls).toEqual([['key1'], ['field1'], ['key2'], ['field2']]); + }); + + it('evaluates extra properties before stripping them', () => { + using consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + + const fieldX = tgpu.comptime(() => { + console.log('fieldX'); + return 6; + }); + const extraKey = tgpu.comptime(() => { + console.log('extraKey'); + return 'extra' as const; + }); + const extraField = tgpu.comptime(() => { + console.log('extraField'); + return 8; + }); + const fieldY = tgpu.comptime(() => { + console.log('fieldY'); + return 7; + }); + + const f = tgpu.fn( + [], + d.struct({ x: d.u32, y: d.u32 }), + )(() => { + 'use gpu'; + return { + x: fieldX(), + [extraKey()]: extraField(), + y: fieldY(), + }; + }); + + void tgpu.resolve([f]); + + expect(consoleLogSpy.mock.calls).toEqual([ + ['fieldX'], + ['extraKey'], + ['extraField'], + ['fieldY'], + ]); + }); + + it('rejects duplicate keys', () => { + const getKey = tgpu.comptime(() => 'field' as const); + + const f = () => { + 'use gpu'; + // @ts-ignore + return Struct({ field: 1, [getKey()]: 2 }); + }; + + expect(() => tgpu.resolve([f])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:f + - fn*:f(): Duplicate object property key found: 'field: 1' and '[getKey()]: 2'.] + `); + }); + + it('rejects runtime-known key', () => { + const f = tgpu.fn( + [d.u32], + Struct, + )((key) => { + return { + field: 1, + [key]: 2, + }; + }); + + expect(() => tgpu.resolve([f])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn:f: Computed object property key '[key]: 2' must be known at comptime.] + `); + }); + + it('rejects symbol', () => { + const s = Symbol('field'); + + const f = tgpu.fn( + [], + Struct, + )(() => { + return { + field: 1, + [s]: 2, + }; + }); + + expect(() => tgpu.resolve([f])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn:f: Object property keys must be strings in TypeGPU functions.] + `); + }); + + it('rejects numeric', () => { + const x = 7; + + const f = tgpu.fn( + [], + Struct, + )(() => { + return { + field: 1, + [x]: 2, + }; + }); + + expect(() => tgpu.resolve([f])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn:f: Object property keys must be strings in TypeGPU functions.] + `); + }); + + it('rejects string concatenation', () => { + const pre = 'fie'; + const f = () => { + 'use gpu'; + // @ts-ignore + return Struct({ + [pre + 'ld']: 1, + }); + }; + + expect(() => tgpu.resolve([f])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:f + - fn*:f(): Left-hand side of '+' is of unknown type] + `); + }); + }); }); From 787628ffe59d3dd0fffb266679a9383a6927d407 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Fri, 28 Aug 2026 13:45:03 +0200 Subject: [PATCH 5/8] glslgenerator --- packages/typegpu-gl/src/glslGenerator.ts | 70 +++++-- .../typegpu-gl/tests/glslGenerator.test.ts | 177 +++++++++++++++++- 2 files changed, 231 insertions(+), 16 deletions(-) diff --git a/packages/typegpu-gl/src/glslGenerator.ts b/packages/typegpu-gl/src/glslGenerator.ts index deb15ac312..abea500db9 100644 --- a/packages/typegpu-gl/src/glslGenerator.ts +++ b/packages/typegpu-gl/src/glslGenerator.ts @@ -1,5 +1,11 @@ import { NodeTypeCatalog as NODE } from 'tinyest'; -import type { Expression, Return } from 'tinyest'; +import type { + Expression, + Return, + ObjectExpression, + ObjectExpressionWithComputedProps, + ObjectProperty, +} from 'tinyest'; import { tgpu, d, type ShaderStage, std } from 'typegpu'; import { abstractInt, @@ -944,12 +950,11 @@ export class GlslGenerator extends WgslGenerator { const expectedReturnType = this.ctx.topFunctionReturnType; // Case 1: Object literal return like `return { $position: ..., uv: ... }`. - if (typeof exprNode === 'object' && exprNode[0] === NODE.objectExpr) { - return this.#handleStructReturn( - exprNode as unknown as [number, Record], - expectedReturnType, - entryFnState, - ); + if ( + typeof exprNode === 'object' && + (exprNode[0] === NODE.objectExpr || exprNode[0] === NODE.objectExprWithComputedProps) + ) { + return this.#handleStructReturn(exprNode, expectedReturnType, entryFnState); } // Non-literal return: inspect type to decide how to assign. @@ -1003,10 +1008,29 @@ export class GlslGenerator extends WgslGenerator { } #handleStructReturn( - exprNode: [number, Record], + exprNode: ObjectExpression | ObjectExpressionWithComputedProps, expectedReturnType: d.BaseData | undefined, entryFnState: EntryFnState, ): string { + // Normalize to `objectProperty[]` + const properties = + exprNode[0] === NODE.objectExprWithComputedProps + ? exprNode[1] + : Object.entries(exprNode[1]).map( + ([key, value]) => [NODE.objectProperty, key, value, false] satisfies ObjectProperty, + ); + + const seenKeys = new Map(); + const resolveUniqueKey = (prop: ObjectProperty): string => { + const key = this._resolveObjectPropertyKey(prop); + const dupProp = seenKeys.get(key); + if (dupProp) { + throw new Error(`Duplicate object property key: '${key}'.`); + } + seenKeys.set(key, prop); + return key; + }; + // Is this an auto-detected output struct? If so, register each prop so the // output struct's propTypes reflects what the body actually returns. const isAutoStruct = expectedReturnType?.type === 'auto-struct'; @@ -1020,20 +1044,36 @@ export class GlslGenerator extends WgslGenerator { // Resolve each RHS first so module-level references get reserved (and types become // available) before we allocate our LHS output identifiers. - const resolved = Object.entries(exprNode[1]).map(([prop, rhsNode]) => { - // oxlint-disable-next-line typescript/no-explicit-any - const rhsExpr = this._expression(rhsNode as any); + const resolved: { + prop: string; + rhsStr: string; + dataType: d.BaseData; + }[] = []; + for (const prop of properties) { + const key = resolveUniqueKey(prop); + const rhsNode = prop[2]; + const rhsExpr = this._expression(rhsNode); const dataType = rhsExpr.dataType as d.BaseData; const rhsStr = this.ctx.resolve(rhsExpr.value, dataType).value; + // Register the prop on the auto-struct so the caller's completeStruct picks it up. if (autoStruct) { - const existing = autoStruct.accessProp(prop); + const existing = autoStruct.accessProp(key); if (!existing) { - autoStruct.provideProp(prop, dataType); + autoStruct.provideProp(key, dataType); } } - return { prop, rhsStr, dataType }; - }); + + if ( + expectedReturnType && + d.isWgslStruct(expectedReturnType) && + expectedReturnType.propTypes[key] === undefined + ) { + continue; + } + + resolved.push({ prop: key, rhsStr, dataType }); + } const lines: string[] = []; for (const { prop, rhsStr, dataType } of resolved) { diff --git a/packages/typegpu-gl/tests/glslGenerator.test.ts b/packages/typegpu-gl/tests/glslGenerator.test.ts index 27956bddcb..71dc4102b2 100644 --- a/packages/typegpu-gl/tests/glslGenerator.test.ts +++ b/packages/typegpu-gl/tests/glslGenerator.test.ts @@ -1,4 +1,4 @@ -import { describe, expect } from 'vitest'; +import { describe, expect, vi } from 'vitest'; import { tgpu, d, std } from 'typegpu'; import { dualGlOptions, glOptions } from '@typegpu/gl'; import { translateWgslTypeToGlsl } from '../src/glslGenerator.ts'; @@ -567,4 +567,179 @@ describe('GlslGenerator - entry point generation with JS functions', () => { - fn*:foo(): User-defined variables cannot start with 'gl_'] `); }); + + it('resolves computed properties in entry point return', () => { + const positionKey = 'position' as const; + const getUvKey = tgpu.comptime(() => 'uv' as const); + + const vertFn = tgpu.vertexFn({ + out: { + position: d.builtin.position, + uv: d.vec2f, + }, + })(() => { + 'use gpu'; + return { + [positionKey]: d.vec4f(0, 0, 0, 1), + [getUvKey()]: d.vec2f(1, 2), + }; + }); + + expect(tgpu.resolve([vertFn], dualGlOptions().vertex)).toMatchInlineSnapshot(` + "out vec2 vary_uv; + + void main() { + { + gl_Position = vec4(0, 0, 0, 1); + vary_uv = vec2(1, 2); + return; + } + }" + `); + }); + + it('evaluates object properties in the order they are written in entry point return', () => { + using consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + + const fieldX = tgpu.comptime(() => { + console.log('fieldX'); + return 6; + }); + const fieldY = tgpu.comptime(() => { + console.log('fieldY'); + return 7; + }); + + const vertFn = tgpu.vertexFn({ + out: { + position: d.builtin.position, + x: d.u32, + y: d.u32, + }, + })(() => { + 'use gpu'; + return { + position: d.vec4f(), + y: d.u32(fieldY()), + x: d.u32(fieldX()), + }; + }); + + void tgpu.resolve([vertFn], dualGlOptions().vertex); + + expect(consoleLogSpy.mock.calls).toEqual([['fieldY'], ['fieldX']]); + }); + + it('evaluates extra properties in entry point return before stripping them', () => { + using consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + + const fieldX = tgpu.comptime(() => { + console.log('fieldX'); + return 6; + }); + const extraKey = tgpu.comptime(() => { + console.log('extraKey'); + return 'extra' as const; + }); + const extraField = tgpu.comptime(() => { + console.log('extraField'); + return 8; + }); + const fieldY = tgpu.comptime(() => { + console.log('fieldY'); + return 7; + }); + + const vertFn = tgpu.vertexFn({ + out: { + position: d.builtin.position, + x: d.u32, + y: d.u32, + }, + })(() => { + 'use gpu'; + return { + position: d.vec4f(), + x: d.u32(fieldX()), + [extraKey()]: d.u32(extraField()), + y: d.u32(fieldY()), + }; + }); + + const result = tgpu.resolve([vertFn], dualGlOptions().vertex); + + expect(result).not.toContain('extra'); + expect(consoleLogSpy.mock.calls).toEqual([ + ['fieldX'], + ['extraKey'], + ['extraField'], + ['fieldY'], + ]); + }); + + it('preserves JS evaluation order in entry point return', () => { + using consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + + const key1 = tgpu.comptime(() => { + console.log('key1'); + return 'x' as const; + }); + const key2 = tgpu.comptime(() => { + console.log('key2'); + return 'y' as const; + }); + const field1 = tgpu.comptime(() => { + console.log('field1'); + return 6; + }); + const field2 = tgpu.comptime(() => { + console.log('field2'); + return 7; + }); + + const vertFn = tgpu.vertexFn({ + out: { + position: d.builtin.position, + x: d.u32, + y: d.u32, + }, + })(() => { + 'use gpu'; + return { + position: d.vec4f(), + [key1()]: d.u32(field1()), + [key2()]: d.u32(field2()), + }; + }); + + void tgpu.resolve([vertFn], dualGlOptions().vertex); + + expect(consoleLogSpy.mock.calls).toEqual([['key1'], ['field1'], ['key2'], ['field2']]); + }); + + it('rejects duplicate keys in entry point return', () => { + const getKey = tgpu.comptime(() => 'uv' as const); + + const vertFn = tgpu.vertexFn({ + out: { + position: d.builtin.position, + uv: d.vec2f, + }, + })(() => { + 'use gpu'; + return { + position: d.vec4f(), + uv: d.vec2f(1, 2), + // @ts-ignore + [getKey()]: d.vec2f(3, 4), + }; + }); + + expect(() => tgpu.resolve([vertFn], dualGlOptions().vertex)) + .toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - vertexFn:vertFn: Duplicate object property key: 'uv'.] + `); + }); }); From bff1162c6e7e10f2082456df084d076946a09501 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Mon, 31 Aug 2026 14:34:14 +0200 Subject: [PATCH 6/8] fix fields order in examples --- apps/typegpu-docs/src/examples/rendering/ray-marching/scene.ts | 2 +- apps/typegpu-docs/src/examples/simple/vaporrave/scene.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/typegpu-docs/src/examples/rendering/ray-marching/scene.ts b/apps/typegpu-docs/src/examples/rendering/ray-marching/scene.ts index 6c2b50b216..2234840267 100644 --- a/apps/typegpu-docs/src/examples/rendering/ray-marching/scene.ts +++ b/apps/typegpu-docs/src/examples/rendering/ray-marching/scene.ts @@ -113,8 +113,8 @@ export async function setupScene(root: TgpuRoot, context: GPUCanvasContext) { 'use gpu'; const shape = getMorphingShape(p, time.$); const floor = Shape({ - dist: sdPlane(p, d.vec3f(0, 1, 0), 0), color: std.mix(d.vec3f(1), d.vec3f(0.2), checkerBoard(std.mul(p.xz, 2))), + dist: sdPlane(p, d.vec3f(0, 1, 0), 0), }); return shapeUnion(shape, floor); diff --git a/apps/typegpu-docs/src/examples/simple/vaporrave/scene.ts b/apps/typegpu-docs/src/examples/simple/vaporrave/scene.ts index 0eab46071a..034966f9d5 100644 --- a/apps/typegpu-docs/src/examples/simple/vaporrave/scene.ts +++ b/apps/typegpu-docs/src/examples/simple/vaporrave/scene.ts @@ -35,8 +35,8 @@ export async function setupScene(root: TgpuRoot, context: GPUCanvasContext) { Ray, )((p) => { const floor = Ray({ - dist: sdPlane(p, c.planeOrthonormal, c.PLANE_OFFSET), color: floorPatternSlot.$(p.xz, floorAngleUniform.$), + dist: sdPlane(p, c.planeOrthonormal, c.PLANE_OFFSET), }); const sphere = getSphere(p, sphereColorUniform.$.rgb, c.sphereCenter, sphereAngleUniform.$); From c635205c11eabb3430544f936efae70b0e171043 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Tue, 1 Sep 2026 12:44:14 +0200 Subject: [PATCH 7/8] better error in glsl (tseynit) --- packages/typegpu-gl/src/glslGenerator.ts | 5 ++++- packages/typegpu-gl/tests/glslGenerator.test.ts | 8 ++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/typegpu-gl/src/glslGenerator.ts b/packages/typegpu-gl/src/glslGenerator.ts index abea500db9..a00805803d 100644 --- a/packages/typegpu-gl/src/glslGenerator.ts +++ b/packages/typegpu-gl/src/glslGenerator.ts @@ -11,6 +11,7 @@ import { abstractInt, getName, snip, + stringifyNode, UnknownData, WgslGenerator, withValue, @@ -1025,7 +1026,9 @@ export class GlslGenerator extends WgslGenerator { const key = this._resolveObjectPropertyKey(prop); const dupProp = seenKeys.get(key); if (dupProp) { - throw new Error(`Duplicate object property key: '${key}'.`); + throw new Error( + `Duplicate object property key found: '${stringifyNode(dupProp)}' and '${stringifyNode(prop)}'.`, + ); } seenKeys.set(key, prop); return key; diff --git a/packages/typegpu-gl/tests/glslGenerator.test.ts b/packages/typegpu-gl/tests/glslGenerator.test.ts index 71dc4102b2..d60fb0222b 100644 --- a/packages/typegpu-gl/tests/glslGenerator.test.ts +++ b/packages/typegpu-gl/tests/glslGenerator.test.ts @@ -737,9 +737,9 @@ describe('GlslGenerator - entry point generation with JS functions', () => { expect(() => tgpu.resolve([vertFn], dualGlOptions().vertex)) .toThrowErrorMatchingInlineSnapshot(` - [Error: Resolution of the following tree failed: - - - - vertexFn:vertFn: Duplicate object property key: 'uv'.] - `); + [Error: Resolution of the following tree failed: + - + - vertexFn:vertFn: Duplicate object property key found: 'uv: d.vec2f(1, 2)' and '[getKey()]: d.vec2f(3, 4)'.] + `); }); }); From 5384ac1572b22b3c89d168d2e1cb600314aa58b3 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Fri, 4 Sep 2026 17:35:38 +0200 Subject: [PATCH 8/8] legacy transpilers support computed properties --- packages/tinyest-for-wgsl/src/parsers.ts | 54 +++++++------------ .../tinyest-for-wgsl/tests/parsers.test.ts | 13 ++--- .../unplugin-typegpu/test/obfuscation.test.ts | 2 +- 3 files changed, 28 insertions(+), 41 deletions(-) diff --git a/packages/tinyest-for-wgsl/src/parsers.ts b/packages/tinyest-for-wgsl/src/parsers.ts index c328db18ba..4940a877ff 100644 --- a/packages/tinyest-for-wgsl/src/parsers.ts +++ b/packages/tinyest-for-wgsl/src/parsers.ts @@ -31,9 +31,7 @@ function createLegacyTraspilers() { ...acornTranspilers, ObjectExpression(ctx, node, transpile) { - const properties: Record = {}; - - for (const prop of node.properties) { + const objectProperties = node.properties.map((prop) => { if (prop.type === 'SpreadElement') { throw new Error('Spread elements are not supported in TGSL.'); } @@ -42,41 +40,29 @@ function createLegacyTraspilers() { throw new Error('Object method elements are not supported in TGSL.'); } - if (prop.computed) { - throw new Error('Computed object properties are not supported in TGSL.'); - } + return transpile(ctx, prop) as tinyest.ObjectProperty; + }); - let key: string; - - switch (prop.key.type) { - // Shared - case 'Identifier': - key = prop.key.name; - break; - - // Babel - case 'StringLiteral': - case 'NumericLiteral': - case 'BigIntLiteral': - key = String(prop.key.value); - break; - - // Acorn - case 'Literal': - if (prop.key.raw !== null && !prop.key.regex) { - key = String(prop.key.value); - break; - } - - default: - throw new Error(`Unsupported non-computed object property key.`); - } + if (objectProperties.some((prop) => /* computed */ prop[3])) { + return [ + NODE.objectExprWithComputedProps, + objectProperties, + ] as tinyest.ObjectExpressionWithComputedProps; + } + + const obj: Record = {}; + const seenKeys = new Set(); - const value = transpile(ctx, prop.value) as tinyest.Expression; - properties[key] = value; + for (const prop of objectProperties) { + const key = prop[1] as string; + if (seenKeys.has(key)) { + throw new Error(`Duplicate object property key: '${key}'.`); + } + seenKeys.add(key); + obj[key] = /* value */ prop[2]; } - return [NODE.objectExpr, properties]; + return [NODE.objectExpr, obj] as tinyest.ObjectExpression; }, } as Transpilers; } diff --git a/packages/tinyest-for-wgsl/tests/parsers.test.ts b/packages/tinyest-for-wgsl/tests/parsers.test.ts index c7c56e0295..7a18d24627 100644 --- a/packages/tinyest-for-wgsl/tests/parsers.test.ts +++ b/packages/tinyest-for-wgsl/tests/parsers.test.ts @@ -492,16 +492,17 @@ describe('legacy transpileFn', () => { ); }); - it('rejects computed object properties', () => { + it('parses computed object properties', () => { const code = `() => ({ - [1]: 2, + [id]: 1, + [getId()]: 2, });`; - expect(() => transpileFn(parseBabel(code))).toThrowErrorMatchingInlineSnapshot( - `[Error: Computed object properties are not supported in TGSL.]`, + expect(JSON.stringify(transpileFn(parseBabel(code)).body)).toMatchInlineSnapshot( + `"[0,[[10,[108,[[107,"id",[5,"1"],true],[107,[6,"getId",[]],[5,"2"],true]]]]]]"`, ); - expect(() => transpileFn(parseRollup(code))).toThrowErrorMatchingInlineSnapshot( - `[Error: Computed object properties are not supported in TGSL.]`, + expect(JSON.stringify(transpileFn(parseRollup(code)).body)).toMatchInlineSnapshot( + `"[0,[[10,[108,[[107,"id",[5,"1"],true],[107,[6,"getId",[]],[5,"2"],true]]]]]]"`, ); }); diff --git a/packages/unplugin-typegpu/test/obfuscation.test.ts b/packages/unplugin-typegpu/test/obfuscation.test.ts index 7e4bf32ee3..858a366c7a 100644 --- a/packages/unplugin-typegpu/test/obfuscation.test.ts +++ b/packages/unplugin-typegpu/test/obfuscation.test.ts @@ -426,7 +426,7 @@ describe('obfuscate', () => { it('obfuscates computed struct keys', () => { const code = `() => { const prop = 'field'; const struct = { [prop]: 1, [getProp()]: 2, ['prop']: 3 }; return struct[prop] + struct[getProp()] + struct['prop']; }`; - const transpiled = transpileFn(parse(code)); + const transpiled = transpileFnBabel(parse(code)); const { body, externalNames } = obfuscate(transpiled);