From ac1b4234d95bd16ae75336c7e8a9a1531cb01aaf Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Thu, 3 Sep 2026 18:02:11 +0200 Subject: [PATCH 1/8] Add boolean node --- packages/tinyest/src/nodes.ts | 6 +++++- packages/typegpu/src/shared/tseynit.ts | 6 +++++- packages/typegpu/src/tgsl/wgslGenerator.ts | 3 +++ packages/typegpu/tests/internal/tseynit.test.ts | 14 ++++++++++++++ packages/unplugin-typegpu/src/core/obfuscate.ts | 6 ++++++ packages/unplugin-typegpu/test/obfuscation.test.ts | 4 ++++ 6 files changed, 37 insertions(+), 2 deletions(-) diff --git a/packages/tinyest/src/nodes.ts b/packages/tinyest/src/nodes.ts index 942afe4d78..7184e79a73 100644 --- a/packages/tinyest/src/nodes.ts +++ b/packages/tinyest/src/nodes.ts @@ -33,6 +33,7 @@ export const NodeTypeCatalog = { objectExpr: 104, conditionalExpr: 105, nullLiteral: 106, + booleanLiteral: 107, } as const; export type NodeTypeCatalog = typeof NodeTypeCatalog; @@ -241,7 +242,10 @@ export type Str = readonly [type: NodeTypeCatalog['stringLiteral'], string]; /** null literal */ export type Null = readonly [type: NodeTypeCatalog['nullLiteral']]; -export type Literal = Num | Str | boolean | Null; +/** boolean can either be encoded as a node, or as a plain literal */ +export type Bool = boolean | readonly [type: NodeTypeCatalog['booleanLiteral'], boolean]; + +export type Literal = Num | Str | Bool | Null; /** Identifiers are just strings, since string literals are rare in WGSL, and identifiers are everywhere. */ export type Expression = diff --git a/packages/typegpu/src/shared/tseynit.ts b/packages/typegpu/src/shared/tseynit.ts index ef7a8479df..e990cd4dee 100644 --- a/packages/typegpu/src/shared/tseynit.ts +++ b/packages/typegpu/src/shared/tseynit.ts @@ -89,6 +89,9 @@ function stringifyExpression(node: tinyest.Expression, ident: string): string { if (typeof node === 'boolean') { return `${node}`; } + if (node[0] === NODE.booleanLiteral) { + return `${node[1]}`; + } if (node[0] === NODE.numericLiteral) { return node[1]; @@ -186,7 +189,8 @@ function isExpression(node: tinyest.AnyNode): node is tinyest.Expression { node[0] === NODE.postUpdate || node[0] === NODE.objectExpr || node[0] === NODE.conditionalExpr || - node[0] === NODE.nullLiteral + node[0] === NODE.nullLiteral || + node[0] === NODE.booleanLiteral ) { node satisfies tinyest.Expression; return true; diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index 33d64ec786..4e668e0981 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -433,6 +433,9 @@ export class WgslGenerator implements ShaderGenerator { if (typeof expression === 'boolean') { return snip(expression, bool, /* origin */ 'constant', false); } + if (expression[0] === NODE.booleanLiteral) { + return snip(expression[1], bool, /* origin */ 'constant', false); + } if (expression[0] === NODE.logicalExpr) { const [_, lhs, op, rhs] = expression; diff --git a/packages/typegpu/tests/internal/tseynit.test.ts b/packages/typegpu/tests/internal/tseynit.test.ts index 96cccef9b0..65536e4518 100644 --- a/packages/typegpu/tests/internal/tseynit.test.ts +++ b/packages/typegpu/tests/internal/tseynit.test.ts @@ -3,6 +3,8 @@ import * as tinyest from 'tinyest'; import { getFunctionMetadata } from '../../src/shared/meta.ts'; import { stringifyNode } from '../../src/shared/tseynit.ts'; import { tgpu, d } from '../../src/index.js'; +import type { BinaryExpression } from 'tinyest'; +import type { LogicalExpression } from 'tinyest'; function getBodyAst(fn: () => void) { const meta = getFunctionMetadata(fn); @@ -355,5 +357,17 @@ describe('ast to JS transformation', () => { }" `); }); + + it('handles boolean node', () => { + const NODE = tinyest.NodeTypeCatalog; + const ast: LogicalExpression = [ + NODE.logicalExpr, + [NODE.booleanLiteral, true], + '||', + [NODE.booleanLiteral, false], + ]; + + expect(stringifyNode(ast)).toMatchInlineSnapshot(`"(true) || (false)"`); + }); }); }); diff --git a/packages/unplugin-typegpu/src/core/obfuscate.ts b/packages/unplugin-typegpu/src/core/obfuscate.ts index d547efef11..0f16054de7 100644 --- a/packages/unplugin-typegpu/src/core/obfuscate.ts +++ b/packages/unplugin-typegpu/src/core/obfuscate.ts @@ -175,6 +175,12 @@ const visitors = { nullLiteral(_: Context, node: tinyest.Null) { return node; }, + booleanLiteral(_: Context, node: tinyest.Bool) { + if (typeof node === 'boolean') { + return node; + } + return [NODE.booleanLiteral, node[1]]; + }, } as const satisfies { [N in keyof typeof NODE]: ( ctx: Context, diff --git a/packages/unplugin-typegpu/test/obfuscation.test.ts b/packages/unplugin-typegpu/test/obfuscation.test.ts index 3105e515e3..42b053251b 100644 --- a/packages/unplugin-typegpu/test/obfuscation.test.ts +++ b/packages/unplugin-typegpu/test/obfuscation.test.ts @@ -617,4 +617,8 @@ describe('obfuscate', () => { expect(stringifiedBody).toContain('ab'); expect(externalNames).toMatchInlineSnapshot(`Map {}`); }); + + it('supports boolean nodes', () => { + // TODO + }); }); From 2c919ac0c9927a426e50ce8dba5d7742c30077b9 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 4 Sep 2026 10:40:46 +0200 Subject: [PATCH 2/8] Add identifier node --- packages/tinyest/src/nodes.ts | 6 ++++-- packages/typegpu/src/shared/tseynit.ts | 10 ++++++++-- packages/typegpu/src/tgsl/wgslGenerator.ts | 5 +++++ packages/unplugin-typegpu/src/core/obfuscate.ts | 6 ++++++ 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/packages/tinyest/src/nodes.ts b/packages/tinyest/src/nodes.ts index 7184e79a73..8ae36200a9 100644 --- a/packages/tinyest/src/nodes.ts +++ b/packages/tinyest/src/nodes.ts @@ -13,6 +13,7 @@ export const NodeTypeCatalog = { call: 6, memberAccess: 7, indexAccess: 8, + identifier: 9, // regular return: 10, @@ -38,6 +39,8 @@ export const NodeTypeCatalog = { export type NodeTypeCatalog = typeof NodeTypeCatalog; +export type Identifier = string | readonly [type: NodeTypeCatalog['identifier'], id: string]; + /** * Represents a return statement */ @@ -247,9 +250,8 @@ export type Bool = boolean | readonly [type: NodeTypeCatalog['booleanLiteral'], export type Literal = Num | Str | Bool | Null; -/** Identifiers are just strings, since string literals are rare in WGSL, and identifiers are everywhere. */ export type Expression = - | string + | Identifier | BinaryExpression | AssignmentExpression | LogicalExpression diff --git a/packages/typegpu/src/shared/tseynit.ts b/packages/typegpu/src/shared/tseynit.ts index e990cd4dee..c55155d228 100644 --- a/packages/typegpu/src/shared/tseynit.ts +++ b/packages/typegpu/src/shared/tseynit.ts @@ -89,6 +89,11 @@ function stringifyExpression(node: tinyest.Expression, ident: string): string { if (typeof node === 'boolean') { return `${node}`; } + + if (node[0] === NODE.identifier) { + return node[1]; + } + if (node[0] === NODE.booleanLiteral) { return `${node[1]}`; } @@ -175,6 +180,8 @@ function isExpression(node: tinyest.AnyNode): node is tinyest.Expression { if ( typeof node === 'string' || typeof node === 'boolean' || + node[0] === NODE.identifier || + node[0] === NODE.booleanLiteral || node[0] === NODE.numericLiteral || node[0] === NODE.stringLiteral || node[0] === NODE.arrayExpr || @@ -189,8 +196,7 @@ function isExpression(node: tinyest.AnyNode): node is tinyest.Expression { node[0] === NODE.postUpdate || node[0] === NODE.objectExpr || node[0] === NODE.conditionalExpr || - node[0] === NODE.nullLiteral || - node[0] === NODE.booleanLiteral + node[0] === NODE.nullLiteral ) { node satisfies tinyest.Expression; return true; diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index 4e668e0981..4b1412ef24 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -433,6 +433,11 @@ export class WgslGenerator implements ShaderGenerator { if (typeof expression === 'boolean') { return snip(expression, bool, /* origin */ 'constant', false); } + + if (expression[0] === NODE.identifier) { + return this._identifier(expression[1]); + } + if (expression[0] === NODE.booleanLiteral) { return snip(expression[1], bool, /* origin */ 'constant', false); } diff --git a/packages/unplugin-typegpu/src/core/obfuscate.ts b/packages/unplugin-typegpu/src/core/obfuscate.ts index 0f16054de7..5afcb5e0f1 100644 --- a/packages/unplugin-typegpu/src/core/obfuscate.ts +++ b/packages/unplugin-typegpu/src/core/obfuscate.ts @@ -181,6 +181,12 @@ const visitors = { } return [NODE.booleanLiteral, node[1]]; }, + identifier(ctx: Context, node: tinyest.Identifier) { + if (typeof node === 'string') { + return obf(ctx, node); + } + return [NODE.identifier, obf(ctx, node[1])]; + }, } as const satisfies { [N in keyof typeof NODE]: ( ctx: Context, From cab271ae9f84cbb1d2f99852ea9e9092192eebd3 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 4 Sep 2026 10:45:30 +0200 Subject: [PATCH 3/8] Change let and const to use identifiers --- packages/tinyest/src/nodes.ts | 8 ++-- packages/typegpu/src/tgsl/wgslGenerator.ts | 45 +++++++++++++--------- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/packages/tinyest/src/nodes.ts b/packages/tinyest/src/nodes.ts index 8ae36200a9..3df2226d8c 100644 --- a/packages/tinyest/src/nodes.ts +++ b/packages/tinyest/src/nodes.ts @@ -64,15 +64,15 @@ export type Block = readonly [type: NodeTypeCatalog['block'], Statement[]]; * Represents a let statement */ export type Let = - | readonly [type: NodeTypeCatalog['let'], identifier: string] - | readonly [type: NodeTypeCatalog['let'], identifier: string, value: Expression]; + | readonly [type: NodeTypeCatalog['let'], identifier: Identifier] + | readonly [type: NodeTypeCatalog['let'], identifier: Identifier, value: Expression]; /** * Represents a const statement */ export type Const = - | readonly [type: NodeTypeCatalog['const'], identifier: string] - | readonly [type: NodeTypeCatalog['const'], identifier: string, value: Expression]; + | readonly [type: NodeTypeCatalog['const'], identifier: Identifier] + | readonly [type: NodeTypeCatalog['const'], identifier: Identifier, value: Expression]; export type For = readonly [ type: NodeTypeCatalog['for'], diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index 4b1412ef24..15fa8a80c2 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -1321,6 +1321,7 @@ Try 'return ${typeStr}(${str});' instead. protected _letStatement(statement: tinyest.Let): ResolvedStatement { const [_, rawId, eqNode] = statement; + const rawIdStr = extractId(rawId); if (eqNode === undefined) { throw new Error( @@ -1333,9 +1334,9 @@ Try 'return ${typeStr}(${str});' instead. if (eq.value instanceof RefOperator) { const rhsStr = stringifyNode(eqNode); throw new WgslTypeError( - `'let ${rawId} = ${rhsStr}' is invalid, cannot initialize 'let' variables with d.ref() + `'let ${rawIdStr} = ${rhsStr}' is invalid, cannot initialize 'let' variables with d.ref() ----- -- Try 'const ${rawId} = ${rhsStr}'. +- Try 'const ${rawIdStr} = ${rhsStr}'. -----`, ); } @@ -1345,9 +1346,9 @@ Try 'return ${typeStr}(${str});' instead. if (definitionDataType === UnknownData) { const rhsStr = stringifyNode(eqNode); throw new WgslTypeError( - `'let ${rawId} = ${rhsStr}' is invalid, cannot determine WGSL type of '${rhsStr}' + `'let ${rawIdStr} = ${rhsStr}' is invalid, cannot determine WGSL type of '${rhsStr}' ----- -- Try using or defining a schema that matches your desired value the most, and wrap the value with it: 'let ${rawId} = Schema(${rhsStr})' +- Try using or defining a schema that matches your desired value the most, and wrap the value with it: 'let ${rawIdStr} = Schema(${rhsStr})' -----`, ); } @@ -1358,22 +1359,22 @@ Try 'return ${typeStr}(${str});' instead. const rhsTypeStr = this.ctx.resolve(unptr(eq.dataType)).value; throw new WgslTypeError( - `'let ${rawId} = ${rhsStr}' is invalid, because references cannot be assigned to 'let' variable declarations. + `'let ${rawIdStr} = ${rhsStr}' is invalid, because references cannot be assigned to 'let' variable declarations. ----- -- Try 'let ${rawId} = ${rhsTypeStr}(${rhsStr})' if you need to reassign '${rawId}' later -- Try 'const ${rawId} = ${rhsStr}' if you won't reassign '${rawId}' later. +- Try 'let ${rawIdStr} = ${rhsTypeStr}(${rhsStr})' if you need to reassign '${rawIdStr}' later +- Try 'const ${rawIdStr} = ${rhsStr}' if you won't reassign '${rawIdStr}' later. -----`, ); } const concreteType = concretize(definitionDataType); const snippet = snip( - this.ctx.makeUniqueIdentifier(rawId, 'block'), + this.ctx.makeUniqueIdentifier(rawIdStr, 'block'), concreteType, /* origin */ 'local-def', false, ); - this.ctx.defineVariable(rawId, snippet); + this.ctx.defineVariable(rawIdStr, snippet); const rhsSnippet = tryConvertSnippet(this.ctx, eq, definitionDataType, false); const rhsStr = this.ctx.resolveSnippet(rhsSnippet).value; @@ -1382,7 +1383,7 @@ Try 'return ${typeStr}(${str});' instead. // reassignment might happen in a pruned branch, in which case we can generate // more optimised code by emitting 'let' or 'const' instead of 'var'. const scope = this.ctx.topFunctionScope; - invariant(scope, `Expected function scope to be present for ${rawId}`); + invariant(scope, `Expected function scope to be present for ${rawIdStr}`); const emittedVarType = `#VAR_${scope.placeholderForVariable.size}#` as const; scope.placeholderForVariable.set(snippet, emittedVarType); @@ -1394,6 +1395,7 @@ Try 'return ${typeStr}(${str});' instead. protected _constStatement(statement: tinyest.Const): ResolvedStatement { const [_, rawId, eqNode] = statement; + const rawIdStr = extractId(rawId); if (eqNode === undefined) { throw new Error( @@ -1412,7 +1414,7 @@ Try 'return ${typeStr}(${str});' instead. } const refSnippet = eq.value.snippet; const varName = this.refVariable( - rawId, + rawIdStr, concretize(refSnippet.dataType as wgsl.BaseData) as wgsl.StorableData, ); return { @@ -1434,9 +1436,9 @@ Try 'return ${typeStr}(${str});' instead. if (definitionDataType === UnknownData) { const rhsStr = stringifyNode(eqNode); throw new WgslTypeError( - `'const ${rawId} = ${rhsStr}' is invalid, cannot determine WGSL type of '${rhsStr}' + `'const ${rawIdStr} = ${rhsStr}' is invalid, cannot determine WGSL type of '${rhsStr}' ----- -- Try using or defining a schema that matches your desired value the most, and wrap the value with it: 'const ${rawId} = Schema(${rhsStr})' +- Try using or defining a schema that matches your desired value the most, and wrap the value with it: 'const ${rawIdStr} = Schema(${rhsStr})' -----`, ); } @@ -1468,17 +1470,17 @@ Try 'return ${typeStr}(${str});' instead. varType = ''; varOrigin = 'local-def'; } else { - return this._aliasConstStatement(rawId, eqNode, eq); + return this._aliasConstStatement(rawIdStr, eqNode, eq); } const concreteType = concretize(definitionDataType); const snippet = snip( - this.ctx.makeUniqueIdentifier(rawId, 'block'), + this.ctx.makeUniqueIdentifier(rawIdStr, 'block'), concreteType, /* origin */ varOrigin, false, ); - this.ctx.defineVariable(rawId, snippet); + this.ctx.defineVariable(rawIdStr, snippet); const rhsSnippet = tryConvertSnippet(this.ctx, eq, definitionDataType, false); const rhsStr = this.ctx.resolveSnippet(rhsSnippet).value; @@ -1486,7 +1488,7 @@ Try 'return ${typeStr}(${str});' instead. let emittedVarType: 'var' | 'let' | 'const' | `#VAR_${number}#`; if (varType === '') { const scope = this.ctx.topFunctionScope; - invariant(scope, `Expected function scope to be present for ${rawId}`); + invariant(scope, `Expected function scope to be present for ${rawIdStr}`); emittedVarType = `#VAR_${scope.placeholderForVariable.size}#`; scope.placeholderForVariable.set(snippet, emittedVarType); } else { @@ -1692,7 +1694,7 @@ ${this.ctx.pre}else ${alternate}`, const shouldUnroll = iterableExpr.value instanceof UnrollableIterable; const iterableSnippet = shouldUnroll ? iterableExpr.value.snippet : iterableExpr; const range = forOfUtils.getRangeSnippets(this.ctx, iterableSnippet, shouldUnroll); - const originalLoopVarName = loopVar[1]; + const originalLoopVarName = extractId(loopVar[1]); const blockified = blockifySingleStatement(body); if (shouldUnroll) { @@ -1942,3 +1944,10 @@ function extractObject(expr: tinyest.Expression): string | undefined { return object; } } + +function extractId(ident: tinyest.Identifier): string { + if (typeof ident === 'string') { + return ident; + } + return ident[1]; +} From ac418b77412e05da2f55335c8ae66719062f32ab Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 4 Sep 2026 10:48:15 +0200 Subject: [PATCH 4/8] Change member expression to use identifier --- packages/tinyest/src/nodes.ts | 2 +- packages/typegpu/src/tgsl/wgslGenerator.ts | 45 +++++++++++----------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/packages/tinyest/src/nodes.ts b/packages/tinyest/src/nodes.ts index 3df2226d8c..fa1a1730b5 100644 --- a/packages/tinyest/src/nodes.ts +++ b/packages/tinyest/src/nodes.ts @@ -209,7 +209,7 @@ export type ConditionalExpression = readonly [ export type MemberAccess = readonly [ type: NodeTypeCatalog['memberAccess'], object: Expression, - member: string, + member: Identifier, ]; export type IndexAccess = readonly [ diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index 15fa8a80c2..c60a5042eb 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -682,8 +682,9 @@ export class WgslGenerator implements ShaderGenerator { if (expression[0] === NODE.memberAccess) { // Member Access - const [_, targetNode, property] = expression; + const [_, targetNode, propertyNode] = expression; const target = this._expression(targetNode); + const property = extractId(propertyNode); const accessed = accessProp(target, property); if (!accessed) { @@ -1320,8 +1321,8 @@ Try 'return ${typeStr}(${str});' instead. } protected _letStatement(statement: tinyest.Let): ResolvedStatement { - const [_, rawId, eqNode] = statement; - const rawIdStr = extractId(rawId); + const [_, rawIdNode, eqNode] = statement; + const rawId = extractId(rawIdNode); if (eqNode === undefined) { throw new Error( @@ -1334,9 +1335,9 @@ Try 'return ${typeStr}(${str});' instead. if (eq.value instanceof RefOperator) { const rhsStr = stringifyNode(eqNode); throw new WgslTypeError( - `'let ${rawIdStr} = ${rhsStr}' is invalid, cannot initialize 'let' variables with d.ref() + `'let ${rawId} = ${rhsStr}' is invalid, cannot initialize 'let' variables with d.ref() ----- -- Try 'const ${rawIdStr} = ${rhsStr}'. +- Try 'const ${rawId} = ${rhsStr}'. -----`, ); } @@ -1346,9 +1347,9 @@ Try 'return ${typeStr}(${str});' instead. if (definitionDataType === UnknownData) { const rhsStr = stringifyNode(eqNode); throw new WgslTypeError( - `'let ${rawIdStr} = ${rhsStr}' is invalid, cannot determine WGSL type of '${rhsStr}' + `'let ${rawId} = ${rhsStr}' is invalid, cannot determine WGSL type of '${rhsStr}' ----- -- Try using or defining a schema that matches your desired value the most, and wrap the value with it: 'let ${rawIdStr} = Schema(${rhsStr})' +- Try using or defining a schema that matches your desired value the most, and wrap the value with it: 'let ${rawId} = Schema(${rhsStr})' -----`, ); } @@ -1359,22 +1360,22 @@ Try 'return ${typeStr}(${str});' instead. const rhsTypeStr = this.ctx.resolve(unptr(eq.dataType)).value; throw new WgslTypeError( - `'let ${rawIdStr} = ${rhsStr}' is invalid, because references cannot be assigned to 'let' variable declarations. + `'let ${rawId} = ${rhsStr}' is invalid, because references cannot be assigned to 'let' variable declarations. ----- -- Try 'let ${rawIdStr} = ${rhsTypeStr}(${rhsStr})' if you need to reassign '${rawIdStr}' later -- Try 'const ${rawIdStr} = ${rhsStr}' if you won't reassign '${rawIdStr}' later. +- Try 'let ${rawId} = ${rhsTypeStr}(${rhsStr})' if you need to reassign '${rawId}' later +- Try 'const ${rawId} = ${rhsStr}' if you won't reassign '${rawId}' later. -----`, ); } const concreteType = concretize(definitionDataType); const snippet = snip( - this.ctx.makeUniqueIdentifier(rawIdStr, 'block'), + this.ctx.makeUniqueIdentifier(rawId, 'block'), concreteType, /* origin */ 'local-def', false, ); - this.ctx.defineVariable(rawIdStr, snippet); + this.ctx.defineVariable(rawId, snippet); const rhsSnippet = tryConvertSnippet(this.ctx, eq, definitionDataType, false); const rhsStr = this.ctx.resolveSnippet(rhsSnippet).value; @@ -1383,7 +1384,7 @@ Try 'return ${typeStr}(${str});' instead. // reassignment might happen in a pruned branch, in which case we can generate // more optimised code by emitting 'let' or 'const' instead of 'var'. const scope = this.ctx.topFunctionScope; - invariant(scope, `Expected function scope to be present for ${rawIdStr}`); + invariant(scope, `Expected function scope to be present for ${rawId}`); const emittedVarType = `#VAR_${scope.placeholderForVariable.size}#` as const; scope.placeholderForVariable.set(snippet, emittedVarType); @@ -1394,8 +1395,8 @@ Try 'return ${typeStr}(${str});' instead. } protected _constStatement(statement: tinyest.Const): ResolvedStatement { - const [_, rawId, eqNode] = statement; - const rawIdStr = extractId(rawId); + const [_, rawIdNode, eqNode] = statement; + const rawId = extractId(rawIdNode); if (eqNode === undefined) { throw new Error( @@ -1414,7 +1415,7 @@ Try 'return ${typeStr}(${str});' instead. } const refSnippet = eq.value.snippet; const varName = this.refVariable( - rawIdStr, + rawId, concretize(refSnippet.dataType as wgsl.BaseData) as wgsl.StorableData, ); return { @@ -1436,9 +1437,9 @@ Try 'return ${typeStr}(${str});' instead. if (definitionDataType === UnknownData) { const rhsStr = stringifyNode(eqNode); throw new WgslTypeError( - `'const ${rawIdStr} = ${rhsStr}' is invalid, cannot determine WGSL type of '${rhsStr}' + `'const ${rawId} = ${rhsStr}' is invalid, cannot determine WGSL type of '${rhsStr}' ----- -- Try using or defining a schema that matches your desired value the most, and wrap the value with it: 'const ${rawIdStr} = Schema(${rhsStr})' +- Try using or defining a schema that matches your desired value the most, and wrap the value with it: 'const ${rawId} = Schema(${rhsStr})' -----`, ); } @@ -1470,17 +1471,17 @@ Try 'return ${typeStr}(${str});' instead. varType = ''; varOrigin = 'local-def'; } else { - return this._aliasConstStatement(rawIdStr, eqNode, eq); + return this._aliasConstStatement(rawId, eqNode, eq); } const concreteType = concretize(definitionDataType); const snippet = snip( - this.ctx.makeUniqueIdentifier(rawIdStr, 'block'), + this.ctx.makeUniqueIdentifier(rawId, 'block'), concreteType, /* origin */ varOrigin, false, ); - this.ctx.defineVariable(rawIdStr, snippet); + this.ctx.defineVariable(rawId, snippet); const rhsSnippet = tryConvertSnippet(this.ctx, eq, definitionDataType, false); const rhsStr = this.ctx.resolveSnippet(rhsSnippet).value; @@ -1488,7 +1489,7 @@ Try 'return ${typeStr}(${str});' instead. let emittedVarType: 'var' | 'let' | 'const' | `#VAR_${number}#`; if (varType === '') { const scope = this.ctx.topFunctionScope; - invariant(scope, `Expected function scope to be present for ${rawIdStr}`); + invariant(scope, `Expected function scope to be present for ${rawId}`); emittedVarType = `#VAR_${scope.placeholderForVariable.size}#`; scope.placeholderForVariable.set(snippet, emittedVarType); } else { From 3f1fb555f38c9e1b0e852c344b791582cbd9e616 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 4 Sep 2026 11:49:29 +0200 Subject: [PATCH 5/8] Allow for emitting verbose nodes --- packages/tinyest-for-wgsl/src/parsers.ts | 22 +-- packages/tinyest-for-wgsl/src/transpilers.ts | 36 +++-- packages/tinyest-for-wgsl/src/types.ts | 8 + packages/tinyest-for-wgsl/tests/helpers.ts | 12 +- .../tests/verboseNodes.test.ts | 152 ++++++++++++++++++ 5 files changed, 203 insertions(+), 27 deletions(-) create mode 100644 packages/tinyest-for-wgsl/tests/verboseNodes.test.ts diff --git a/packages/tinyest-for-wgsl/src/parsers.ts b/packages/tinyest-for-wgsl/src/parsers.ts index d69ef7e622..b0b889b244 100644 --- a/packages/tinyest-for-wgsl/src/parsers.ts +++ b/packages/tinyest-for-wgsl/src/parsers.ts @@ -16,7 +16,7 @@ import { extractFunctionParts } from './functionParts.ts'; const { NodeTypeCatalog: NODE } = tinyest; -function createContext(params: tinyest.FuncParameter[]): Context { +function createContext(params: tinyest.FuncParameter[], opts: TranspilationOptions): Context { return { externalNames: new Map(), ignoreExternalDepth: 0, @@ -30,6 +30,7 @@ function createContext(params: tinyest.FuncParameter[]): Context { ), }, ], + opts, }; } @@ -51,6 +52,9 @@ function createParser(ast: AstKind) { const externalChain = tryFindExternalChain(ctx, node); if (externalChain) { ctx.externalNames.set(externalChain, externalChain); + if (ctx.opts.verboseNodes) { + return [NODE.identifier, externalChain]; + } return externalChain; } } @@ -60,9 +64,9 @@ function createParser(ast: AstKind) { }; return { - transpileFn(rootNode: JsNode): TranspilationResult { + transpileFn(rootNode: JsNode, options: TranspilationOptions): TranspilationResult { const { params, body } = extractFunctionParts(rootNode); - const ctx = createContext(params); + const ctx = createContext(params, options); const tinyestBody = transpile(ctx, body); @@ -81,8 +85,8 @@ function createParser(ast: AstKind) { }; }, - transpileNode(node: JsNode): tinyest.AnyNode { - return transpile(createContext([]), node); + transpileNode(node: JsNode, options: TranspilationOptions): tinyest.AnyNode { + return transpile(createContext([], options), node); }, }; } @@ -100,8 +104,8 @@ export function transpileFn( rootNode: babel.Node, options: TranspilationOptions<'babel'>, ): TranspilationResult; -export function transpileFn(rootNode: JsNode, { ast }: TranspilationOptions): TranspilationResult { - return parsers[ast].transpileFn(rootNode); +export function transpileFn(rootNode: JsNode, opts: TranspilationOptions): TranspilationResult { + return parsers[opts.ast].transpileFn(rootNode, opts); } export function transpileNode( @@ -112,6 +116,6 @@ 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); +export function transpileNode(rootNode: JsNode, opts: TranspilationOptions): tinyest.AnyNode { + return parsers[opts.ast].transpileNode(rootNode, opts); } diff --git a/packages/tinyest-for-wgsl/src/transpilers.ts b/packages/tinyest-for-wgsl/src/transpilers.ts index 719684dddc..9b63d133e5 100644 --- a/packages/tinyest-for-wgsl/src/transpilers.ts +++ b/packages/tinyest-for-wgsl/src/transpilers.ts @@ -45,7 +45,10 @@ export const baseTranspilers = { : [NODE.return]; }, - Identifier(_ctx, node) { + Identifier(ctx, node) { + if (ctx.opts.verboseNodes) { + return [NODE.identifier, node.name]; + } return node.name; }, @@ -92,13 +95,9 @@ export const baseTranspilers = { // 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; + const property = transpile(ctx, node.property) as tinyest.Identifier; ctx.ignoreExternalDepth--; - if (typeof property !== 'string') { - throw new Error('Expected identifier as property access key.'); - } - return [NODE.memberAccess, object, property]; }, @@ -147,14 +146,10 @@ export const baseTranspilers = { const decl = node.declarations[0]; ctx.ignoreExternalDepth++; - const id = transpile(ctx, decl.id); + const id = transpile(ctx, decl.id) as tinyest.Identifier; ctx.ignoreExternalDepth--; - if (typeof id !== 'string') { - throw new Error('Invalid variable declaration, expected identifier.'); - } - - ctx.stack[ctx.stack.length - 1]?.declaredNames.push(id); + ctx.stack[ctx.stack.length - 1]?.declaredNames.push(extractId(id)); const init = decl.init ? (transpile(ctx, decl.init) as tinyest.Expression) : undefined; @@ -221,7 +216,7 @@ export const baseTranspilers = { } satisfies Pick, SharedTranspilers>; const acornSpecificTranspilers = { - Literal(_ctx, node) { + Literal(ctx, node) { if (node.regex) { throw new Error('Regular expression literals are not representable in WGSL.'); } @@ -229,6 +224,9 @@ const acornSpecificTranspilers = { return [NODE.nullLiteral]; } if (typeof node.value === 'boolean') { + if (ctx.opts.verboseNodes) { + return [NODE.booleanLiteral, node.value]; + } return node.value; } if (typeof node.value === 'string') { @@ -298,7 +296,10 @@ const babelSpecificTranspilers = { return [NODE.numericLiteral, String(Number(node.value))]; }, - BooleanLiteral(_ctx, node) { + BooleanLiteral(ctx, node) { + if (ctx.opts.verboseNodes) { + return [NODE.booleanLiteral, node.value]; + } return node.value; }, @@ -362,3 +363,10 @@ export const babelTranspilers = { ...(baseTranspilers as Pick, SharedTranspilers>), ...babelSpecificTranspilers, } satisfies Transpilers; + +function extractId(ident: tinyest.Identifier): string { + if (typeof ident === 'string') { + return ident; + } + return ident[1]; +} diff --git a/packages/tinyest-for-wgsl/src/types.ts b/packages/tinyest-for-wgsl/src/types.ts index fc812258c8..837aad21f8 100644 --- a/packages/tinyest-for-wgsl/src/types.ts +++ b/packages/tinyest-for-wgsl/src/types.ts @@ -22,6 +22,7 @@ export type Context = { */ visitedNodes: Set; stack: Scope[]; + opts: TranspilationOptions; }; export type TranspilationResult = { @@ -50,4 +51,11 @@ export type AstKind = 'acorn' | 'babel'; export type TranspilationOptions = { ast: TAst; + /** + * With this option enabled, identifiers and boolean literals will be wrapped + * in dedicated nodes, instead of being transpiled as string/boolean. + * + * @default false + */ + verboseNodes?: boolean; }; diff --git a/packages/tinyest-for-wgsl/tests/helpers.ts b/packages/tinyest-for-wgsl/tests/helpers.ts index 745b1b8977..ac909fb0c3 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 { transpileFn, type TranspilationOptions, type TranspilationResult } from 'tinyest-for-wgsl'; export const parseRollup = (code: string) => acorn.parse(code, { ecmaVersion: 'latest' }); export const parseBabel = (code: string) => @@ -10,11 +10,15 @@ export const parseBabel = (code: string) => export function dualTest( test: ( p: (code: string) => TNode, - transpileFn: (node: TNode) => TranspilationResult, + transpileFn: (node: TNode, options?: Partial) => TranspilationResult, ) => void, ) { return () => { - test(parseBabel, (node) => transpileFn(node, { ast: 'babel' })); - test(parseRollup, (node) => transpileFn(node, { ast: 'acorn' })); + test(parseBabel, (node, options) => + transpileFn(node, { ast: 'babel', ...options } as TranspilationOptions<'babel'>), + ); + test(parseRollup, (node, options) => + transpileFn(node, { ast: 'acorn', ...options } as TranspilationOptions<'acorn'>), + ); }; } diff --git a/packages/tinyest-for-wgsl/tests/verboseNodes.test.ts b/packages/tinyest-for-wgsl/tests/verboseNodes.test.ts new file mode 100644 index 0000000000..efce4fb93f --- /dev/null +++ b/packages/tinyest-for-wgsl/tests/verboseNodes.test.ts @@ -0,0 +1,152 @@ +import type { ClassDeclaration, ClassProperty, Expression, Node } 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'; + +describe('verbose nodes', () => { + it( + 'uses nodes for identifiers', + dualTest((p, transpileFn) => { + const { params, body, externalNames } = transpileFn( + p(`(a, b, c) => { + return a + b + c; + }`), + { verboseNodes: true }, + ); + + expect(params).toMatchInlineSnapshot(` + [ + { + "name": "a", + "type": "i", + }, + { + "name": "b", + "type": "i", + }, + { + "name": "c", + "type": "i", + }, + ] + `); + expect(JSON.stringify(body)).toMatchInlineSnapshot( + `"[0,[[10,[1,[1,[9,"a"],"+",[9,"b"]],"+",[9,"c"]]]]]"`, + ); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); + }), + ); + + it( + 'uses nodes for boolean literals', + dualTest((p, transpileFn) => { + const { params, body, externalNames } = transpileFn( + p(`() => { + return true && false; + }`), + { verboseNodes: true }, + ); + + expect(params).toMatchInlineSnapshot(`[]`); + expect(JSON.stringify(body)).toMatchInlineSnapshot( + `"[0,[[10,[3,[107,true],"&&",[107,false]]]]]"`, + ); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); + }), + ); + + it( + 'uses nodes for const declarations', + dualTest((p, transpileFn) => { + const { params, body, externalNames } = transpileFn( + p(`() => { + const a = 1; + }`), + { verboseNodes: true }, + ); + + expect(params).toMatchInlineSnapshot(`[]`); + expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[[13,[9,"a"],[5,"1"]]]]"`); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); + }), + ); + + it( + 'uses nodes for let declarations', + dualTest((p, transpileFn) => { + const { params, body, externalNames } = transpileFn( + p(`() => { + let a = 1; + }`), + { verboseNodes: true }, + ); + + expect(params).toMatchInlineSnapshot(`[]`); + expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[[12,[9,"a"],[5,"1"]]]]"`); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); + }), + ); + + it( + 'uses nodes for member expressions', + dualTest((p, transpileFn) => { + const { params, body, externalNames } = transpileFn( + p(`() => { + const o = {}; + return o.prop; + }`), + { verboseNodes: true }, + ); + + expect(params).toMatchInlineSnapshot(`[]`); + expect(JSON.stringify(body)).toMatchInlineSnapshot( + `"[0,[[13,[9,"o"],[104,{}]],[10,[7,[9,"o"],[9,"prop"]]]]]"`, + ); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); + }), + ); + + it( + 'uses nodes for externals', + dualTest((p, transpileFn) => { + const { params, body, externalNames } = transpileFn( + p(`() => { + return ext + ext.prop; + }`), + { verboseNodes: true }, + ); + + expect(params).toMatchInlineSnapshot(`[]`); + expect(JSON.stringify(body)).toMatchInlineSnapshot( + `"[0,[[10,[1,[9,"ext"],"+",[9,"ext.prop"]]]]]"`, + ); + expect(externalNames).toMatchInlineSnapshot(` + Map { + "ext" => "ext", + "ext.prop" => "ext.prop", + } + `); + }), + ); + + it( + 'does not use nodes for object expressions', + dualTest((p, transpileFn) => { + const { params, body, externalNames } = transpileFn( + p(`() => { + return { p: ext }; + }`), + { verboseNodes: true }, + ); + + expect(params).toMatchInlineSnapshot(`[]`); + expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[[10,[104,{"p":[9,"ext"]}]]]]"`); + expect(externalNames).toMatchInlineSnapshot(` + Map { + "ext" => "ext", + } + `); + }), + ); +}); From a23aa38c65572211bf3a7675fdb7e996940a6d19 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 4 Sep 2026 11:57:07 +0200 Subject: [PATCH 6/8] Add obfuscation tests --- .../unplugin-typegpu/test/obfuscation.test.ts | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/packages/unplugin-typegpu/test/obfuscation.test.ts b/packages/unplugin-typegpu/test/obfuscation.test.ts index 42b053251b..500b909d3c 100644 --- a/packages/unplugin-typegpu/test/obfuscation.test.ts +++ b/packages/unplugin-typegpu/test/obfuscation.test.ts @@ -619,6 +619,39 @@ describe('obfuscate', () => { }); it('supports boolean nodes', () => { - // TODO + const code = `() => { return true || false; }`; + const transpiled = _transpileFn(parse(code), { ast: 'babel', verboseNodes: true }); + + const { params, body, externalNames } = obfuscate(transpiled); + + expect(params).toMatchInlineSnapshot(`[]`); + expect(stringifyNode(body)).toMatchInlineSnapshot(` + "{ + return (true) || (false); + }" + `); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); + }); + + it('supports identifier nodes', () => { + const code = `(a) => { return a; }`; + const transpiled = _transpileFn(parse(code), { ast: 'babel', verboseNodes: true }); + + const { params, body, externalNames } = obfuscate(transpiled); + + expect(params).toMatchInlineSnapshot(` + [ + { + "name": "a", + "type": "i", + }, + ] + `); + expect(stringifyNode(body)).toMatchInlineSnapshot(` + "{ + return a; + }" + `); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); }); }); From 43161d228fa689e7831f38781c719c0d1440a666 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 4 Sep 2026 12:08:58 +0200 Subject: [PATCH 7/8] Add missing tseynit tests, fix tseynit --- packages/typegpu/src/shared/tseynit.ts | 8 ++++---- .../typegpu/tests/internal/tseynit.test.ts | 20 +++++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/typegpu/src/shared/tseynit.ts b/packages/typegpu/src/shared/tseynit.ts index c55155d228..b8e2700049 100644 --- a/packages/typegpu/src/shared/tseynit.ts +++ b/packages/typegpu/src/shared/tseynit.ts @@ -36,14 +36,14 @@ function stringifyStatement(node: tinyest.Statement, ident: string): string { if (node[0] === NODE.let) { if (node[2] !== undefined) { - return `${ident}let ${node[1]} = ${stringifyExpression(node[2], ident)};`; + return `${ident}let ${stringifyExpression(node[1], ident)} = ${stringifyExpression(node[2], ident)};`; } return `${ident}let ${node[1]};`; } if (node[0] === NODE.const) { if (node[2] !== undefined) { - return `${ident}const ${node[1]} = ${stringifyExpression(node[2], ident)};`; + return `${ident}const ${stringifyExpression(node[1], ident)} = ${stringifyExpression(node[2], ident)};`; } return `${ident}const ${node[1]};`; } @@ -137,9 +137,9 @@ function stringifyExpression(node: tinyest.Expression, ident: string): string { if (node[0] === NODE.memberAccess) { if (Array.isArray(node[1]) && node[1][0] === NODE.numericLiteral) { - return `(${stringifyExpression(node[1], ident)}).${node[2]}`; + return `(${stringifyExpression(node[1], ident)}).${stringifyExpression(node[2], ident)}`; } - return `${wrapIfComplex(node[1], ident)}.${node[2]}`; + return `${wrapIfComplex(node[1], ident)}.${stringifyExpression(node[2], ident)}`; } if (node[0] === NODE.indexAccess) { diff --git a/packages/typegpu/tests/internal/tseynit.test.ts b/packages/typegpu/tests/internal/tseynit.test.ts index 65536e4518..3dde362bf3 100644 --- a/packages/typegpu/tests/internal/tseynit.test.ts +++ b/packages/typegpu/tests/internal/tseynit.test.ts @@ -369,5 +369,25 @@ describe('ast to JS transformation', () => { expect(stringifyNode(ast)).toMatchInlineSnapshot(`"(true) || (false)"`); }); + + it('handles identifier node', () => { + const NODE = tinyest.NodeTypeCatalog; + const ast: tinyest.Block = [ + NODE.block, + [ + [NODE.let, [NODE.identifier, 'ident1'], [NODE.identifier, 'other1']], + [NODE.const, [NODE.identifier, 'ident2'], [NODE.identifier, 'other2']], + [NODE.memberAccess, [NODE.identifier, 'ident3'], [NODE.identifier, 'other3']], + ], + ]; + + expect(stringifyNode(ast)).toMatchInlineSnapshot(` + "{ + let ident1 = other1; + const ident2 = other2; + (ident3).other3; + }" + `); + }); }); }); From f185e999bbc2337bbc3c51e78f7dd1e568c9d86c Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 4 Sep 2026 12:17:38 +0200 Subject: [PATCH 8/8] Update docs --- packages/tinyest/src/nodes.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/tinyest/src/nodes.ts b/packages/tinyest/src/nodes.ts index fa1a1730b5..de25e80bfe 100644 --- a/packages/tinyest/src/nodes.ts +++ b/packages/tinyest/src/nodes.ts @@ -39,6 +39,7 @@ export const NodeTypeCatalog = { export type NodeTypeCatalog = typeof NodeTypeCatalog; +/** Identifier can either be encoded as a node, or as a plain string */ export type Identifier = string | readonly [type: NodeTypeCatalog['identifier'], id: string]; /**