diff --git a/packages/typegpu/src/core/function/extractArgs.ts b/packages/typegpu/src/core/function/extractArgs.ts index 6e7050fd99..4a1ba3dc14 100644 --- a/packages/typegpu/src/core/function/extractArgs.ts +++ b/packages/typegpu/src/core/function/extractArgs.ts @@ -1,4 +1,4 @@ -import { blankSpaces, lineBreaks } from '../whitespaces.ts'; +import { blankSpaces, lineBreaks } from '../../rawShaderCodeUtils.ts'; interface FunctionArgsInfo { args: ArgInfo[]; diff --git a/packages/typegpu/src/core/function/fnCore.ts b/packages/typegpu/src/core/function/fnCore.ts index 020207d21d..8792063e50 100644 --- a/packages/typegpu/src/core/function/fnCore.ts +++ b/packages/typegpu/src/core/function/fnCore.ts @@ -4,7 +4,8 @@ import { type ResolvedSnippet, snip } from '../../data/snippet.ts'; import { type BaseData, isWgslData, isWgslStruct, Void } from '../../data/wgslTypes.ts'; import { validateIdentifier } from '../../nameUtils.ts'; import { getFunctionMetadata, getName } from '../../shared/meta.ts'; -import { $getNameForward } from '../../shared/symbols.ts'; +import { extractIdentifierLikeTokens, renameIdentifiers } from '../../rawShaderCodeUtils.ts'; +import { $getNameForward, $internal } from '../../shared/symbols.ts'; import type { ResolutionCtx, ShaderStage } from '../../types.ts'; import { type ExternalMap, @@ -14,6 +15,7 @@ import { import { ResolvableString } from '../resolve/resolvableString.ts'; import { extractArgs } from './extractArgs.ts'; import type { Implementation, SeparatedEntryArgs } from './fnTypes.ts'; +import { parentFunctionNameSlot } from '../slot/internalSlots.ts'; export type FnExternals = { /** @@ -119,7 +121,7 @@ export function createFnCore( `Invalid argument name "${arg.schemaKey}"${result.error ? `: ${result.error}` : ''}`, ); } - if (ctx.isIdentifierBanned(arg.schemaKey)) { + if (ctx.gen.isBannedToken(arg.schemaKey)) { throw new Error( `Invalid argument name "${arg.schemaKey}", the identifier is a reserved keyword.`, ); @@ -136,75 +138,119 @@ export function createFnCore( }); } - const replacedImpl = replaceExternalsInWgsl( - ctx, - mergeFunctionExternals(externals), - implementation, - ); + const externalMap = mergeFunctionExternals(externals); + try { + const scope = ctx[$internal].itemStateStack.pushFunctionScope( + functionType, + {}, + returnType, + externalMap, + ); + // Pushing a block scope as well, so that any identifiers declared at this point will be scoped to the function body. + ctx.pushBlockScope(); + + const externalKeys = Object.keys(externalMap); + const identifiers = [ + ...new Set( + extractIdentifierLikeTokens(implementation).filter((ident) => { + return ( + !ctx.gen.isBannedToken(ident) && + !ctx.gen.isBuiltinGlobal(ident) && + !externalKeys.some((key) => key === ident || key.startsWith(`${ident}.`)) + ); + }), + ), + ]; - let header = ''; - let body = ''; + const clashingIdentifiers = identifiers.filter((ident) => + ctx.isIdentifierTaken(ident, 'block'), + ); - if (functionType !== 'normal' && entryInput) { - const { dataSchema, positionalArgs } = entryInput; - const parts: string[] = []; - if (dataSchema && isArgUsedInBody('in', replacedImpl)) { - parts.push(`in: ${ctx.resolve(dataSchema).value}`); - } - for (const a of positionalArgs) { - const argName = a.schemaKey; - if (isArgUsedInBody(argName, replacedImpl)) { - parts.push(`${getAttributesString(a.type)}${argName}: ${ctx.resolve(a.type).value}`); - } + const uniqueIdentifiers = identifiers.filter( + (ident) => !ctx.isIdentifierTaken(ident, 'block'), + ); + + for (const ident of clashingIdentifiers) { + const renamed = ctx.makeUniqueIdentifier(ident, 'block'); + scope.localRenames.set(ident, renamed); } - const input = `(${parts.join(', ')})`; - - const attributes = isWgslData(returnType) ? getAttributesString(returnType) : ''; - const output = - returnType !== Void - ? isWgslStruct(returnType) - ? ` -> ${ctx.resolve(returnType).value} ` - : ` -> ${attributes !== '' ? attributes : '@location(0)'} ${ - ctx.resolve(returnType).value - } ` - : ' '; - - header = `${input}${output}`; - body = replacedImpl; - } else { - const providedArgs = extractArgs(replacedImpl); - - if (providedArgs.args.length !== argTypes.length) { - throw new Error( - `WGSL implementation has ${providedArgs.args.length} arguments, while the shell has ${argTypes.length} arguments.`, - ); + const renamedImpl = renameIdentifiers(implementation, scope.localRenames); + for (const ident of uniqueIdentifiers) { + ctx.reserveIdentifier(ident, 'block'); } + const replacedImpl = ctx.withSlots([[parentFunctionNameSlot, id]], () => + replaceExternalsInWgsl(ctx, externalMap, renamedImpl), + ); + + let header = ''; + let body = ''; + + if (functionType !== 'normal' && entryInput) { + const { dataSchema, positionalArgs } = entryInput; + const parts: string[] = []; + if (dataSchema && isArgUsedInBody('in', replacedImpl)) { + parts.push(`in: ${ctx.resolve(dataSchema).value}`); + } + for (const a of positionalArgs) { + const argName = a.schemaKey; + if (isArgUsedInBody(argName, replacedImpl)) { + parts.push( + `${getAttributesString(a.type)}${argName}: ${ctx.resolve(a.type).value}`, + ); + } + } + const input = `(${parts.join(', ')})`; + + const attributes = isWgslData(returnType) ? getAttributesString(returnType) : ''; + const output = + returnType !== Void + ? isWgslStruct(returnType) + ? ` -> ${ctx.resolve(returnType).value} ` + : ` -> ${attributes !== '' ? attributes : '@location(0)'} ${ + ctx.resolve(returnType).value + } ` + : ' '; + + header = `${input}${output}`; + body = replacedImpl; + } else { + const providedArgs = extractArgs(replacedImpl); + + if (providedArgs.args.length !== argTypes.length) { + throw new Error( + `WGSL implementation has ${providedArgs.args.length} arguments, while the shell has ${argTypes.length} arguments.`, + ); + } - const input = providedArgs.args - .map( - (argInfo, i) => - `${argInfo.identifier}: ${checkAndReturnType( - ctx, - `parameter ${argInfo.identifier}`, - argInfo.type, - argTypes[i], - )}`, - ) - .join(', '); - - const output = - returnType === Void - ? ' ' - : ` -> ${checkAndReturnType(ctx, 'return type', providedArgs.ret?.type, returnType)} `; - - header = `(${input})${output}`; - - body = replacedImpl.slice(providedArgs.range.end); - } + const input = providedArgs.args + .map( + (argInfo, i) => + `${argInfo.identifier}: ${checkAndReturnType( + ctx, + `parameter ${argInfo.identifier}`, + argInfo.type, + argTypes[i], + )}`, + ) + .join(', '); + + const output = + returnType === Void + ? ' ' + : ` -> ${checkAndReturnType(ctx, 'return type', providedArgs.ret?.type, returnType)} `; + + header = `(${input})${output}`; + + body = replacedImpl.slice(providedArgs.range.end); + } - ctx.addDeclaration(`${attributes}fn ${id}${header}${body}`, id); + ctx.addDeclaration(`${attributes}fn ${id}${header}${body}`, id); - return snip(id, returnType, /* origin */ 'runtime'); + return snip(id, returnType, /* origin */ 'runtime'); + } finally { + ctx[$internal].itemStateStack.pop('blockScope'); + ctx[$internal].itemStateStack.pop('functionScope'); + } } // get data generated by the plugin diff --git a/packages/typegpu/src/core/rawCodeSnippet/tgpuRawCodeSnippet.ts b/packages/typegpu/src/core/rawCodeSnippet/tgpuRawCodeSnippet.ts index 30a105e90c..fac415ab69 100644 --- a/packages/typegpu/src/core/rawCodeSnippet/tgpuRawCodeSnippet.ts +++ b/packages/typegpu/src/core/rawCodeSnippet/tgpuRawCodeSnippet.ts @@ -6,6 +6,8 @@ import { makeResolvable } from '../../tgsl/makeResolvable.ts'; import type { InferGPU } from '../../shared/repr.ts'; import { $gpuValueOf, $internal } from '../../shared/symbols.ts'; import { type ExternalMap, replaceExternalsInWgsl } from '../resolve/externals.ts'; +import { renameIdentifiers } from '../../rawShaderCodeUtils.ts'; +import { parentFunctionNameSlot } from '../slot/internalSlots.ts'; // ---------- // Public API @@ -106,11 +108,17 @@ class TgpuRawCodeSnippetImpl implements TgpuRawCodeS return `raw(${String(this.dataType)}): "${this.#expression}"`; }, resolve(ctx) { - const replacedExpression = replaceExternalsInWgsl( - ctx, - this.#externals ?? {}, - this.#expression, - ); + // The code of the snippet can actually change depending on which function it's referenced in. + // For example an identifier can refer to a locally defined variable or argument, or a global, + // and therefore can be renamed or left as is. + void parentFunctionNameSlot.$; + + let expression = this.#expression; + if (ctx.topFunctionScope) { + expression = renameIdentifiers(expression, ctx.topFunctionScope.localRenames); + } + + const replacedExpression = replaceExternalsInWgsl(ctx, this.#externals ?? {}, expression); return snip(replacedExpression, this.dataType, this.origin, this.possibleSideEffects); }, diff --git a/packages/typegpu/src/core/resolve/externals.ts b/packages/typegpu/src/core/resolve/externals.ts index 4d25e11068..beda9ec281 100644 --- a/packages/typegpu/src/core/resolve/externals.ts +++ b/packages/typegpu/src/core/resolve/externals.ts @@ -1,6 +1,7 @@ import { isLooseData } from '../../data/dataTypes.ts'; import { isWgslStruct } from '../../data/wgslTypes.ts'; import { getName, hasTinyestMetadata, isNamable, setName } from '../../shared/meta.ts'; +import { anyIdent } from '../../rawShaderCodeUtils.ts'; import { logger } from '../../tgpuLogger.ts'; import { isWgsl, type ResolutionCtx } from '../../types.ts'; import type { FnExternals } from '../function/fnCore.ts'; @@ -67,7 +68,6 @@ export function addReturnTypeToExternals( } } -export const anyIdent = /([$_\p{XID_Start}][$\p{XID_Continue}]*)/u; // WGSL ident, modified to include $ const anyPropChain = new RegExp(`(${anyIdent.source})(\\.${anyIdent.source})*`, 'ug'); export const boundedPropChain = new RegExp( `(? { + const result = validateProp(ctx, key); + if (!result.success) { + throw new Error(`Invalid property key '${key}'${result.error ? `: ${result.error}` : ''}`); + } + }); + if (struct[$internal].isAbstruct) { throw new Error('Cannot resolve abstract struct types to WGSL.'); } diff --git a/packages/typegpu/src/core/resolve/tgpuResolve.ts b/packages/typegpu/src/core/resolve/tgpuResolve.ts index ea11f04dc1..52fa9838fb 100644 --- a/packages/typegpu/src/core/resolve/tgpuResolve.ts +++ b/packages/typegpu/src/core/resolve/tgpuResolve.ts @@ -1,6 +1,7 @@ import { type ResolvedSnippet, snip } from '../../data/snippet.ts'; import { Void } from '../../data/wgslTypes.ts'; import { getName } from '../../internal.ts'; +import { extractIdentifierLikeTokens, renameIdentifiers } from '../../rawShaderCodeUtils.ts'; import { type ResolutionResult, resolve as resolveImpl } from '../../resolutionCtx.ts'; import { $internal, $resolve, $soul } from '../../shared/symbols.ts'; import { isBindGroupLayout } from '../../tgpuBindGroupLayout.ts'; @@ -211,11 +212,55 @@ function resolveFromTemplate(options: TgpuExtendedResolveOptions): ResolutionRes const resolutionObj: SelfResolvable = { [$internal]: true, [$resolve](ctx): ResolvedSnippet { - return snip( - replaceExternalsInWgsl(ctx, externals, template ?? ''), - Void, - /* origin */ 'runtime', - ); + try { + // It's technically not a function we're resolving, but we need a place to store local renames, so we treat the whole + const scope = ctx[$internal].itemStateStack.pushFunctionScope( + 'normal', + {}, + Void, + externals, + ); + // Pushing a block scope as well, so that any identifiers declared at this point will be scoped to the function body. + ctx.pushBlockScope(); + + const identifiers = [ + ...new Set( + extractIdentifierLikeTokens(template ?? '').filter((ident) => { + return ( + !ctx.gen.isBannedToken(ident) && + !ctx.gen.isBuiltinGlobal(ident) && + externals[ident] === undefined + ); + }), + ), + ]; + + const clashingIdentifiers = identifiers.filter((ident) => + ctx.isIdentifierTaken(ident, 'global'), + ); + + const uniqueIdentifiers = identifiers.filter( + (ident) => !ctx.isIdentifierTaken(ident, 'global'), + ); + + for (const ident of clashingIdentifiers) { + const renamed = ctx.makeUniqueIdentifier(ident, 'global'); + scope.localRenames.set(ident, renamed); + } + const renamedImpl = renameIdentifiers(template ?? '', scope.localRenames); + for (const ident of uniqueIdentifiers) { + ctx.reserveIdentifier(ident, 'global'); + } + + return snip( + replaceExternalsInWgsl(ctx, externals, renamedImpl), + Void, + /* origin */ 'runtime', + ); + } finally { + ctx[$internal].itemStateStack.pop('blockScope'); + ctx[$internal].itemStateStack.pop('functionScope'); + } }, toString: () => '', }; diff --git a/packages/typegpu/src/core/slot/internalSlots.ts b/packages/typegpu/src/core/slot/internalSlots.ts index 0057d1036d..a3e13df950 100644 --- a/packages/typegpu/src/core/slot/internalSlots.ts +++ b/packages/typegpu/src/core/slot/internalSlots.ts @@ -2,3 +2,4 @@ import type { ShaderStage } from '../../types.ts'; import { slot } from './slot.ts'; export const shaderStageSlot = slot(null); +export const parentFunctionNameSlot = slot(null); diff --git a/packages/typegpu/src/core/whitespaces.ts b/packages/typegpu/src/core/whitespaces.ts deleted file mode 100644 index d37f18fde0..0000000000 --- a/packages/typegpu/src/core/whitespaces.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const lineBreaks = new Set([ - '\u000A', // line feed - '\u000B', // vertical tab - '\u000C', // form feed - '\u000D', // carriage return - '\u0085', // next line - '\u2028', // line separator - '\u2029', // paragraph separator -]); - -export const blankSpaces = new Set([ - ...lineBreaks, - '\u0020', // space - '\u0009', // horizontal tab - '\u200E', // left-to-right mark - '\u200F', // right-to-left mark -]); diff --git a/packages/typegpu/src/data/autoStruct.ts b/packages/typegpu/src/data/autoStruct.ts index 4fc745cccb..fce1860e25 100644 --- a/packages/typegpu/src/data/autoStruct.ts +++ b/packages/typegpu/src/data/autoStruct.ts @@ -1,5 +1,4 @@ import { createIoSchema } from '../core/function/ioSchema.ts'; -import { validateProp } from '../nameUtils.ts'; import { getName, setName } from '../shared/meta.ts'; import { $internal, $repr, $resolve } from '../shared/symbols.ts'; import type { ResolutionCtx, SelfResolvable } from '../types.ts'; @@ -75,10 +74,6 @@ export class AutoStruct implements BaseData, SelfResolvable { `Property name '${wgslKey}' causes naming clashes. Choose a different name.`, ); } - const result = validateProp(wgslKey); - if (!result.success) { - throw new Error(`Invalid property key '${key}'${result.error ? `: ${result.error}` : ''}`); - } this.#usedWgslKeys.add(wgslKey); alloc = { prop: wgslKey, type: dataType }; diff --git a/packages/typegpu/src/data/struct.ts b/packages/typegpu/src/data/struct.ts index 24b400f2d4..3cebb5c1ab 100644 --- a/packages/typegpu/src/data/struct.ts +++ b/packages/typegpu/src/data/struct.ts @@ -1,4 +1,3 @@ -import { validateProp } from '../nameUtils.ts'; import { getName, setName } from '../shared/meta.ts'; import { $internal } from '../shared/symbols.ts'; import { schemaCallWrapper } from './schemaCallWrapper.ts'; @@ -39,13 +38,6 @@ export function INTERNAL_createStruct>( props: TProps, isAbstruct: boolean, ): WgslStruct { - Object.keys(props).forEach((key) => { - const result = validateProp(key); - if (!result.success) { - throw new Error(`Invalid property key '${key}'${result.error ? `: ${result.error}` : ''}`); - } - }); - // In the schema call, create and return a deep copy // by wrapping all the values in corresponding schema calls. const structSchema = (instanceProps?: TProps) => diff --git a/packages/typegpu/src/minify.ts b/packages/typegpu/src/minify.ts index 50eda3c2d0..f428fa074e 100644 --- a/packages/typegpu/src/minify.ts +++ b/packages/typegpu/src/minify.ts @@ -1,4 +1,4 @@ -import { blankSpaces, lineBreaks } from './core/whitespaces.ts'; +import { blankSpaces, stripWGSLComments } from './rawShaderCodeUtils.ts'; import { invariant } from './errors.ts'; /** @@ -60,61 +60,3 @@ function isSpaceRequired(current: string, next: string | undefined) { } return false; } - -// Based on tint implementation. -function stripWGSLComments(code: string): string { - let result = ''; - let copiedUpTo = 0; - let offset = 0; - - while (offset < code.length) { - if (code.startsWith('//', offset)) { - result += `${code.slice(copiedUpTo, offset)} `; - offset += 2; - - while (offset < code.length && !lineBreaks.has(code.charAt(offset))) { - offset += 1; - } - - copiedUpTo = offset; - continue; - } - - if (code.startsWith('/*', offset)) { - result += `${code.slice(copiedUpTo, offset)} `; - let depth = 1; - offset += 2; - - while (offset < code.length && depth > 0) { - if (code.startsWith('/*', offset)) { - depth += 1; - offset += 2; - } else if (code.startsWith('*/', offset)) { - depth -= 1; - offset += 2; - } else { - offset += 1; - } - } - - if (depth > 0) { - throw new SyntaxError( - `Block comment opening without corresponding closing found during minification.`, - ); - } - - copiedUpTo = offset; - continue; - } - - if (code.startsWith('*/', offset)) { - throw new SyntaxError( - `Block comment closing without corresponding opening found during minification.`, - ); - } - - offset += 1; - } - - return result + code.slice(copiedUpTo); -} diff --git a/packages/typegpu/src/nameUtils.ts b/packages/typegpu/src/nameUtils.ts index cd2c82846b..80bc3579f6 100644 --- a/packages/typegpu/src/nameUtils.ts +++ b/packages/typegpu/src/nameUtils.ts @@ -1,363 +1,4 @@ -export const bannedTokens = new Set([ - // keywords - 'alias', - 'break', - 'case', - 'const', - 'const_assert', - 'continue', - 'continuing', - 'default', - 'diagnostic', - 'discard', - 'else', - 'enable', - 'false', - 'fn', - 'for', - 'if', - 'let', - 'loop', - 'override', - 'requires', - 'return', - 'struct', - 'switch', - 'true', - 'var', - 'while', - // reserved words - 'NULL', - 'Self', - 'abstract', - 'active', - 'alignas', - 'alignof', - 'as', - 'asm', - 'asm_fragment', - 'async', - 'attribute', - 'auto', - 'await', - 'become', - 'cast', - 'catch', - 'class', - 'co_await', - 'co_return', - 'co_yield', - 'coherent', - 'column_major', - 'common', - 'compile', - 'compile_fragment', - 'concept', - 'const_cast', - 'consteval', - 'constexpr', - 'constinit', - 'crate', - 'debugger', - 'decltype', - 'delete', - 'demote', - 'demote_to_helper', - 'do', - 'dynamic_cast', - 'enum', - 'explicit', - 'export', - 'extends', - 'extern', - 'external', - 'fallthrough', - 'filter', - 'final', - 'finally', - 'friend', - 'from', - 'fxgroup', - 'get', - 'goto', - 'groupshared', - 'highp', - 'impl', - 'implements', - 'import', - 'inline', - 'instanceof', - 'interface', - 'layout', - 'lowp', - 'macro', - 'macro_rules', - 'match', - 'mediump', - 'meta', - 'mod', - 'module', - 'move', - 'mut', - 'mutable', - 'namespace', - 'new', - 'nil', - 'noexcept', - 'noinline', - 'nointerpolation', - 'non_coherent', - 'noncoherent', - 'noperspective', - 'null', - 'nullptr', - 'of', - 'operator', - 'package', - 'packoffset', - 'partition', - 'pass', - 'patch', - 'pixelfragment', - 'precise', - 'precision', - 'premerge', - 'priv', - 'protected', - 'pub', - 'public', - 'readonly', - 'ref', - 'regardless', - 'register', - 'reinterpret_cast', - 'require', - 'resource', - 'restrict', - 'self', - 'set', - 'shared', - 'sizeof', - 'smooth', - 'snorm', - 'static', - 'static_assert', - 'static_cast', - 'std', - 'subroutine', - 'super', - 'target', - 'template', - 'this', - 'thread_local', - 'throw', - 'trait', - 'try', - 'type', - 'typedef', - 'typeid', - 'typename', - 'typeof', - 'union', - 'unless', - 'unorm', - 'unsafe', - 'unsized', - 'use', - 'using', - 'varying', - 'virtual', - 'volatile', - 'wgsl', - 'where', - 'with', - 'writeonly', - 'yield', - // Keywords that should be reserved - 'sampler', - 'uniform', - 'storage', -]); - -export const builtins = new Set([ - // constructors - 'array', - 'bool', - 'f16', - 'f32', - 'i32', - 'u32', - 'mat2x2', - 'mat2x3', - 'mat2x4', - 'mat3x2', - 'mat3x3', - 'mat3x4', - 'mat4x2', - 'mat4x3', - 'mat4x4', - 'vec2', - 'vec3', - 'vec4', - // bitcast - 'bitcast', - // logical - 'all', - 'any', - 'select', - // array - 'arrayLength', - // numeric - 'abs', - 'acos', - 'acosh', - 'asin', - 'asinh', - 'atan', - 'atanh', - 'atan2', - 'ceil', - 'clamp', - 'cos', - 'cosh', - 'countLeadingZeros', - 'countOneBits', - 'countTrailingZeros', - 'cross', - 'degrees', - 'determinant', - 'distance', - 'dot', - 'dot4U8Packed', - 'dot4I8Packed', - 'exp', - 'exp2', - 'extractBits', - 'faceForward', - 'firstLeadingBit', - 'firstTrailingBit', - 'floor', - 'fma', - 'fract', - 'frexp', - 'insertBits', - 'inverseSqrt', - 'ldexp', - 'length', - 'log', - 'log2', - 'max', - 'min', - 'mix', - 'modf', - 'normalize', - 'pow', - 'quantizeToF16', - 'radians', - 'reflect', - 'refract', - 'reverseBits', - 'round', - 'saturate', - 'sign', - 'sin', - 'sinh', - 'smoothstep', - 'sqrt', - 'step', - 'tan', - 'tanh', - 'transpose', - 'trunc', - // derivative - 'dpdx', - 'dpdxCoarse', - 'dpdxFine', - 'dpdy', - 'dpdyCoarse', - 'dpdyFine', - 'fwidth', - 'fwidthCoarse', - 'fwidthFine', - // texture - 'textureDimensions', - 'textureGather', - 'textureGatherCompare', - 'textureLoad', - 'textureNumLayers', - 'textureNumLevels', - 'textureNumSamples', - 'textureSample', - 'textureSampleBias', - 'textureSampleCompare', - 'textureSampleCompareLevel', - 'textureSampleGrad', - 'textureSampleLevel', - 'textureSampleBaseClampToEdge', - 'textureStore', - // atomic - 'atomicLoad', - 'atomicStore', - 'atomicAdd', - 'atomicSub', - 'atomicMax', - 'atomicMin', - 'atomicAnd', - 'atomicOr', - 'atomicXor', - 'atomicExchange', - 'atomicCompareExchangeWeak', - // data packing - 'pack4x8snorm', - 'pack4x8unorm', - 'pack4xI8', - 'pack4xU8', - 'pack4xI8Clamp', - 'pack4xU8Clamp', - 'pack2x16snorm', - 'pack2x16unorm', - 'pack2x16float', - // data unpacking - 'unpack4x8snorm', - 'unpack4x8unorm', - 'unpack4xI8', - 'unpack4xU8', - 'unpack2x16snorm', - 'unpack2x16unorm', - 'unpack2x16float', - // synchronization - 'storageBarrier', - 'textureBarrier', - 'workgroupBarrier', - 'workgroupUniformLoad', - // subgroup - 'subgroupAdd', - 'subgroupExclusiveAdd', - 'subgroupInclusiveAdd', - 'subgroupAll', - 'subgroupAnd', - 'subgroupAny', - 'subgroupBallot', - 'subgroupBroadcast', - 'subgroupBroadcastFirst', - 'subgroupElect', - 'subgroupMax', - 'subgroupMin', - 'subgroupMul', - 'subgroupExclusiveMul', - 'subgroupInclusiveMul', - 'subgroupOr', - 'subgroupShuffle', - 'subgroupShuffleDown', - 'subgroupShuffleUp', - 'subgroupShuffleXor', - 'subgroupXor', - // quad operations - 'quadBroadcast', - 'quadSwapDiagonal', - 'quadSwapX', - 'quadSwapY', -]); +import type { ResolutionCtx } from './types.ts'; /** * Sanitizes the primer so that it is compliant with WGSL guidelines. @@ -433,13 +74,13 @@ export function validateIdentifier(ident: string): ValidationResult { * Same as `validateIdentifier`, except also checks for bannedToken clashes. */ /*#__NO_SIDE_EFFECTS__*/ -export function validateProp(ident: string): ValidationResult { +export function validateProp(ctx: ResolutionCtx, ident: string): ValidationResult { const identResult = validateIdentifier(ident); if (!identResult.success) { return identResult; } - if (bannedTokens.has(ident)) { + if (ctx.gen.isBannedToken(ident)) { return { success: false, error: `Identifiers cannot start with reserved keywords.`, diff --git a/packages/typegpu/src/rawShaderCodeUtils.ts b/packages/typegpu/src/rawShaderCodeUtils.ts new file mode 100644 index 0000000000..fc1e837e78 --- /dev/null +++ b/packages/typegpu/src/rawShaderCodeUtils.ts @@ -0,0 +1,197 @@ +export const lineBreaks = new Set([ + '\u000A', // line feed + '\u000B', // vertical tab + '\u000C', // form feed + '\u000D', // carriage return + '\u0085', // next line + '\u2028', // line separator + '\u2029', // paragraph separator +]); + +export const blankSpaces = new Set([ + ...lineBreaks, + '\u0020', // space + '\u0009', // horizontal tab + '\u200E', // left-to-right mark + '\u200F', // right-to-left mark +]); + +export const anyIdent = /([$_\p{XID_Start}][$\p{XID_Continue}]*)/u; // WGSL ident, modified to include $ + +// Based on tint implementation. +export function stripWGSLComments(code: string): string { + let result = ''; + let copiedUpTo = 0; + let offset = 0; + + while (offset < code.length) { + if (code.startsWith('//', offset)) { + result += `${code.slice(copiedUpTo, offset)} `; + offset += 2; + + while (offset < code.length && !lineBreaks.has(code.charAt(offset))) { + offset += 1; + } + + copiedUpTo = offset; + continue; + } + + if (code.startsWith('/*', offset)) { + result += `${code.slice(copiedUpTo, offset)} `; + let depth = 1; + offset += 2; + + while (offset < code.length && depth > 0) { + if (code.startsWith('/*', offset)) { + depth += 1; + offset += 2; + } else if (code.startsWith('*/', offset)) { + depth -= 1; + offset += 2; + } else { + offset += 1; + } + } + + if (depth > 0) { + throw new SyntaxError(`Found block comment opening without corresponding closing.`); + } + + copiedUpTo = offset; + continue; + } + + if (code.startsWith('*/', offset)) { + throw new SyntaxError(`Found block comment closing without corresponding opening.`); + } + + offset += 1; + } + + return result + code.slice(copiedUpTo); +} + +function swapChars(source: string, offset: number, replacement: string) { + return source.slice(0, offset) + replacement + source.slice(offset + replacement.length); +} + +/** + * Same as `stripWGSLComments`, but keeps all non-comment code at + * the same location where it originally was, and replaces the comments + * with whitespace. + */ +export function blankOutWGSLComments(code: string): string { + let result = code; + let offset = 0; + + while (offset < code.length) { + if (code.startsWith('//', offset)) { + result = swapChars(result, offset, ' '); + offset += 2; + + while (offset < code.length && !lineBreaks.has(code.charAt(offset))) { + result = swapChars(result, offset, ' '); + offset += 1; + } + + continue; + } + + if (code.startsWith('/*', offset)) { + let depth = 1; + result = swapChars(result, offset, ' '); + offset += 2; + + while (offset < code.length && depth > 0) { + if (code.startsWith('/*', offset)) { + depth += 1; + result = swapChars(result, offset, ' '); + offset += 2; + } else if (code.startsWith('*/', offset)) { + depth -= 1; + result = swapChars(result, offset, ' '); + offset += 2; + } else { + result = swapChars(result, offset, ' '); + offset += 1; + } + } + + if (depth > 0) { + throw new SyntaxError(`Found block comment opening without corresponding closing.`); + } + + continue; + } + + if (code.startsWith('*/', offset)) { + throw new SyntaxError(`Found block comment closing without corresponding opening.`); + } + + offset += 1; + } + + return result; +} + +export function extractIdentifierLikeTokens(source: string): string[] { + // Adding a space at the beginning of `source` so all potential identifiers + // have a preceeding character (see the regex for more context). + const noCommentsSource = stripWGSLComments(' ' + source); + // Capturing the preceeding character (irrespective of whitespace) to make sure that it's not a + // chained member access, nor a typed numeric literal (e.g. 1f) + const expr = new RegExp(`[^\\d]\\s*${anyIdent.source}`, 'ug'); + + const identifiers: string[] = []; + + let result: RegExpExecArray | null; + while ((result = expr.exec(noCommentsSource)) !== null) { + if (result[0][0] === '.') { + // Skipping member accesses. + continue; + } + + if (result[1]) { + identifiers.push(result[1]); + } + } + + return identifiers; +} + +export function renameIdentifiers(_source: string, renames: Map) { + // Adding a space at the beginning of `source` so all potential identifiers + // have a preceeding character (see the regex for more context). + const source = ' ' + _source; + const noCommentsSource = blankOutWGSLComments(source); + // Capturing the preceeding character (irrespective of whitespace) to make sure that it's not a + // chained member access, nor a typed numeric literal (e.g. 1f) + const expr = new RegExp(`[^\\d]\\s*${anyIdent.source}`, 'ug'); + + let replaced = ''; + let copiedUpTo = 0; + + let result: RegExpExecArray | null; + while ((result = expr.exec(noCommentsSource)) !== null) { + if (result[0][0] === '.') { + // Skipping member accesses. + continue; + } + + const identifier = result[1]?.trim(); + if (!identifier) { + continue; + } + + const end = result.index + result[0].length; + // counting back from the end to keep any extra whitespace that was there + const start = end - identifier.length; + replaced += source.slice(copiedUpTo, start); + replaced += renames.get(identifier) ?? identifier; + copiedUpTo = end; + } + + // Removing the first space we added at the beginning + return (replaced + source.slice(copiedUpTo)).slice(1); +} diff --git a/packages/typegpu/src/resolutionCtx.ts b/packages/typegpu/src/resolutionCtx.ts index b05bd26529..08275d58ab 100644 --- a/packages/typegpu/src/resolutionCtx.ts +++ b/packages/typegpu/src/resolutionCtx.ts @@ -65,8 +65,9 @@ import { isTgpuFn } from './core/function/tgpuFn.ts'; import type { IOData } from './core/function/fnTypes.ts'; import { AutoStruct } from './data/autoStruct.ts'; import { EntryInputRouter } from './core/function/entryInputRouter.ts'; -import { validateIdentifier, sanitizePrimer, bannedTokens } from './nameUtils.ts'; +import { validateIdentifier, sanitizePrimer } from './nameUtils.ts'; import { minify } from './minify.ts'; +import { parentFunctionNameSlot } from './core/slot/internalSlots.ts'; /** * Inserted into bind group entry definitions that belong @@ -154,6 +155,7 @@ class ItemStateStackImpl implements ItemStateStack { argAccess, returnType, externalMap, + localRenames: new Map(), reportedReturnTypes: new Set(), placeholderForVariable: new Map(), modifiedVariables: new Set(), @@ -467,13 +469,10 @@ export class ResolutionCtxImpl implements ResolutionCtx { this.gen.initGenerator(this); } - isIdentifierBanned(name: string): boolean { - return bannedTokens.has(name); - } - isIdentifierTaken(name: string, scope: 'global' | 'block'): boolean { return ( this.#namespaceInternal.takenGlobalIdentifiers.has(name) || + this.gen.isBannedToken(name) || (scope === 'block' ? this._itemStateStack.isIdentifierTakenLocally(name) : this._itemStateStack.isIdentifierTakenInCallStack(name)) @@ -718,58 +717,60 @@ export class ResolutionCtxImpl implements ResolutionCtx { let returnType: BaseData | undefined; - const code = this.gen.functionDefinition({ - functionType: options.functionType, - name: options.name, - workgroupSize: options.workgroupSize, - args, - body: options.body, - determineReturnType: () => { - if (returnType) { - // Already determined - return returnType; - } - - returnType = options.returnType; - if (returnType instanceof AutoStruct) { - // We're expecting an "auto" return type, so if there were structs returned, - // we accept the struct, otherwise we let the rest of the code unify on a - // primitive type. - if (isWgslStruct(scope.reportedReturnTypes.values().next().value)) { - returnType = returnType.completeStruct; - } else { - returnType = undefined; + const code = this.withSlots([[parentFunctionNameSlot, options.name]], () => + this.gen.functionDefinition({ + functionType: options.functionType, + name: options.name, + workgroupSize: options.workgroupSize, + args, + body: options.body, + determineReturnType: () => { + if (returnType) { + // Already determined + return returnType; } - } - if (!returnType) { - const returnTypes = [...scope.reportedReturnTypes]; - if (returnTypes.length === 0) { - returnType = Void; - } else { - const conversion = getBestConversion(returnTypes); - if (conversion && !conversion.hasImplicitConversions) { - returnType = conversion.targetType; + returnType = options.returnType; + if (returnType instanceof AutoStruct) { + // We're expecting an "auto" return type, so if there were structs returned, + // we accept the struct, otherwise we let the rest of the code unify on a + // primitive type. + if (isWgslStruct(scope.reportedReturnTypes.values().next().value)) { + returnType = returnType.completeStruct; + } else { + returnType = undefined; } } if (!returnType) { - throw new Error( - `Expected function to have a single return type, got [${returnTypes.join( - ', ', - )}]. Cast explicitly to the desired type.`, - ); - } + const returnTypes = [...scope.reportedReturnTypes]; + if (returnTypes.length === 0) { + returnType = Void; + } else { + const conversion = getBestConversion(returnTypes); + if (conversion && !conversion.hasImplicitConversions) { + returnType = conversion.targetType; + } + } + + if (!returnType) { + throw new Error( + `Expected function to have a single return type, got [${returnTypes.join( + ', ', + )}]. Cast explicitly to the desired type.`, + ); + } - returnType = concretize(returnType); + returnType = concretize(returnType); - if (options.functionType === 'vertex' || options.functionType === 'fragment') { - returnType = createIoSchema(returnType as IOData); + if (options.functionType === 'vertex' || options.functionType === 'fragment') { + returnType = createIoSchema(returnType as IOData); + } } - } - return returnType; - }, - }); + return returnType; + }, + }), + ); if (!returnType) { throw new Error(`Failed to determine return type`); diff --git a/packages/typegpu/src/tgsl/shaderGenerator.ts b/packages/typegpu/src/tgsl/shaderGenerator.ts index 110c174dc1..333f5ccfe2 100644 --- a/packages/typegpu/src/tgsl/shaderGenerator.ts +++ b/packages/typegpu/src/tgsl/shaderGenerator.ts @@ -116,6 +116,9 @@ export interface ShaderGenerator { initGenerator(ctx: ResolutionCtx): void; + isBannedToken(token: string): boolean; + isBuiltinGlobal(identifier: string): boolean; + declareGlobalConst(options: ConstantDefinitionOptions): ResolvedSnippet; declareGlobalVar(options: VariableDefinitionOptions): ResolvedSnippet; functionDefinition(options: FunctionDefinitionOptions): string; diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index 242a4a7c5e..850f3b7c07 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -218,6 +218,379 @@ const usageToVarTemplateMap: Record */ const functionInitialBlockDepth = 2; +const builtinGlobals = new Set([ + // constructors + 'array', + 'bool', + 'f16', + 'f32', + 'i32', + 'u32', + 'mat2x2', + 'mat2x3', + 'mat2x4', + 'mat3x2', + 'mat3x3', + 'mat3x4', + 'mat4x2', + 'mat4x3', + 'mat4x4', + 'vec2', + 'vec3', + 'vec4', + 'mat2x2f', + 'mat2x3f', + 'mat2x4f', + 'mat3x2f', + 'mat3x3f', + 'mat3x4f', + 'mat4x2f', + 'mat4x3f', + 'mat4x4f', + 'vec2f', + 'vec3f', + 'vec4f', + // bitcast + 'bitcast', + // logical + 'all', + 'any', + 'select', + // array + 'arrayLength', + // numeric + 'abs', + 'acos', + 'acosh', + 'asin', + 'asinh', + 'atan', + 'atanh', + 'atan2', + 'ceil', + 'clamp', + 'cos', + 'cosh', + 'countLeadingZeros', + 'countOneBits', + 'countTrailingZeros', + 'cross', + 'degrees', + 'determinant', + 'distance', + 'dot', + 'dot4U8Packed', + 'dot4I8Packed', + 'exp', + 'exp2', + 'extractBits', + 'faceForward', + 'firstLeadingBit', + 'firstTrailingBit', + 'floor', + 'fma', + 'fract', + 'frexp', + 'insertBits', + 'inverseSqrt', + 'ldexp', + 'length', + 'log', + 'log2', + 'max', + 'min', + 'mix', + 'modf', + 'normalize', + 'pow', + 'quantizeToF16', + 'radians', + 'reflect', + 'refract', + 'reverseBits', + 'round', + 'saturate', + 'sign', + 'sin', + 'sinh', + 'smoothstep', + 'sqrt', + 'step', + 'tan', + 'tanh', + 'transpose', + 'trunc', + // derivative + 'dpdx', + 'dpdxCoarse', + 'dpdxFine', + 'dpdy', + 'dpdyCoarse', + 'dpdyFine', + 'fwidth', + 'fwidthCoarse', + 'fwidthFine', + // texture + 'textureDimensions', + 'textureGather', + 'textureGatherCompare', + 'textureLoad', + 'textureNumLayers', + 'textureNumLevels', + 'textureNumSamples', + 'textureSample', + 'textureSampleBias', + 'textureSampleCompare', + 'textureSampleCompareLevel', + 'textureSampleGrad', + 'textureSampleLevel', + 'textureSampleBaseClampToEdge', + 'textureStore', + // atomic + 'atomicLoad', + 'atomicStore', + 'atomicAdd', + 'atomicSub', + 'atomicMax', + 'atomicMin', + 'atomicAnd', + 'atomicOr', + 'atomicXor', + 'atomicExchange', + 'atomicCompareExchangeWeak', + // data packing + 'pack4x8snorm', + 'pack4x8unorm', + 'pack4xI8', + 'pack4xU8', + 'pack4xI8Clamp', + 'pack4xU8Clamp', + 'pack2x16snorm', + 'pack2x16unorm', + 'pack2x16float', + // data unpacking + 'unpack4x8snorm', + 'unpack4x8unorm', + 'unpack4xI8', + 'unpack4xU8', + 'unpack2x16snorm', + 'unpack2x16unorm', + 'unpack2x16float', + // synchronization + 'storageBarrier', + 'textureBarrier', + 'workgroupBarrier', + 'workgroupUniformLoad', + // subgroup + 'subgroupAdd', + 'subgroupExclusiveAdd', + 'subgroupInclusiveAdd', + 'subgroupAll', + 'subgroupAnd', + 'subgroupAny', + 'subgroupBallot', + 'subgroupBroadcast', + 'subgroupBroadcastFirst', + 'subgroupElect', + 'subgroupMax', + 'subgroupMin', + 'subgroupMul', + 'subgroupExclusiveMul', + 'subgroupInclusiveMul', + 'subgroupOr', + 'subgroupShuffle', + 'subgroupShuffleDown', + 'subgroupShuffleUp', + 'subgroupShuffleXor', + 'subgroupXor', + // quad operations + 'quadBroadcast', + 'quadSwapDiagonal', + 'quadSwapX', + 'quadSwapY', +]); + +const bannedTokens = new Set([ + // keywords + 'alias', + 'break', + 'case', + 'const', + 'const_assert', + 'continue', + 'continuing', + 'default', + 'diagnostic', + 'discard', + 'else', + 'enable', + 'false', + 'fn', + 'for', + 'if', + 'let', + 'loop', + 'override', + 'requires', + 'return', + 'struct', + 'switch', + 'true', + 'var', + 'while', + // reserved words + 'NULL', + 'Self', + 'abstract', + 'active', + 'alignas', + 'alignof', + 'as', + 'asm', + 'asm_fragment', + 'async', + 'attribute', + 'auto', + 'await', + 'become', + 'cast', + 'catch', + 'class', + 'co_await', + 'co_return', + 'co_yield', + 'coherent', + 'column_major', + 'common', + 'compile', + 'compile_fragment', + 'concept', + 'const_cast', + 'consteval', + 'constexpr', + 'constinit', + 'crate', + 'debugger', + 'decltype', + 'delete', + 'demote', + 'demote_to_helper', + 'do', + 'dynamic_cast', + 'enum', + 'explicit', + 'export', + 'extends', + 'extern', + 'external', + 'fallthrough', + 'filter', + 'final', + 'finally', + 'friend', + 'from', + 'fxgroup', + 'get', + 'goto', + 'groupshared', + 'highp', + 'impl', + 'implements', + 'import', + 'inline', + 'instanceof', + 'interface', + 'layout', + 'lowp', + 'macro', + 'macro_rules', + 'match', + 'mediump', + 'meta', + 'mod', + 'module', + 'move', + 'mut', + 'mutable', + 'namespace', + 'new', + 'nil', + 'noexcept', + 'noinline', + 'nointerpolation', + 'non_coherent', + 'noncoherent', + 'noperspective', + 'null', + 'nullptr', + 'of', + 'operator', + 'package', + 'packoffset', + 'partition', + 'pass', + 'patch', + 'pixelfragment', + 'precise', + 'precision', + 'premerge', + 'priv', + 'protected', + 'pub', + 'public', + 'readonly', + 'ref', + 'regardless', + 'register', + 'reinterpret_cast', + 'require', + 'resource', + 'restrict', + 'self', + 'set', + 'shared', + 'sizeof', + 'smooth', + 'snorm', + 'static', + 'static_assert', + 'static_cast', + 'std', + 'subroutine', + 'super', + 'target', + 'template', + 'this', + 'thread_local', + 'throw', + 'trait', + 'try', + 'type', + 'typedef', + 'typeid', + 'typename', + 'typeof', + 'union', + 'unless', + 'unorm', + 'unsafe', + 'unsized', + 'use', + 'using', + 'varying', + 'virtual', + 'volatile', + 'wgsl', + 'where', + 'with', + 'writeonly', + 'yield', + // Keywords that should be reserved + 'sampler', + 'uniform', + 'storage', +]); + export class WgslGenerator implements ShaderGenerator { #ctx: ResolutionCtx | undefined = undefined; // used to detect `continue` and `break` nodes in loop body, as well as label @@ -237,7 +610,18 @@ export class WgslGenerator implements ShaderGenerator { `Cannot initialize shader generators twice. Create one generator per resolution.`, ); } + this.#ctx = ctx; + this._reserveGlobals(); + } + + /** + * Reserves all builtin WGSL globals. Can be overriden to change behavior. + */ + protected _reserveGlobals() { + for (const ident of builtinGlobals) { + this.ctx.reserveIdentifier(ident, 'global'); + } } protected get ctx(): ResolutionCtx { @@ -249,6 +633,14 @@ export class WgslGenerator implements ShaderGenerator { return this.#ctx; } + public isBannedToken(token: string): boolean { + return bannedTokens.has(token); + } + + public isBuiltinGlobal(identifier: string): boolean { + return builtinGlobals.has(identifier); + } + protected _block( [_, statementNodes]: tinyest.Block, allowInlining: boolean, diff --git a/packages/typegpu/src/types.ts b/packages/typegpu/src/types.ts index 62b711d5c3..b487135dfa 100644 --- a/packages/typegpu/src/types.ts +++ b/packages/typegpu/src/types.ts @@ -115,6 +115,7 @@ export type FunctionScopeLayer = { functionType: 'normal' | 'compute' | 'vertex' | 'fragment'; argAccess: Record; externalMap: Record; + localRenames: Map; /** * The return type of the function. If undefined, the type should be inferred * from the implementation (relevant for shellless functions). @@ -374,8 +375,6 @@ export interface ResolutionCtx { */ makeUniqueIdentifier(primer: string | undefined, scope: 'global' | 'block'): string; - isIdentifierBanned(name: string): boolean; - /** * @param name The name to check. * @param scope The scope in which we want to place the identifier. diff --git a/packages/typegpu/tests/internal/blankOutWGSLComments.test.ts b/packages/typegpu/tests/internal/blankOutWGSLComments.test.ts new file mode 100644 index 0000000000..7491b5509c --- /dev/null +++ b/packages/typegpu/tests/internal/blankOutWGSLComments.test.ts @@ -0,0 +1,38 @@ +import { expect } from 'vitest'; +import { test } from 'typegpu-testing-utility'; +import { blankOutWGSLComments } from '../../src/rawShaderCodeUtils.ts'; + +test('blankOutWGSLComments', () => { + const examples = [ + `hello/*a comment*/world`, + ` + // line comment + hello/*a comment /*nested */ */world`, + ` +/* docs */ +fn() { + const hello = 1 /* yup */; +}`, + ]; + + const blanks = []; + for (const example of examples) { + const blank = blankOutWGSLComments(example); + expect(blank.length).toEqual(example.length); + blanks.push(blank); + } + + expect(blanks).toMatchInlineSnapshot(` + [ + "hello world", + " + + hello world", + " + + fn() { + const hello = 1 ; + }", + ] + `); +}); diff --git a/packages/typegpu/tests/internal/externals.test.ts b/packages/typegpu/tests/internal/externals.test.ts index d2d3600d5e..76c9826d80 100644 --- a/packages/typegpu/tests/internal/externals.test.ts +++ b/packages/typegpu/tests/internal/externals.test.ts @@ -1,11 +1,11 @@ import { describe, expect, it } from 'vitest'; import { addArgTypesToExternals, - anyIdent, boundedPropChain, type ExternalMap, } from '../../src/core/resolve/externals.ts'; import { tgpu, d } from 'typegpu'; +import { anyIdent } from '../../src/rawShaderCodeUtils.ts'; describe('addArgTypesToExternals', () => { const Particle = d.struct({ @@ -56,12 +56,12 @@ describe('addArgTypesToExternals', () => { const externals: ExternalMap[] = []; addArgTypesToExternals( ` WorkGroupID : vec3u - , - a : A , + , + a : A , (@builtin(workgroup_id) b - - : B, - + + : B, + c: C ) -> vec4f {}`, [d.vec3u, Particle, Particle, Particle], diff --git a/packages/typegpu/tests/internal/extractIdentifiers.test.ts b/packages/typegpu/tests/internal/extractIdentifiers.test.ts new file mode 100644 index 0000000000..b2af919eea --- /dev/null +++ b/packages/typegpu/tests/internal/extractIdentifiers.test.ts @@ -0,0 +1,28 @@ +import { test } from 'typegpu-testing-utility'; +import { expect } from 'vitest'; +import { extractIdentifierLikeTokens } from '../../src/rawShaderCodeUtils.ts'; + +test('extractIdentifierLikeTokens extracts identifiers, skips typed numeric literal suffix (1f) and comments', () => { + expect( + extractIdentifierLikeTokens( + `(a: i32, b: u32) -> vec3f { + // a nice comment + const hello = 1f; + const point = boid.pos; + }`, + ), + ).toMatchInlineSnapshot(` + [ + "a", + "i32", + "b", + "u32", + "vec3f", + "const", + "hello", + "const", + "point", + "boid", + ] + `); +}); diff --git a/packages/typegpu/tests/internal/renameIdentifiers.test.ts b/packages/typegpu/tests/internal/renameIdentifiers.test.ts new file mode 100644 index 0000000000..7230c7b0e6 --- /dev/null +++ b/packages/typegpu/tests/internal/renameIdentifiers.test.ts @@ -0,0 +1,30 @@ +import { test } from 'typegpu-testing-utility'; +import { expect } from 'vitest'; +import { renameIdentifiers } from '../../src/rawShaderCodeUtils.ts'; + +test('renameIdentifiers', () => { + const examples: [string, Map][] = [ + ['f.f + 1f + f2', new Map([['f', 'g']])], + ['const a = 1; const b = 1;', new Map([['a', 'a_1']])], + [ + `fn foo() { + const a = 1; + const b = 1; + }`, + new Map([['a', 'a_1']]), + ], + ]; + + expect(examples.map((e) => renameIdentifiers(...e))).toMatchInlineSnapshot( + ` + [ + "g.f + 1f + f2", + "const a_1 = 1; const b = 1;", + "fn foo() { + const a_1 = 1; + const b = 1; + }", + ] + `, + ); +}); diff --git a/packages/typegpu/tests/minification.test.ts b/packages/typegpu/tests/minification.test.ts index 08044716ee..788a83dcac 100644 --- a/packages/typegpu/tests/minification.test.ts +++ b/packages/typegpu/tests/minification.test.ts @@ -248,7 +248,11 @@ describe('minification', () => { expect(() => tgpu.resolve([rawFn], { unstable_minify: true }), ).toThrowErrorMatchingInlineSnapshot( - `[SyntaxError: Block comment closing without corresponding opening found during minification.]`, + ` + [Error: Resolution of the following tree failed: + - + - fn:rawFn: Found block comment closing without corresponding opening.] + `, ); }); @@ -260,7 +264,11 @@ describe('minification', () => { expect(() => tgpu.resolve([rawFn], { unstable_minify: true }), ).toThrowErrorMatchingInlineSnapshot( - `[SyntaxError: Block comment opening without corresponding closing found during minification.]`, + ` + [Error: Resolution of the following tree failed: + - + - fn:rawFn: Found block comment opening without corresponding closing.] + `, ); }); }); diff --git a/packages/typegpu/tests/namespace.test.ts b/packages/typegpu/tests/namespace.test.ts index 262f4a5908..435ff9486e 100644 --- a/packages/typegpu/tests/namespace.test.ts +++ b/packages/typegpu/tests/namespace.test.ts @@ -29,7 +29,7 @@ describe('tgpu.namespace', () => { `); // Should be just the template, as Boid was already defined in the namespace - expect(code2).toMatchInlineSnapshot(`"var foo: Boid"`); + expect(code2).toMatchInlineSnapshot(`"var foo_1: Boid"`); }); it('defines transitive dependencies only once', () => { diff --git a/packages/typegpu/tests/rawFn.test.ts b/packages/typegpu/tests/rawFn.test.ts index ce773fbc8a..62a2b560d7 100644 --- a/packages/typegpu/tests/rawFn.test.ts +++ b/packages/typegpu/tests/rawFn.test.ts @@ -543,10 +543,10 @@ describe('tgpu.fn with raw wgsl and missing types', () => { const c1 = (() => tgpu.const(d.vec2u, d.vec2u(1)))(); // unnamed const c2 = (() => tgpu.const(d.vec2u, d.vec2u(2)))(); // unnamed const c3 = (() => tgpu.const(d.vec2u, d.vec2u(3)))(); // unnamed - const fn = tgpu.fn([])`() { - let a = n1; - let b = ext.n2; - let c = ext.n3.x; + const fn = tgpu.fn([])`() { + let a = n1; + let b = ext.n2; + let c = ext.n3.x; }`.$uses({ n1: c1, ext: { n2: c2, n3: c3 }, @@ -559,10 +559,10 @@ describe('tgpu.fn with raw wgsl and missing types', () => { const ext_n3: vec2u = vec2u(3); - fn fn_1() { - let a = n1; - let b = ext_n2; - let c = ext_n3.x; + fn fn_1() { + let a = n1; + let b = ext_n2; + let c = ext_n3.x; }" `); }); @@ -635,3 +635,45 @@ describe('string injection', () => { `); }); }); + +describe('name clash avoidance', () => { + it('reserves local identifiers, forcing subsequent globals to be named differently', () => { + const constant = tgpu.const(d.f32, 13).$name('a'); + + const foo = tgpu.fn([d.f32, d.f32], d.f32)`(a, b) { + return a + b + constant; + }`.$uses({ constant }); + + expect(tgpu.resolve([foo])).toMatchInlineSnapshot(` + "const a_1: f32 = 13f; + + fn foo(a: f32, b: f32) -> f32 { + return a + b + a_1; + }" + `); + }); + + it('renames local identifiers if they clash with already declared identifiers', () => { + const constant = tgpu.const(d.f32, 13).$name('a'); + + const foo = tgpu.fn([d.f32, d.f32], d.f32)`(a, b) { + return a + b + constant; + }`.$uses({ constant }); + + const main = tgpu.fn([])`() { + const value = constant + foo(1, 2); + }`.$uses({ constant, foo }); + + expect(tgpu.resolve([main])).toMatchInlineSnapshot(` + "const a: f32 = 13f; + + fn foo(a_1: f32, b: f32) -> f32 { + return a_1 + b + a; + } + + fn main() { + const value = a + foo(1, 2); + }" + `); + }); +}); diff --git a/packages/typegpu/tests/renderPipeline.test.ts b/packages/typegpu/tests/renderPipeline.test.ts index 334693f172..59b6276a38 100644 --- a/packages/typegpu/tests/renderPipeline.test.ts +++ b/packages/typegpu/tests/renderPipeline.test.ts @@ -927,7 +927,9 @@ describe('root.createRenderPipeline', () => { - - renderPipeline:pipeline - renderPipelineCore - - autoVertexFn: Invalid property key '__myProp': Identifiers cannot start with double underscores.] + - autoVertexFn + - auto-struct:VertexOut + - struct:VertexOut: Invalid property key '__myProp': Identifiers cannot start with double underscores.] `); }); @@ -949,7 +951,9 @@ describe('root.createRenderPipeline', () => { - - renderPipeline:pipeline - renderPipelineCore - - autoVertexFn: Invalid property key 'loop': Identifiers cannot start with reserved keywords.] + - autoVertexFn + - auto-struct:VertexOut + - struct:VertexOut: Invalid property key 'loop': Identifiers cannot start with reserved keywords.] `); }); diff --git a/packages/typegpu/tests/struct.test.ts b/packages/typegpu/tests/struct.test.ts index c5c48a4bf9..8455ff814c 100644 --- a/packages/typegpu/tests/struct.test.ts +++ b/packages/typegpu/tests/struct.test.ts @@ -367,14 +367,22 @@ describe('struct', () => { }); it('throws when struct prop has whitespace in name', () => { - expect(() => struct({ 'my prop': f32 })).toThrowErrorMatchingInlineSnapshot( - `[Error: Invalid property key 'my prop': Identifiers cannot contain whitespace.]`, + expect(() => tgpu.resolve([struct({ 'my prop': f32 })])).toThrowErrorMatchingInlineSnapshot( + ` + [Error: Resolution of the following tree failed: + - + - struct:: Invalid property key 'my prop': Identifiers cannot contain whitespace.] + `, ); }); it('throws when struct prop uses a reserved word', () => { - expect(() => struct({ struct: f32 })).toThrowErrorMatchingInlineSnapshot( - `[Error: Invalid property key 'struct': Identifiers cannot start with reserved keywords.]`, + expect(() => tgpu.resolve([struct({ struct: f32 })])).toThrowErrorMatchingInlineSnapshot( + ` + [Error: Resolution of the following tree failed: + - + - struct:: Invalid property key 'struct': Identifiers cannot start with reserved keywords.] + `, ); }); diff --git a/packages/typegpu/tests/tgsl/rawCodeSnippet.test.ts b/packages/typegpu/tests/tgsl/rawCodeSnippet.test.ts index d6755ab82c..fe7cabdd2c 100644 --- a/packages/typegpu/tests/tgsl/rawCodeSnippet.test.ts +++ b/packages/typegpu/tests/tgsl/rawCodeSnippet.test.ts @@ -151,4 +151,46 @@ describe('rawCodeSnippet', () => { }" `); }); + + it('should rename references to local variables when they have been renamed in the owner function', () => { + const constant = tgpu.const(d.f32, 123).$name('a'); + + // `a` refers to the nearest definition, in this case, the argument of each respective function + const snippet = tgpu['~unstable'].rawCodeSnippet('a * 2 + constant', d.f32).$uses({ constant }); + + const wgslFn = tgpu.fn([d.f32], d.f32)`(a) { + return snippet; + }`.$uses({ snippet }); + + const jsFn = tgpu.fn( + [d.f32], + d.f32, + )((a) => { + 'use gpu'; + return snippet.$; + }); + + const main = () => { + 'use gpu'; + const first = constant.$; + return first + (jsFn(2) + wgslFn(1)); + }; + + expect(tgpu.resolve([main])).toMatchInlineSnapshot(` + "const a: f32 = 123f; + + fn jsFn(a_1: f32) -> f32 { + return a * 2 + a; + } + + fn wgslFn(a_1: f32) -> f32 { + return a_1 * 2 + a; + } + + fn main() -> f32 { + const first = a; + return (first + (jsFn(2f) + wgslFn(1f))); + }" + `); + }); }); diff --git a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts index 883cb66b88..c5869002ea 100644 --- a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts +++ b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts @@ -1107,16 +1107,32 @@ describe('WgslGenerator', () => { it('throws when struct prop is named wrongly', () => { expect(() => tgpu.resolve([d.struct({ '': d.u32 })])).toThrowErrorMatchingInlineSnapshot( - `[Error: Invalid property key '': Identifiers cannot be equal to '' or '_']`, + ` + [Error: Resolution of the following tree failed: + - + - struct:: Invalid property key '': Identifiers cannot be equal to '' or '_'] + `, ); expect(() => tgpu.resolve([d.struct({ '0': d.u32 })])).toThrowErrorMatchingInlineSnapshot( - `[Error: Invalid property key '0': Not compliant with WGSL guidelines.]`, + ` + [Error: Resolution of the following tree failed: + - + - struct:: Invalid property key '0': Not compliant with WGSL guidelines.] + `, ); expect(() => tgpu.resolve([d.struct({ __: d.u32 })])).toThrowErrorMatchingInlineSnapshot( - `[Error: Invalid property key '__': Identifiers cannot start with double underscores.]`, + ` + [Error: Resolution of the following tree failed: + - + - struct:: Invalid property key '__': Identifiers cannot start with double underscores.] + `, ); expect(() => tgpu.resolve([d.struct({ struct: d.u32 })])).toThrowErrorMatchingInlineSnapshot( - `[Error: Invalid property key 'struct': Identifiers cannot start with reserved keywords.]`, + ` + [Error: Resolution of the following tree failed: + - + - struct:: Invalid property key 'struct': Identifiers cannot start with reserved keywords.] + `, ); });