diff --git a/packages/tinyest-for-wgsl/package.json b/packages/tinyest-for-wgsl/package.json index 5822a091e6..868b120461 100644 --- a/packages/tinyest-for-wgsl/package.json +++ b/packages/tinyest-for-wgsl/package.json @@ -63,6 +63,18 @@ "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/externals.ts b/packages/tinyest-for-wgsl/src/externals.ts index 479ba914d4..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 (node.property.type === 'PrivateName') { + } else if (node.property.type === /* babel */ 'PrivateName') { property = `#${node.property.id.name}`; - } else if (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 new file mode 100644 index 0000000000..4fad4b9330 --- /dev/null +++ b/packages/tinyest-for-wgsl/src/functionParts.ts @@ -0,0 +1,122 @@ +import type * as babel from '@babel/types'; +import type * as acorn from 'acorn'; +import * as tinyest from 'tinyest'; +import type { JsNode } 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; + } + + 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, + )}`, + ); + } + + return functionNode; +} + +/** + * Rejects TypeGPU functions that cannot be represented. + */ +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 parseParams(functionNode: FunctionNode): tinyest.FuncParameter[] { + return ( + functionNode.params as ( + | babel.Identifier + | acorn.Identifier + | babel.ObjectPattern + | acorn.ObjectPattern + )[] + ).map((param) => + param.type === 'ObjectPattern' + ? { + type: tinyest.FuncParameterType.destructuredObject, + props: param.properties.flatMap((prop) => + (prop.type === /* acorn */ 'Property' || prop.type === /* babel */ 'ObjectProperty') && + prop.key.type === 'Identifier' && + prop.value.type === 'Identifier' + ? [{ name: prop.key.name, alias: prop.value.name }] + : [], + ), + } + : { + type: tinyest.FuncParameterType.identifier, + name: param.name, + }, + ); +} + +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, + }; +} diff --git a/packages/tinyest-for-wgsl/src/index.ts b/packages/tinyest-for-wgsl/src/index.ts index b9d34d2db3..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 } 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 3bdc854d73..c328db18ba 100644 --- a/packages/tinyest-for-wgsl/src/parsers.ts +++ b/packages/tinyest-for-wgsl/src/parsers.ts @@ -1,466 +1,183 @@ 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 { Context, JsNode, 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.'); - } - if (node.raw === 'null') { - return [NODE.nullLiteral]; - } - 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 = {}; +function createContext(params: tinyest.FuncParameter[]): Context { + return { + externalNames: new Map(), + ignoreExternalDepth: 0, + visitedNodes: new Set(), + stack: [ + { + declaredNames: params.flatMap((param) => + param.type === tinyest.FuncParameterType.identifier + ? param.name + : param.props.map((prop) => prop.alias), + ), + }, + ], + }; +} - for (const prop of node.properties) { - // TODO: Handle SpreadElement - if (prop.type === 'SpreadElement') { - throw new Error('Spread elements are not supported in TGSL.'); +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; } - // 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.'); - } + return [NODE.objectExpr, properties]; + }, + } as Transpilers; +} - // TODO: Handle Object method - if (prop.type === 'ObjectMethod') { - throw new Error('Object method elements are not supported in TGSL.'); +function createParser(kind: 'acorn' | 'babel' | 'legacy') { + const transpilers = ( + kind === 'acorn' + ? acornTranspilers + : kind === 'babel' + ? babelTranspilers + : createLegacyTraspilers() + ) as Transpilers; + + const transpile: Transpile = (ctx, node) => { + 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; } - - 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]; - }, + // @ts-ignore + return transpiler(ctx, node, transpile); + }; - ContinueStatement() { - return [NODE.continue]; - }, + return { + 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, + }; + } - BreakStatement() { - return [NODE.break]; - }, + return { + params, + body: [NODE.block, [[NODE.return, tinyestBody as tinyest.Expression]]], + externalNames: ctx.externalNames, + }; + }, - NullLiteral() { - return [NODE.nullLiteral]; - }, + transpileNode(node: JsNode): tinyest.AnyNode { + return transpile(createContext([]), node); + }, + }; +} - TSAsExpression: tsFallthrough, - TSSatisfiesExpression: tsFallthrough, - TSNonNullExpression: tsFallthrough, +const parsers = { + acorn: createParser('acorn'), + babel: createParser('babel'), }; -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; - } - } +let legacyParser: ReturnType | undefined = undefined; - // @ts-expect-error - return transpiler(ctx, node); +export function transpileFnAcorn(rootNode: acorn.AnyNode): TranspilationResult { + return parsers.acorn.transpileFn(rootNode); } -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'); - } +export function transpileNodeAcorn(rootNode: acorn.AnyNode): tinyest.AnyNode { + return parsers.acorn.transpileNode(rootNode); +} - 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(', ')}`); - } +export function transpileFnBabel(rootNode: babel.Node): TranspilationResult { + return parsers.babel.transpileFn(rootNode); +} - 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 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 { - const { params, body } = extractFunctionParts(rootNode); - - const ctx: Context = { - externalNames: new Map(), - ignoreExternalDepth: 0, - visitedNodes: new Set(), - stack: [ - { - declaredNames: params.flatMap((param) => - param.type === FuncParameterType.identifier - ? param.name - : param.props.map((prop) => prop.alias), - ), - }, - ], - }; - - const tinyestBody = transpile(ctx, body); - - if (body.type === 'BlockStatement') { - return { - params, - body: tinyestBody as tinyest.Block, - externalNames: ctx.externalNames, - }; + if (legacyParser === undefined) { + legacyParser = createParser('legacy'); } - - return { - params, - body: [NODE.block, [[NODE.return, tinyestBody as tinyest.Expression]]], - externalNames: ctx.externalNames, - }; + return legacyParser.transpileFn(rootNode); } -export function transpileNode(node: JsNode): tinyest.AnyNode { - const ctx: Context = { - externalNames: new Map(), - ignoreExternalDepth: 0, - visitedNodes: new Set(), - stack: [ - { - declaredNames: [], - }, - ], - }; - - return transpile(ctx, node); +/** + * @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/transpilers.ts b/packages/tinyest-for-wgsl/src/transpilers.ts new file mode 100644 index 0000000000..719684dddc --- /dev/null +++ b/packages/tinyest-for-wgsl/src/transpilers.ts @@ -0,0 +1,364 @@ +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'; + +const { NodeTypeCatalog: NODE } = tinyest; + +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') { + return [NODE.nullLiteral]; + } + 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) { + 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; + } + + return [NODE.objectExpr, properties]; + }, +} satisfies Transpilers; + +export const acornTranspilers = { + ...(baseTranspilers as Pick, SharedTranspilers>), + ...acornSpecificTranspilers, +} satisfies Transpilers; + +const tsFallthrough = ( + ctx: Context, + node: { expression: babel.Expression }, + transpile: Transpile, +) => { + 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(node.value))]; + }, + + BooleanLiteral(_ctx, node) { + return node.value; + }, + + StringLiteral(_ctx, node) { + return [NODE.stringLiteral, node.value]; + }, + + NullLiteral() { + return [NODE.nullLiteral]; + }, + + 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.`); + } + + 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 as Pick, SharedTranspilers>), + ...babelSpecificTranspilers, +} satisfies Transpilers; diff --git a/packages/tinyest-for-wgsl/src/types.ts b/packages/tinyest-for-wgsl/src/types.ts index 5f27600786..331847ccf7 100644 --- a/packages/tinyest-for-wgsl/src/types.ts +++ b/packages/tinyest-for-wgsl/src/types.ts @@ -35,3 +35,13 @@ export type TranspilationResult = { }; 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; +}>; diff --git a/packages/tinyest-for-wgsl/tests/helpers.ts b/packages/tinyest-for-wgsl/tests/helpers.ts index 7bd671e13c..d560fb7100 100644 --- a/packages/tinyest-for-wgsl/tests/helpers.ts +++ b/packages/tinyest-for-wgsl/tests/helpers.ts @@ -1,14 +1,20 @@ import babel from '@babel/parser'; import type { Node } from '@babel/types'; import * as acorn from 'acorn'; +import { transpileFnAcorn, transpileFnBabel, 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: (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) => 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 eb1344c27d..9b0ee0944e 100644 --- a/packages/tinyest-for-wgsl/tests/parsers.test.ts +++ b/packages/tinyest-for-wgsl/tests/parsers.test.ts @@ -1,13 +1,13 @@ -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) => { + dualTest((p, transpileFn) => { const { params, body, externalNames } = transpileFn( p(`() => { const a = undefined; @@ -33,7 +33,7 @@ describe('transpileFn', () => { it( 'parses null', - dualTest((p) => { + dualTest((p, transpileFn) => { const { params, body, externalNames } = transpileFn( p(`() => { const a = null; @@ -48,14 +48,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([]); @@ -66,7 +66,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([]); @@ -77,7 +77,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([ @@ -97,7 +97,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; @@ -120,7 +120,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; @@ -145,7 +145,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([]); @@ -161,7 +161,7 @@ describe('transpileFn', () => { it( 'handles destructured args', - dualTest((p) => { + dualTest((p, transpileFn) => { const { params, externalNames } = transpileFn( p(`({ pos, a: b }) => { const x = pos.x; @@ -190,7 +190,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; @@ -235,14 +235,14 @@ describe('transpileFn', () => { ); it('handles TSNonNullExpression', () => { - const { body } = transpileFn(parseBabel('() => x!.y')); + const { body } = transpileFnBabel(parseBabel('() => x!.y')); 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; @@ -264,7 +264,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; @@ -286,7 +286,7 @@ describe('transpileFn', () => { it( 'handles complex external trees', - dualTest((p) => { + dualTest((p, transpileFn) => { const { externalNames, body } = transpileFn( p(`() => { const a = ext.p; @@ -335,7 +335,7 @@ describe('transpileFn', () => { it( 'does not duplicate externals', - dualTest((p) => { + dualTest((p, transpileFn) => { const { externalNames } = transpileFn( p(`() => { const a = ext; @@ -353,7 +353,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; @@ -379,7 +379,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 { @@ -396,7 +396,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 { @@ -405,4 +405,84 @@ describe('transpileFn', () => { `); }), ); + + it( + 'parses binary bigints', + dualTest((p, transpileFn) => { + expect(JSON.stringify(transpileFn(p('() => 0b101n')).body)).toMatchInlineSnapshot( + `"[0,[[10,[5,"5"]]]]"`, + ); + }), + ); + + it( + 'rejects computed object properties', + dualTest((p, transpileFn) => { + expect(() => transpileFn(p('() => ({ [k]: 1 })'))).toThrowErrorMatchingInlineSnapshot( + `[Error: Computed object properties are not supported in TGSL.]`, + ); + }), + ); +}); + +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 e141f090d3..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); +): 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 0e115fa078..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 } 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'; @@ -176,7 +176,7 @@ function parse(code: string): ArrowFunctionExpression { describe('obfuscate', () => { 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); @@ -193,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); @@ -209,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); @@ -226,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); @@ -247,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); @@ -273,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); @@ -288,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); @@ -314,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); @@ -345,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); @@ -380,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); @@ -403,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); @@ -426,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); @@ -450,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); @@ -479,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); @@ -513,7 +513,7 @@ describe('obfuscate', () => { } return variable; }`; - const transpiled = transpileFn(parse(code)); + const transpiled = transpileFnBabel(parse(code)); const { params, body, externalNames } = obfuscate(transpiled); @@ -543,7 +543,7 @@ describe('obfuscate', () => { } return parameter; }`; - const transpiled = transpileFn(parse(code)); + const transpiled = transpileFnBabel(parse(code)); const { params, body, externalNames } = obfuscate(transpiled); @@ -578,7 +578,7 @@ describe('obfuscate', () => { } return external; }`; - const transpiled = transpileFn(parse(code)); + const transpiled = transpileFnBabel(parse(code)); const { params, body, externalNames } = obfuscate(transpiled); @@ -602,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); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 464eea5af9..2553faf28d 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: @@ -4551,8 +4551,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 +5331,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 +6089,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 +13129,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 +14192,7 @@ snapshots: dependencies: '@types/node': 24.10.0 - bun-types@1.3.14: + bun-types@1.4.0: dependencies: '@types/node': 24.10.0