From 87e93abe8c8f57c20feb17fb3328af8fba3d603b Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Mon, 24 Aug 2026 17:32:50 +0200 Subject: [PATCH 01/15] new transpilers --- packages/tinyest-for-wgsl/src/transpilers.ts | 361 +++++++++++++++++++ 1 file changed, 361 insertions(+) create mode 100644 packages/tinyest-for-wgsl/src/transpilers.ts diff --git a/packages/tinyest-for-wgsl/src/transpilers.ts b/packages/tinyest-for-wgsl/src/transpilers.ts new file mode 100644 index 0000000000..70cb0230a3 --- /dev/null +++ b/packages/tinyest-for-wgsl/src/transpilers.ts @@ -0,0 +1,361 @@ +import * as acorn from 'acorn'; +import * as babel from '@babel/types'; +import * as tinyest from 'tinyest'; +import type { Context, JsNode } from './types.ts'; + +const { NodeTypeCatalog: NODE } = tinyest; + +type Transpilers = Partial<{ + [Type in TNode['type']]: ( + ctx: Context, + node: Extract, + transpile: (ctx: Context, node: JsNode) => tinyest.AnyNode, + ) => tinyest.AnyNode; +}>; + +type SharedTranspilers = Extract; + +export const baseTranspilers = { + Program(ctx, node, transpile) { + const body = node.body[0]; + + if (!body) { + throw new Error('tgpu.fn was not implemented correctly.'); + } + + return transpile(ctx, body); + }, + + ExpressionStatement(ctx, node, transpile) { + return transpile(ctx, node.expression); + }, + + ArrowFunctionExpression() { + throw new Error('Arrow functions are not supported inside TGSL.'); + }, + + BlockStatement(ctx, node, transpile) { + ctx.stack.push({ declaredNames: [] }); + + try { + return [ + NODE.block, + node.body.map((statement) => transpile(ctx, statement) as tinyest.Statement), + ] as const; + } finally { + ctx.stack.pop(); + } + }, + + ReturnStatement(ctx, node, transpile) { + return node.argument + ? [NODE.return, transpile(ctx, node.argument) as tinyest.Expression] + : [NODE.return]; + }, + + Identifier(_ctx, node) { + return node.name; + }, + + ThisExpression() { + return 'this'; + }, + + BinaryExpression(ctx, node, transpile) { + const left = transpile(ctx, node.left) as tinyest.Expression; + const right = transpile(ctx, node.right) as tinyest.Expression; + + return [NODE.binaryExpr, left, node.operator as tinyest.BinaryOperator, right]; + }, + + LogicalExpression(ctx, node, transpile) { + const left = transpile(ctx, node.left) as tinyest.Expression; + const right = transpile(ctx, node.right) as tinyest.Expression; + + return [NODE.logicalExpr, left, node.operator as tinyest.LogicalOperator, right]; + }, + + AssignmentExpression(ctx, node, transpile) { + const left = transpile(ctx, node.left) as tinyest.Expression; + const right = transpile(ctx, node.right) as tinyest.Expression; + + return [NODE.assignmentExpr, left, node.operator as tinyest.AssignmentOperator, right]; + }, + + UnaryExpression(ctx, node, transpile) { + const wgslOp = node.operator; + const argument = transpile(ctx, node.argument) as tinyest.Expression; + + return [NODE.unaryExpr, wgslOp, argument] as tinyest.UnaryExpression; + }, + + MemberExpression(ctx, node, transpile) { + const object = transpile(ctx, node.object) as tinyest.Expression; + + // If the property is computed, it could potentially be an external identifier. + if (node.computed) { + const property = transpile(ctx, node.property) as tinyest.Expression; + return [NODE.indexAccess, object, property]; + } + + // If the property is not computed, we don't want to register identifiers as external. + ctx.ignoreExternalDepth++; + const property = transpile(ctx, node.property) as tinyest.Expression; + ctx.ignoreExternalDepth--; + + if (typeof property !== 'string') { + throw new Error('Expected identifier as property access key.'); + } + + return [NODE.memberAccess, object, property]; + }, + + UpdateExpression(ctx, node, transpile) { + const operator = node.operator; + const argument = transpile(ctx, node.argument) as tinyest.Expression; + + if (node.prefix) { + throw new Error('Prefix update expressions are not supported in WGSL.'); + } + + return [NODE.postUpdate, operator, argument]; + }, + + ConditionalExpression(ctx, node, transpile) { + const test = transpile(ctx, node.test) as tinyest.Expression; + const consequent = transpile(ctx, node.consequent) as tinyest.Expression; + const alternative = transpile(ctx, node.alternate) as tinyest.Expression; + + return [NODE.conditionalExpr, test, consequent, alternative]; + }, + + CallExpression(ctx, node, transpile) { + const callee = transpile(ctx, node.callee) as tinyest.Expression; + const args = node.arguments.map((argument) => transpile(ctx, argument) as tinyest.Expression); + + return [NODE.call, callee, args]; + }, + + ArrayExpression(ctx, node, transpile) { + return [ + NODE.arrayExpr, + node.elements.map((element) => { + if (!element || element.type === 'SpreadElement') { + throw new Error('Spread elements are not supported in TGSL.'); + } + return transpile(ctx, element) as tinyest.Expression; + }), + ]; + }, + + VariableDeclaration(ctx, node, transpile) { + if (node.declarations.length !== 1 || !node.declarations[0]) { + throw new Error('Currently only one declaration in a statement is supported.'); + } + + const decl = node.declarations[0]; + ctx.ignoreExternalDepth++; + const id = transpile(ctx, decl.id); + ctx.ignoreExternalDepth--; + + if (typeof id !== 'string') { + throw new Error('Invalid variable declaration, expected identifier.'); + } + + ctx.stack[ctx.stack.length - 1]?.declaredNames.push(id); + + const init = decl.init ? (transpile(ctx, decl.init) as tinyest.Expression) : undefined; + + if (node.kind === 'var') { + throw new Error('`var` declarations are not supported.'); + } + + if (node.kind === 'const') { + return init !== undefined ? [NODE.const, id, init] : [NODE.const, id]; + } + + return init !== undefined ? [NODE.let, id, init] : [NODE.let, id]; + }, + + IfStatement(ctx, node, transpile) { + const test = transpile(ctx, node.test) as tinyest.Expression; + const consequent = transpile(ctx, node.consequent) as tinyest.Statement; + const alternate = node.alternate + ? (transpile(ctx, node.alternate) as tinyest.Statement) + : undefined; + + return alternate ? [NODE.if, test, consequent, alternate] : [NODE.if, test, consequent]; + }, + + ForStatement(ctx, node, transpile) { + ctx.stack.push({ declaredNames: [] }); + + const init = node.init ? (transpile(ctx, node.init) as tinyest.Statement) : null; + const condition = node.test ? (transpile(ctx, node.test) as tinyest.Expression) : null; + const update = node.update ? (transpile(ctx, node.update) as tinyest.Statement) : null; + const body = transpile(ctx, node.body) as tinyest.Statement; + + ctx.stack.pop(); + + return [NODE.for, init, condition, update, body]; + }, + + WhileStatement(ctx, node, transpile) { + const condition = transpile(ctx, node.test) as tinyest.Expression; + const body = transpile(ctx, node.body) as tinyest.Statement; + + return [NODE.while, condition, body]; + }, + + ForOfStatement(ctx, node, transpile) { + ctx.stack.push({ declaredNames: [] }); + + const loopVar = transpile(ctx, node.left) as tinyest.Const | tinyest.Let; + const iterable = transpile(ctx, node.right) as tinyest.Expression; + const body = transpile(ctx, node.body) as tinyest.Statement; + + ctx.stack.pop(); + + return [NODE.forOf, loopVar, iterable, body]; + }, + + ContinueStatement() { + return [NODE.continue]; + }, + + BreakStatement() { + return [NODE.break]; + }, +} satisfies Pick, SharedTranspilers>; + +const acornSpecificTranspilers = { + Literal(_ctx, node) { + if (node.regex) { + throw new Error('Regular expression literals are not representable in WGSL.'); + } + if (node.raw === 'null') { + throw new Error('null is not representable in WGSL.'); + } + if (typeof node.value === 'boolean') { + return node.value; + } + if (typeof node.value === 'string') { + return [NODE.stringLiteral, node.value]; + } + if (node.bigint) { + console.warn('BigInt literals are represented as numbers - loss of precision may occur.'); + } + return [NODE.numericLiteral, String(Number(node.value))]; + }, + + ObjectExpression(ctx, node, transpile) { + const properties: Record = {}; + + for (const prop of node.properties) { + // TODO: Handle SpreadElement + if (prop.type === 'SpreadElement') { + throw new Error('Spread elements are not supported in TGSL.'); + } + + // TODO: Handle Object method + if (prop.method) { + throw new Error('Object method elements are not supported in TGSL.'); + } + + // TODO: Handle computed properties + if (prop.computed || (prop.key.type !== 'Identifier' && prop.key.type !== 'Literal')) { + throw new Error('Computed object properties are not supported in TGSL.'); + } + + 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; + } + + return [NODE.objectExpr, properties]; + }, +} satisfies Transpilers; + +export const acornTranspilers = { + ...baseTranspilers, + ...acornSpecificTranspilers, +} satisfies Transpilers; + +const tsFallthrough = ( + ctx: Context, + node: { expression: babel.Expression }, + transpile: (ctx: Context, node: babel.Node) => tinyest.AnyNode, +) => { + return transpile(ctx, node.expression); +}; + +const babelSpecificTranspilers = { + NumericLiteral(_ctx, node) { + return [NODE.numericLiteral, String(node.value)]; + }, + + BigIntLiteral(_ctx, node) { + console.warn('BigInt literals are represented as numbers - loss of precision may occur.'); + return [NODE.numericLiteral, String(Number.parseInt(node.value))]; + }, + + BooleanLiteral(_ctx, node) { + return node.value; + }, + + StringLiteral(_ctx, node) { + return [NODE.stringLiteral, node.value]; + }, + + ObjectExpression(ctx, node, transpile) { + const properties: Record = {}; + + for (const prop of node.properties) { + // 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; + + switch (prop.key.type) { + case 'Identifier': + key = prop.key.name; + break; + + case 'StringLiteral': + case 'NumericLiteral': + case 'BigIntLiteral': + key = String(prop.key.value); + break; + + default: + throw new Error(`Unsupported non-computed object property key: ${prop.key.type}`); + } + + const value = transpile(ctx, prop.value) as tinyest.Expression; + properties[key] = value; + } + + return [NODE.objectExpr, properties]; + }, + + TSAsExpression: tsFallthrough, + TSSatisfiesExpression: tsFallthrough, + TSNonNullExpression: tsFallthrough, +} satisfies Transpilers; + +export const babelTranspilers = { + ...baseTranspilers, + ...babelSpecificTranspilers, +} satisfies Transpilers; From 36275c8021bf350f27b91ab399e818c5064d7330 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Tue, 25 Aug 2026 13:48:38 +0200 Subject: [PATCH 02/15] external chain --- packages/tinyest-for-wgsl/src/externals.ts | 92 +++++++++++++++++++++- 1 file changed, 88 insertions(+), 4 deletions(-) diff --git a/packages/tinyest-for-wgsl/src/externals.ts b/packages/tinyest-for-wgsl/src/externals.ts index 479ba914d4..ac0ac49dd6 100644 --- a/packages/tinyest-for-wgsl/src/externals.ts +++ b/packages/tinyest-for-wgsl/src/externals.ts @@ -1,9 +1,7 @@ +import * as acorn from 'acorn'; +import * as babel from '@babel/types'; import type { Context, JsNode } from './types.ts'; -function isDeclared(ctx: Context, name: string) { - return ctx.stack.some((scope) => scope.declaredNames.includes(name)); -} - /** * Checks if the provided node is an external chain access. * @example @@ -43,3 +41,89 @@ export function tryFindExternalChain(ctx: Context, node: JsNode): string | undef } } } + +function isDeclared(ctx: Context, name: string) { + return ctx.stack.some((scope) => scope.declaredNames.includes(name)); +} + +type PrivateNameGetter = (node: TMemberExpression) => string | undefined; + +type ExternalChainFinder = (ctx: Context, node: TNode) => string | undefined; + +function createExternalChainFinder( + getPrivatePropertyName: PrivateNameGetter, +): ExternalChainFinder; +function createExternalChainFinder( + getPrivatePropertyName: PrivateNameGetter, +): ExternalChainFinder; +function createExternalChainFinder( + getPrivatePropertyName: + | PrivateNameGetter + | PrivateNameGetter, +) { + const find: ExternalChainFinder = (ctx, node) => { + if (node.type === 'Identifier' && !isDeclared(ctx, node.name)) { + return node.name; + } + if (node.type === 'ThisExpression') { + return 'this'; + } + + if (node.type === 'MemberExpression' && !node.computed) { + if (ctx.visitedNodes.has(node)) { + return; + } + ctx.visitedNodes.add(node); + + const property = + node.property.type === 'Identifier' && node.property.name !== '$' + ? node.property.name + : ( + getPrivatePropertyName as PrivateNameGetter< + acorn.MemberExpression | babel.MemberExpression + > + )(node); + + if (!property) { + return; + } + + const lhs = find(ctx, node.object); + if (lhs) { + return `${lhs}.${property}`; + } + } + }; + + return find; +} + +function getPrivatePropertyNameAcorn(node: acorn.MemberExpression): string | undefined { + return node.property.type === 'PrivateIdentifier' ? `#${node.property.name}` : undefined; +} + +function getPrivatePropertyNameBabel(node: babel.MemberExpression): string | undefined { + return node.property.type === 'PrivateName' ? `#${node.property.id.name}` : undefined; +} + +/** + * Checks if the provided node is an external chain access. + * @example + * tryFindExternalChainAcorn(ctx, node`ext`); // 'ext' + * tryFindExternalChainAcorn(ctx, node`ext.p.q`); // 'ext.p.q' + * tryFindExternalChainAcorn(ctx, node`ext.p.q().r`); // undefined + * tryFindExternalChainAcorn(ctx, node`local.p.q`); // undefined + * tryFindExternalChainAcorn(ctx, node`ext.$.q`); // undefined + */ +export const tryFindExternalChainAcorn = createExternalChainFinder(getPrivatePropertyNameAcorn); + +/** + * Checks if the provided node is an external chain access. + * @example + * tryFindExternalChainBabel(ctx, node`ext`); // 'ext' + * tryFindExternalChainBabel(ctx, node`ext.p.q`); // 'ext.p.q' + * tryFindExternalChainBabel(ctx, node`ext.p.q().r`); // undefined + * tryFindExternalChainBabel(ctx, node`local.p.q`); // undefined + * tryFindExternalChainBabel(ctx, node`ext.$.q`); // undefined + */ +export const tryFindExternalChainBabel = createExternalChainFinder(getPrivatePropertyNameBabel); From 27c42f9f914f7c405779cb6f205789d5213418d8 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Wed, 26 Aug 2026 00:17:46 +0200 Subject: [PATCH 03/15] function parts --- .../tinyest-for-wgsl/src/functionParts.ts | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 packages/tinyest-for-wgsl/src/functionParts.ts diff --git a/packages/tinyest-for-wgsl/src/functionParts.ts b/packages/tinyest-for-wgsl/src/functionParts.ts new file mode 100644 index 0000000000..a3ed741011 --- /dev/null +++ b/packages/tinyest-for-wgsl/src/functionParts.ts @@ -0,0 +1,151 @@ +import type * as babel from '@babel/types'; +import type * as acorn from 'acorn'; +import * as tinyest from 'tinyest'; +import { FuncParameterType } from 'tinyest'; +import type { JsNode } from './types.ts'; + +type DestructuredProps = Extract< + tinyest.FuncParameter, + { type: typeof FuncParameterType.destructuredObject } +>['props']; + +type FunctionParts = { + params: tinyest.FuncParameter[]; + body: TBody; +}; + +type DestructuredPropsGetter = (pattern: TObjectPattern) => DestructuredProps; + +type FunctionPartsExtractor = (rootNode: TRootNode) => FunctionParts; + +function createFunctionPartsExtractor( + getDestructuredProps: DestructuredPropsGetter, +): FunctionPartsExtractor; +function createFunctionPartsExtractor( + getDestructuredProps: DestructuredPropsGetter, +): FunctionPartsExtractor; +function createFunctionPartsExtractor( + getDestructuredProps: + | DestructuredPropsGetter + | DestructuredPropsGetter, +) { + const extract = (rootNode: JsNode) => { + let functionNode: + | acorn.ArrowFunctionExpression + | acorn.FunctionExpression + | acorn.FunctionDeclaration + | acorn.AnonymousFunctionDeclaration + | babel.ArrowFunctionExpression + | babel.FunctionExpression + | babel.FunctionDeclaration + | null = null; + + // Unwrapping until we get to a function + let unwrappedNode = rootNode; + while (true) { + if (unwrappedNode.type === 'Program') { + const statement = unwrappedNode.body.filter( + (n) => n.type === 'ExpressionStatement' || n.type === 'FunctionDeclaration', + )[0]; // <- assuming only one function declaration + + if (!statement) { + break; + } + + unwrappedNode = statement; + } else if (unwrappedNode.type === 'ExpressionStatement') { + unwrappedNode = unwrappedNode.expression; + } else if (unwrappedNode.type === 'ArrowFunctionExpression') { + functionNode = unwrappedNode; + break; // We got a function + } else if (unwrappedNode.type === 'FunctionExpression') { + functionNode = unwrappedNode; + break; // We got a function + } else if (unwrappedNode.type === 'FunctionDeclaration') { + functionNode = unwrappedNode; + break; // We got a function + } else { + // Unsupported node + break; + } + } + + if (!functionNode) { + throw new Error( + `tgpu.fn expected a single function to be passed as implementation ${JSON.stringify( + unwrappedNode, + )}`, + ); + } + + if (functionNode.async) { + throw new Error('tgpu.fn cannot be async'); + } + + if (functionNode.generator) { + throw new Error('tgpu.fn cannot be a generator'); + } + + const unsupportedTypes = new Set( + functionNode.params.flatMap((param) => + param.type === 'ObjectPattern' || param.type === 'Identifier' ? [] : [param.type], + ), + ); + if (unsupportedTypes.size > 0) { + throw new Error( + `Unsupported function parameter type(s): ${[...unsupportedTypes].join(', ')}`, + ); + } + + return { + params: ( + functionNode.params as ( + | babel.Identifier + | acorn.Identifier + | babel.ObjectPattern + | acorn.ObjectPattern + )[] + ).map((param) => + param.type === 'ObjectPattern' + ? { + type: FuncParameterType.destructuredObject, + props: ( + getDestructuredProps as DestructuredPropsGetter< + acorn.ObjectPattern | babel.ObjectPattern + > + )(param), + } + : { + type: FuncParameterType.identifier, + name: param.name, + }, + ), + body: functionNode.body, + }; + }; + + return extract as + | FunctionPartsExtractor + | FunctionPartsExtractor; +} + +function getDestructuredPropsAcorn(node: acorn.ObjectPattern): DestructuredProps { + return node.properties.flatMap((prop) => + prop.type === 'Property' && prop.key.type === 'Identifier' && prop.value.type === 'Identifier' + ? [{ name: prop.key.name, alias: prop.value.name }] + : [], + ); +} + +function getDestructuredPropsBabel(node: babel.ObjectPattern): DestructuredProps { + return node.properties.flatMap((prop) => + prop.type === 'ObjectProperty' && + prop.key.type === 'Identifier' && + prop.value.type === 'Identifier' + ? [{ name: prop.key.name, alias: prop.value.name }] + : [], + ); +} + +export const extractFunctionPartsAcorn = createFunctionPartsExtractor(getDestructuredPropsAcorn); +export const extractFunctionPartsBabel = createFunctionPartsExtractor(getDestructuredPropsBabel); From c7a512f4c1e8a0bf5b8eb9ae86ef7f68c316a363 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Wed, 26 Aug 2026 20:37:31 +0200 Subject: [PATCH 04/15] simplify --- packages/tinyest-for-wgsl/src/externals.ts | 100 +------ .../tinyest-for-wgsl/src/functionParts.ts | 244 ++++++++---------- packages/tinyest-for-wgsl/src/transpilers.ts | 14 +- 3 files changed, 126 insertions(+), 232 deletions(-) diff --git a/packages/tinyest-for-wgsl/src/externals.ts b/packages/tinyest-for-wgsl/src/externals.ts index ac0ac49dd6..06c589c9d9 100644 --- a/packages/tinyest-for-wgsl/src/externals.ts +++ b/packages/tinyest-for-wgsl/src/externals.ts @@ -1,6 +1,10 @@ -import * as acorn from 'acorn'; -import * as babel from '@babel/types'; -import type { Context, JsNode } from './types.ts'; +import type * as acorn from 'acorn'; +import type * as babel from '@babel/types'; +import type { Context, JsNode, ExternalChainFinder } from './types.ts'; + +function isDeclared(ctx: Context, name: string) { + return ctx.stack.some((scope) => scope.declaredNames.includes(name)); +} /** * Checks if the provided node is an external chain access. @@ -27,9 +31,9 @@ export function tryFindExternalChain(ctx: Context, node: JsNode): string | undef let property; if (node.property.type === 'Identifier' && node.property.name !== '$') { property = node.property.name; - } else if (node.property.type === 'PrivateName') { + } else if (/* babel */ node.property.type === 'PrivateName') { property = `#${node.property.id.name}`; - } else if (node.property.type === 'PrivateIdentifier') { + } else if (/* acorn */ node.property.type === 'PrivateIdentifier') { property = `#${node.property.name}`; } else { return; @@ -42,88 +46,6 @@ export function tryFindExternalChain(ctx: Context, node: JsNode): string | undef } } -function isDeclared(ctx: Context, name: string) { - return ctx.stack.some((scope) => scope.declaredNames.includes(name)); -} - -type PrivateNameGetter = (node: TMemberExpression) => string | undefined; - -type ExternalChainFinder = (ctx: Context, node: TNode) => string | undefined; - -function createExternalChainFinder( - getPrivatePropertyName: PrivateNameGetter, -): ExternalChainFinder; -function createExternalChainFinder( - getPrivatePropertyName: PrivateNameGetter, -): ExternalChainFinder; -function createExternalChainFinder( - getPrivatePropertyName: - | PrivateNameGetter - | PrivateNameGetter, -) { - const find: ExternalChainFinder = (ctx, node) => { - if (node.type === 'Identifier' && !isDeclared(ctx, node.name)) { - return node.name; - } - if (node.type === 'ThisExpression') { - return 'this'; - } - - if (node.type === 'MemberExpression' && !node.computed) { - if (ctx.visitedNodes.has(node)) { - return; - } - ctx.visitedNodes.add(node); - - const property = - node.property.type === 'Identifier' && node.property.name !== '$' - ? node.property.name - : ( - getPrivatePropertyName as PrivateNameGetter< - acorn.MemberExpression | babel.MemberExpression - > - )(node); - - if (!property) { - return; - } - - const lhs = find(ctx, node.object); - if (lhs) { - return `${lhs}.${property}`; - } - } - }; - - return find; -} - -function getPrivatePropertyNameAcorn(node: acorn.MemberExpression): string | undefined { - return node.property.type === 'PrivateIdentifier' ? `#${node.property.name}` : undefined; -} - -function getPrivatePropertyNameBabel(node: babel.MemberExpression): string | undefined { - return node.property.type === 'PrivateName' ? `#${node.property.id.name}` : undefined; -} - -/** - * Checks if the provided node is an external chain access. - * @example - * tryFindExternalChainAcorn(ctx, node`ext`); // 'ext' - * tryFindExternalChainAcorn(ctx, node`ext.p.q`); // 'ext.p.q' - * tryFindExternalChainAcorn(ctx, node`ext.p.q().r`); // undefined - * tryFindExternalChainAcorn(ctx, node`local.p.q`); // undefined - * tryFindExternalChainAcorn(ctx, node`ext.$.q`); // undefined - */ -export const tryFindExternalChainAcorn = createExternalChainFinder(getPrivatePropertyNameAcorn); +export const tryFindExternalChainAcorn: ExternalChainFinder = tryFindExternalChain; -/** - * Checks if the provided node is an external chain access. - * @example - * tryFindExternalChainBabel(ctx, node`ext`); // 'ext' - * tryFindExternalChainBabel(ctx, node`ext.p.q`); // 'ext.p.q' - * tryFindExternalChainBabel(ctx, node`ext.p.q().r`); // undefined - * tryFindExternalChainBabel(ctx, node`local.p.q`); // undefined - * tryFindExternalChainBabel(ctx, node`ext.$.q`); // undefined - */ -export const tryFindExternalChainBabel = createExternalChainFinder(getPrivatePropertyNameBabel); +export const tryFindExternalChainBabel: ExternalChainFinder = tryFindExternalChain; diff --git a/packages/tinyest-for-wgsl/src/functionParts.ts b/packages/tinyest-for-wgsl/src/functionParts.ts index a3ed741011..43c1af70f2 100644 --- a/packages/tinyest-for-wgsl/src/functionParts.ts +++ b/packages/tinyest-for-wgsl/src/functionParts.ts @@ -2,150 +2,130 @@ import type * as babel from '@babel/types'; import type * as acorn from 'acorn'; import * as tinyest from 'tinyest'; import { FuncParameterType } from 'tinyest'; -import type { JsNode } from './types.ts'; - -type DestructuredProps = Extract< - tinyest.FuncParameter, - { type: typeof FuncParameterType.destructuredObject } ->['props']; - -type FunctionParts = { - params: tinyest.FuncParameter[]; - body: TBody; -}; - -type DestructuredPropsGetter = (pattern: TObjectPattern) => DestructuredProps; - -type FunctionPartsExtractor = (rootNode: TRootNode) => FunctionParts; - -function createFunctionPartsExtractor( - getDestructuredProps: DestructuredPropsGetter, -): FunctionPartsExtractor; -function createFunctionPartsExtractor( - getDestructuredProps: DestructuredPropsGetter, -): FunctionPartsExtractor; -function createFunctionPartsExtractor( - getDestructuredProps: - | DestructuredPropsGetter - | DestructuredPropsGetter, -) { - const extract = (rootNode: JsNode) => { - let functionNode: - | acorn.ArrowFunctionExpression - | acorn.FunctionExpression - | acorn.FunctionDeclaration - | acorn.AnonymousFunctionDeclaration - | babel.ArrowFunctionExpression - | babel.FunctionExpression - | babel.FunctionDeclaration - | null = null; - - // Unwrapping until we get to a function - let unwrappedNode = rootNode; - while (true) { - if (unwrappedNode.type === 'Program') { - const statement = unwrappedNode.body.filter( - (n) => n.type === 'ExpressionStatement' || n.type === 'FunctionDeclaration', - )[0]; // <- assuming only one function declaration - - if (!statement) { - break; - } - - unwrappedNode = statement; - } else if (unwrappedNode.type === 'ExpressionStatement') { - unwrappedNode = unwrappedNode.expression; - } else if (unwrappedNode.type === 'ArrowFunctionExpression') { - functionNode = unwrappedNode; - break; // We got a function - } else if (unwrappedNode.type === 'FunctionExpression') { - functionNode = unwrappedNode; - break; // We got a function - } else if (unwrappedNode.type === 'FunctionDeclaration') { - functionNode = unwrappedNode; - break; // We got a function - } else { - // Unsupported node +import type { JsNode, FunctionPartsExtractor } from './types.ts'; + +type FunctionNode = + | acorn.ArrowFunctionExpression + | acorn.FunctionExpression + | acorn.FunctionDeclaration + | acorn.AnonymousFunctionDeclaration + | babel.ArrowFunctionExpression + | babel.FunctionExpression + | babel.FunctionDeclaration; + +/** + * Unwraps the root node until we get to a function. + */ +function unwrapToFunction(rootNode: JsNode): FunctionNode { + let functionNode: FunctionNode | null = null; + + let unwrappedNode = rootNode; + while (true) { + if (unwrappedNode.type === 'Program') { + const statement = unwrappedNode.body.filter( + (n) => n.type === 'ExpressionStatement' || n.type === 'FunctionDeclaration', + )[0]; // <- assuming only one function declaration + + if (!statement) { break; } - } - if (!functionNode) { - throw new Error( - `tgpu.fn expected a single function to be passed as implementation ${JSON.stringify( - unwrappedNode, - )}`, - ); + unwrappedNode = statement; + } else if (unwrappedNode.type === 'ExpressionStatement') { + unwrappedNode = unwrappedNode.expression; + } else if (unwrappedNode.type === 'ArrowFunctionExpression') { + functionNode = unwrappedNode; + break; // We got a function + } else if (unwrappedNode.type === 'FunctionExpression') { + functionNode = unwrappedNode; + break; // We got a function + } else if (unwrappedNode.type === 'FunctionDeclaration') { + functionNode = unwrappedNode; + break; // We got a function + } else { + // Unsupported node + break; } + } - if (functionNode.async) { - throw new Error('tgpu.fn cannot be async'); - } - - if (functionNode.generator) { - throw new Error('tgpu.fn cannot be a generator'); - } - - const unsupportedTypes = new Set( - functionNode.params.flatMap((param) => - param.type === 'ObjectPattern' || param.type === 'Identifier' ? [] : [param.type], - ), + if (!functionNode) { + throw new Error( + `tgpu.fn expected a single function to be passed as implementation ${JSON.stringify( + unwrappedNode, + )}`, ); - if (unsupportedTypes.size > 0) { - throw new Error( - `Unsupported function parameter type(s): ${[...unsupportedTypes].join(', ')}`, - ); - } + } - return { - params: ( - functionNode.params as ( - | babel.Identifier - | acorn.Identifier - | babel.ObjectPattern - | acorn.ObjectPattern - )[] - ).map((param) => - param.type === 'ObjectPattern' - ? { - type: FuncParameterType.destructuredObject, - props: ( - getDestructuredProps as DestructuredPropsGetter< - acorn.ObjectPattern | babel.ObjectPattern - > - )(param), - } - : { - type: FuncParameterType.identifier, - name: param.name, - }, - ), - body: functionNode.body, - }; - }; - - return extract as - | FunctionPartsExtractor - | FunctionPartsExtractor; + return functionNode; } -function getDestructuredPropsAcorn(node: acorn.ObjectPattern): DestructuredProps { - return node.properties.flatMap((prop) => - prop.type === 'Property' && prop.key.type === 'Identifier' && prop.value.type === 'Identifier' - ? [{ name: prop.key.name, alias: prop.value.name }] - : [], +/** + * Rejects functions TGSL cannot represent. + */ +function validateFunction(functionNode: FunctionNode): void { + if (functionNode.async) { + throw new Error('tgpu.fn cannot be async'); + } + + if (functionNode.generator) { + throw new Error('tgpu.fn cannot be a generator'); + } + + const unsupportedTypes = new Set( + functionNode.params.flatMap((param) => + param.type === 'ObjectPattern' || param.type === 'Identifier' ? [] : [param.type], + ), ); + if (unsupportedTypes.size > 0) { + throw new Error(`Unsupported function parameter type(s): ${[...unsupportedTypes].join(', ')}`); + } } -function getDestructuredPropsBabel(node: babel.ObjectPattern): DestructuredProps { - return node.properties.flatMap((prop) => - prop.type === 'ObjectProperty' && - prop.key.type === 'Identifier' && - prop.value.type === 'Identifier' - ? [{ name: prop.key.name, alias: prop.value.name }] - : [], +/** + * Assumes `validateFunction` has already rejected unsupported parameter kinds. + */ +function parseParams(functionNode: FunctionNode): tinyest.FuncParameter[] { + return ( + functionNode.params as ( + | babel.Identifier + | acorn.Identifier + | babel.ObjectPattern + | acorn.ObjectPattern + )[] + ).map((param) => + param.type === 'ObjectPattern' + ? { + type: FuncParameterType.destructuredObject, + props: param.properties.flatMap((prop) => + (prop.type === 'Property' /* acorn */ || prop.type === 'ObjectProperty') /* babel */ && + prop.key.type === 'Identifier' && + prop.value.type === 'Identifier' + ? [{ name: prop.key.name, alias: prop.value.name }] + : [], + ), + } + : { + type: FuncParameterType.identifier, + name: param.name, + }, ); } -export const extractFunctionPartsAcorn = createFunctionPartsExtractor(getDestructuredPropsAcorn); -export const extractFunctionPartsBabel = createFunctionPartsExtractor(getDestructuredPropsBabel); +export function extractFunctionParts(rootNode: JsNode): { + params: tinyest.FuncParameter[]; + body: acorn.BlockStatement | acorn.Expression | babel.BlockStatement | babel.Expression; +} { + const functionNode = unwrapToFunction(rootNode); + + validateFunction(functionNode); + + return { + params: parseParams(functionNode), + body: functionNode.body, + }; +} + +export const extractFunctionPartsAcorn = + extractFunctionParts as FunctionPartsExtractor; + +export const extractFunctionPartsBabel = extractFunctionParts as FunctionPartsExtractor; diff --git a/packages/tinyest-for-wgsl/src/transpilers.ts b/packages/tinyest-for-wgsl/src/transpilers.ts index 70cb0230a3..033de9ce91 100644 --- a/packages/tinyest-for-wgsl/src/transpilers.ts +++ b/packages/tinyest-for-wgsl/src/transpilers.ts @@ -1,18 +1,10 @@ import * as acorn from 'acorn'; import * as babel from '@babel/types'; import * as tinyest from 'tinyest'; -import type { Context, JsNode } from './types.ts'; +import type { Context, JsNode, Transpilers } from './types.ts'; const { NodeTypeCatalog: NODE } = tinyest; -type Transpilers = Partial<{ - [Type in TNode['type']]: ( - ctx: Context, - node: Extract, - transpile: (ctx: Context, node: JsNode) => tinyest.AnyNode, - ) => tinyest.AnyNode; -}>; - type SharedTranspilers = Extract; export const baseTranspilers = { @@ -277,7 +269,7 @@ const acornSpecificTranspilers = { } satisfies Transpilers; export const acornTranspilers = { - ...baseTranspilers, + ...(baseTranspilers as Pick, SharedTranspilers>), ...acornSpecificTranspilers, } satisfies Transpilers; @@ -356,6 +348,6 @@ const babelSpecificTranspilers = { } satisfies Transpilers; export const babelTranspilers = { - ...baseTranspilers, + ...(baseTranspilers as Pick, SharedTranspilers>), ...babelSpecificTranspilers, } satisfies Transpilers; From 9546dce89bbbddd05fb281545fa67beb50d288d7 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Wed, 26 Aug 2026 20:39:14 +0200 Subject: [PATCH 05/15] transpilation options --- packages/tinyest-for-wgsl/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tinyest-for-wgsl/src/index.ts b/packages/tinyest-for-wgsl/src/index.ts index b9d34d2db3..efdcc6b131 100644 --- a/packages/tinyest-for-wgsl/src/index.ts +++ b/packages/tinyest-for-wgsl/src/index.ts @@ -1,2 +1,2 @@ export { transpileFn, transpileNode } from './parsers.ts'; -export { type Externals } from './types.ts'; +export type { Externals, TranspilationOptions } from './types.ts'; From bc4e4d4b238dc58c71f55e53fa45a2130bd89916 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Wed, 26 Aug 2026 20:48:07 +0200 Subject: [PATCH 06/15] more simplification --- packages/tinyest-for-wgsl/src/externals.ts | 8 +------- packages/tinyest-for-wgsl/src/functionParts.ts | 12 ++---------- packages/tinyest-for-wgsl/src/types.ts | 16 +++++++++++++++- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/tinyest-for-wgsl/src/externals.ts b/packages/tinyest-for-wgsl/src/externals.ts index 06c589c9d9..75a746ab69 100644 --- a/packages/tinyest-for-wgsl/src/externals.ts +++ b/packages/tinyest-for-wgsl/src/externals.ts @@ -1,6 +1,4 @@ -import type * as acorn from 'acorn'; -import type * as babel from '@babel/types'; -import type { Context, JsNode, ExternalChainFinder } from './types.ts'; +import type { Context, JsNode } from './types.ts'; function isDeclared(ctx: Context, name: string) { return ctx.stack.some((scope) => scope.declaredNames.includes(name)); @@ -45,7 +43,3 @@ export function tryFindExternalChain(ctx: Context, node: JsNode): string | undef } } } - -export const tryFindExternalChainAcorn: ExternalChainFinder = tryFindExternalChain; - -export const tryFindExternalChainBabel: ExternalChainFinder = tryFindExternalChain; diff --git a/packages/tinyest-for-wgsl/src/functionParts.ts b/packages/tinyest-for-wgsl/src/functionParts.ts index 43c1af70f2..55f067b3a4 100644 --- a/packages/tinyest-for-wgsl/src/functionParts.ts +++ b/packages/tinyest-for-wgsl/src/functionParts.ts @@ -2,7 +2,7 @@ import type * as babel from '@babel/types'; import type * as acorn from 'acorn'; import * as tinyest from 'tinyest'; import { FuncParameterType } from 'tinyest'; -import type { JsNode, FunctionPartsExtractor } from './types.ts'; +import type { JsNode } from './types.ts'; type FunctionNode = | acorn.ArrowFunctionExpression @@ -60,7 +60,7 @@ function unwrapToFunction(rootNode: JsNode): FunctionNode { } /** - * Rejects functions TGSL cannot represent. + * Rejects TypeGPU functions that cannot be represented. */ function validateFunction(functionNode: FunctionNode): void { if (functionNode.async) { @@ -81,9 +81,6 @@ function validateFunction(functionNode: FunctionNode): void { } } -/** - * Assumes `validateFunction` has already rejected unsupported parameter kinds. - */ function parseParams(functionNode: FunctionNode): tinyest.FuncParameter[] { return ( functionNode.params as ( @@ -124,8 +121,3 @@ export function extractFunctionParts(rootNode: JsNode): { body: functionNode.body, }; } - -export const extractFunctionPartsAcorn = - extractFunctionParts as FunctionPartsExtractor; - -export const extractFunctionPartsBabel = extractFunctionParts as FunctionPartsExtractor; diff --git a/packages/tinyest-for-wgsl/src/types.ts b/packages/tinyest-for-wgsl/src/types.ts index 5f27600786..81c9fe0503 100644 --- a/packages/tinyest-for-wgsl/src/types.ts +++ b/packages/tinyest-for-wgsl/src/types.ts @@ -9,6 +9,16 @@ export type Scope = { export type Externals = Map; +export type JsNode = babel.Node | acorn.AnyNode; + +export type Transpilers = Partial<{ + [Type in TNode['type']]: ( + ctx: Context, + node: Extract, + transpile: (ctx: Context, node: TNode) => tinyest.AnyNode, + ) => tinyest.AnyNode; +}>; + export type Context = { /** Holds a set of all identifiers that were used in code, but were not declared in code. */ externalNames: Externals; @@ -34,4 +44,8 @@ export type TranspilationResult = { externalNames: Externals; }; -export type JsNode = babel.Node | acorn.AnyNode; +export type AstKind = 'acorn' | 'babel'; + +export type TranspilationOptions = { + ast: TAst; +}; From 60a2b438e0814d44d0aded352abde0681db912d4 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Wed, 26 Aug 2026 23:20:01 +0200 Subject: [PATCH 07/15] working version --- packages/tinyest-for-wgsl/src/parsers.ts | 515 +++--------------- packages/tinyest-for-wgsl/src/transpilers.ts | 4 +- packages/tinyest-for-wgsl/src/types.ts | 22 +- packages/tinyest-for-wgsl/tests/helpers.ts | 13 +- .../tinyest-for-wgsl/tests/parsers.test.ts | 36 +- 5 files changed, 129 insertions(+), 461 deletions(-) diff --git a/packages/tinyest-for-wgsl/src/parsers.ts b/packages/tinyest-for-wgsl/src/parsers.ts index 9e17fef3c4..0fb5e9b9e6 100644 --- a/packages/tinyest-for-wgsl/src/parsers.ts +++ b/packages/tinyest-for-wgsl/src/parsers.ts @@ -2,416 +2,23 @@ import type * as babel from '@babel/types'; import type * as acorn from 'acorn'; import * as tinyest from 'tinyest'; import { FuncParameterType } from 'tinyest'; -import type { Context, JsNode, TranspilationResult } from './types.ts'; +import type { + AstKind, + Context, + JsNode, + TranspilationOptions, + TranspilationResult, + Transpile, + Transpilers, +} from './types.ts'; import { tryFindExternalChain } from './externals.ts'; +import { acornTranspilers, babelTranspilers } from './transpilers.ts'; +import { extractFunctionParts } from './functionParts.ts'; const { NodeTypeCatalog: NODE } = tinyest; -const tsFallthrough = (ctx: Context, node: { expression: babel.Expression }): tinyest.AnyNode => { - return transpile(ctx, node.expression); -}; - -const Transpilers: Partial<{ - [Type in JsNode['type']]: ( - ctx: Context, - node: Extract, - ) => tinyest.AnyNode; -}> = { - Program(ctx, node) { - const body = node.body[0]; - - if (!body) { - throw new Error('tgpu.fn was not implemented correctly.'); - } - - return transpile(ctx, body); - }, - - ExpressionStatement: (ctx, node) => transpile(ctx, node.expression), - - ArrowFunctionExpression: () => { - throw new Error('Arrow functions are not supported inside TGSL.'); - }, - - BlockStatement(ctx, node) { - ctx.stack.push({ declaredNames: [] }); - - const result = [ - NODE.block, - node.body.map((statement) => transpile(ctx, statement) as tinyest.Statement), - ] as const; - - ctx.stack.pop(); - - return result; - }, - - ReturnStatement: (ctx, node) => - node.argument - ? [NODE.return, transpile(ctx, node.argument) as tinyest.Expression] - : [NODE.return], - - Identifier(ctx, node) { - return node.name; - }, - - ThisExpression() { - return 'this'; - }, - - BinaryExpression(ctx, node) { - const left = transpile(ctx, node.left) as tinyest.Expression; - const right = transpile(ctx, node.right) as tinyest.Expression; - return [NODE.binaryExpr, left, node.operator as tinyest.BinaryOperator, right]; - }, - - LogicalExpression(ctx, node) { - const left = transpile(ctx, node.left) as tinyest.Expression; - const right = transpile(ctx, node.right) as tinyest.Expression; - return [NODE.logicalExpr, left, node.operator as tinyest.LogicalOperator, right]; - }, - - AssignmentExpression(ctx, node) { - const left = transpile(ctx, node.left) as tinyest.Expression; - const right = transpile(ctx, node.right) as tinyest.Expression; - return [NODE.assignmentExpr, left, node.operator as tinyest.AssignmentOperator, right]; - }, - - UnaryExpression(ctx, node) { - const wgslOp = node.operator; - const argument = transpile(ctx, node.argument) as tinyest.Expression; - return [NODE.unaryExpr, wgslOp, argument] as tinyest.UnaryExpression; - }, - - MemberExpression(ctx, node) { - const object = transpile(ctx, node.object) as tinyest.Expression; - - // If the property is computed, it could potentially be an external identifier. - if (node.computed) { - const property = transpile(ctx, node.property) as tinyest.Expression; - return [NODE.indexAccess, object, property]; - } - - // If the property is not computed, we don't want to register identifiers as external. - ctx.ignoreExternalDepth++; - const property = transpile(ctx, node.property) as tinyest.Expression; - ctx.ignoreExternalDepth--; - - if (typeof property !== 'string') { - throw new Error('Expected identifier as property access key.'); - } - - return [NODE.memberAccess, object, property]; - }, - - UpdateExpression(ctx, node) { - const operator = node.operator; - const argument = transpile(ctx, node.argument) as tinyest.Expression; - if (node.prefix) { - throw new Error('Prefix update expressions are not supported in WGSL.'); - } - return [NODE.postUpdate, operator, argument]; - }, - - ConditionalExpression(ctx, node) { - const test = transpile(ctx, node.test) as tinyest.Expression; - const consequent = transpile(ctx, node.consequent) as tinyest.Expression; - const alternative = transpile(ctx, node.alternate) as tinyest.Expression; - - return [NODE.conditionalExpr, test, consequent, alternative]; - }, - - Literal(ctx, node) { - if (typeof node.value === 'boolean') { - return node.value; - } - if (typeof node.value === 'string') { - return [NODE.stringLiteral, node.value]; - } - if (node.regex) { - throw new Error('Regular expression literals are not representable in WGSL.'); - } - if (node.bigint) { - console.warn('BigInt literals are represented as numbers - loss of precision may occur.'); - } - return [NODE.numericLiteral, String(Number(node.value))]; - }, - - NumericLiteral(ctx, node) { - return [NODE.numericLiteral, String(node.value)]; - }, - - BigIntLiteral(ctx, node) { - console.warn('BigInt literals are represented as numbers - loss of precision may occur.'); - return [NODE.numericLiteral, String(Number.parseInt(node.value))]; - }, - - BooleanLiteral(ctx, node) { - return node.value; - }, - - StringLiteral(ctx, node) { - return [NODE.stringLiteral, node.value]; - }, - - CallExpression(ctx, node) { - const callee = transpile(ctx, node.callee) as tinyest.Expression; - - const args = node.arguments.map((arg) => transpile(ctx, arg)) as tinyest.Expression[]; - - return [NODE.call, callee, args]; - }, - - ArrayExpression: (ctx, node) => [ - NODE.arrayExpr, - node.elements.map((elem) => { - if (!elem || elem.type === 'SpreadElement') { - throw new Error('Spread elements are not supported in TGSL.'); - } - return transpile(ctx, elem) as tinyest.Expression; - }), - ], - - VariableDeclaration(ctx, node) { - if (node.declarations.length !== 1 || !node.declarations[0]) { - throw new Error('Currently only one declaration in a statement is supported.'); - } - - const decl = node.declarations[0]; - ctx.ignoreExternalDepth++; - const id = transpile(ctx, decl.id); - ctx.ignoreExternalDepth--; - - if (typeof id !== 'string') { - throw new Error('Invalid variable declaration, expected identifier.'); - } - - ctx.stack[ctx.stack.length - 1]?.declaredNames.push(id); - - const init = decl.init ? (transpile(ctx, decl.init) as tinyest.Expression) : undefined; - - if (node.kind === 'var') { - throw new Error('`var` declarations are not supported.'); - } - - if (node.kind === 'const') { - return init !== undefined ? [NODE.const, id, init] : [NODE.const, id]; - } - - return init !== undefined ? [NODE.let, id, init] : [NODE.let, id]; - }, - - IfStatement(ctx, node) { - const test = transpile(ctx, node.test) as tinyest.Expression; - const consequent = transpile(ctx, node.consequent) as tinyest.Statement; - const alternate = node.alternate - ? (transpile(ctx, node.alternate) as tinyest.Statement) - : undefined; - - return alternate ? [NODE.if, test, consequent, alternate] : [NODE.if, test, consequent]; - }, - - ObjectExpression(ctx, node) { - const properties: Record = {}; - - for (const prop of node.properties) { - // TODO: Handle SpreadElement - if (prop.type === 'SpreadElement') { - throw new Error('Spread elements are not supported in TGSL.'); - } - - // TODO: Handle computed properties - if (prop.key.type !== 'Identifier' && prop.key.type !== 'Literal') { - throw new Error('Only Identifier and Literal keys are supported as object keys.'); - } - - // TODO: Handle Object method - if (prop.type === 'ObjectMethod') { - throw new Error('Object method elements are not supported in TGSL.'); - } - - ctx.ignoreExternalDepth++; - const key = - prop.key.type === 'Identifier' - ? (transpile(ctx, prop.key) as string) - : String(prop.key.value); - ctx.ignoreExternalDepth--; - const value = transpile(ctx, prop.value) as tinyest.Expression; - - properties[key] = value; - } - - return [NODE.objectExpr, properties]; - }, - - ForStatement(ctx, node) { - ctx.stack.push({ declaredNames: [] }); - - const init = node.init ? (transpile(ctx, node.init) as tinyest.Statement) : null; - const condition = node.test ? (transpile(ctx, node.test) as tinyest.Expression) : null; - const update = node.update ? (transpile(ctx, node.update) as tinyest.Statement) : null; - const body = transpile(ctx, node.body) as tinyest.Statement; - - ctx.stack.pop(); - - return [NODE.for, init, condition, update, body]; - }, - - WhileStatement(ctx, node) { - const condition = transpile(ctx, node.test) as tinyest.Expression; - const body = transpile(ctx, node.body) as tinyest.Statement; - - return [NODE.while, condition, body]; - }, - - ForOfStatement(ctx, node) { - ctx.stack.push({ declaredNames: [] }); - - const loopVar = transpile(ctx, node.left) as tinyest.Const | tinyest.Let; - const iterable = transpile(ctx, node.right) as tinyest.Expression; - const body = transpile(ctx, node.body) as tinyest.Statement; - - ctx.stack.pop(); - - return [NODE.forOf, loopVar, iterable, body]; - }, - - ContinueStatement() { - return [NODE.continue]; - }, - - BreakStatement() { - return [NODE.break]; - }, - - TSAsExpression: tsFallthrough, - TSSatisfiesExpression: tsFallthrough, - TSNonNullExpression: tsFallthrough, -}; - -function transpile(ctx: Context, node: JsNode): tinyest.AnyNode { - const transpiler = Transpilers[node.type]; - - if (!transpiler) { - throw new Error(`Unsupported JS functionality: ${node.type}`); - } - - if (ctx.ignoreExternalDepth === 0) { - // Check if the node is an external prop access chain, and if so, - // add it to externals and swap the AST node for an identifier. - const externalChain = tryFindExternalChain(ctx, node); - if (externalChain) { - ctx.externalNames.set(externalChain, externalChain); - return externalChain; - } - } - - // @ts-expect-error - return transpiler(ctx, node); -} - -export function extractFunctionParts(rootNode: JsNode): { - params: tinyest.FuncParameter[]; - body: acorn.BlockStatement | acorn.Expression | babel.BlockStatement | babel.Expression; -} { - let functionNode: - | acorn.ArrowFunctionExpression - | acorn.FunctionExpression - | acorn.FunctionDeclaration - | acorn.AnonymousFunctionDeclaration - | babel.ArrowFunctionExpression - | babel.FunctionExpression - | babel.FunctionDeclaration - | null = null; - - // Unwrapping until we get to a function - let unwrappedNode = rootNode; - while (true) { - if (unwrappedNode.type === 'Program') { - const statement = unwrappedNode.body.filter( - (n) => n.type === 'ExpressionStatement' || n.type === 'FunctionDeclaration', - )[0]; // <- assuming only one function declaration - - if (!statement) { - break; - } - - unwrappedNode = statement; - } else if (unwrappedNode.type === 'ExpressionStatement') { - unwrappedNode = unwrappedNode.expression; - } else if (unwrappedNode.type === 'ArrowFunctionExpression') { - functionNode = unwrappedNode; - break; // We got a function - } else if (unwrappedNode.type === 'FunctionExpression') { - functionNode = unwrappedNode; - break; // We got a function - } else if (unwrappedNode.type === 'FunctionDeclaration') { - functionNode = unwrappedNode; - break; // We got a function - } else { - // Unsupported node - break; - } - } - - if (!functionNode) { - throw new Error( - `tgpu.fn expected a single function to be passed as implementation ${JSON.stringify( - unwrappedNode, - )}`, - ); - } - - if (functionNode.async) { - throw new Error('tgpu.fn cannot be async'); - } - - if (functionNode.generator) { - throw new Error('tgpu.fn cannot be a generator'); - } - - const unsupportedTypes = new Set( - functionNode.params.flatMap((param) => - param.type === 'ObjectPattern' || param.type === 'Identifier' ? [] : [param.type], - ), - ); - if (unsupportedTypes.size > 0) { - throw new Error(`Unsupported function parameter type(s): ${[...unsupportedTypes].join(', ')}`); - } - +function createContext(params: tinyest.FuncParameter[]): Context { return { - params: ( - functionNode.params as ( - | babel.Identifier - | acorn.Identifier - | babel.ObjectPattern - | acorn.ObjectPattern - )[] - ).map((param) => - param.type === 'ObjectPattern' - ? { - type: FuncParameterType.destructuredObject, - props: param.properties.flatMap((prop) => - (prop.type === 'Property' || prop.type === 'ObjectProperty') && - prop.key.type === 'Identifier' && - prop.value.type === 'Identifier' - ? [{ name: prop.key.name, alias: prop.value.name }] - : [], - ), - } - : { - type: FuncParameterType.identifier, - name: param.name, - }, - ), - body: functionNode.body, - }; -} - -export function transpileFn(rootNode: JsNode): TranspilationResult { - const { params, body } = extractFunctionParts(rootNode); - - const ctx: Context = { externalNames: new Map(), ignoreExternalDepth: 0, visitedNodes: new Set(), @@ -425,35 +32,87 @@ export function transpileFn(rootNode: JsNode): TranspilationResult { }, ], }; +} + +function createParser(ast: AstKind) { + const transpilers = ( + ast === 'acorn' ? acornTranspilers : babelTranspilers + ) as Transpilers; + + const transpile: Transpile = (ctx, node) => { + const transpiler = transpilers[node.type]; - const tinyestBody = transpile(ctx, body); + if (!transpiler) { + throw new Error(`Unsupported JS functionality: ${node.type}`); + } - if (body.type === 'BlockStatement') { - return { - params, - body: tinyestBody as tinyest.Block, - externalNames: ctx.externalNames, - }; - } + if (ctx.ignoreExternalDepth === 0) { + // Check if the node is an external prop access chain, and if so, + // add it to externals and swap the AST node for an identifier. + const externalChain = tryFindExternalChain(ctx, node); + if (externalChain) { + ctx.externalNames.set(externalChain, externalChain); + return externalChain; + } + } + + // @ts-ignore + return transpiler(ctx, node, transpile); + }; return { - params, - body: [NODE.block, [[NODE.return, tinyestBody as tinyest.Expression]]], - externalNames: ctx.externalNames, + transpileFn(rootNode: JsNode): TranspilationResult { + const { params, body } = extractFunctionParts(rootNode); + const ctx = createContext(params); + + const tinyestBody = transpile(ctx, body); + + if (body.type === 'BlockStatement') { + return { + params, + body: tinyestBody as tinyest.Block, + externalNames: ctx.externalNames, + }; + } + + return { + params, + body: [NODE.block, [[NODE.return, tinyestBody as tinyest.Expression]]], + externalNames: ctx.externalNames, + }; + }, + + transpileNode(node: JsNode): tinyest.AnyNode { + return transpile(createContext([]), node); + }, }; } -export function transpileNode(node: JsNode): tinyest.AnyNode { - const ctx: Context = { - externalNames: new Map(), - ignoreExternalDepth: 0, - visitedNodes: new Set(), - stack: [ - { - declaredNames: [], - }, - ], - }; +const parsers = { + acorn: createParser('acorn'), + babel: createParser('babel'), +}; + +export function transpileFn( + rootNode: acorn.AnyNode, + options: TranspilationOptions<'acorn'>, +): TranspilationResult; +export function transpileFn( + rootNode: babel.Node, + options: TranspilationOptions<'babel'>, +): TranspilationResult; +export function transpileFn(rootNode: JsNode, { ast }: TranspilationOptions): TranspilationResult { + return parsers[ast].transpileFn(rootNode); +} - return transpile(ctx, node); +export function transpileNode( + rootNode: acorn.AnyNode, + options: TranspilationOptions<'acorn'>, +): tinyest.AnyNode; +export function transpileNode( + rootNode: babel.Node, + options: TranspilationOptions<'babel'>, +): tinyest.AnyNode; +export function transpileNode(rootNode: JsNode, { ast }: TranspilationOptions): tinyest.AnyNode { + return parsers[ast].transpileNode(rootNode); } diff --git a/packages/tinyest-for-wgsl/src/transpilers.ts b/packages/tinyest-for-wgsl/src/transpilers.ts index 033de9ce91..1c542d01a8 100644 --- a/packages/tinyest-for-wgsl/src/transpilers.ts +++ b/packages/tinyest-for-wgsl/src/transpilers.ts @@ -1,7 +1,7 @@ import * as acorn from 'acorn'; import * as babel from '@babel/types'; import * as tinyest from 'tinyest'; -import type { Context, JsNode, Transpilers } from './types.ts'; +import type { Context, JsNode, Transpile, Transpilers } from './types.ts'; const { NodeTypeCatalog: NODE } = tinyest; @@ -276,7 +276,7 @@ export const acornTranspilers = { const tsFallthrough = ( ctx: Context, node: { expression: babel.Expression }, - transpile: (ctx: Context, node: babel.Node) => tinyest.AnyNode, + transpile: Transpile, ) => { return transpile(ctx, node.expression); }; diff --git a/packages/tinyest-for-wgsl/src/types.ts b/packages/tinyest-for-wgsl/src/types.ts index 81c9fe0503..fc812258c8 100644 --- a/packages/tinyest-for-wgsl/src/types.ts +++ b/packages/tinyest-for-wgsl/src/types.ts @@ -9,16 +9,6 @@ export type Scope = { export type Externals = Map; -export type JsNode = babel.Node | acorn.AnyNode; - -export type Transpilers = Partial<{ - [Type in TNode['type']]: ( - ctx: Context, - node: Extract, - transpile: (ctx: Context, node: TNode) => tinyest.AnyNode, - ) => tinyest.AnyNode; -}>; - export type Context = { /** Holds a set of all identifiers that were used in code, but were not declared in code. */ externalNames: Externals; @@ -44,6 +34,18 @@ export type TranspilationResult = { externalNames: Externals; }; +export type JsNode = babel.Node | acorn.AnyNode; + +export type Transpile = (ctx: Context, node: TNode) => tinyest.AnyNode; + +export type Transpilers = Partial<{ + [Type in TNode['type']]: ( + ctx: Context, + node: Extract, + transpile: Transpile, + ) => tinyest.AnyNode; +}>; + export type AstKind = 'acorn' | 'babel'; export type TranspilationOptions = { diff --git a/packages/tinyest-for-wgsl/tests/helpers.ts b/packages/tinyest-for-wgsl/tests/helpers.ts index 7bd671e13c..3d82466f53 100644 --- a/packages/tinyest-for-wgsl/tests/helpers.ts +++ b/packages/tinyest-for-wgsl/tests/helpers.ts @@ -1,14 +1,21 @@ import babel from '@babel/parser'; import type { Node } from '@babel/types'; import * as acorn from 'acorn'; +import { transpileFn } from 'tinyest-for-wgsl'; +import type { JsNode, TranspilationResult } from '../src/types.ts'; export const parseRollup = (code: string) => acorn.parse(code, { ecmaVersion: 'latest' }); export const parseBabel = (code: string) => babel.parse(code, { sourceType: 'module', plugins: ['typescript'] }).program.body[0] as Node; -export function dualTest(test: (p: (code: string) => Node | acorn.AnyNode) => void) { +export function dualTest( + test: ( + p: (code: string) => TNode, + transpileFn: (node: TNode) => TranspilationResult, + ) => void, +) { return () => { - test(parseBabel); - test(parseRollup); + test(parseBabel, (node) => transpileFn(node, { ast: 'babel' })); + test(parseRollup, (node) => transpileFn(node, { ast: 'acorn' })); }; } diff --git a/packages/tinyest-for-wgsl/tests/parsers.test.ts b/packages/tinyest-for-wgsl/tests/parsers.test.ts index 7ef040a636..3359fc06b3 100644 --- a/packages/tinyest-for-wgsl/tests/parsers.test.ts +++ b/packages/tinyest-for-wgsl/tests/parsers.test.ts @@ -7,7 +7,7 @@ import { dualTest, parseBabel } from './helpers.ts'; describe('transpileFn', () => { it( 'handles weird identifiers', - dualTest((p) => { + dualTest((p, transpileFn) => { const { params, body, externalNames } = transpileFn( p(`() => { const a = undefined; @@ -33,14 +33,14 @@ describe('transpileFn', () => { it( 'fails when the input is not a function', - dualTest((p) => { + dualTest((p, transpileFn) => { expect(() => transpileFn(p('1 + 2'))).toThrow(); }), ); it( 'parses an empty arrow function', - dualTest((p) => { + dualTest((p, transpileFn) => { const { params, body, externalNames } = transpileFn(p('() => {}')); expect(params).toStrictEqual([]); @@ -51,7 +51,7 @@ describe('transpileFn', () => { it( 'parses an empty named function', - dualTest((p) => { + dualTest((p, transpileFn) => { const { params, body, externalNames } = transpileFn(p('function example() {}')); expect(params).toStrictEqual([]); @@ -62,7 +62,7 @@ describe('transpileFn', () => { it( 'gathers external names', - dualTest((p) => { + dualTest((p, transpileFn) => { const { params, body, externalNames } = transpileFn(p('(a, b) => a + b - c')); expect(params).toStrictEqual([ @@ -82,7 +82,7 @@ describe('transpileFn', () => { it( 'respects local declarations when gathering external names', - dualTest((p) => { + dualTest((p, transpileFn) => { const { params, body, externalNames } = transpileFn( p(`() => { const a = 0; @@ -105,7 +105,7 @@ describe('transpileFn', () => { it( 'respects outer scope when gathering external names', - dualTest((p) => { + dualTest((p, transpileFn) => { const { params, body, externalNames } = transpileFn( p(`() => { const a = 0; @@ -130,7 +130,7 @@ describe('transpileFn', () => { it( 'treats the object as a possible external value when accessing a member', - dualTest((p) => { + dualTest((p, transpileFn) => { const { params, body, externalNames } = transpileFn(p('() => external.outside.prop')); expect(params).toStrictEqual([]); @@ -146,7 +146,7 @@ describe('transpileFn', () => { it( 'handles destructured args', - dualTest((p) => { + dualTest((p, transpileFn) => { const { params, externalNames } = transpileFn( p(`({ pos, a: b }) => { const x = pos.x; @@ -175,7 +175,7 @@ describe('transpileFn', () => { it( 'handles mixed type parameters', - dualTest((p) => { + dualTest((p, transpileFn) => { const { params, externalNames } = transpileFn( p(`(y, { pos, a: b }, {c, d}) => { const x = pos.x; @@ -220,14 +220,14 @@ describe('transpileFn', () => { ); it('handles TSNonNullExpression', () => { - const { body } = transpileFn(parseBabel('() => x!.y')); + const { body } = transpileFn(parseBabel('() => x!.y'), { ast: 'babel' }); expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[[10,[7,"x","y"]]]]"`); }); it( 'defines a new scope for variables defined in the head of a `for` loop', - dualTest((p) => { + dualTest((p, transpileFn) => { const { externalNames } = transpileFn( p(`() => { let value = 0; @@ -249,7 +249,7 @@ describe('transpileFn', () => { it( 'defines a new scope for the iterator in a `for ... of` loop', - dualTest((p) => { + dualTest((p, transpileFn) => { const { externalNames } = transpileFn( p(`() => { let value = 0; @@ -271,7 +271,7 @@ describe('transpileFn', () => { it( 'handles complex external trees', - dualTest((p) => { + dualTest((p, transpileFn) => { const { externalNames, body } = transpileFn( p(`() => { const a = ext.p; @@ -320,7 +320,7 @@ describe('transpileFn', () => { it( 'does not duplicate externals', - dualTest((p) => { + dualTest((p, transpileFn) => { const { externalNames } = transpileFn( p(`() => { const a = ext; @@ -338,7 +338,7 @@ describe('transpileFn', () => { it( 'does not prune externals when they reappear', - dualTest((p) => { + dualTest((p, transpileFn) => { const { externalNames, body } = transpileFn( p(`() => { const a = ext.value; @@ -364,7 +364,7 @@ describe('transpileFn', () => { it( 'handles private property access', - dualTest((p) => { + dualTest((p, transpileFn) => { // `this.#v` is only valid inside a class body, so we parse a class and pluck out the arrow function. const tree = p(` class Foo { @@ -381,7 +381,7 @@ describe('transpileFn', () => { const lastProp = props.at(-1) as ClassProperty | acorn.PropertyDefinition; const fn = lastProp.value as Expression | acorn.Expression; - const { externalNames } = transpileFn(fn); + const { externalNames } = transpileFn(fn as Parameters[0]); expect(externalNames).toMatchInlineSnapshot(` Map { From fd3d918f9ac84797047fd13c0d422cff32ef6b16 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Wed, 26 Aug 2026 23:26:28 +0200 Subject: [PATCH 08/15] fix types --- packages/tinyest-for-wgsl/package.json | 13 ++++++++++++- packages/tinyest-for-wgsl/src/parsers.ts | 3 +-- packages/tinyest-for-wgsl/src/transpilers.ts | 4 ++-- pnpm-lock.yaml | 20 +++++++++----------- 4 files changed, 24 insertions(+), 16 deletions(-) diff --git a/packages/tinyest-for-wgsl/package.json b/packages/tinyest-for-wgsl/package.json index de205327a3..dac6abb0c6 100644 --- a/packages/tinyest-for-wgsl/package.json +++ b/packages/tinyest-for-wgsl/package.json @@ -58,11 +58,22 @@ "devDependencies": { "@babel/parser": "^7.27.0", "@babel/types": "catalog:", - "@typegpu/tgpu-dev-cli": "workspace:*", "acorn": "^8.14.1", "tsdown": "catalog:build", "typescript": "catalog:types" }, + "peerDependencies": { + "@babel/types": "catalog:", + "acorn": "^8.14.1" + }, + "peerDependenciesMeta": { + "acorn": { + "optional": true + }, + "@babel/types": { + "optional": true + } + }, "engines": { "node": ">=12.20.0" }, diff --git a/packages/tinyest-for-wgsl/src/parsers.ts b/packages/tinyest-for-wgsl/src/parsers.ts index 0fb5e9b9e6..d69ef7e622 100644 --- a/packages/tinyest-for-wgsl/src/parsers.ts +++ b/packages/tinyest-for-wgsl/src/parsers.ts @@ -1,7 +1,6 @@ import type * as babel from '@babel/types'; import type * as acorn from 'acorn'; import * as tinyest from 'tinyest'; -import { FuncParameterType } from 'tinyest'; import type { AstKind, Context, @@ -25,7 +24,7 @@ function createContext(params: tinyest.FuncParameter[]): Context { stack: [ { declaredNames: params.flatMap((param) => - param.type === FuncParameterType.identifier + param.type === tinyest.FuncParameterType.identifier ? param.name : param.props.map((prop) => prop.alias), ), diff --git a/packages/tinyest-for-wgsl/src/transpilers.ts b/packages/tinyest-for-wgsl/src/transpilers.ts index 1c542d01a8..544892d973 100644 --- a/packages/tinyest-for-wgsl/src/transpilers.ts +++ b/packages/tinyest-for-wgsl/src/transpilers.ts @@ -1,5 +1,5 @@ -import * as acorn from 'acorn'; -import * as babel from '@babel/types'; +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'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 464eea5af9..9bc1716263 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -156,7 +156,7 @@ importers: devDependencies: '@types/bun': specifier: latest - version: 1.3.14 + version: 1.4.0 apps/infra-benchmarks: devDependencies: @@ -638,9 +638,6 @@ importers: '@babel/types': specifier: ^7.29.0 version: 7.29.0 - '@typegpu/tgpu-dev-cli': - specifier: workspace:* - version: link:../tgpu-dev-cli acorn: specifier: ^8.14.1 version: 8.14.1 @@ -4551,8 +4548,8 @@ packages: '@types/bun@1.3.12': resolution: {integrity: sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A==} - '@types/bun@1.3.14': - resolution: {integrity: sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw==} + '@types/bun@1.4.0': + resolution: {integrity: sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ==} '@types/chai@5.2.2': resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} @@ -5331,8 +5328,8 @@ packages: bun-types@1.3.12: resolution: {integrity: sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA==} - bun-types@1.3.14: - resolution: {integrity: sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ==} + bun-types@1.4.0: + resolution: {integrity: sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q==} bun@1.3.10: resolution: {integrity: sha512-S/CXaXXIyA4CMjdMkYQ4T2YMqnAn4s0ysD3mlsY4bUiOCqGlv28zck4Wd4H4kpvbekx15S9mUeLQ7Uxd0tYTLA==} @@ -6089,6 +6086,7 @@ packages: eslint@9.39.2: resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true peerDependencies: jiti: '*' @@ -13128,9 +13126,9 @@ snapshots: dependencies: bun-types: 1.3.12 - '@types/bun@1.3.14': + '@types/bun@1.4.0': dependencies: - bun-types: 1.3.14 + bun-types: 1.4.0 '@types/chai@5.2.2': dependencies: @@ -14191,7 +14189,7 @@ snapshots: dependencies: '@types/node': 24.10.0 - bun-types@1.3.14: + bun-types@1.4.0: dependencies: '@types/node': 24.10.0 From 6341eebe4ed18f3ab9a6b9f0a53a033f2c693504 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Wed, 26 Aug 2026 23:44:12 +0200 Subject: [PATCH 09/15] polish --- packages/tinyest-for-wgsl/src/transpilers.ts | 2 +- .../tinyest-for-wgsl/tests/parsers.test.ts | 20 ++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/tinyest-for-wgsl/src/transpilers.ts b/packages/tinyest-for-wgsl/src/transpilers.ts index 544892d973..d97a3c4751 100644 --- a/packages/tinyest-for-wgsl/src/transpilers.ts +++ b/packages/tinyest-for-wgsl/src/transpilers.ts @@ -288,7 +288,7 @@ const babelSpecificTranspilers = { BigIntLiteral(_ctx, node) { console.warn('BigInt literals are represented as numbers - loss of precision may occur.'); - return [NODE.numericLiteral, String(Number.parseInt(node.value))]; + return [NODE.numericLiteral, String(Number(node.value))]; }, BooleanLiteral(_ctx, node) { diff --git a/packages/tinyest-for-wgsl/tests/parsers.test.ts b/packages/tinyest-for-wgsl/tests/parsers.test.ts index 3359fc06b3..a07d058fb4 100644 --- a/packages/tinyest-for-wgsl/tests/parsers.test.ts +++ b/packages/tinyest-for-wgsl/tests/parsers.test.ts @@ -2,7 +2,7 @@ import type { ClassDeclaration, ClassProperty, Expression, Node } from '@babel/t import * as acorn from 'acorn'; import { describe, expect, it } from 'vitest'; import { transpileFn } from '../src/parsers.ts'; -import { dualTest, parseBabel } from './helpers.ts'; +import { dualTest, parseBabel, parseRollup } from './helpers.ts'; describe('transpileFn', () => { it( @@ -390,4 +390,22 @@ describe('transpileFn', () => { `); }), ); + + it( + 'rejects computed object properties', + dualTest((p, transpileFn) => { + expect(() => transpileFn(p('() => ({ [k]: 1 })'))).toThrowErrorMatchingInlineSnapshot( + `[Error: Computed object properties are not supported in TGSL.]`, + ); + }), + ); + + it( + 'parses binary numbers', + dualTest((p, transpileFn) => { + expect(JSON.stringify(transpileFn(p('() => 0b101n')).body)).toMatchInlineSnapshot( + `"[0,[[10,[5,"5"]]]]"`, + ); + }), + ); }); From 535e416b1a108fed94b62064640ff251a82237f1 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Wed, 26 Aug 2026 23:49:18 +0200 Subject: [PATCH 10/15] unplugin update --- packages/unplugin-typegpu/src/core/common.ts | 2 +- packages/unplugin-typegpu/test/obfuscation.test.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/unplugin-typegpu/src/core/common.ts b/packages/unplugin-typegpu/src/core/common.ts index e141f090d3..8df86a3bd8 100644 --- a/packages/unplugin-typegpu/src/core/common.ts +++ b/packages/unplugin-typegpu/src/core/common.ts @@ -492,7 +492,7 @@ function transpile( rootNode: Parameters[0], obf: boolean, ): ReturnType { - const result = transpileFn(rootNode); + const result = transpileFn(rootNode, { ast: 'babel' }); if (obf) { return obfuscate(result); } diff --git a/packages/unplugin-typegpu/test/obfuscation.test.ts b/packages/unplugin-typegpu/test/obfuscation.test.ts index e0f47931b6..9f3d441d85 100644 --- a/packages/unplugin-typegpu/test/obfuscation.test.ts +++ b/packages/unplugin-typegpu/test/obfuscation.test.ts @@ -1,5 +1,5 @@ import { type ArrowFunctionExpression } from '@babel/types'; -import { transpileFn } from 'tinyest-for-wgsl'; +import { transpileFn as _transpileFn } from 'tinyest-for-wgsl'; import { describe, expect, it, test } from 'vitest'; import { obfuscate } from '../src/core/obfuscate.ts'; import babelParser from '@babel/parser'; @@ -174,6 +174,9 @@ function parse(code: string): ArrowFunctionExpression { } describe('obfuscate', () => { + const transpileFn = (node: ArrowFunctionExpression) => { + return _transpileFn(node, { ast: 'babel' }); + }; it('obfuscates used variables', () => { const code = `() => { const variable = 1; const other = 2; const sensitiveName = 3; }`; const transpiled = transpileFn(parse(code)); From 44c2c517437dc5e3ee237dd11f794ceb2bf55228 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Thu, 27 Aug 2026 00:59:44 +0200 Subject: [PATCH 11/15] review fixes --- packages/tinyest-for-wgsl/package.json | 1 + pnpm-lock.yaml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/packages/tinyest-for-wgsl/package.json b/packages/tinyest-for-wgsl/package.json index dac6abb0c6..9c985ad825 100644 --- a/packages/tinyest-for-wgsl/package.json +++ b/packages/tinyest-for-wgsl/package.json @@ -58,6 +58,7 @@ "devDependencies": { "@babel/parser": "^7.27.0", "@babel/types": "catalog:", + "@typegpu/tgpu-dev-cli": "workspace:*", "acorn": "^8.14.1", "tsdown": "catalog:build", "typescript": "catalog:types" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9bc1716263..2553faf28d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -638,6 +638,9 @@ importers: '@babel/types': specifier: ^7.29.0 version: 7.29.0 + '@typegpu/tgpu-dev-cli': + specifier: workspace:* + version: link:../tgpu-dev-cli acorn: specifier: ^8.14.1 version: 8.14.1 From 3d1805c4e508211e501924655dea1ba7f951e281 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Thu, 27 Aug 2026 12:30:55 +0200 Subject: [PATCH 12/15] consistent comments --- packages/tinyest-for-wgsl/src/externals.ts | 4 ++-- packages/tinyest-for-wgsl/src/functionParts.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/tinyest-for-wgsl/src/externals.ts b/packages/tinyest-for-wgsl/src/externals.ts index 75a746ab69..8af9d3bef1 100644 --- a/packages/tinyest-for-wgsl/src/externals.ts +++ b/packages/tinyest-for-wgsl/src/externals.ts @@ -29,9 +29,9 @@ export function tryFindExternalChain(ctx: Context, node: JsNode): string | undef let property; if (node.property.type === 'Identifier' && node.property.name !== '$') { property = node.property.name; - } else if (/* babel */ node.property.type === 'PrivateName') { + } else if (node.property.type === /* babel */ 'PrivateName') { property = `#${node.property.id.name}`; - } else if (/* acorn */ node.property.type === 'PrivateIdentifier') { + } else if (node.property.type === /* acorn */ 'PrivateIdentifier') { property = `#${node.property.name}`; } else { return; diff --git a/packages/tinyest-for-wgsl/src/functionParts.ts b/packages/tinyest-for-wgsl/src/functionParts.ts index 55f067b3a4..0752f58760 100644 --- a/packages/tinyest-for-wgsl/src/functionParts.ts +++ b/packages/tinyest-for-wgsl/src/functionParts.ts @@ -94,7 +94,7 @@ function parseParams(functionNode: FunctionNode): tinyest.FuncParameter[] { ? { type: FuncParameterType.destructuredObject, props: param.properties.flatMap((prop) => - (prop.type === 'Property' /* acorn */ || prop.type === 'ObjectProperty') /* babel */ && + (prop.type === /* acorn */ 'Property' || prop.type === /* babel */ 'ObjectProperty') && prop.key.type === 'Identifier' && prop.value.type === 'Identifier' ? [{ name: prop.key.name, alias: prop.value.name }] From 36fe8f1e762cdd85d68a78bf0a3af225dad4d8ff Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Thu, 27 Aug 2026 12:34:52 +0200 Subject: [PATCH 13/15] unnecessary import --- packages/tinyest-for-wgsl/src/functionParts.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/tinyest-for-wgsl/src/functionParts.ts b/packages/tinyest-for-wgsl/src/functionParts.ts index 0752f58760..4fad4b9330 100644 --- a/packages/tinyest-for-wgsl/src/functionParts.ts +++ b/packages/tinyest-for-wgsl/src/functionParts.ts @@ -1,7 +1,6 @@ import type * as babel from '@babel/types'; import type * as acorn from 'acorn'; import * as tinyest from 'tinyest'; -import { FuncParameterType } from 'tinyest'; import type { JsNode } from './types.ts'; type FunctionNode = @@ -92,7 +91,7 @@ function parseParams(functionNode: FunctionNode): tinyest.FuncParameter[] { ).map((param) => param.type === 'ObjectPattern' ? { - type: FuncParameterType.destructuredObject, + type: tinyest.FuncParameterType.destructuredObject, props: param.properties.flatMap((prop) => (prop.type === /* acorn */ 'Property' || prop.type === /* babel */ 'ObjectProperty') && prop.key.type === 'Identifier' && @@ -102,7 +101,7 @@ function parseParams(functionNode: FunctionNode): tinyest.FuncParameter[] { ), } : { - type: FuncParameterType.identifier, + type: tinyest.FuncParameterType.identifier, name: param.name, }, ); From 1de82e158178afe59fee8cb9a16150938ec6fdc3 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Thu, 27 Aug 2026 13:41:37 +0200 Subject: [PATCH 14/15] cleanup --- packages/tinyest-for-wgsl/src/index.ts | 2 +- packages/tinyest-for-wgsl/src/transpilers.ts | 11 +++++++++-- packages/tinyest-for-wgsl/tests/helpers.ts | 5 ++--- packages/tinyest-for-wgsl/tests/parsers.test.ts | 14 +++++++------- packages/unplugin-typegpu/test/obfuscation.test.ts | 1 + 5 files changed, 20 insertions(+), 13 deletions(-) diff --git a/packages/tinyest-for-wgsl/src/index.ts b/packages/tinyest-for-wgsl/src/index.ts index efdcc6b131..805f92df49 100644 --- a/packages/tinyest-for-wgsl/src/index.ts +++ b/packages/tinyest-for-wgsl/src/index.ts @@ -1,2 +1,2 @@ export { transpileFn, transpileNode } from './parsers.ts'; -export type { Externals, TranspilationOptions } from './types.ts'; +export type { Externals, TranspilationOptions, TranspilationResult } from './types.ts'; diff --git a/packages/tinyest-for-wgsl/src/transpilers.ts b/packages/tinyest-for-wgsl/src/transpilers.ts index d97a3c4751..53415e8027 100644 --- a/packages/tinyest-for-wgsl/src/transpilers.ts +++ b/packages/tinyest-for-wgsl/src/transpilers.ts @@ -255,10 +255,17 @@ const acornSpecificTranspilers = { } // TODO: Handle computed properties - if (prop.computed || (prop.key.type !== 'Identifier' && prop.key.type !== 'Literal')) { + if (prop.computed) { throw new Error('Computed object properties are not supported in TGSL.'); } + 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.`); + } + 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; @@ -332,7 +339,7 @@ const babelSpecificTranspilers = { break; default: - throw new Error(`Unsupported non-computed object property key: ${prop.key.type}`); + throw new Error(`Unsupported non-computed object property key.`); } const value = transpile(ctx, prop.value) as tinyest.Expression; diff --git a/packages/tinyest-for-wgsl/tests/helpers.ts b/packages/tinyest-for-wgsl/tests/helpers.ts index 3d82466f53..745b1b8977 100644 --- a/packages/tinyest-for-wgsl/tests/helpers.ts +++ b/packages/tinyest-for-wgsl/tests/helpers.ts @@ -1,15 +1,14 @@ import babel from '@babel/parser'; import type { Node } from '@babel/types'; import * as acorn from 'acorn'; -import { transpileFn } from 'tinyest-for-wgsl'; -import type { JsNode, TranspilationResult } from '../src/types.ts'; +import { transpileFn, type TranspilationResult } from 'tinyest-for-wgsl'; export const parseRollup = (code: string) => acorn.parse(code, { ecmaVersion: 'latest' }); export const parseBabel = (code: string) => babel.parse(code, { sourceType: 'module', plugins: ['typescript'] }).program.body[0] as Node; export function dualTest( - test: ( + test: ( p: (code: string) => TNode, transpileFn: (node: TNode) => TranspilationResult, ) => void, diff --git a/packages/tinyest-for-wgsl/tests/parsers.test.ts b/packages/tinyest-for-wgsl/tests/parsers.test.ts index a07d058fb4..d6f1937366 100644 --- a/packages/tinyest-for-wgsl/tests/parsers.test.ts +++ b/packages/tinyest-for-wgsl/tests/parsers.test.ts @@ -2,7 +2,7 @@ import type { ClassDeclaration, ClassProperty, Expression, Node } from '@babel/t import * as acorn from 'acorn'; import { describe, expect, it } from 'vitest'; import { transpileFn } from '../src/parsers.ts'; -import { dualTest, parseBabel, parseRollup } from './helpers.ts'; +import { dualTest, parseBabel } from './helpers.ts'; describe('transpileFn', () => { it( @@ -392,19 +392,19 @@ describe('transpileFn', () => { ); it( - 'rejects computed object properties', + 'parses binary bigints', dualTest((p, transpileFn) => { - expect(() => transpileFn(p('() => ({ [k]: 1 })'))).toThrowErrorMatchingInlineSnapshot( - `[Error: Computed object properties are not supported in TGSL.]`, + expect(JSON.stringify(transpileFn(p('() => 0b101n')).body)).toMatchInlineSnapshot( + `"[0,[[10,[5,"5"]]]]"`, ); }), ); it( - 'parses binary numbers', + 'rejects computed object properties', dualTest((p, transpileFn) => { - expect(JSON.stringify(transpileFn(p('() => 0b101n')).body)).toMatchInlineSnapshot( - `"[0,[[10,[5,"5"]]]]"`, + expect(() => transpileFn(p('() => ({ [k]: 1 })'))).toThrowErrorMatchingInlineSnapshot( + `[Error: Computed object properties are not supported in TGSL.]`, ); }), ); diff --git a/packages/unplugin-typegpu/test/obfuscation.test.ts b/packages/unplugin-typegpu/test/obfuscation.test.ts index 9f3d441d85..d88a796533 100644 --- a/packages/unplugin-typegpu/test/obfuscation.test.ts +++ b/packages/unplugin-typegpu/test/obfuscation.test.ts @@ -177,6 +177,7 @@ describe('obfuscate', () => { const transpileFn = (node: ArrowFunctionExpression) => { return _transpileFn(node, { ast: 'babel' }); }; + it('obfuscates used variables', () => { const code = `() => { const variable = 1; const other = 2; const sensitiveName = 3; }`; const transpiled = transpileFn(parse(code)); From 44245d79d3156f03d12ceee12288d64c197ea6fd Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Fri, 4 Sep 2026 16:59:19 +0200 Subject: [PATCH 15/15] legacy --- packages/tinyest-for-wgsl/src/index.ts | 11 +- packages/tinyest-for-wgsl/src/parsers.ts | 128 +++++++++++++----- packages/tinyest-for-wgsl/src/types.ts | 6 - packages/tinyest-for-wgsl/tests/helpers.ts | 6 +- .../tinyest-for-wgsl/tests/parsers.test.ts | 72 +++++++++- packages/unplugin-typegpu/src/babel.ts | 4 +- packages/unplugin-typegpu/src/core/common.ts | 12 +- packages/unplugin-typegpu/src/core/factory.ts | 4 +- .../unplugin-typegpu/src/core/obfuscate.ts | 6 +- .../unplugin-typegpu/test/obfuscation.test.ts | 44 +++--- 10 files changed, 210 insertions(+), 83 deletions(-) diff --git a/packages/tinyest-for-wgsl/src/index.ts b/packages/tinyest-for-wgsl/src/index.ts index 805f92df49..2a628f6466 100644 --- a/packages/tinyest-for-wgsl/src/index.ts +++ b/packages/tinyest-for-wgsl/src/index.ts @@ -1,2 +1,9 @@ -export { transpileFn, transpileNode } from './parsers.ts'; -export type { Externals, TranspilationOptions, TranspilationResult } from './types.ts'; +export { + transpileFnAcorn, + transpileFnBabel, + transpileFn, + transpileNodeAcorn, + transpileNodeBabel, + transpileNode, +} from './parsers.ts'; +export type { Externals, TranspilationResult } from './types.ts'; diff --git a/packages/tinyest-for-wgsl/src/parsers.ts b/packages/tinyest-for-wgsl/src/parsers.ts index d69ef7e622..c328db18ba 100644 --- a/packages/tinyest-for-wgsl/src/parsers.ts +++ b/packages/tinyest-for-wgsl/src/parsers.ts @@ -1,15 +1,7 @@ import type * as babel from '@babel/types'; import type * as acorn from 'acorn'; import * as tinyest from 'tinyest'; -import type { - AstKind, - Context, - JsNode, - TranspilationOptions, - TranspilationResult, - Transpile, - Transpilers, -} from './types.ts'; +import type { Context, JsNode, TranspilationResult, Transpile, Transpilers } from './types.ts'; import { tryFindExternalChain } from './externals.ts'; import { acornTranspilers, babelTranspilers } from './transpilers.ts'; import { extractFunctionParts } from './functionParts.ts'; @@ -33,9 +25,69 @@ function createContext(params: tinyest.FuncParameter[]): Context { }; } -function createParser(ast: AstKind) { +function createLegacyTraspilers() { + return { + ...babelTranspilers, + ...acornTranspilers, + + ObjectExpression(ctx, node, transpile) { + const properties: Record = {}; + + for (const prop of node.properties) { + if (prop.type === 'SpreadElement') { + throw new Error('Spread elements are not supported in TGSL.'); + } + + if (prop.type === 'ObjectMethod' || (prop.type === 'Property' && prop.method)) { + 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.'); + } + + 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.`); + } + + const value = transpile(ctx, prop.value) as tinyest.Expression; + properties[key] = value; + } + + return [NODE.objectExpr, properties]; + }, + } as Transpilers; +} + +function createParser(kind: 'acorn' | 'babel' | 'legacy') { const transpilers = ( - ast === 'acorn' ? acornTranspilers : babelTranspilers + kind === 'acorn' + ? acornTranspilers + : kind === 'babel' + ? babelTranspilers + : createLegacyTraspilers() ) as Transpilers; const transpile: Transpile = (ctx, node) => { @@ -92,26 +144,40 @@ const parsers = { babel: createParser('babel'), }; -export function transpileFn( - rootNode: acorn.AnyNode, - options: TranspilationOptions<'acorn'>, -): TranspilationResult; -export function transpileFn( - rootNode: babel.Node, - options: TranspilationOptions<'babel'>, -): TranspilationResult; -export function transpileFn(rootNode: JsNode, { ast }: TranspilationOptions): TranspilationResult { - return parsers[ast].transpileFn(rootNode); +let legacyParser: ReturnType | undefined = undefined; + +export function transpileFnAcorn(rootNode: acorn.AnyNode): TranspilationResult { + return parsers.acorn.transpileFn(rootNode); +} + +export function transpileNodeAcorn(rootNode: acorn.AnyNode): tinyest.AnyNode { + return parsers.acorn.transpileNode(rootNode); +} + +export function transpileFnBabel(rootNode: babel.Node): TranspilationResult { + return parsers.babel.transpileFn(rootNode); +} + +export function transpileNodeBabel(rootNode: babel.Node): tinyest.AnyNode { + return parsers.babel.transpileNode(rootNode); +} + +/** + * @deprecated Use {@link transpileFnAcorn} or {@link transpileFnBabel} instead. + */ +export function transpileFn(rootNode: JsNode): TranspilationResult { + if (legacyParser === undefined) { + legacyParser = createParser('legacy'); + } + return legacyParser.transpileFn(rootNode); } -export function transpileNode( - rootNode: acorn.AnyNode, - options: TranspilationOptions<'acorn'>, -): tinyest.AnyNode; -export function transpileNode( - rootNode: babel.Node, - options: TranspilationOptions<'babel'>, -): tinyest.AnyNode; -export function transpileNode(rootNode: JsNode, { ast }: TranspilationOptions): tinyest.AnyNode { - return parsers[ast].transpileNode(rootNode); +/** + * @deprecated Use {@link transpileNodeAcorn} or {@link transpileNodeBabel} instead. + */ +export function transpileNode(rootNode: JsNode): tinyest.AnyNode { + if (legacyParser === undefined) { + legacyParser = createParser('legacy'); + } + return legacyParser.transpileNode(rootNode); } diff --git a/packages/tinyest-for-wgsl/src/types.ts b/packages/tinyest-for-wgsl/src/types.ts index fc812258c8..331847ccf7 100644 --- a/packages/tinyest-for-wgsl/src/types.ts +++ b/packages/tinyest-for-wgsl/src/types.ts @@ -45,9 +45,3 @@ export type Transpilers = Partial<{ transpile: Transpile, ) => tinyest.AnyNode; }>; - -export type AstKind = 'acorn' | 'babel'; - -export type TranspilationOptions = { - ast: TAst; -}; diff --git a/packages/tinyest-for-wgsl/tests/helpers.ts b/packages/tinyest-for-wgsl/tests/helpers.ts index 745b1b8977..d560fb7100 100644 --- a/packages/tinyest-for-wgsl/tests/helpers.ts +++ b/packages/tinyest-for-wgsl/tests/helpers.ts @@ -1,7 +1,7 @@ import babel from '@babel/parser'; import type { Node } from '@babel/types'; import * as acorn from 'acorn'; -import { transpileFn, type TranspilationResult } from 'tinyest-for-wgsl'; +import { transpileFnAcorn, transpileFnBabel, type TranspilationResult } from 'tinyest-for-wgsl'; export const parseRollup = (code: string) => acorn.parse(code, { ecmaVersion: 'latest' }); export const parseBabel = (code: string) => @@ -14,7 +14,7 @@ export function dualTest( ) => void, ) { return () => { - test(parseBabel, (node) => transpileFn(node, { ast: 'babel' })); - test(parseRollup, (node) => transpileFn(node, { ast: 'acorn' })); + test(parseBabel, (node) => transpileFnBabel(node)); + test(parseRollup, (node) => transpileFnAcorn(node)); }; } diff --git a/packages/tinyest-for-wgsl/tests/parsers.test.ts b/packages/tinyest-for-wgsl/tests/parsers.test.ts index 665dc7cb61..9b0ee0944e 100644 --- a/packages/tinyest-for-wgsl/tests/parsers.test.ts +++ b/packages/tinyest-for-wgsl/tests/parsers.test.ts @@ -1,10 +1,10 @@ -import type { ClassDeclaration, ClassProperty, Expression, Node } from '@babel/types'; +import type { ClassDeclaration, ClassProperty, Expression } from '@babel/types'; import * as acorn from 'acorn'; import { describe, expect, it } from 'vitest'; -import { transpileFn } from '../src/parsers.ts'; -import { dualTest, parseBabel } from './helpers.ts'; +import { transpileFnBabel, transpileFn } from 'tinyest-for-wgsl'; +import { dualTest, parseBabel, parseRollup } from './helpers.ts'; -describe('transpileFn', () => { +describe('transpileFnBabel and transpileFnAcorn', () => { it( 'handles weird identifiers', dualTest((p, transpileFn) => { @@ -235,7 +235,7 @@ describe('transpileFn', () => { ); it('handles TSNonNullExpression', () => { - const { body } = transpileFn(parseBabel('() => x!.y'), { ast: 'babel' }); + const { body } = transpileFnBabel(parseBabel('() => x!.y')); expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[[10,[7,"x","y"]]]]"`); }); @@ -424,3 +424,65 @@ describe('transpileFn', () => { }), ); }); + +describe('legacy transpileFn', () => { + it('parsers object expression with identifier and literal keys', () => { + const code = `() => ({ + identifier: 1, + 'string key': 2, + 3: 4, + 5n: 6, + });`; + + const babelResult = transpileFn(parseBabel(code)); + expect(JSON.stringify(babelResult.body)).toMatchInlineSnapshot( + `"[0,[[10,[104,{"3":[5,"4"],"5":[5,"6"],"identifier":[5,"1"],"string key":[5,"2"]}]]]]"`, + ); + + const acornResult = transpileFn(parseRollup(code)); + expect(JSON.stringify(acornResult.body)).toMatchInlineSnapshot( + `"[0,[[10,[104,{"3":[5,"4"],"5":[5,"6"],"identifier":[5,"1"],"string key":[5,"2"]}]]]]"`, + ); + }); + + it('rejects computed object properties', () => { + const code = `() => ({ + [1]: 2, + });`; + + expect(() => transpileFn(parseBabel(code))).toThrowErrorMatchingInlineSnapshot( + `[Error: Computed object properties are not supported in TGSL.]`, + ); + expect(() => transpileFn(parseRollup(code))).toThrowErrorMatchingInlineSnapshot( + `[Error: Computed object properties are not supported in TGSL.]`, + ); + }); + + it('rejects spread elements', () => { + const code = `() => ({ + ...obj, + });`; + + expect(() => transpileFn(parseBabel(code))).toThrowErrorMatchingInlineSnapshot( + `[Error: Spread elements are not supported in TGSL.]`, + ); + expect(() => transpileFn(parseRollup(code))).toThrowErrorMatchingInlineSnapshot( + `[Error: Spread elements are not supported in TGSL.]`, + ); + }); + + it('rejects object methods', () => { + const code = `() => ({ + foo() { + return 1; + }, + });`; + + expect(() => transpileFn(parseBabel(code))).toThrowErrorMatchingInlineSnapshot( + `[Error: Object method elements are not supported in TGSL.]`, + ); + expect(() => transpileFn(parseRollup(code))).toThrowErrorMatchingInlineSnapshot( + `[Error: Object method elements are not supported in TGSL.]`, + ); + }); +}); diff --git a/packages/unplugin-typegpu/src/babel.ts b/packages/unplugin-typegpu/src/babel.ts index 685d54ce6c..cf76020e36 100644 --- a/packages/unplugin-typegpu/src/babel.ts +++ b/packages/unplugin-typegpu/src/babel.ts @@ -1,6 +1,6 @@ import type { NodePath, TraverseOptions } from '@babel/traverse'; import defu from 'defu'; -import { transpileFn, type Externals } from 'tinyest-for-wgsl'; +import { transpileFnBabel, type Externals } from 'tinyest-for-wgsl'; import * as t from '@babel/types'; import { METADATA_FORMAT_VERSION, @@ -42,7 +42,7 @@ function assignMetadata( this: PluginState, path: NodePath, name: string | undefined, - ast: ReturnType, + ast: ReturnType, ): void { const metadata = t.objectExpression([ t.objectProperty(i('v'), t.numericLiteral(METADATA_FORMAT_VERSION)), diff --git a/packages/unplugin-typegpu/src/core/common.ts b/packages/unplugin-typegpu/src/core/common.ts index 8df86a3bd8..87503e7e2e 100644 --- a/packages/unplugin-typegpu/src/core/common.ts +++ b/packages/unplugin-typegpu/src/core/common.ts @@ -2,7 +2,7 @@ import * as t from '@babel/types'; import type { NodePath, TraverseOptions } from '@babel/traverse'; import type { FilterPattern } from 'unplugin'; import MagicString from 'magic-string'; -import { transpileFn } from 'tinyest-for-wgsl'; +import { transpileFnBabel } from 'tinyest-for-wgsl'; import { getEmbeddedTypegpuMetadata } from './embeddedMetadata.ts'; import { obfuscate } from './obfuscate.ts'; @@ -96,7 +96,7 @@ export interface TransformMethods { this: PluginState, path: NodePath, name: string | undefined, - ast: ReturnType, + ast: ReturnType, ): void; wrapInAutoName(this: PluginState, path: NodePath, name: string): void; @@ -465,7 +465,7 @@ function containsUseGpuDirective( const fnNodeToTranspiledMap = new WeakMap< t.FunctionDeclaration | t.FunctionExpression | t.ArrowFunctionExpression, - ReturnType + ReturnType >(); function functionOnExit( @@ -489,10 +489,10 @@ function functionOnExit( } function transpile( - rootNode: Parameters[0], + rootNode: Parameters[0], obf: boolean, -): ReturnType { - const result = transpileFn(rootNode, { ast: 'babel' }); +): ReturnType { + const result = transpileFnBabel(rootNode); if (obf) { return obfuscate(result); } diff --git a/packages/unplugin-typegpu/src/core/factory.ts b/packages/unplugin-typegpu/src/core/factory.ts index 77db2347fb..40b856092d 100644 --- a/packages/unplugin-typegpu/src/core/factory.ts +++ b/packages/unplugin-typegpu/src/core/factory.ts @@ -3,7 +3,7 @@ import MagicString from 'magic-string'; import { getBabelParserOptions, getLang } from 'ast-kit'; import type { UnpluginBuildContext, UnpluginContext, UnpluginFactory } from 'unplugin'; import _traverse, { type NodePath } from '@babel/traverse'; -import { transpileFn, type Externals } from 'tinyest-for-wgsl'; +import { transpileFnBabel, type Externals } from 'tinyest-for-wgsl'; import * as parser from '@babel/parser'; import * as t from '@babel/types'; import { @@ -42,7 +42,7 @@ function assignMetadata( this: UnpluginPluginState, path: NodePath, name: string | undefined, - ast: ReturnType, + ast: ReturnType, ): void { const metadata = `{ v: ${METADATA_FORMAT_VERSION}, diff --git a/packages/unplugin-typegpu/src/core/obfuscate.ts b/packages/unplugin-typegpu/src/core/obfuscate.ts index d547efef11..00629bb377 100644 --- a/packages/unplugin-typegpu/src/core/obfuscate.ts +++ b/packages/unplugin-typegpu/src/core/obfuscate.ts @@ -1,4 +1,4 @@ -import type { transpileFn } from 'tinyest-for-wgsl'; +import type { transpileFnBabel } from 'tinyest-for-wgsl'; import * as tinyest from 'tinyest'; const { NodeTypeCatalog: NODE } = tinyest; @@ -60,7 +60,9 @@ class Context { } } -export function obfuscate(fn: ReturnType): ReturnType { +export function obfuscate( + fn: ReturnType, +): ReturnType { const ctx = new Context(); const params = fn.params.map((param) => { diff --git a/packages/unplugin-typegpu/test/obfuscation.test.ts b/packages/unplugin-typegpu/test/obfuscation.test.ts index 3105e515e3..0718d2c933 100644 --- a/packages/unplugin-typegpu/test/obfuscation.test.ts +++ b/packages/unplugin-typegpu/test/obfuscation.test.ts @@ -1,5 +1,5 @@ -import { type ArrowFunctionExpression } from '@babel/types'; -import { transpileFn as _transpileFn } from 'tinyest-for-wgsl'; +import type { ArrowFunctionExpression } from '@babel/types'; +import { transpileFnBabel } from 'tinyest-for-wgsl'; import { describe, expect, it, test } from 'vitest'; import { obfuscate } from '../src/core/obfuscate.ts'; import babelParser from '@babel/parser'; @@ -174,13 +174,9 @@ function parse(code: string): ArrowFunctionExpression { } describe('obfuscate', () => { - const transpileFn = (node: ArrowFunctionExpression) => { - return _transpileFn(node, { ast: 'babel' }); - }; - it('obfuscates used variables', () => { const code = `() => { const variable = 1; const other = 2; const sensitiveName = 3; }`; - const transpiled = transpileFn(parse(code)); + const transpiled = transpileFnBabel(parse(code)); const { params, body, externalNames } = obfuscate(transpiled); @@ -197,7 +193,7 @@ describe('obfuscate', () => { it('remembers obfuscated names', () => { const code = `() => { const variable = 1; return variable; }`; - const transpiled = transpileFn(parse(code)); + const transpiled = transpileFnBabel(parse(code)); const { params, body, externalNames } = obfuscate(transpiled); @@ -213,7 +209,7 @@ describe('obfuscate', () => { it('remembers obfuscated names in computed access', () => { const code = `() => { const variable = 1; const array = [1, 2]; return array[variable]; }`; - const transpiled = transpileFn(parse(code)); + const transpiled = transpileFnBabel(parse(code)); const { params, body, externalNames } = obfuscate(transpiled); @@ -230,7 +226,7 @@ describe('obfuscate', () => { it('remembers obfuscated names in for loops', () => { const code = `() => { for (let i = 0; i< 10; i++) { return i; } }`; - const transpiled = transpileFn(parse(code)); + const transpiled = transpileFnBabel(parse(code)); const { params, body, externalNames } = obfuscate(transpiled); @@ -251,7 +247,7 @@ describe('obfuscate', () => { const b = Infinity; const c = NaN; }`; - const transpiled = transpileFn(parse(code)); + const transpiled = transpileFnBabel(parse(code)); const { params, body, externalNames } = obfuscate(transpiled); @@ -277,7 +273,7 @@ describe('obfuscate', () => { const code = `() => { const variable = null; }`; - const transpiled = transpileFn(parse(code)); + const transpiled = transpileFnBabel(parse(code)); const { params, body, externalNames } = obfuscate(transpiled); @@ -292,7 +288,7 @@ describe('obfuscate', () => { it('obfuscates parameters', () => { const code = `(param1, param2) => { return param2 + param1; }`; - const transpiled = transpileFn(parse(code)); + const transpiled = transpileFnBabel(parse(code)); const { params, body, externalNames } = obfuscate(transpiled); @@ -318,7 +314,7 @@ describe('obfuscate', () => { it('obfuscates destructured parameters', () => { const code = `(param, { prop }) => { return param + prop; }`; - const transpiled = transpileFn(parse(code)); + const transpiled = transpileFnBabel(parse(code)); const { params, body, externalNames } = obfuscate(transpiled); @@ -349,7 +345,7 @@ describe('obfuscate', () => { it('obfuscates destructured parameters with aliases', () => { const code = `(param, { prop, other: alias }) => { return param + prop + alias; }`; - const transpiled = transpileFn(parse(code)); + const transpiled = transpileFnBabel(parse(code)); const { params, body, externalNames } = obfuscate(transpiled); @@ -384,7 +380,7 @@ describe('obfuscate', () => { it('does not obfuscate struct props', () => { const code = `(param) => { let struct; return param.prop + struct.field; }`; - const transpiled = transpileFn(parse(code)); + const transpiled = transpileFnBabel(parse(code)); const { params, body, externalNames } = obfuscate(transpiled); @@ -407,7 +403,7 @@ describe('obfuscate', () => { it('does not obfuscate struct keys', () => { const code = `(param) => { let struct = { field: 1 }; return struct.field; }`; - const transpiled = transpileFn(parse(code)); + const transpiled = transpileFnBabel(parse(code)); const { params, body, externalNames } = obfuscate(transpiled); @@ -430,7 +426,7 @@ describe('obfuscate', () => { it("obfuscates 'this'", () => { const code = `() => { return this.prop1.prop2; }`; - const transpiled = transpileFn(parse(code)); + const transpiled = transpileFnBabel(parse(code)); const { params, body, externalNames } = obfuscate(transpiled); @@ -454,7 +450,7 @@ describe('obfuscate', () => { const var3 = ext.config.zero; const var4 = ext.config.multiplier; }`; - const transpiled = transpileFn(parse(code)); + const transpiled = transpileFnBabel(parse(code)); const { params, body, externalNames } = obfuscate(transpiled); @@ -483,7 +479,7 @@ describe('obfuscate', () => { const j = ext.t.$.prop; const k = (ext).prop; }`; - const transpiled = transpileFn(parse(code)); + const transpiled = transpileFnBabel(parse(code)); const { params, body, externalNames } = obfuscate(transpiled); @@ -517,7 +513,7 @@ describe('obfuscate', () => { } return variable; }`; - const transpiled = transpileFn(parse(code)); + const transpiled = transpileFnBabel(parse(code)); const { params, body, externalNames } = obfuscate(transpiled); @@ -547,7 +543,7 @@ describe('obfuscate', () => { } return parameter; }`; - const transpiled = transpileFn(parse(code)); + const transpiled = transpileFnBabel(parse(code)); const { params, body, externalNames } = obfuscate(transpiled); @@ -582,7 +578,7 @@ describe('obfuscate', () => { } return external; }`; - const transpiled = transpileFn(parse(code)); + const transpiled = transpileFnBabel(parse(code)); const { params, body, externalNames } = obfuscate(transpiled); @@ -606,7 +602,7 @@ describe('obfuscate', () => { it('supports more than 26 names', () => { const code = `() => { ${Array.from({ length: 100 }, (_, i) => `let v${i};`).join('\n')} }`; - const transpiled = transpileFn(parse(code)); + const transpiled = transpileFnBabel(parse(code)); const { params, body, externalNames } = obfuscate(transpiled);