From 669e47f2a4301c028b87bc02b4450f48155c0bdb Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Tue, 16 Jun 2026 10:35:50 +0200 Subject: [PATCH 01/44] Remove setName from mergeExternals --- .../individual-example-tests/shifting-gradient.test.ts | 8 ++++---- .../individual-example-tests/spinning-triangle.test.ts | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/typegpu-docs/tests/individual-example-tests/shifting-gradient.test.ts b/apps/typegpu-docs/tests/individual-example-tests/shifting-gradient.test.ts index c848d13d86..1c4373cfaf 100644 --- a/apps/typegpu-docs/tests/individual-example-tests/shifting-gradient.test.ts +++ b/apps/typegpu-docs/tests/individual-example-tests/shifting-gradient.test.ts @@ -34,7 +34,7 @@ describe('react/shifting-gradient example', () => { return fullScreenTriangle_Output(vec4f(pos[vertexIndex], 0, 1), uv[vertexIndex]); } - @group(0) @binding(0) var time: f32; + @group(0) @binding(0) var item: f32; fn computeMaxSaturation(a: f32, b: f32) -> f32 { var k0 = 0f; @@ -218,11 +218,11 @@ describe('react/shifting-gradient example', () => { @fragment fn fragment(_arg_0: FragmentIn) -> @location(0) vec4f { let fromStart = vec3f(0.6279553771018982, 0.22486300766468048, 0.1258462816476822); let fromEnd = vec3f(0.45201370120048523, -0.03245693817734718, -0.31152817606925964); - let from_1 = mix(fromStart, fromEnd, ((sin(time) * 0.5f) + 0.5f)); + let from_1 = mix(fromStart, fromEnd, ((sin(item) * 0.5f) + 0.5f)); let toStart = vec3f(0.8664395809173584, -0.2338874489068985, 0.17949843406677246); let toEnd = vec3f(0.7016738653182983, 0.27456632256507874, -0.16915608942508698); - let to = mix(toStart, toEnd, ((cos((time * 1.5f)) * 0.5f) + 0.5f)); - let mixed = mix(from_1, to, ((((_arg_0.uv.x * 2f) - 1f) * 0.5f) + (sin((time + (_arg_0.uv.y * 3f))) * 0.5f))); + let to = mix(toStart, toEnd, ((cos((item * 1.5f)) * 0.5f) + 0.5f)); + let mixed = mix(from_1, to, ((((_arg_0.uv.x * 2f) - 1f) * 0.5f) + (sin((item + (_arg_0.uv.y * 3f))) * 0.5f))); return vec4f(oklabToRgb(mixed), 1f); }" `); diff --git a/apps/typegpu-docs/tests/individual-example-tests/spinning-triangle.test.ts b/apps/typegpu-docs/tests/individual-example-tests/spinning-triangle.test.ts index b6d81fc47a..6f21c2472d 100644 --- a/apps/typegpu-docs/tests/individual-example-tests/spinning-triangle.test.ts +++ b/apps/typegpu-docs/tests/individual-example-tests/spinning-triangle.test.ts @@ -24,7 +24,7 @@ describe('react/spinning-triangle example', () => { expect(shaderCodes).toMatchInlineSnapshot(` "const vertices: array = array(vec2f(0, 1), vec2f(-0.8660253882408142, -0.5), vec2f(0.8660253882408142, -0.5)); - @group(0) @binding(0) var time: f32; + @group(0) @binding(0) var item: f32; fn rotate(v: vec2f, angle: f32) -> vec2f { let pos = vec2f(((v.x * cos(angle)) - (v.y * sin(angle))), ((v.x * sin(angle)) + (v.y * cos(angle)))); @@ -44,7 +44,7 @@ describe('react/spinning-triangle example', () => { @vertex fn vertex(_arg_0: VertexIn) -> VertexOut { let local = vertices[_arg_0.vertexIndex]; - let rotated = rotate(local, (time * 0.1f)); + let rotated = rotate(local, (item * 0.1f)); return VertexOut(vec4f((rotated * 0.7f), 0f, 1f), length((local - vertices[0i])), length((local - vertices[1i])), length((local - vertices[2i]))); } @@ -235,7 +235,7 @@ describe('react/spinning-triangle example', () => { @fragment fn fragment(_arg_0: FragmentIn) -> @location(0) vec4f { let dist = (1f / (1.4f - min(min(_arg_0.dist0, _arg_0.dist1), _arg_0.dist2))); - let albedo = getGradientColor((((fract(((dist * 2f) - time)) * 2f) - 1f) + cos(time))); + let albedo = getGradientColor((((fract(((dist * 2f) - item)) * 2f) - 1f) + cos(item))); return vec4f(albedo, 1f); }" `); From 692ac90136fa6ec90f307e45f37407c10cad6335 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Tue, 16 Jun 2026 13:43:46 +0200 Subject: [PATCH 02/44] Name unnamed externals in propAccess and getById --- .../individual-example-tests/shifting-gradient.test.ts | 8 ++++---- .../individual-example-tests/spinning-triangle.test.ts | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/typegpu-docs/tests/individual-example-tests/shifting-gradient.test.ts b/apps/typegpu-docs/tests/individual-example-tests/shifting-gradient.test.ts index 1c4373cfaf..c848d13d86 100644 --- a/apps/typegpu-docs/tests/individual-example-tests/shifting-gradient.test.ts +++ b/apps/typegpu-docs/tests/individual-example-tests/shifting-gradient.test.ts @@ -34,7 +34,7 @@ describe('react/shifting-gradient example', () => { return fullScreenTriangle_Output(vec4f(pos[vertexIndex], 0, 1), uv[vertexIndex]); } - @group(0) @binding(0) var item: f32; + @group(0) @binding(0) var time: f32; fn computeMaxSaturation(a: f32, b: f32) -> f32 { var k0 = 0f; @@ -218,11 +218,11 @@ describe('react/shifting-gradient example', () => { @fragment fn fragment(_arg_0: FragmentIn) -> @location(0) vec4f { let fromStart = vec3f(0.6279553771018982, 0.22486300766468048, 0.1258462816476822); let fromEnd = vec3f(0.45201370120048523, -0.03245693817734718, -0.31152817606925964); - let from_1 = mix(fromStart, fromEnd, ((sin(item) * 0.5f) + 0.5f)); + let from_1 = mix(fromStart, fromEnd, ((sin(time) * 0.5f) + 0.5f)); let toStart = vec3f(0.8664395809173584, -0.2338874489068985, 0.17949843406677246); let toEnd = vec3f(0.7016738653182983, 0.27456632256507874, -0.16915608942508698); - let to = mix(toStart, toEnd, ((cos((item * 1.5f)) * 0.5f) + 0.5f)); - let mixed = mix(from_1, to, ((((_arg_0.uv.x * 2f) - 1f) * 0.5f) + (sin((item + (_arg_0.uv.y * 3f))) * 0.5f))); + let to = mix(toStart, toEnd, ((cos((time * 1.5f)) * 0.5f) + 0.5f)); + let mixed = mix(from_1, to, ((((_arg_0.uv.x * 2f) - 1f) * 0.5f) + (sin((time + (_arg_0.uv.y * 3f))) * 0.5f))); return vec4f(oklabToRgb(mixed), 1f); }" `); diff --git a/apps/typegpu-docs/tests/individual-example-tests/spinning-triangle.test.ts b/apps/typegpu-docs/tests/individual-example-tests/spinning-triangle.test.ts index 6f21c2472d..b6d81fc47a 100644 --- a/apps/typegpu-docs/tests/individual-example-tests/spinning-triangle.test.ts +++ b/apps/typegpu-docs/tests/individual-example-tests/spinning-triangle.test.ts @@ -24,7 +24,7 @@ describe('react/spinning-triangle example', () => { expect(shaderCodes).toMatchInlineSnapshot(` "const vertices: array = array(vec2f(0, 1), vec2f(-0.8660253882408142, -0.5), vec2f(0.8660253882408142, -0.5)); - @group(0) @binding(0) var item: f32; + @group(0) @binding(0) var time: f32; fn rotate(v: vec2f, angle: f32) -> vec2f { let pos = vec2f(((v.x * cos(angle)) - (v.y * sin(angle))), ((v.x * sin(angle)) + (v.y * cos(angle)))); @@ -44,7 +44,7 @@ describe('react/spinning-triangle example', () => { @vertex fn vertex(_arg_0: VertexIn) -> VertexOut { let local = vertices[_arg_0.vertexIndex]; - let rotated = rotate(local, (item * 0.1f)); + let rotated = rotate(local, (time * 0.1f)); return VertexOut(vec4f((rotated * 0.7f), 0f, 1f), length((local - vertices[0i])), length((local - vertices[1i])), length((local - vertices[2i]))); } @@ -235,7 +235,7 @@ describe('react/spinning-triangle example', () => { @fragment fn fragment(_arg_0: FragmentIn) -> @location(0) vec4f { let dist = (1f / (1.4f - min(min(_arg_0.dist0, _arg_0.dist1), _arg_0.dist2))); - let albedo = getGradientColor((((fract(((dist * 2f) - item)) * 2f) - 1f) + cos(item))); + let albedo = getGradientColor((((fract(((dist * 2f) - time)) * 2f) - 1f) + cos(time))); return vec4f(albedo, 1f); }" `); From a944c600397cef1fb31414649334a0c007fe5233 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:06:21 +0200 Subject: [PATCH 03/44] Limit tgpuDeclare to one external --- .../typegpu/src/core/declare/tgpuDeclare.ts | 23 +++++++++++-------- packages/typegpu/tests/declare.test.ts | 10 ++++++++ 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/packages/typegpu/src/core/declare/tgpuDeclare.ts b/packages/typegpu/src/core/declare/tgpuDeclare.ts index f30cd357e0..c1a3c2466c 100644 --- a/packages/typegpu/src/core/declare/tgpuDeclare.ts +++ b/packages/typegpu/src/core/declare/tgpuDeclare.ts @@ -2,7 +2,7 @@ import { type ResolvedSnippet, snip } from '../../data/snippet.ts'; import { Void } from '../../data/wgslTypes.ts'; import { $internal, $resolve } from '../../shared/symbols.ts'; import type { ResolutionCtx, SelfResolvable } from '../../types.ts'; -import { mergeExternals, type ExternalMap, replaceExternalsInWgsl } from '../resolve/externals.ts'; +import { type ExternalMap, replaceExternalsInWgsl } from '../resolve/externals.ts'; // ---------- // Public API @@ -33,7 +33,7 @@ export function declare(declaration: string): TgpuDeclare { class TgpuDeclareImpl implements TgpuDeclare, SelfResolvable { readonly [$internal] = true; - #externalsToApply: ExternalMap[] = []; + #externals: ExternalMap | undefined; #declaration: string; constructor(declaration: string) { @@ -41,18 +41,21 @@ class TgpuDeclareImpl implements TgpuDeclare, SelfResolvable { } $uses(dependencyMap: Record): this { - this.#externalsToApply.push(dependencyMap); + if (this.#externals !== undefined) { + throw new Error( + "Cannot call '$uses' multiple times. If you wish to override dependencies, use slots or accessors instead.", + ); + } + this.#externals = dependencyMap; return this; } [$resolve](ctx: ResolutionCtx): ResolvedSnippet { - const externalMap: ExternalMap = {}; - - for (const externals of this.#externalsToApply) { - mergeExternals(externalMap, externals); - } - - const replacedDeclaration = replaceExternalsInWgsl(ctx, externalMap, this.#declaration); + const replacedDeclaration = replaceExternalsInWgsl( + ctx, + this.#externals ?? {}, + this.#declaration, + ); ctx.addDeclaration(replacedDeclaration); return snip('', Void, /* origin */ 'constant'); diff --git a/packages/typegpu/tests/declare.test.ts b/packages/typegpu/tests/declare.test.ts index a4247ba832..8883c5e3fd 100644 --- a/packages/typegpu/tests/declare.test.ts +++ b/packages/typegpu/tests/declare.test.ts @@ -105,4 +105,14 @@ struct Output { }" `); }); + + it("throws when '$uses' is called multiple times", () => { + const declaration = tgpu['~unstable'] + .declare('@group(0) @binding(0) var val: f32;') + .$uses({ myStruct: d.struct({ p: d.u32 }) }); + + expect(() => + declaration.$uses({ myStruct: d.struct({ p: d.u32 }) }), + ).toThrowErrorMatchingInlineSnapshot(`[Error: Cannot call '$uses' multiple times. If you wish to override dependencies, use slots or accessors instead.]`); + }); }); From 754eb92a5abe03528555ba7ef47f7f2d0274ed11 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:10:46 +0200 Subject: [PATCH 04/44] Remove unnecessary merge from tgpuResolve --- packages/typegpu/src/core/resolve/tgpuResolve.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/typegpu/src/core/resolve/tgpuResolve.ts b/packages/typegpu/src/core/resolve/tgpuResolve.ts index 710fe14c33..77d95a9403 100644 --- a/packages/typegpu/src/core/resolve/tgpuResolve.ts +++ b/packages/typegpu/src/core/resolve/tgpuResolve.ts @@ -195,14 +195,11 @@ function resolveFromTemplate(options: TgpuExtendedResolveOptions): ResolutionRes ); } - const dependencies = {} as Record; - mergeExternals(dependencies, externals ?? {}); - const resolutionObj: SelfResolvable = { [$internal]: true, [$resolve](ctx): ResolvedSnippet { return snip( - replaceExternalsInWgsl(ctx, dependencies, template ?? ''), + replaceExternalsInWgsl(ctx, externals, template ?? ''), Void, /* origin */ 'runtime', ); From a2179404efe51014847ffd5d03746f05ecf347a2 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:14:36 +0200 Subject: [PATCH 05/44] Limit rawCodeSnippet to one $uses --- .../core/rawCodeSnippet/tgpuRawCodeSnippet.ts | 24 ++++++++++--------- .../typegpu/tests/tgsl/rawCodeSnippet.test.ts | 12 ++++++++++ 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/packages/typegpu/src/core/rawCodeSnippet/tgpuRawCodeSnippet.ts b/packages/typegpu/src/core/rawCodeSnippet/tgpuRawCodeSnippet.ts index 7c0af44152..1f27ea9737 100644 --- a/packages/typegpu/src/core/rawCodeSnippet/tgpuRawCodeSnippet.ts +++ b/packages/typegpu/src/core/rawCodeSnippet/tgpuRawCodeSnippet.ts @@ -5,7 +5,7 @@ import { inCodegenMode } from '../../execMode.ts'; import type { InferGPU } from '../../shared/repr.ts'; import { $gpuValueOf, $internal, $ownSnippet, $resolve } from '../../shared/symbols.ts'; import type { ResolutionCtx, SelfResolvable } from '../../types.ts'; -import { mergeExternals, type ExternalMap, replaceExternalsInWgsl } from '../resolve/externals.ts'; +import { type ExternalMap, replaceExternalsInWgsl } from '../resolve/externals.ts'; import { valueProxyHandler } from '../valueProxyUtils.ts'; // ---------- @@ -92,7 +92,7 @@ class TgpuRawCodeSnippetImpl readonly origin: RawCodeSnippetOrigin; #expression: string; - #externalsToApply: ExternalMap[]; + #externals: ExternalMap | undefined; constructor(expression: string, type: TDataType, origin: RawCodeSnippetOrigin) { this[$internal] = true; @@ -100,22 +100,24 @@ class TgpuRawCodeSnippetImpl this.origin = origin; this.#expression = expression; - this.#externalsToApply = []; } $uses(dependencyMap: Record): this { - this.#externalsToApply.push(dependencyMap); + if (this.#externals !== undefined) { + throw new Error( + "Cannot call '$uses' multiple times. If you wish to override dependencies, use slots or accessors instead.", + ); + } + this.#externals = dependencyMap; return this; } [$resolve](ctx: ResolutionCtx): ResolvedSnippet { - const externalMap: ExternalMap = {}; - - for (const externals of this.#externalsToApply) { - mergeExternals(externalMap, externals); - } - - const replacedExpression = replaceExternalsInWgsl(ctx, externalMap, this.#expression); + const replacedExpression = replaceExternalsInWgsl( + ctx, + this.#externals ?? {}, + this.#expression, + ); return snip(replacedExpression, this.dataType, this.origin); } diff --git a/packages/typegpu/tests/tgsl/rawCodeSnippet.test.ts b/packages/typegpu/tests/tgsl/rawCodeSnippet.test.ts index b0dcd5f70b..d4f71be7a5 100644 --- a/packages/typegpu/tests/tgsl/rawCodeSnippet.test.ts +++ b/packages/typegpu/tests/tgsl/rawCodeSnippet.test.ts @@ -124,6 +124,18 @@ describe('rawCodeSnippet', () => { `); }); + it("throws when '$uses' is called multiple times", ({ root }) => { + const myBuffer = root.createUniform(d.u32, 7); + + const rawSnippet = tgpu['~unstable'] + .rawCodeSnippet('myBuffer', d.u32, 'uniform') + .$uses({ myBuffer }); + + expect(() => rawSnippet.$uses({ myBuffer })).toThrowErrorMatchingInlineSnapshot( + `[Error: Cannot call '$uses' multiple times. If you wish to override dependencies, use slots or accessors instead.]`, + ); + }); + it('should be accessed transitively through a slot', () => { const exprSlot = tgpu.slot(tgpu['~unstable'].rawCodeSnippet('0.5 + 0.2', d.f32, 'constant')); From 1f8bccbde6a2e99eb26db2cb32ca75565cc30dca Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Tue, 16 Jun 2026 17:43:07 +0200 Subject: [PATCH 06/44] Add FnExternals, and throw if $uses is called multiple times --- packages/typegpu/src/core/function/fnCore.ts | 20 ++++ .../src/core/function/tgpuComputeFn.ts | 1 + packages/typegpu/src/core/function/tgpuFn.ts | 1 + .../src/core/function/tgpuFragmentFn.ts | 9 +- .../typegpu/src/core/function/tgpuVertexFn.ts | 2 + .../typegpu/src/core/resolve/tgpuResolve.ts | 2 +- packages/typegpu/tests/function.test.ts | 107 +++--------------- 7 files changed, 46 insertions(+), 96 deletions(-) diff --git a/packages/typegpu/src/core/function/fnCore.ts b/packages/typegpu/src/core/function/fnCore.ts index d1dfe4249c..27ee0c501d 100644 --- a/packages/typegpu/src/core/function/fnCore.ts +++ b/packages/typegpu/src/core/function/fnCore.ts @@ -10,8 +10,16 @@ import { mergeExternals, type ExternalMap, replaceExternalsInWgsl } from '../res import { extractArgs } from './extractArgs.ts'; import type { Implementation, SeparatedEntryArgs } from './fnTypes.ts'; +export type FnExternals = { + userProvided?: ExternalMap; + pluginProvided?: ExternalMap; + args?: ExternalMap; + out?: ExternalMap; +}; + export interface FnCore { applyExternals: (newExternals: ExternalMap) => void; + setExternals: (key: keyof FnExternals, newExternal: ExternalMap) => void; resolve( ctx: ResolutionCtx, /** @@ -44,6 +52,7 @@ export function createFnCore( * entry fn). */ const externalsToApply: ExternalMap[] = []; + const externals: FnExternals = {}; const core = { // Making the implementation the holder of the name, as long as it's @@ -53,6 +62,17 @@ export function createFnCore( externalsToApply.push(newExternals); }, + setExternals(key: keyof typeof externals, newExternal: ExternalMap): void { + if (key in externals) { + if (key === 'userProvided') { + throw new Error( + "Cannot call '$uses' multiple times. If you wish to override dependencies, use slots or accessors instead.", + ); + } + } + externals[key] = newExternal; + }, + resolve( ctx: ResolutionCtx, argTypes: BaseData[], diff --git a/packages/typegpu/src/core/function/tgpuComputeFn.ts b/packages/typegpu/src/core/function/tgpuComputeFn.ts index 9cef2d49cc..06a33cfc58 100644 --- a/packages/typegpu/src/core/function/tgpuComputeFn.ts +++ b/packages/typegpu/src/core/function/tgpuComputeFn.ts @@ -121,6 +121,7 @@ function createComputeFn>( $uses(newExternals) { core.applyExternals(newExternals); + core.setExternals('userProvided', newExternals); return this; }, diff --git a/packages/typegpu/src/core/function/tgpuFn.ts b/packages/typegpu/src/core/function/tgpuFn.ts index d7e316e092..c5dd9421fc 100644 --- a/packages/typegpu/src/core/function/tgpuFn.ts +++ b/packages/typegpu/src/core/function/tgpuFn.ts @@ -182,6 +182,7 @@ function createFn( $uses(newExternals: Record) { core.applyExternals(newExternals); + core.setExternals('userProvided', newExternals); return this; }, diff --git a/packages/typegpu/src/core/function/tgpuFragmentFn.ts b/packages/typegpu/src/core/function/tgpuFragmentFn.ts index cb3b4c990f..d987fbfa89 100644 --- a/packages/typegpu/src/core/function/tgpuFragmentFn.ts +++ b/packages/typegpu/src/core/function/tgpuFragmentFn.ts @@ -186,9 +186,10 @@ function createFragmentFn( const core = createFnCore(implementation, 'fragment'); const outputType = shell.returnType; if (typeof implementation === 'string') { - addReturnTypeToExternals(implementation, outputType, (externals) => - core.applyExternals(externals), - ); + addReturnTypeToExternals(implementation, outputType, (externals) => { + core.applyExternals(externals); + core.setExternals('out', externals); + }); } const result: This = { @@ -197,6 +198,7 @@ function createFragmentFn( $uses(newExternals) { core.applyExternals(newExternals); + core.setExternals('userProvided', newExternals); return this; }, @@ -217,6 +219,7 @@ function createFragmentFn( entryInput.dataSchema.$name(`${getName(this) ?? ''}_Input`); } core.applyExternals({ Out: outputType }); + core.setExternals('out', { Out: outputType }); return ctx.withSlots([[shaderStageSlot, 'fragment']], () => core.resolve(ctx, [], shell.returnType, entryInput), diff --git a/packages/typegpu/src/core/function/tgpuVertexFn.ts b/packages/typegpu/src/core/function/tgpuVertexFn.ts index 8c5b230891..66839245be 100644 --- a/packages/typegpu/src/core/function/tgpuVertexFn.ts +++ b/packages/typegpu/src/core/function/tgpuVertexFn.ts @@ -166,6 +166,7 @@ function createVertexFn( $uses(newExternals) { core.applyExternals(newExternals); + core.setExternals('userProvided', newExternals); return this; }, @@ -183,6 +184,7 @@ function createVertexFn( if (typeof implementation === 'string') { core.applyExternals({ Out: outputWithLocation }); + core.setExternals('out', { Out: outputWithLocation }); } return ctx.withSlots([[shaderStageSlot, 'vertex']], () => diff --git a/packages/typegpu/src/core/resolve/tgpuResolve.ts b/packages/typegpu/src/core/resolve/tgpuResolve.ts index 77d95a9403..71ff0487f6 100644 --- a/packages/typegpu/src/core/resolve/tgpuResolve.ts +++ b/packages/typegpu/src/core/resolve/tgpuResolve.ts @@ -8,7 +8,7 @@ import type { ResolvableObject, SelfResolvable, Wgsl } from '../../types.ts'; import type { WgslEnableExtension } from '../../wgslExtensions.ts'; import { isPipeline } from '../pipeline/typeGuards.ts'; import type { Configurable, ExperimentalTgpuRoot } from '../root/rootTypes.ts'; -import { mergeExternals, replaceExternalsInWgsl } from './externals.ts'; +import { replaceExternalsInWgsl } from './externals.ts'; import { type Namespace, namespace } from './namespace.ts'; export interface TgpuResolveOptions { diff --git a/packages/typegpu/tests/function.test.ts b/packages/typegpu/tests/function.test.ts index e9ae56ecc6..f44c3c9dd0 100644 --- a/packages/typegpu/tests/function.test.ts +++ b/packages/typegpu/tests/function.test.ts @@ -84,99 +84,22 @@ describe('tgpu.fn', () => { >(); }); - it('applies multiple externals', () => { - const fn = tgpu.fn([])`() { - let a = X; - let b = Y; - let c = Z; -}` - .$uses({ X: 1 }) - .$uses({ Y: 2 }) - .$uses({ Z: 3 }); - - expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` - "fn fn_1() { - let a = 1; - let b = 2; - let c = 3; - }" - `); - }); - - it('applies multiple externals in correct order', () => { - const fn = tgpu.fn([])`() { - let a = X; - let b = Y; - let c = Z; -}` - .$uses({ X: 'Y' }) - .$uses({ Y: 'Z' }) - .$uses({ Z: 3 }); - - expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` - "fn fn_1() { - let a = 3; - let b = 3; - let c = 3; - }" - `); - }); - - it('applies multiple nested externals', () => { - const fn = tgpu.fn([])`() { - let a = EXT.X; - let b = EXT.Y; - let c = EXT.Z; -}` - .$uses({ EXT: { X: 1 } }) - .$uses({ EXT: { Y: 2 } }) - .$uses({ EXT: { Z: 3 } }); - - expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` - "fn fn_1() { - let a = 1; - let b = 2; - let c = 3; - }" - `); - }); - - it('does not mutate original externals', () => { - const SHARED_EXT = { X: 1 }; - - const fn = tgpu.fn([])`() { - let a = X; - let b = Y; -}` - .$uses(SHARED_EXT) - .$uses({ Y: 2 }); - - expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` - "fn fn_1() { - let a = 1; - let b = 2; - }" - `); - expect(SHARED_EXT).toStrictEqual({ X: 1 }); - }); - it('does not mutate values of original externals', () => { - const SHARED_EXT = { EXT: { X: 1 } }; - - const fn = tgpu.fn([])`() { - let a = EXT.X; - let b = EXT.Y; -}` - .$uses(SHARED_EXT) - .$uses({ EXT: { Y: 2 } }); - - expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` - "fn fn_1() { - let a = 1; - let b = 2; - }" - `); - expect(SHARED_EXT).toStrictEqual({ EXT: { X: 1 } }); + // TODO: check out/in keys for entry points + // const SHARED_EXT = { EXT: { X: 1 } }; + // const fn = tgpu.fn([])`() { + // let a = EXT.X; + // let b = EXT.Y; + // }` + // .$uses(SHARED_EXT) + // .$uses({ EXT: { Y: 2 } }); + // expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` + // "fn fn_1() { + // let a = 1; + // let b = 2; + // }" + // `); + // expect(SHARED_EXT).toStrictEqual({ EXT: { X: 1 } }); }); it('does not break when an unused unresolvable external is passed', () => { From b0ec4478b294c8772a60738377dc45309ebd36d0 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Tue, 16 Jun 2026 18:15:29 +0200 Subject: [PATCH 07/44] Prepare string-implemented functions for external swap --- packages/typegpu/src/core/function/fnCore.ts | 22 ++++++- packages/typegpu/src/core/function/tgpuFn.ts | 4 +- .../src/core/function/tgpuFragmentFn.ts | 5 +- .../typegpu/src/core/resolve/externals.ts | 56 ++++++++++++----- packages/typegpu/tests/externals.test.ts | 61 ++++++++++--------- 5 files changed, 96 insertions(+), 52 deletions(-) diff --git a/packages/typegpu/src/core/function/fnCore.ts b/packages/typegpu/src/core/function/fnCore.ts index 27ee0c501d..0ea141fc37 100644 --- a/packages/typegpu/src/core/function/fnCore.ts +++ b/packages/typegpu/src/core/function/fnCore.ts @@ -6,7 +6,12 @@ import { validateIdentifier } from '../../nameUtils.ts'; import { getFunctionMetadata, getName } from '../../shared/meta.ts'; import { $getNameForward } from '../../shared/symbols.ts'; import type { ResolutionCtx, TgpuShaderStage } from '../../types.ts'; -import { mergeExternals, type ExternalMap, replaceExternalsInWgsl } from '../resolve/externals.ts'; +import { + mergeExternals, + type ExternalMap, + replaceExternalsInWgsl, + mergeFunctionExternals, +} from '../resolve/externals.ts'; import { extractArgs } from './extractArgs.ts'; import type { Implementation, SeparatedEntryArgs } from './fnTypes.ts'; @@ -62,7 +67,7 @@ export function createFnCore( externalsToApply.push(newExternals); }, - setExternals(key: keyof typeof externals, newExternal: ExternalMap): void { + setExternals(key: keyof FnExternals, newExternal: ExternalMap): void { if (key in externals) { if (key === 'userProvided') { throw new Error( @@ -121,9 +126,22 @@ export function createFnCore( entryInput.positionalArgs.map((a) => [a.schemaKey, a.schemaKey]), ), }); + this.setExternals('args', { + in: Object.fromEntries( + entryInput.positionalArgs.map((a) => [a.schemaKey, a.schemaKey]), + ), + }); } + console.log(externalsToApply); + console.log(externalMap); + console.log(externals); const replacedImpl = replaceExternalsInWgsl(ctx, externalMap, implementation); + // const replacedImpl = replaceExternalsInWgsl( + // ctx, + // mergeFunctionExternals(externals), + // implementation, + // ); let header = ''; let body = ''; diff --git a/packages/typegpu/src/core/function/tgpuFn.ts b/packages/typegpu/src/core/function/tgpuFn.ts index c5dd9421fc..2c500fc45f 100644 --- a/packages/typegpu/src/core/function/tgpuFn.ts +++ b/packages/typegpu/src/core/function/tgpuFn.ts @@ -204,8 +204,8 @@ function createFn( [$resolve](ctx: ResolutionCtx): ResolvedSnippet { if (typeof implementation === 'string') { - addArgTypesToExternals(implementation, shell.argTypes, core.applyExternals); - addReturnTypeToExternals(implementation, shell.returnType, core.applyExternals); + addArgTypesToExternals(implementation, shell.argTypes, core); + addReturnTypeToExternals(implementation, shell.returnType, core); } return core.resolve(ctx, shell.argTypes, shell.returnType); diff --git a/packages/typegpu/src/core/function/tgpuFragmentFn.ts b/packages/typegpu/src/core/function/tgpuFragmentFn.ts index d987fbfa89..e2862fb939 100644 --- a/packages/typegpu/src/core/function/tgpuFragmentFn.ts +++ b/packages/typegpu/src/core/function/tgpuFragmentFn.ts @@ -186,10 +186,7 @@ function createFragmentFn( const core = createFnCore(implementation, 'fragment'); const outputType = shell.returnType; if (typeof implementation === 'string') { - addReturnTypeToExternals(implementation, outputType, (externals) => { - core.applyExternals(externals); - core.setExternals('out', externals); - }); + addReturnTypeToExternals(implementation, outputType, core); } const result: This = { diff --git a/packages/typegpu/src/core/resolve/externals.ts b/packages/typegpu/src/core/resolve/externals.ts index ab3c885d0c..2082799ad7 100644 --- a/packages/typegpu/src/core/resolve/externals.ts +++ b/packages/typegpu/src/core/resolve/externals.ts @@ -2,6 +2,7 @@ import { isLooseData } from '../../data/dataTypes.ts'; import { isWgslStruct } from '../../data/wgslTypes.ts'; import { getName, hasTinyestMetadata, isNamable, setName } from '../../shared/meta.ts'; import { isWgsl, type ResolutionCtx } from '../../types.ts'; +import type { FnCore, FnExternals } from '../function/fnCore.ts'; /** * A key-value mapping where keys represent identifiers within shader code, @@ -13,6 +14,35 @@ function isResolvable(value: unknown) { return isWgsl(value) || isLooseData(value) || hasTinyestMetadata(value); } +/** + * Merges function externals into one map. + * Assumes that there is at most one map with non-trivial structure. + */ +export function mergeFunctionExternals(fnExternals: FnExternals): ExternalMap { + console.log('MERGING'); + console.log(fnExternals); + + if (fnExternals.pluginProvided !== undefined && fnExternals.userProvided !== undefined) { + throw new Error( + "Cannot call '$uses' on functions whose metadata was provided by unplugin-typegpu.", + ); + } + const base = fnExternals.pluginProvided ?? fnExternals.userProvided ?? {}; + // avoid calling any of the getters + const result: ExternalMap = Object.defineProperties({}, Object.getOwnPropertyDescriptors(base)); + for (const flatExternal of [fnExternals.args, fnExternals.out].filter((e) => e !== undefined)) { + for (const [key, value] of Object.entries(flatExternal)) { + if (key in result) { + throw new Error( + `Key '${key}' appears in externals while being reserved for internals. Please rename this external.`, + ); + } + result[key] = value; + } + } + return result; +} + /** * Merges two external maps into one. * If the external value is a namable object, it is given a name if it does not already have one. @@ -45,35 +75,33 @@ export function mergeExternals(existing: ExternalMap, newExternals: ExternalMap) } } -export function addArgTypesToExternals( - implementation: string, - argTypes: unknown[], - applyExternals: (externals: ExternalMap) => void, -) { +export function addArgTypesToExternals(implementation: string, argTypes: unknown[], core: FnCore) { const argTypeNames = [...implementation.matchAll(/:\s*(?.*?)\s*[,)]/g)].map((found) => found ? found[1] : undefined, ); - applyExternals( - Object.fromEntries( - argTypes.flatMap((argType, i) => { - const argTypeName = argTypeNames ? argTypeNames[i] : undefined; - return isWgslStruct(argType) && argTypeName !== undefined ? [[argTypeName, argType]] : []; - }), - ), + const args = Object.fromEntries( + argTypes.flatMap((argType, i) => { + const argTypeName = argTypeNames ? argTypeNames[i] : undefined; + return isWgslStruct(argType) && argTypeName !== undefined ? [[argTypeName, argType]] : []; + }), ); + + core.applyExternals(args); + core.setExternals('args', args); } export function addReturnTypeToExternals( implementation: string, returnType: unknown, - applyExternals: (externals: ExternalMap) => void, + core: FnCore, ) { const matched = implementation.match(/->\s(?[\w\d_]+)\s{/); const outputName = matched ? matched[1]?.trim() : undefined; if (isWgslStruct(returnType) && outputName && !/\s/g.test(outputName)) { - applyExternals({ [outputName]: returnType }); + core.applyExternals({ [outputName]: returnType }); + core.setExternals('out', { [outputName]: returnType }); } } diff --git a/packages/typegpu/tests/externals.test.ts b/packages/typegpu/tests/externals.test.ts index f35c22cd93..841d5c7a33 100644 --- a/packages/typegpu/tests/externals.test.ts +++ b/packages/typegpu/tests/externals.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'; import { addArgTypesToExternals, type ExternalMap } from '../src/core/resolve/externals.ts'; import * as d from '../src/data/index.ts'; +// TODO: fix these tests describe('addArgTypesToExternals', () => { const Particle = d.struct({ position: d.vec3f, @@ -15,47 +16,47 @@ describe('addArgTypesToExternals', () => { it('extracts struct argument types with their names', () => { const externals: ExternalMap[] = []; - addArgTypesToExternals( - '(a: vec4f, b: Particle, c: Light) {}', - [d.vec4f, Particle, Light], - (result) => externals.push(result), - ); - expect(externals).toStrictEqual([{ Particle, Light }]); + // addArgTypesToExternals( + // '(a: vec4f, b: Particle, c: Light) {}', + // [d.vec4f, Particle, Light], + // (result) => externals.push(result), + // ); + // expect(externals).toStrictEqual([{ Particle, Light }]); }); it('gets the names from argument list in WGSL implementation', () => { const externals: ExternalMap[] = []; - addArgTypesToExternals('(b: P, a: vec4f, c: L) -> L {}', [Particle, d.vec4f, Light], (result) => - externals.push(result), - ); - expect(externals).toStrictEqual([{ P: Particle, L: Light }]); + // addArgTypesToExternals('(b: P, a: vec4f, c: L) -> L {}', [Particle, d.vec4f, Light], (result) => + // externals.push(result), + // ); + // expect(externals).toStrictEqual([{ P: Particle, L: Light }]); }); it('works when builtins are present', () => { const externals: ExternalMap[] = []; - addArgTypesToExternals( - '(@builtin(workgroup_id) WorkGroupID : vec3u, a: vec4f, b: Particle, c: Light) {}', - [d.vec3u, d.vec4f, Particle, Light], - (result) => externals.push(result), - ); - expect(externals).toStrictEqual([{ Particle, Light }]); + // addArgTypesToExternals( + // '(@builtin(workgroup_id) WorkGroupID : vec3u, a: vec4f, b: Particle, c: Light) {}', + // [d.vec3u, d.vec4f, Particle, Light], + // (result) => externals.push(result), + // ); + // expect(externals).toStrictEqual([{ Particle, Light }]); }); it('works with unusual whitespace', () => { const externals: ExternalMap[] = []; - addArgTypesToExternals( - ` WorkGroupID : vec3u - , - a : A , - (@builtin(workgroup_id) b - - : B, - - c: C - ) -> vec4f {}`, - [d.vec3u, Particle, Particle, Particle], - (result) => externals.push(result), - ); - expect(externals).toStrictEqual([{ A: Particle, B: Particle, C: Particle }]); + // addArgTypesToExternals( + // ` WorkGroupID : vec3u + // , + // a : A , + // (@builtin(workgroup_id) b + + // : B, + + // c: C + // ) -> vec4f {}`, + // [d.vec3u, Particle, Particle, Particle], + // (result) => externals.push(result), + // ); + // expect(externals).toStrictEqual([{ A: Particle, B: Particle, C: Particle }]); }); }); From 236075cd62ca35ed9ddc32a072834775d52e4b38 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 19 Jun 2026 13:23:11 +0200 Subject: [PATCH 08/44] Finish replacing old externals with new externals && cleanup --- packages/typegpu/src/core/function/fnCore.ts | 51 +++++-------------- .../src/core/function/tgpuComputeFn.ts | 1 - packages/typegpu/src/core/function/tgpuFn.ts | 1 - .../src/core/function/tgpuFragmentFn.ts | 2 - .../typegpu/src/core/function/tgpuVertexFn.ts | 2 - .../typegpu/src/core/resolve/externals.ts | 2 - .../typegpu/tests/computePipeline.test.ts | 8 ++- packages/typegpu/tests/tgslFn.test.ts | 50 ++++++++---------- 8 files changed, 38 insertions(+), 79 deletions(-) diff --git a/packages/typegpu/src/core/function/fnCore.ts b/packages/typegpu/src/core/function/fnCore.ts index 0ea141fc37..696983cde7 100644 --- a/packages/typegpu/src/core/function/fnCore.ts +++ b/packages/typegpu/src/core/function/fnCore.ts @@ -7,7 +7,6 @@ import { getFunctionMetadata, getName } from '../../shared/meta.ts'; import { $getNameForward } from '../../shared/symbols.ts'; import type { ResolutionCtx, TgpuShaderStage } from '../../types.ts'; import { - mergeExternals, type ExternalMap, replaceExternalsInWgsl, mergeFunctionExternals, @@ -56,24 +55,19 @@ export function createFnCore( * initialized yet (like when accessing the Output struct of a vertex * entry fn). */ - const externalsToApply: ExternalMap[] = []; const externals: FnExternals = {}; const core = { // Making the implementation the holder of the name, as long as it's // a function (and not a string implementation) [$getNameForward]: typeof implementation === 'function' ? implementation : undefined, - applyExternals(newExternals: ExternalMap): void { - externalsToApply.push(newExternals); - }, setExternals(key: keyof FnExternals, newExternal: ExternalMap): void { - if (key in externals) { - if (key === 'userProvided') { - throw new Error( - "Cannot call '$uses' multiple times. If you wish to override dependencies, use slots or accessors instead.", - ); - } + if (key === 'userProvided' && key in externals) { + // other external keys may be set multiple times by multiple resolves + throw new Error( + "Cannot call '$uses' multiple times. If you wish to override dependencies, use slots or accessors instead.", + ); } externals[key] = newExternal; }, @@ -84,8 +78,6 @@ export function createFnCore( returnType: BaseData | undefined, entryInput?: SeparatedEntryArgs, ): ResolvedSnippet { - const externalMap: ExternalMap = {}; - let attributes = ''; if (functionType === 'compute') { attributes = `@compute @workgroup_size(${workgroupSize?.join(', ')}) `; @@ -95,10 +87,6 @@ export function createFnCore( attributes = `@fragment `; } - for (const externals of externalsToApply) { - mergeExternals(externalMap, externals); - } - const id = ctx.makeUniqueIdentifier(getName(this), 'global'); if (typeof implementation === 'string') { @@ -121,11 +109,6 @@ export function createFnCore( } } - mergeExternals(externalMap, { - in: Object.fromEntries( - entryInput.positionalArgs.map((a) => [a.schemaKey, a.schemaKey]), - ), - }); this.setExternals('args', { in: Object.fromEntries( entryInput.positionalArgs.map((a) => [a.schemaKey, a.schemaKey]), @@ -133,15 +116,11 @@ export function createFnCore( }); } - console.log(externalsToApply); - console.log(externalMap); - console.log(externals); - const replacedImpl = replaceExternalsInWgsl(ctx, externalMap, implementation); - // const replacedImpl = replaceExternalsInWgsl( - // ctx, - // mergeFunctionExternals(externals), - // implementation, - // ); + const replacedImpl = replaceExternalsInWgsl( + ctx, + mergeFunctionExternals(externals), + implementation, + ); let header = ''; let body = ''; @@ -213,11 +192,7 @@ export function createFnCore( const pluginExternals = pluginData?.externals(); if (pluginExternals) { - const missing = Object.fromEntries( - Object.entries(pluginExternals).filter(([name]) => !(name in externalMap)), - ); - - mergeExternals(externalMap, missing); + this.setExternals('pluginProvided', pluginExternals); } const ast = pluginData?.ast; @@ -231,7 +206,7 @@ export function createFnCore( // We look at the identifier chosen by the user and add it to externals. const maybeSecondArg = ast.params[1]; if (maybeSecondArg && maybeSecondArg.type === 'i' && functionType !== 'normal') { - mergeExternals(externalMap, { + this.setExternals('out', { // oxlint-disable-next-line typescript/no-non-null-assertion -- entry functions cannot be shellless [maybeSecondArg.name]: undecorate(returnType!), }); @@ -248,7 +223,7 @@ export function createFnCore( params: ast.params, returnType, body: ast.body, - externalMap, + externalMap: mergeFunctionExternals(externals), }); ctx.addDeclaration(code); diff --git a/packages/typegpu/src/core/function/tgpuComputeFn.ts b/packages/typegpu/src/core/function/tgpuComputeFn.ts index 06a33cfc58..f7118526a6 100644 --- a/packages/typegpu/src/core/function/tgpuComputeFn.ts +++ b/packages/typegpu/src/core/function/tgpuComputeFn.ts @@ -120,7 +120,6 @@ function createComputeFn>( shell, $uses(newExternals) { - core.applyExternals(newExternals); core.setExternals('userProvided', newExternals); return this; }, diff --git a/packages/typegpu/src/core/function/tgpuFn.ts b/packages/typegpu/src/core/function/tgpuFn.ts index 2c500fc45f..a9840c57cb 100644 --- a/packages/typegpu/src/core/function/tgpuFn.ts +++ b/packages/typegpu/src/core/function/tgpuFn.ts @@ -181,7 +181,6 @@ function createFn( [$internal]: { implementation }, $uses(newExternals: Record) { - core.applyExternals(newExternals); core.setExternals('userProvided', newExternals); return this; }, diff --git a/packages/typegpu/src/core/function/tgpuFragmentFn.ts b/packages/typegpu/src/core/function/tgpuFragmentFn.ts index e2862fb939..676ad2828b 100644 --- a/packages/typegpu/src/core/function/tgpuFragmentFn.ts +++ b/packages/typegpu/src/core/function/tgpuFragmentFn.ts @@ -194,7 +194,6 @@ function createFragmentFn( outputType, $uses(newExternals) { - core.applyExternals(newExternals); core.setExternals('userProvided', newExternals); return this; }, @@ -215,7 +214,6 @@ function createFragmentFn( if (entryInput.dataSchema && isNamable(entryInput.dataSchema)) { entryInput.dataSchema.$name(`${getName(this) ?? ''}_Input`); } - core.applyExternals({ Out: outputType }); core.setExternals('out', { Out: outputType }); return ctx.withSlots([[shaderStageSlot, 'fragment']], () => diff --git a/packages/typegpu/src/core/function/tgpuVertexFn.ts b/packages/typegpu/src/core/function/tgpuVertexFn.ts index 66839245be..68b7bee7d3 100644 --- a/packages/typegpu/src/core/function/tgpuVertexFn.ts +++ b/packages/typegpu/src/core/function/tgpuVertexFn.ts @@ -165,7 +165,6 @@ function createVertexFn( shell, $uses(newExternals) { - core.applyExternals(newExternals); core.setExternals('userProvided', newExternals); return this; }, @@ -183,7 +182,6 @@ function createVertexFn( ); if (typeof implementation === 'string') { - core.applyExternals({ Out: outputWithLocation }); core.setExternals('out', { Out: outputWithLocation }); } diff --git a/packages/typegpu/src/core/resolve/externals.ts b/packages/typegpu/src/core/resolve/externals.ts index 2082799ad7..303cc7d378 100644 --- a/packages/typegpu/src/core/resolve/externals.ts +++ b/packages/typegpu/src/core/resolve/externals.ts @@ -87,7 +87,6 @@ export function addArgTypesToExternals(implementation: string, argTypes: unknown }), ); - core.applyExternals(args); core.setExternals('args', args); } @@ -100,7 +99,6 @@ export function addReturnTypeToExternals( const outputName = matched ? matched[1]?.trim() : undefined; if (isWgslStruct(returnType) && outputName && !/\s/g.test(outputName)) { - core.applyExternals({ [outputName]: returnType }); core.setExternals('out', { [outputName]: returnType }); } } diff --git a/packages/typegpu/tests/computePipeline.test.ts b/packages/typegpu/tests/computePipeline.test.ts index 3cbea14d08..091a6fece0 100644 --- a/packages/typegpu/tests/computePipeline.test.ts +++ b/packages/typegpu/tests/computePipeline.test.ts @@ -280,11 +280,9 @@ describe('TgpuComputePipeline', () => { data: buffer, }); - const entryFn = tgpu - .computeFn({ workgroupSize: [1] })(() => { - layout.$.data; - }) - .$uses({ layout }); + const entryFn = tgpu.computeFn({ workgroupSize: [1] })(() => { + layout.$.data; + }); const querySet = root.createQuerySet('timestamp', 4); diff --git a/packages/typegpu/tests/tgslFn.test.ts b/packages/typegpu/tests/tgslFn.test.ts index 6b6be0d13c..d44b94d87a 100644 --- a/packages/typegpu/tests/tgslFn.test.ts +++ b/packages/typegpu/tests/tgslFn.test.ts @@ -28,36 +28,30 @@ describe('TGSL tgpu.fn function', () => { }); it('resolves externals', () => { - const getColor = tgpu - .fn( - [], - d.vec3f, - )(() => { - const color = d.vec3f(); - const color2 = d.vec3f(1, 2, 3); - return color; - }) - .$uses({ v: d.vec3f }); + const getColor = tgpu.fn( + [], + d.vec3f, + )(() => { + const color = d.vec3f(); + const color2 = d.vec3f(1, 2, 3); + return color; + }); - const getX = tgpu - .fn( - [], - d.f32, - )(() => { - const color = getColor(); - return 3; - }) - .$uses({ getColor }); + const getX = tgpu.fn( + [], + d.f32, + )(() => { + const color = getColor(); + return 3; + }); - const getY = tgpu - .fn( - [], - d.f32, - )(() => { - const c = getColor(); - return getX(); - }) - .$uses({ getX, getColor }); + const getY = tgpu.fn( + [], + d.f32, + )(() => { + const c = getColor(); + return getX(); + }); expect(tgpu.resolve([getY])).toMatchInlineSnapshot(` "fn getColor() -> vec3f { From 7c7faad51ff717ccf1098d8ed24b3cbd46c28c17 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 19 Jun 2026 13:27:17 +0200 Subject: [PATCH 09/44] More cleanup --- packages/typegpu/src/core/function/fnCore.ts | 1 - .../core/rawCodeSnippet/tgpuRawCodeSnippet.ts | 6 +--- .../typegpu/src/core/resolve/externals.ts | 35 ------------------- packages/typegpu/tests/declare.test.ts | 4 ++- 4 files changed, 4 insertions(+), 42 deletions(-) diff --git a/packages/typegpu/src/core/function/fnCore.ts b/packages/typegpu/src/core/function/fnCore.ts index 696983cde7..4e73230be5 100644 --- a/packages/typegpu/src/core/function/fnCore.ts +++ b/packages/typegpu/src/core/function/fnCore.ts @@ -22,7 +22,6 @@ export type FnExternals = { }; export interface FnCore { - applyExternals: (newExternals: ExternalMap) => void; setExternals: (key: keyof FnExternals, newExternal: ExternalMap) => void; resolve( ctx: ResolutionCtx, diff --git a/packages/typegpu/src/core/rawCodeSnippet/tgpuRawCodeSnippet.ts b/packages/typegpu/src/core/rawCodeSnippet/tgpuRawCodeSnippet.ts index 1f27ea9737..b07010e7f3 100644 --- a/packages/typegpu/src/core/rawCodeSnippet/tgpuRawCodeSnippet.ts +++ b/packages/typegpu/src/core/rawCodeSnippet/tgpuRawCodeSnippet.ts @@ -113,11 +113,7 @@ class TgpuRawCodeSnippetImpl } [$resolve](ctx: ResolutionCtx): ResolvedSnippet { - const replacedExpression = replaceExternalsInWgsl( - ctx, - this.#externals ?? {}, - this.#expression, - ); + const replacedExpression = replaceExternalsInWgsl(ctx, this.#externals ?? {}, this.#expression); return snip(replacedExpression, this.dataType, this.origin); } diff --git a/packages/typegpu/src/core/resolve/externals.ts b/packages/typegpu/src/core/resolve/externals.ts index 303cc7d378..1698e5a92b 100644 --- a/packages/typegpu/src/core/resolve/externals.ts +++ b/packages/typegpu/src/core/resolve/externals.ts @@ -19,9 +19,6 @@ function isResolvable(value: unknown) { * Assumes that there is at most one map with non-trivial structure. */ export function mergeFunctionExternals(fnExternals: FnExternals): ExternalMap { - console.log('MERGING'); - console.log(fnExternals); - if (fnExternals.pluginProvided !== undefined && fnExternals.userProvided !== undefined) { throw new Error( "Cannot call '$uses' on functions whose metadata was provided by unplugin-typegpu.", @@ -43,38 +40,6 @@ export function mergeFunctionExternals(fnExternals: FnExternals): ExternalMap { return result; } -/** - * Merges two external maps into one. - * If the external value is a namable object, it is given a name if it does not already have one. - * @param existing - The existing external map. - * @param newExternals - The new external map. - * - * NOTE: - * This function attempts to avoid accidental reference modification - * by performing a shallow copy before each modification, - * but it cannot avoid `existing` modification. - * Make sure that `existing` is created internally, instead of being passed in by users. - */ -export function mergeExternals(existing: ExternalMap, newExternals: ExternalMap) { - for (const [key, value] of Object.entries(newExternals)) { - const existingValue = existing[key]; - if ( - existingValue !== null && - typeof existingValue === 'object' && - value !== null && - typeof value === 'object' && - !isResolvable(existingValue) && - !isResolvable(value) - ) { - const copiedValue = { ...(existingValue as ExternalMap) }; - mergeExternals(copiedValue, value as ExternalMap); - existing[key] = copiedValue; - } else { - existing[key] = value; - } - } -} - export function addArgTypesToExternals(implementation: string, argTypes: unknown[], core: FnCore) { const argTypeNames = [...implementation.matchAll(/:\s*(?.*?)\s*[,)]/g)].map((found) => found ? found[1] : undefined, diff --git a/packages/typegpu/tests/declare.test.ts b/packages/typegpu/tests/declare.test.ts index 8883c5e3fd..2b018c3192 100644 --- a/packages/typegpu/tests/declare.test.ts +++ b/packages/typegpu/tests/declare.test.ts @@ -113,6 +113,8 @@ struct Output { expect(() => declaration.$uses({ myStruct: d.struct({ p: d.u32 }) }), - ).toThrowErrorMatchingInlineSnapshot(`[Error: Cannot call '$uses' multiple times. If you wish to override dependencies, use slots or accessors instead.]`); + ).toThrowErrorMatchingInlineSnapshot( + `[Error: Cannot call '$uses' multiple times. If you wish to override dependencies, use slots or accessors instead.]`, + ); }); }); From a03b83fb22efc41e8cf596fffb2ca5f9c22a6f25 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 19 Jun 2026 15:58:50 +0200 Subject: [PATCH 10/44] Detect $uses on function with plugin metadata earlier --- packages/typegpu/src/core/function/fnCore.ts | 17 ++++++++++++----- packages/typegpu/src/core/resolve/externals.ts | 5 ----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/typegpu/src/core/function/fnCore.ts b/packages/typegpu/src/core/function/fnCore.ts index 4e73230be5..b2bd340c15 100644 --- a/packages/typegpu/src/core/function/fnCore.ts +++ b/packages/typegpu/src/core/function/fnCore.ts @@ -62,11 +62,18 @@ export function createFnCore( [$getNameForward]: typeof implementation === 'function' ? implementation : undefined, setExternals(key: keyof FnExternals, newExternal: ExternalMap): void { - if (key === 'userProvided' && key in externals) { - // other external keys may be set multiple times by multiple resolves - throw new Error( - "Cannot call '$uses' multiple times. If you wish to override dependencies, use slots or accessors instead.", - ); + if (key === 'userProvided') { + if ('userProvided' in externals) { + // other external keys may be set multiple times by multiple resolves + throw new Error( + "Cannot call '$uses' multiple times. If you wish to override dependencies, use slots or accessors instead.", + ); + } + if ('pluginProvided' in externals) { + throw new Error( + "Cannot call '$uses' on functions whose metadata was provided by unplugin-typegpu.", + ); + } } externals[key] = newExternal; }, diff --git a/packages/typegpu/src/core/resolve/externals.ts b/packages/typegpu/src/core/resolve/externals.ts index 1698e5a92b..a1accaacdb 100644 --- a/packages/typegpu/src/core/resolve/externals.ts +++ b/packages/typegpu/src/core/resolve/externals.ts @@ -19,11 +19,6 @@ function isResolvable(value: unknown) { * Assumes that there is at most one map with non-trivial structure. */ export function mergeFunctionExternals(fnExternals: FnExternals): ExternalMap { - if (fnExternals.pluginProvided !== undefined && fnExternals.userProvided !== undefined) { - throw new Error( - "Cannot call '$uses' on functions whose metadata was provided by unplugin-typegpu.", - ); - } const base = fnExternals.pluginProvided ?? fnExternals.userProvided ?? {}; // avoid calling any of the getters const result: ExternalMap = Object.defineProperties({}, Object.getOwnPropertyDescriptors(base)); From 1a16a8b3daa5de1a2ec63c789d55eec2904f4bd9 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 19 Jun 2026 16:57:06 +0200 Subject: [PATCH 11/44] Add FnExternals docs --- packages/typegpu/src/core/function/fnCore.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/typegpu/src/core/function/fnCore.ts b/packages/typegpu/src/core/function/fnCore.ts index b2bd340c15..9988a549f7 100644 --- a/packages/typegpu/src/core/function/fnCore.ts +++ b/packages/typegpu/src/core/function/fnCore.ts @@ -15,9 +15,25 @@ import { extractArgs } from './extractArgs.ts'; import type { Implementation, SeparatedEntryArgs } from './fnTypes.ts'; export type FnExternals = { + /** + * Externals provided by calling `$uses()`. + * May be nested. + */ userProvided?: ExternalMap; + /** + * Externals provided by unplugin-typegpu via function metadata. + * May be nested. + */ pluginProvided?: ExternalMap; + /** + * Function arguments, for example `{ S: Schema }` in `tgpu.fn([Schema])('(arg: S) => {}')`. + * Must be flat (every value must be resolvable). + */ args?: ExternalMap; + /** + * Function return type, for example `{ Out: ... }` in rawWgsl entrypoint functions. + * Must be flat (every value must be resolvable). + */ out?: ExternalMap; }; From 9f336913e5c405ad72ee9b300863937f3dd75871 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 19 Jun 2026 16:57:26 +0200 Subject: [PATCH 12/44] Add external name collision tests --- packages/typegpu/tests/externals.test.ts | 76 ++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/packages/typegpu/tests/externals.test.ts b/packages/typegpu/tests/externals.test.ts index 841d5c7a33..15d8afea65 100644 --- a/packages/typegpu/tests/externals.test.ts +++ b/packages/typegpu/tests/externals.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; import { addArgTypesToExternals, type ExternalMap } from '../src/core/resolve/externals.ts'; import * as d from '../src/data/index.ts'; +import tgpu from '../src/index.js'; // TODO: fix these tests describe('addArgTypesToExternals', () => { @@ -60,3 +61,78 @@ describe('addArgTypesToExternals', () => { // expect(externals).toStrictEqual([{ A: Particle, B: Particle, C: Particle }]); }); }); + +describe('external name collisions', () => { + it("throws when rawWgsl fn has an 'Out' external", () => { + const vertexFn = tgpu.vertexFn({ + out: { position: d.builtin.position }, + })`{ return Out(); }`.$uses({ Out: d.struct({ prop: d.u32 }).$name('myOut') }); + + expect(() => tgpu.resolve([vertexFn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - vertexFn:vertexFn: Key 'Out' appears in externals while being reserved for internals. Please rename this external.] + `); + }); + + it("allows an 'Out' external in TGSL implemented functions", () => { + const Out = d.struct({ prop: d.u32 }); + const vertexFn = tgpu.vertexFn({ + out: { position: d.builtin.position }, + })(() => { + 'use gpu'; + const out = Out(); + return { position: d.vec4f() }; + }); + + expect(tgpu.resolve([vertexFn])).toMatchInlineSnapshot(` + "struct Out { + prop: u32, + } + + struct vertexFn_Output { + @builtin(position) position: vec4f, + } + + @vertex fn vertexFn() -> vertexFn_Output { + let out = Out(); + return vertexFn_Output(vec4f()); + }" + `); + }); + + it("throws when rawWgsl fn has an 'in' external", () => { + const vertexFn = tgpu.vertexFn({ + in: { vId: d.builtin.vertexIndex }, + out: { position: d.builtin.position }, + })`{ return d.vec4f(in); }`.$uses({ in: 1 }); + + expect(() => tgpu.resolve([vertexFn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - vertexFn:vertexFn: Key 'in' appears in externals while being reserved for internals. Please rename this external.] + `); + }); + + it("allows an 'in' external in TGSL implemented functions", () => { + const EXT = { in: 1 }; + const vertexFn = tgpu.vertexFn({ + out: { position: d.builtin.position }, + })(() => { + 'use gpu'; + const x = EXT.in; + return { position: d.vec4f() }; + }); + + expect(tgpu.resolve([vertexFn])).toMatchInlineSnapshot(` + "struct vertexFn_Output { + @builtin(position) position: vec4f, + } + + @vertex fn vertexFn() -> vertexFn_Output { + const x = 1; + return vertexFn_Output(vec4f()); + }" + `); + }); +}); From 052f36b39bd07ec7ef1a2389107db03a204925ba Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 19 Jun 2026 17:13:52 +0200 Subject: [PATCH 13/44] Add more tests & fix fragment out external --- .../src/core/function/tgpuFragmentFn.ts | 4 +- packages/typegpu/tests/externals.test.ts | 51 ++++++++++++++++++- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/packages/typegpu/src/core/function/tgpuFragmentFn.ts b/packages/typegpu/src/core/function/tgpuFragmentFn.ts index 676ad2828b..7409cbdbb5 100644 --- a/packages/typegpu/src/core/function/tgpuFragmentFn.ts +++ b/packages/typegpu/src/core/function/tgpuFragmentFn.ts @@ -214,7 +214,9 @@ function createFragmentFn( if (entryInput.dataSchema && isNamable(entryInput.dataSchema)) { entryInput.dataSchema.$name(`${getName(this) ?? ''}_Input`); } - core.setExternals('out', { Out: outputType }); + if (typeof implementation === 'string') { + core.setExternals('out', { Out: outputType }); + } return ctx.withSlots([[shaderStageSlot, 'fragment']], () => core.resolve(ctx, [], shell.returnType, entryInput), diff --git a/packages/typegpu/tests/externals.test.ts b/packages/typegpu/tests/externals.test.ts index 15d8afea65..8beb2abf4a 100644 --- a/packages/typegpu/tests/externals.test.ts +++ b/packages/typegpu/tests/externals.test.ts @@ -66,13 +66,22 @@ describe('external name collisions', () => { it("throws when rawWgsl fn has an 'Out' external", () => { const vertexFn = tgpu.vertexFn({ out: { position: d.builtin.position }, - })`{ return Out(); }`.$uses({ Out: d.struct({ prop: d.u32 }).$name('myOut') }); + })`{ return Out(); }`.$uses({ Out: d.struct({ prop: d.u32 }) }); + const fragmentFn = tgpu.fragmentFn({ + out: { color: d.location(0, d.vec4f) }, + })`{ return Out(); }`.$uses({ Out: d.struct({ prop: d.u32 }) }); expect(() => tgpu.resolve([vertexFn])).toThrowErrorMatchingInlineSnapshot(` [Error: Resolution of the following tree failed: - - vertexFn:vertexFn: Key 'Out' appears in externals while being reserved for internals. Please rename this external.] `); + + expect(() => tgpu.resolve([fragmentFn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fragmentFn:fragmentFn: Key 'Out' appears in externals while being reserved for internals. Please rename this external.] + `); }); it("allows an 'Out' external in TGSL implemented functions", () => { @@ -84,6 +93,13 @@ describe('external name collisions', () => { const out = Out(); return { position: d.vec4f() }; }); + const fragmentFn = tgpu.fragmentFn({ + out: d.vec4f, + })(() => { + 'use gpu'; + const out = Out(); + return d.vec4f(); + }); expect(tgpu.resolve([vertexFn])).toMatchInlineSnapshot(` "struct Out { @@ -99,6 +115,17 @@ describe('external name collisions', () => { return vertexFn_Output(vec4f()); }" `); + + expect(tgpu.resolve([fragmentFn])).toMatchInlineSnapshot(` + "struct Out { + prop: u32, + } + + @fragment fn fragmentFn() -> @location(0) vec4f { + let out = Out(); + return vec4f(); + }" + `); }); it("throws when rawWgsl fn has an 'in' external", () => { @@ -106,12 +133,21 @@ describe('external name collisions', () => { in: { vId: d.builtin.vertexIndex }, out: { position: d.builtin.position }, })`{ return d.vec4f(in); }`.$uses({ in: 1 }); + const fragmentFn = tgpu.fragmentFn({ + in: { uv: d.vec2f }, + out: d.vec4f, + })`{ return d.vec4f(in); }`.$uses({ in: 1 }); expect(() => tgpu.resolve([vertexFn])).toThrowErrorMatchingInlineSnapshot(` [Error: Resolution of the following tree failed: - - vertexFn:vertexFn: Key 'in' appears in externals while being reserved for internals. Please rename this external.] `); + expect(() => tgpu.resolve([fragmentFn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fragmentFn:fragmentFn: Key 'in' appears in externals while being reserved for internals. Please rename this external.] + `); }); it("allows an 'in' external in TGSL implemented functions", () => { @@ -123,6 +159,13 @@ describe('external name collisions', () => { const x = EXT.in; return { position: d.vec4f() }; }); + const fragmentFn = tgpu.fragmentFn({ + out: d.vec4f, + })(() => { + 'use gpu'; + const x = EXT.in; + return d.vec4f(); + }); expect(tgpu.resolve([vertexFn])).toMatchInlineSnapshot(` "struct vertexFn_Output { @@ -134,5 +177,11 @@ describe('external name collisions', () => { return vertexFn_Output(vec4f()); }" `); + expect(tgpu.resolve([fragmentFn])).toMatchInlineSnapshot(` + "@fragment fn fragmentFn() -> @location(0) vec4f { + const x = 1; + return vec4f(); + }" + `); }); }); From 5ec7052d1a8874c58f6c0b553128578058d3359b Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 19 Jun 2026 17:28:11 +0200 Subject: [PATCH 14/44] More tests && allow redundant external --- .../typegpu/src/core/resolve/externals.ts | 4 +- packages/typegpu/tests/externals.test.ts | 64 ++++++++++++++++--- 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/packages/typegpu/src/core/resolve/externals.ts b/packages/typegpu/src/core/resolve/externals.ts index a1accaacdb..882ff83d84 100644 --- a/packages/typegpu/src/core/resolve/externals.ts +++ b/packages/typegpu/src/core/resolve/externals.ts @@ -24,9 +24,9 @@ export function mergeFunctionExternals(fnExternals: FnExternals): ExternalMap { const result: ExternalMap = Object.defineProperties({}, Object.getOwnPropertyDescriptors(base)); for (const flatExternal of [fnExternals.args, fnExternals.out].filter((e) => e !== undefined)) { for (const [key, value] of Object.entries(flatExternal)) { - if (key in result) { + if (key in result && result[key] !== value) { throw new Error( - `Key '${key}' appears in externals while being reserved for internals. Please rename this external.`, + `Key '${key}' appears in externals despite already being used for argument/return type. Please rename this external.`, ); } result[key] = value; diff --git a/packages/typegpu/tests/externals.test.ts b/packages/typegpu/tests/externals.test.ts index 8beb2abf4a..2f0e273800 100644 --- a/packages/typegpu/tests/externals.test.ts +++ b/packages/typegpu/tests/externals.test.ts @@ -63,7 +63,7 @@ describe('addArgTypesToExternals', () => { }); describe('external name collisions', () => { - it("throws when rawWgsl fn has an 'Out' external", () => { + it("throws when rawWgsl entrypoint has an 'Out' external", () => { const vertexFn = tgpu.vertexFn({ out: { position: d.builtin.position }, })`{ return Out(); }`.$uses({ Out: d.struct({ prop: d.u32 }) }); @@ -74,17 +74,17 @@ describe('external name collisions', () => { expect(() => tgpu.resolve([vertexFn])).toThrowErrorMatchingInlineSnapshot(` [Error: Resolution of the following tree failed: - - - vertexFn:vertexFn: Key 'Out' appears in externals while being reserved for internals. Please rename this external.] + - vertexFn:vertexFn: Key 'Out' appears in externals despite already being used for argument/return type. Please rename this external.] `); expect(() => tgpu.resolve([fragmentFn])).toThrowErrorMatchingInlineSnapshot(` [Error: Resolution of the following tree failed: - - - fragmentFn:fragmentFn: Key 'Out' appears in externals while being reserved for internals. Please rename this external.] + - fragmentFn:fragmentFn: Key 'Out' appears in externals despite already being used for argument/return type. Please rename this external.] `); }); - it("allows an 'Out' external in TGSL implemented functions", () => { + it("allows an 'Out' external in TGSL implemented entrypoints", () => { const Out = d.struct({ prop: d.u32 }); const vertexFn = tgpu.vertexFn({ out: { position: d.builtin.position }, @@ -128,7 +128,7 @@ describe('external name collisions', () => { `); }); - it("throws when rawWgsl fn has an 'in' external", () => { + it("throws when rawWgsl entrypoint has an 'in' external", () => { const vertexFn = tgpu.vertexFn({ in: { vId: d.builtin.vertexIndex }, out: { position: d.builtin.position }, @@ -141,16 +141,16 @@ describe('external name collisions', () => { expect(() => tgpu.resolve([vertexFn])).toThrowErrorMatchingInlineSnapshot(` [Error: Resolution of the following tree failed: - - - vertexFn:vertexFn: Key 'in' appears in externals while being reserved for internals. Please rename this external.] + - vertexFn:vertexFn: Key 'in' appears in externals despite already being used for argument/return type. Please rename this external.] `); expect(() => tgpu.resolve([fragmentFn])).toThrowErrorMatchingInlineSnapshot(` [Error: Resolution of the following tree failed: - - - fragmentFn:fragmentFn: Key 'in' appears in externals while being reserved for internals. Please rename this external.] + - fragmentFn:fragmentFn: Key 'in' appears in externals despite already being used for argument/return type. Please rename this external.] `); }); - it("allows an 'in' external in TGSL implemented functions", () => { + it("allows an 'in' external in TGSL implemented entrypoints", () => { const EXT = { in: 1 }; const vertexFn = tgpu.vertexFn({ out: { position: d.builtin.position }, @@ -184,4 +184,52 @@ describe('external name collisions', () => { }" `); }); + + it('throws when rawWgsl fn has an external colliding with argument type', () => { + const Schema = d.struct({ p: d.u32 }); + const myFn = tgpu.fn([Schema])`(a: S) { let b = S(); }`.$uses({ S: 1 }); + + expect(() => tgpu.resolve([myFn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn:myFn: Key 'S' appears in externals despite already being used for argument/return type. Please rename this external.] + `); + }); + + it('allows redundant external colliding with argument type', () => { + const Schema = d.struct({ p: d.u32 }); + const myFn = tgpu.fn([Schema])`(a: S) { let b = S(); }`.$uses({ S: Schema }); + + expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(` + "struct Schema { + p: u32, + } + + fn myFn(a: Schema) { let b = Schema(); }" + `); + }); + + it('throws when rawWgsl fn has an external colliding with return type', () => { + const Schema = d.struct({ p: d.u32 }); + const myFn = tgpu.fn([], Schema)`() -> S { let a = S(); }`.$uses({ S: 1 }); + + expect(() => tgpu.resolve([myFn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn:myFn: Key 'S' appears in externals despite already being used for argument/return type. Please rename this external.] + `); + }); + + it('allows redundant external colliding with return type', () => { + const Schema = d.struct({ p: d.u32 }); + const myFn = tgpu.fn([], Schema)`() -> S { let a = S(); }`.$uses({ S: Schema }); + + expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(` + "struct Schema { + p: u32, + } + + fn myFn() -> Schema { let a = Schema(); }" + `); + }); }); From 54b8b27bc41ab9c63dd7e046af42ed97e8fc96ad Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 19 Jun 2026 17:39:11 +0200 Subject: [PATCH 15/44] Update docs --- packages/typegpu/src/core/function/fnCore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typegpu/src/core/function/fnCore.ts b/packages/typegpu/src/core/function/fnCore.ts index 9988a549f7..5ad24a8ac1 100644 --- a/packages/typegpu/src/core/function/fnCore.ts +++ b/packages/typegpu/src/core/function/fnCore.ts @@ -31,7 +31,7 @@ export type FnExternals = { */ args?: ExternalMap; /** - * Function return type, for example `{ Out: ... }` in rawWgsl entrypoint functions. + * Function return type, for example `{ Out: ... }` in both rawWgsl entrypoint functions and `vertexFnShell(in, Out)`. * Must be flat (every value must be resolvable). */ out?: ExternalMap; From d7d48aefd0c969ecac7f00552358bc97800c7d5a Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Mon, 22 Jun 2026 10:06:24 +0200 Subject: [PATCH 16/44] Add compute tests --- packages/typegpu/tests/externals.test.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/typegpu/tests/externals.test.ts b/packages/typegpu/tests/externals.test.ts index 2f0e273800..beb6b99a65 100644 --- a/packages/typegpu/tests/externals.test.ts +++ b/packages/typegpu/tests/externals.test.ts @@ -137,6 +137,10 @@ describe('external name collisions', () => { in: { uv: d.vec2f }, out: d.vec4f, })`{ return d.vec4f(in); }`.$uses({ in: 1 }); + const computeFn = tgpu.computeFn({ + in: { gid: d.builtin.globalInvocationId }, + workgroupSize: [1], + })`{ let x = in; }`.$uses({ in: 1 }); expect(() => tgpu.resolve([vertexFn])).toThrowErrorMatchingInlineSnapshot(` [Error: Resolution of the following tree failed: @@ -148,6 +152,11 @@ describe('external name collisions', () => { - - fragmentFn:fragmentFn: Key 'in' appears in externals despite already being used for argument/return type. Please rename this external.] `); + expect(() => tgpu.resolve([computeFn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - computeFn:computeFn: Key 'in' appears in externals despite already being used for argument/return type. Please rename this external.] + `); }); it("allows an 'in' external in TGSL implemented entrypoints", () => { @@ -166,6 +175,12 @@ describe('external name collisions', () => { const x = EXT.in; return d.vec4f(); }); + const computeFn = tgpu.computeFn({ + workgroupSize: [1], + })(() => { + 'use gpu'; + const x = EXT.in; + }); expect(tgpu.resolve([vertexFn])).toMatchInlineSnapshot(` "struct vertexFn_Output { @@ -183,6 +198,11 @@ describe('external name collisions', () => { return vec4f(); }" `); + expect(tgpu.resolve([computeFn])).toMatchInlineSnapshot(` + "@compute @workgroup_size(1) fn computeFn() { + const x = 1; + }" + `); }); it('throws when rawWgsl fn has an external colliding with argument type', () => { From fc8b1478b8101f6638c27ce3f55b95da5481e940 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Mon, 22 Jun 2026 10:10:57 +0200 Subject: [PATCH 17/44] Move external tests to external.test.ts --- packages/typegpu/tests/externals.test.ts | 29 +++++++++++++++++++++++ packages/typegpu/tests/function.test.ts | 30 ------------------------ 2 files changed, 29 insertions(+), 30 deletions(-) diff --git a/packages/typegpu/tests/externals.test.ts b/packages/typegpu/tests/externals.test.ts index beb6b99a65..f81102b111 100644 --- a/packages/typegpu/tests/externals.test.ts +++ b/packages/typegpu/tests/externals.test.ts @@ -252,4 +252,33 @@ describe('external name collisions', () => { fn myFn() -> Schema { let a = Schema(); }" `); }); + + it('does not modify original externals', () => { + const ext = { out: 1 }; + const vertexFn = tgpu + .vertexFn({ + in: { vId: d.builtin.vertexIndex }, + out: { position: d.builtin.position }, + })((input, Out) => { + const x = input.vId; + return Out(); + }) + .$uses(ext); + + tgpu.resolve([vertexFn]); + + expect(ext).toStrictEqual({ out: 1 }); + }); + + it('does not break when an unused unresolvable external is passed', () => { + const fn = tgpu.fn([])`() { + let a = ext; + }`.$uses({ ext: 1, unused: () => {} }); + + expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` + "fn fn_1() { + let a = 1; + }" + `); + }); }); diff --git a/packages/typegpu/tests/function.test.ts b/packages/typegpu/tests/function.test.ts index f44c3c9dd0..0940c5045a 100644 --- a/packages/typegpu/tests/function.test.ts +++ b/packages/typegpu/tests/function.test.ts @@ -83,36 +83,6 @@ describe('tgpu.fn', () => { TgpuFn<(arg_0: d.F32, arg_1: d.U32) => d.Bool> >(); }); - - it('does not mutate values of original externals', () => { - // TODO: check out/in keys for entry points - // const SHARED_EXT = { EXT: { X: 1 } }; - // const fn = tgpu.fn([])`() { - // let a = EXT.X; - // let b = EXT.Y; - // }` - // .$uses(SHARED_EXT) - // .$uses({ EXT: { Y: 2 } }); - // expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` - // "fn fn_1() { - // let a = 1; - // let b = 2; - // }" - // `); - // expect(SHARED_EXT).toStrictEqual({ EXT: { X: 1 } }); - }); - - it('does not break when an unused unresolvable external is passed', () => { - const fn = tgpu.fn([])`() { - let a = ext; -}`.$uses({ ext: 1, unused: () => {} }); - - expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` - "fn fn_1() { - let a = 1; - }" - `); - }); }); describe('tgpu.computeFn', () => { From cfb074a3ca42caf835317d08580df2845b6baca6 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Mon, 22 Jun 2026 10:59:04 +0200 Subject: [PATCH 18/44] Fix old external tests by tweaking the api --- .../typegpu/src/core/resolve/externals.ts | 8 +- packages/typegpu/tests/externals.test.ts | 74 +++++++++++-------- 2 files changed, 48 insertions(+), 34 deletions(-) diff --git a/packages/typegpu/src/core/resolve/externals.ts b/packages/typegpu/src/core/resolve/externals.ts index 882ff83d84..181637750c 100644 --- a/packages/typegpu/src/core/resolve/externals.ts +++ b/packages/typegpu/src/core/resolve/externals.ts @@ -35,7 +35,11 @@ export function mergeFunctionExternals(fnExternals: FnExternals): ExternalMap { return result; } -export function addArgTypesToExternals(implementation: string, argTypes: unknown[], core: FnCore) { +export function addArgTypesToExternals( + implementation: string, + argTypes: unknown[], + core: { setExternals: (key: 'args', externals: ExternalMap) => void }, +) { const argTypeNames = [...implementation.matchAll(/:\s*(?.*?)\s*[,)]/g)].map((found) => found ? found[1] : undefined, ); @@ -53,7 +57,7 @@ export function addArgTypesToExternals(implementation: string, argTypes: unknown export function addReturnTypeToExternals( implementation: string, returnType: unknown, - core: FnCore, + core: { setExternals: (key: 'out', externals: ExternalMap) => void }, ) { const matched = implementation.match(/->\s(?[\w\d_]+)\s{/); const outputName = matched ? matched[1]?.trim() : undefined; diff --git a/packages/typegpu/tests/externals.test.ts b/packages/typegpu/tests/externals.test.ts index f81102b111..52fce256db 100644 --- a/packages/typegpu/tests/externals.test.ts +++ b/packages/typegpu/tests/externals.test.ts @@ -17,48 +17,58 @@ describe('addArgTypesToExternals', () => { it('extracts struct argument types with their names', () => { const externals: ExternalMap[] = []; - // addArgTypesToExternals( - // '(a: vec4f, b: Particle, c: Light) {}', - // [d.vec4f, Particle, Light], - // (result) => externals.push(result), - // ); - // expect(externals).toStrictEqual([{ Particle, Light }]); + addArgTypesToExternals('(a: vec4f, b: Particle, c: Light) {}', [d.vec4f, Particle, Light], { + setExternals(_, result) { + externals.push(result); + }, + }); + expect(externals).toStrictEqual([{ Particle, Light }]); }); it('gets the names from argument list in WGSL implementation', () => { const externals: ExternalMap[] = []; - // addArgTypesToExternals('(b: P, a: vec4f, c: L) -> L {}', [Particle, d.vec4f, Light], (result) => - // externals.push(result), - // ); - // expect(externals).toStrictEqual([{ P: Particle, L: Light }]); + addArgTypesToExternals('(b: P, a: vec4f, c: L) -> L {}', [Particle, d.vec4f, Light], { + setExternals(_, result) { + externals.push(result); + }, + }); + expect(externals).toStrictEqual([{ P: Particle, L: Light }]); }); it('works when builtins are present', () => { const externals: ExternalMap[] = []; - // addArgTypesToExternals( - // '(@builtin(workgroup_id) WorkGroupID : vec3u, a: vec4f, b: Particle, c: Light) {}', - // [d.vec3u, d.vec4f, Particle, Light], - // (result) => externals.push(result), - // ); - // expect(externals).toStrictEqual([{ Particle, Light }]); + addArgTypesToExternals( + '(@builtin(workgroup_id) WorkGroupID : vec3u, a: vec4f, b: Particle, c: Light) {}', + [d.vec3u, d.vec4f, Particle, Light], + { + setExternals(_, result) { + externals.push(result); + }, + }, + ); + expect(externals).toStrictEqual([{ Particle, Light }]); }); it('works with unusual whitespace', () => { const externals: ExternalMap[] = []; - // addArgTypesToExternals( - // ` WorkGroupID : vec3u - // , - // a : A , - // (@builtin(workgroup_id) b - - // : B, - - // c: C - // ) -> vec4f {}`, - // [d.vec3u, Particle, Particle, Particle], - // (result) => externals.push(result), - // ); - // expect(externals).toStrictEqual([{ A: Particle, B: Particle, C: Particle }]); + addArgTypesToExternals( + ` WorkGroupID : vec3u + , + a : A , + (@builtin(workgroup_id) b + + : B, + + c: C + ) -> vec4f {}`, + [d.vec3u, Particle, Particle, Particle], + { + setExternals(_, result) { + externals.push(result); + }, + }, + ); + expect(externals).toStrictEqual([{ A: Particle, B: Particle, C: Particle }]); }); }); @@ -276,9 +286,9 @@ describe('external name collisions', () => { }`.$uses({ ext: 1, unused: () => {} }); expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` - "fn fn_1() { + "fn fn_1() { let a = 1; }" - `); + `); }); }); From 5b26ee61920f876bf911a8543c1d6a5ce259b0b1 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Mon, 22 Jun 2026 11:03:59 +0200 Subject: [PATCH 19/44] nr fix --- packages/typegpu/src/core/resolve/externals.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typegpu/src/core/resolve/externals.ts b/packages/typegpu/src/core/resolve/externals.ts index 181637750c..330fdbad47 100644 --- a/packages/typegpu/src/core/resolve/externals.ts +++ b/packages/typegpu/src/core/resolve/externals.ts @@ -2,7 +2,7 @@ import { isLooseData } from '../../data/dataTypes.ts'; import { isWgslStruct } from '../../data/wgslTypes.ts'; import { getName, hasTinyestMetadata, isNamable, setName } from '../../shared/meta.ts'; import { isWgsl, type ResolutionCtx } from '../../types.ts'; -import type { FnCore, FnExternals } from '../function/fnCore.ts'; +import type { FnExternals } from '../function/fnCore.ts'; /** * A key-value mapping where keys represent identifiers within shader code, From 29b0354c66e413b2f42be01d535fb8cf60e817a0 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Mon, 22 Jun 2026 11:24:24 +0200 Subject: [PATCH 20/44] self-review fixes --- packages/typegpu/src/core/function/fnCore.ts | 8 ++------ packages/typegpu/src/core/resolve/externals.ts | 1 - packages/typegpu/tests/externals.test.ts | 17 ++++++++--------- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/packages/typegpu/src/core/function/fnCore.ts b/packages/typegpu/src/core/function/fnCore.ts index 5ad24a8ac1..c985791f7f 100644 --- a/packages/typegpu/src/core/function/fnCore.ts +++ b/packages/typegpu/src/core/function/fnCore.ts @@ -17,22 +17,18 @@ import type { Implementation, SeparatedEntryArgs } from './fnTypes.ts'; export type FnExternals = { /** * Externals provided by calling `$uses()`. - * May be nested. */ userProvided?: ExternalMap; /** * Externals provided by unplugin-typegpu via function metadata. - * May be nested. */ pluginProvided?: ExternalMap; /** - * Function arguments, for example `{ S: Schema }` in `tgpu.fn([Schema])('(arg: S) => {}')`. - * Must be flat (every value must be resolvable). + * Function arguments, for example `{ in: { S: Schema } }` in `tgpu.fn([Schema])('(arg: S) => {}')`. */ args?: ExternalMap; /** * Function return type, for example `{ Out: ... }` in both rawWgsl entrypoint functions and `vertexFnShell(in, Out)`. - * Must be flat (every value must be resolvable). */ out?: ExternalMap; }; @@ -79,8 +75,8 @@ export function createFnCore( setExternals(key: keyof FnExternals, newExternal: ExternalMap): void { if (key === 'userProvided') { + // other external keys may be set multiple times by multiple resolves if ('userProvided' in externals) { - // other external keys may be set multiple times by multiple resolves throw new Error( "Cannot call '$uses' multiple times. If you wish to override dependencies, use slots or accessors instead.", ); diff --git a/packages/typegpu/src/core/resolve/externals.ts b/packages/typegpu/src/core/resolve/externals.ts index 330fdbad47..0dca0ae376 100644 --- a/packages/typegpu/src/core/resolve/externals.ts +++ b/packages/typegpu/src/core/resolve/externals.ts @@ -16,7 +16,6 @@ function isResolvable(value: unknown) { /** * Merges function externals into one map. - * Assumes that there is at most one map with non-trivial structure. */ export function mergeFunctionExternals(fnExternals: FnExternals): ExternalMap { const base = fnExternals.pluginProvided ?? fnExternals.userProvided ?? {}; diff --git a/packages/typegpu/tests/externals.test.ts b/packages/typegpu/tests/externals.test.ts index 52fce256db..1c45c95a45 100644 --- a/packages/typegpu/tests/externals.test.ts +++ b/packages/typegpu/tests/externals.test.ts @@ -3,7 +3,6 @@ import { addArgTypesToExternals, type ExternalMap } from '../src/core/resolve/ex import * as d from '../src/data/index.ts'; import tgpu from '../src/index.js'; -// TODO: fix these tests describe('addArgTypesToExternals', () => { const Particle = d.struct({ position: d.vec3f, @@ -53,14 +52,14 @@ describe('addArgTypesToExternals', () => { const externals: ExternalMap[] = []; addArgTypesToExternals( ` WorkGroupID : vec3u - , - a : A , - (@builtin(workgroup_id) b - - : B, - - c: C - ) -> vec4f {}`, + , + a : A , + (@builtin(workgroup_id) b + + : B, + + c: C + ) -> vec4f {}`, [d.vec3u, Particle, Particle, Particle], { setExternals(_, result) { From f9a4385b489f6c20fdfff2a9d0822a42ae6787af Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:19:57 +0200 Subject: [PATCH 21/44] Fix multiple .$uses error --- packages/typegpu/src/core/function/fnCore.ts | 21 +++++------ packages/typegpu/tests/externals.test.ts | 39 ++++++++++++++------ 2 files changed, 37 insertions(+), 23 deletions(-) diff --git a/packages/typegpu/src/core/function/fnCore.ts b/packages/typegpu/src/core/function/fnCore.ts index c985791f7f..86aab12164 100644 --- a/packages/typegpu/src/core/function/fnCore.ts +++ b/packages/typegpu/src/core/function/fnCore.ts @@ -74,20 +74,17 @@ export function createFnCore( [$getNameForward]: typeof implementation === 'function' ? implementation : undefined, setExternals(key: keyof FnExternals, newExternal: ExternalMap): void { - if (key === 'userProvided') { - // other external keys may be set multiple times by multiple resolves - if ('userProvided' in externals) { - throw new Error( - "Cannot call '$uses' multiple times. If you wish to override dependencies, use slots or accessors instead.", - ); - } - if ('pluginProvided' in externals) { - throw new Error( - "Cannot call '$uses' on functions whose metadata was provided by unplugin-typegpu.", - ); - } + if (key === 'userProvided' && 'userProvided' in externals) { + throw new Error( + "Cannot call '$uses' multiple times. If you wish to override dependencies, use slots or accessors instead.", + ); } externals[key] = newExternal; + if (externals.userProvided && externals.pluginProvided) { + throw new Error( + "Cannot call '$uses' on functions whose metadata was provided by unplugin-typegpu.", + ); + } }, resolve( diff --git a/packages/typegpu/tests/externals.test.ts b/packages/typegpu/tests/externals.test.ts index 1c45c95a45..c645a85743 100644 --- a/packages/typegpu/tests/externals.test.ts +++ b/packages/typegpu/tests/externals.test.ts @@ -263,20 +263,15 @@ describe('external name collisions', () => { }); it('does not modify original externals', () => { - const ext = { out: 1 }; - const vertexFn = tgpu - .vertexFn({ - in: { vId: d.builtin.vertexIndex }, - out: { position: d.builtin.position }, - })((input, Out) => { - const x = input.vId; - return Out(); - }) - .$uses(ext); + const ext = { n: 1 }; + const vertexFn = tgpu.vertexFn({ + in: { vId: d.builtin.vertexIndex }, + out: { position: d.builtin.position }, + })`{ let a = n; return Out(); }`.$uses(ext); tgpu.resolve([vertexFn]); - expect(ext).toStrictEqual({ out: 1 }); + expect(ext).toStrictEqual({ n: 1 }); }); it('does not break when an unused unresolvable external is passed', () => { @@ -290,4 +285,26 @@ describe('external name collisions', () => { }" `); }); + + it("throws when calling '$uses' on a function with metadata", () => { + const fn = tgpu.fn([])(() => { + 'use gpu'; + }); + + expect(() => tgpu.resolve([fn.$uses({})])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn:fn: Cannot call '$uses' on functions whose metadata was provided by unplugin-typegpu.] + `); + }); + + it("throws when calling '$uses' on a function twice", () => { + const fn = tgpu.fn([])`{ + let a = EXT; + }`.$uses({ EXT: 1 }); + + expect(() => fn.$uses({ EXT: 2 })).toThrowErrorMatchingInlineSnapshot( + `[Error: Cannot call '$uses' multiple times. If you wish to override dependencies, use slots or accessors instead.]`, + ); + }); }); From a7abca9f3a639d81e07a6b334bce3d2674811781 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Wed, 24 Jun 2026 11:15:54 +0200 Subject: [PATCH 22/44] Initial rewrite --- .../typegpu/src/core/resolve/externals.ts | 102 ++++++++++-------- 1 file changed, 56 insertions(+), 46 deletions(-) diff --git a/packages/typegpu/src/core/resolve/externals.ts b/packages/typegpu/src/core/resolve/externals.ts index 0dca0ae376..44c0e15392 100644 --- a/packages/typegpu/src/core/resolve/externals.ts +++ b/packages/typegpu/src/core/resolve/externals.ts @@ -66,11 +66,8 @@ export function addReturnTypeToExternals( } } -function identifierRegex(name: string) { - return new RegExp( - `(? { - const externalRegex = identifierRegex(externalName); - if (wgsl && externalName !== 'Out' && externalName !== 'in' && !externalRegex.test(wgsl)) { - console.warn(`The external '${externalName}' wasn't used in the resolved template.`); - // continue anyway, we still might need to resolve the external - } + const keys = Object.keys(externalMap); + if (keys.length === 0) { + return wgsl; + } + + // Avoid resolving the same item multiple times during one call. + const cache: Map = new Map(); + + const anyIdent = /([$_\p{XID_Start}][$\p{XID_Continue}]*)/u; + const initialIdents = new RegExp( + keys.map((key) => key.replaceAll('.', '\\.').replaceAll('$', '\\$')).join('|'), + 'u', + ); + const matcher = boundedRegex( + new RegExp(`(${initialIdents.source})(\\.${anyIdent.source})*`, 'ug'), + ); - if (isResolvable(external)) { - if (isNamable(external) && getName(external) === undefined) { - setName(external, externalName.split('.').at(-1) as string); + return wgsl.replaceAll(matcher, (match) => { + const chain = match.split('.'); + let currentItem: unknown = externalMap; + let name: string | undefined = undefined; + let suffix = ''; + let resolvedItem = false; + for (const [i, elem] of chain.entries()) { + currentItem = (currentItem as ExternalMap)[elem]; + name = elem; + if (isResolvable(currentItem)) { + suffix = chain + .slice(i + 1) + .map((s) => `.${s}`) + .join(''); + resolvedItem = true; + break; + } + + if (typeof currentItem !== 'object' || currentItem === null) { + console.warn( + `During resolution, the external '${name}' has been omitted. Only TGPU resources, 'use gpu' functions, primitives, and plain JS objects can be used as externals.`, + ); + return match; } - return acc.replaceAll(externalRegex, ctx.resolve(external).value); } - if (external !== null && typeof external === 'object') { - const foundProperties = [ - ...wgsl.matchAll( - new RegExp( - `${externalName - .replaceAll('.', '\\.') - .replaceAll('$', '\\$')}\\.(?.*?)(?![\\w\\$_])`, - 'g', - ), - ), - ].map((found) => found[1]); - const uniqueProperties = [...new Set(foundProperties)]; - - return uniqueProperties.reduce( - (innerAcc: string, prop) => - prop && prop in external - ? replaceExternalsInWgsl( - ctx, - { - [`${externalName}.${prop}`]: external[prop as keyof typeof external], - }, - innerAcc, - ) - : innerAcc, - acc, - ); + // The chain ended on a nested external map rather than a resolvable value + // (e.g. a bare `in`). Nothing to substitute — leave it untouched. + if (!resolvedItem) { + return match; } - console.warn( - `During resolution, the external '${externalName}' has been omitted. Only TGPU resources, 'use gpu' functions, primitives, and plain JS objects can be used as externals.`, - ); + if (isNamable(currentItem) && getName(currentItem) === undefined) { + setName(currentItem, name); + } + + if (cache.has(currentItem)) { + return cache.get(currentItem) + suffix; + } + const resolved = ctx.resolve(currentItem).value; + cache.set(currentItem, resolved); - return acc; - }, wgsl); + return resolved + suffix; // cache!!!!! + }); } From b0b547264dc0fa3d9d1b558ce23fbc9f5b3e9cc3 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Wed, 24 Jun 2026 11:25:57 +0200 Subject: [PATCH 23/44] Add an end of chain test --- .../typegpu/src/core/resolve/externals.ts | 23 +++++++++++++------ packages/typegpu/tests/resolve.test.ts | 18 +++++++++++++++ 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/packages/typegpu/src/core/resolve/externals.ts b/packages/typegpu/src/core/resolve/externals.ts index 44c0e15392..ed94d2d781 100644 --- a/packages/typegpu/src/core/resolve/externals.ts +++ b/packages/typegpu/src/core/resolve/externals.ts @@ -103,10 +103,12 @@ export function replaceExternalsInWgsl( return wgsl.replaceAll(matcher, (match) => { const chain = match.split('.'); + let currentItem: unknown = externalMap; let name: string | undefined = undefined; let suffix = ''; let resolvedItem = false; + for (const [i, elem] of chain.entries()) { currentItem = (currentItem as ExternalMap)[elem]; name = elem; @@ -119,7 +121,11 @@ export function replaceExternalsInWgsl( break; } - if (typeof currentItem !== 'object' || currentItem === null) { + if ( + typeof currentItem !== 'object' || + currentItem === null /* || i === chain.length - 1 */ + ) { + // throw new Error('aaa'); console.warn( `During resolution, the external '${name}' has been omitted. Only TGPU resources, 'use gpu' functions, primitives, and plain JS objects can be used as externals.`, ); @@ -130,19 +136,22 @@ export function replaceExternalsInWgsl( // The chain ended on a nested external map rather than a resolvable value // (e.g. a bare `in`). Nothing to substitute — leave it untouched. if (!resolvedItem) { + console.warn( + `During resolution, the external '${chain.join('.')}' has been omitted. Only TGPU resources, 'use gpu' functions, primitives, and plain JS objects can be used as externals.`, + ); return match; } - if (isNamable(currentItem) && getName(currentItem) === undefined) { + if (isNamable(currentItem) && getName(currentItem) === undefined && name !== undefined) { setName(currentItem, name); } - if (cache.has(currentItem)) { - return cache.get(currentItem) + suffix; + let resolved = cache.get(currentItem); + if (resolved === undefined) { + resolved = ctx.resolve(currentItem).value; + cache.set(currentItem, resolved); } - const resolved = ctx.resolve(currentItem).value; - cache.set(currentItem, resolved); - return resolved + suffix; // cache!!!!! + return resolved + suffix; }); } diff --git a/packages/typegpu/tests/resolve.test.ts b/packages/typegpu/tests/resolve.test.ts index 83e8e1e787..8dfa7f7a03 100644 --- a/packages/typegpu/tests/resolve.test.ts +++ b/packages/typegpu/tests/resolve.test.ts @@ -527,6 +527,24 @@ describe('tgpu resolveWithContext', () => { ); }); + it('should warn when the end of external chain was reached without a resolvable', () => { + using consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + const getColor = tgpu.fn([])`() { + let color = EXT.p.q; + }`.$uses({ EXT: { p: { q: { r: d.vec3f() } } } }); + + expect(tgpu.resolve([getColor])).toMatchInlineSnapshot(` + "fn getColor() { + let color = EXT.p.q; + }" + `); + + expect(consoleWarnSpy).toHaveBeenCalledWith( + "During resolution, the external 'EXT.p.q' has been omitted. Only TGPU resources, 'use gpu' functions, primitives, and plain JS objects can be used as externals.", + ); + }); + it('should not warn when In/Out are unused', () => { using consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); From ca3e9e1a37c418d60a932db7c8ba8c47778dbd1e Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Wed, 24 Jun 2026 11:30:14 +0200 Subject: [PATCH 24/44] Simplify end of chain tracking --- .../typegpu/src/core/resolve/externals.ts | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/packages/typegpu/src/core/resolve/externals.ts b/packages/typegpu/src/core/resolve/externals.ts index ed94d2d781..0e733c452d 100644 --- a/packages/typegpu/src/core/resolve/externals.ts +++ b/packages/typegpu/src/core/resolve/externals.ts @@ -107,7 +107,6 @@ export function replaceExternalsInWgsl( let currentItem: unknown = externalMap; let name: string | undefined = undefined; let suffix = ''; - let resolvedItem = false; for (const [i, elem] of chain.entries()) { currentItem = (currentItem as ExternalMap)[elem]; @@ -117,31 +116,17 @@ export function replaceExternalsInWgsl( .slice(i + 1) .map((s) => `.${s}`) .join(''); - resolvedItem = true; break; } - if ( - typeof currentItem !== 'object' || - currentItem === null /* || i === chain.length - 1 */ - ) { - // throw new Error('aaa'); + if (typeof currentItem !== 'object' || currentItem === null || i === chain.length - 1) { console.warn( - `During resolution, the external '${name}' has been omitted. Only TGPU resources, 'use gpu' functions, primitives, and plain JS objects can be used as externals.`, + `During resolution, the external '${chain.slice(0, i + 1).join('.')}' has been omitted. Only TGPU resources, 'use gpu' functions, primitives, and plain JS objects can be used as externals.`, ); return match; } } - // The chain ended on a nested external map rather than a resolvable value - // (e.g. a bare `in`). Nothing to substitute — leave it untouched. - if (!resolvedItem) { - console.warn( - `During resolution, the external '${chain.join('.')}' has been omitted. Only TGPU resources, 'use gpu' functions, primitives, and plain JS objects can be used as externals.`, - ); - return match; - } - if (isNamable(currentItem) && getName(currentItem) === undefined && name !== undefined) { setName(currentItem, name); } From 48727bb14b985b01baca761a9db09552d0779c09 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Wed, 24 Jun 2026 11:38:54 +0200 Subject: [PATCH 25/44] Update snapshots --- .../individual-example-tests/log-test.test.ts | 56 +++++++++---------- packages/typegpu/tests/resolve.test.ts | 6 +- packages/typegpu/tests/tgpuGenericFn.test.ts | 4 +- .../typegpu/tests/tgsl/consoleLog.test.ts | 42 +++++++------- 4 files changed, 54 insertions(+), 54 deletions(-) diff --git a/apps/typegpu-docs/tests/individual-example-tests/log-test.test.ts b/apps/typegpu-docs/tests/individual-example-tests/log-test.test.ts index 8cd55aa412..86ba2283ac 100644 --- a/apps/typegpu-docs/tests/individual-example-tests/log-test.test.ts +++ b/apps/typegpu-docs/tests/individual-example-tests/log-test.test.ts @@ -39,6 +39,8 @@ describe('console log example', () => { expect(shaderCodes).toMatchInlineSnapshot(` "@group(0) @binding(0) var sizeUniform: vec3u; + var dataBlockIndex: u32; + @group(0) @binding(1) var indexBuffer: atomic; struct SerializedLogData { @@ -48,8 +50,6 @@ describe('console log example', () => { @group(0) @binding(2) var dataBuffer: array; - var dataBlockIndex: u32; - var dataByteIndex: u32; fn nextByteIndex() -> u32 { @@ -90,6 +90,8 @@ describe('console log example', () => { @group(0) @binding(0) var sizeUniform: vec3u; + var dataBlockIndex: u32; + @group(0) @binding(1) var indexBuffer: atomic; struct SerializedLogData { @@ -99,8 +101,6 @@ describe('console log example', () => { @group(0) @binding(2) var dataBuffer: array; - var dataBlockIndex: u32; - var dataByteIndex: u32; fn nextByteIndex() -> u32 { @@ -150,6 +150,8 @@ describe('console log example', () => { @group(0) @binding(0) var sizeUniform: vec3u; + var dataBlockIndex: u32; + @group(0) @binding(1) var indexBuffer: atomic; struct SerializedLogData { @@ -159,8 +161,6 @@ describe('console log example', () => { @group(0) @binding(2) var dataBuffer: array; - var dataBlockIndex: u32; - var dataByteIndex: u32; fn nextByteIndex() -> u32 { @@ -203,6 +203,8 @@ describe('console log example', () => { @group(0) @binding(0) var sizeUniform: vec3u; + var dataBlockIndex: u32; + @group(0) @binding(1) var indexBuffer: atomic; struct SerializedLogData { @@ -212,8 +214,6 @@ describe('console log example', () => { @group(0) @binding(2) var dataBuffer: array; - var dataBlockIndex: u32; - var dataByteIndex: u32; fn log1serializer() { @@ -834,6 +834,8 @@ describe('console log example', () => { num: u32, } + var dataBlockIndex: u32; + @group(0) @binding(1) var indexBuffer: atomic; struct SerializedLogData { @@ -843,8 +845,6 @@ describe('console log example', () => { @group(0) @binding(2) var dataBuffer: array; - var dataBlockIndex: u32; - var dataByteIndex: u32; fn nextByteIndex() -> u32 { @@ -995,6 +995,8 @@ describe('console log example', () => { @group(0) @binding(0) var sizeUniform: vec3u; + var dataBlockIndex: u32; + @group(0) @binding(1) var indexBuffer: atomic; struct SerializedLogData { @@ -1004,8 +1006,6 @@ describe('console log example', () => { @group(0) @binding(2) var dataBuffer: array; - var dataBlockIndex: u32; - var dataByteIndex: u32; fn log1serializer() { @@ -1052,6 +1052,8 @@ describe('console log example', () => { @group(0) @binding(0) var sizeUniform: vec3u; + var dataBlockIndex: u32; + @group(0) @binding(1) var indexBuffer: atomic; struct SerializedLogData { @@ -1061,8 +1063,6 @@ describe('console log example', () => { @group(0) @binding(2) var dataBuffer: array; - var dataBlockIndex: u32; - var dataByteIndex: u32; fn nextByteIndex() -> u32 { @@ -1103,6 +1103,8 @@ describe('console log example', () => { @group(0) @binding(0) var sizeUniform: vec3u; + var dataBlockIndex: u32; + @group(0) @binding(1) var indexBuffer: atomic; struct SerializedLogData { @@ -1112,8 +1114,6 @@ describe('console log example', () => { @group(0) @binding(2) var dataBuffer: array; - var dataBlockIndex: u32; - var dataByteIndex: u32; fn nextByteIndex() -> u32 { @@ -1158,6 +1158,8 @@ describe('console log example', () => { @group(0) @binding(1) var logCountUniform: u32; + var dataBlockIndex: u32; + @group(0) @binding(2) var indexBuffer: atomic; struct SerializedLogData { @@ -1167,8 +1169,6 @@ describe('console log example', () => { @group(0) @binding(3) var dataBuffer: array; - var dataBlockIndex: u32; - var dataByteIndex: u32; fn nextByteIndex() -> u32 { @@ -1212,6 +1212,8 @@ describe('console log example', () => { @group(0) @binding(0) var sizeUniform: vec3u; + var dataBlockIndex: u32; + @group(0) @binding(1) var indexBuffer: atomic; struct SerializedLogData { @@ -1221,8 +1223,6 @@ describe('console log example', () => { @group(0) @binding(2) var dataBuffer: array; - var dataBlockIndex: u32; - var dataByteIndex: u32; fn nextByteIndex() -> u32 { @@ -1299,6 +1299,8 @@ describe('console log example', () => { @group(0) @binding(0) var sizeUniform: vec3u; + var dataBlockIndex: u32; + @group(0) @binding(1) var indexBuffer: atomic; struct SerializedLogData { @@ -1308,8 +1310,6 @@ describe('console log example', () => { @group(0) @binding(2) var dataBuffer: array; - var dataBlockIndex: u32; - var dataByteIndex: u32; fn log1serializer() { @@ -1453,6 +1453,8 @@ describe('console log example', () => { return mainVertex_Output(vec4f(positions[vertexIndex], 0f, 1f)); } + var dataBlockIndex: u32; + @group(0) @binding(0) var indexBuffer: atomic; struct SerializedLogData { @@ -1462,8 +1464,6 @@ describe('console log example', () => { @group(0) @binding(1) var dataBuffer: array; - var dataBlockIndex: u32; - var dataByteIndex: u32; fn nextByteIndex() -> u32 { @@ -1506,6 +1506,8 @@ describe('console log example', () => { return mainVertex_Output(vec4f(positions[vertexIndex], 0f, 1f)); } + var dataBlockIndex: u32; + @group(0) @binding(0) var indexBuffer: atomic; struct SerializedLogData { @@ -1515,8 +1517,6 @@ describe('console log example', () => { @group(0) @binding(1) var dataBuffer: array; - var dataBlockIndex: u32; - var dataByteIndex: u32; fn nextByteIndex() -> u32 { @@ -1552,6 +1552,8 @@ describe('console log example', () => { @group(0) @binding(0) var sizeUniform: vec3u; + var dataBlockIndex: u32; + @group(0) @binding(1) var indexBuffer: atomic; struct SerializedLogData { @@ -1561,8 +1563,6 @@ describe('console log example', () => { @group(0) @binding(2) var dataBuffer: array; - var dataBlockIndex: u32; - var dataByteIndex: u32; fn nextByteIndex() -> u32 { diff --git a/packages/typegpu/tests/resolve.test.ts b/packages/typegpu/tests/resolve.test.ts index 8dfa7f7a03..6e97b6bbea 100644 --- a/packages/typegpu/tests/resolve.test.ts +++ b/packages/typegpu/tests/resolve.test.ts @@ -310,12 +310,12 @@ fn main() { }); expect(resolved).toMatchInlineSnapshot(` - "@group(0) @binding(0) var intensity: u32; - - fn get_color() -> vec3f { + "fn get_color() -> vec3f { let color = vec3f(); return color; } + + @group(0) @binding(0) var intensity: u32; fn main () { let c = get_color() * intensity; }" diff --git a/packages/typegpu/tests/tgpuGenericFn.test.ts b/packages/typegpu/tests/tgpuGenericFn.test.ts index fd136ced01..df737ded64 100644 --- a/packages/typegpu/tests/tgpuGenericFn.test.ts +++ b/packages/typegpu/tests/tgpuGenericFn.test.ts @@ -318,6 +318,8 @@ describe('TgpuGenericFn - shellless callback wrapper', () => { expect(tgpu.resolve([pipeline.pipeline])).toMatchInlineSnapshot(` "@group(0) @binding(0) var sizeUniform: vec3u; + var dataBlockIndex: u32; + @group(0) @binding(1) var indexBuffer: atomic; struct SerializedLogData { @@ -327,8 +329,6 @@ describe('TgpuGenericFn - shellless callback wrapper', () => { @group(0) @binding(2) var dataBuffer: array; - var dataBlockIndex: u32; - var dataByteIndex: u32; fn nextByteIndex() -> u32 { diff --git a/packages/typegpu/tests/tgsl/consoleLog.test.ts b/packages/typegpu/tests/tgsl/consoleLog.test.ts index 997b15d46f..c1a8e19607 100644 --- a/packages/typegpu/tests/tgsl/consoleLog.test.ts +++ b/packages/typegpu/tests/tgsl/consoleLog.test.ts @@ -84,6 +84,8 @@ describe('wgslGenerator with console.log', () => { return vs_Output(vec4f()); } + var dataBlockIndex: u32; + @group(0) @binding(0) var indexBuffer: atomic; struct SerializedLogData { @@ -93,8 +95,6 @@ describe('wgslGenerator with console.log', () => { @group(0) @binding(1) var dataBuffer: array; - var dataBlockIndex: u32; - var dataByteIndex: u32; fn nextByteIndex() -> u32 { @@ -166,6 +166,8 @@ describe('wgslGenerator with console.log', () => { return vs_Output(vec4f()); } + var dataBlockIndex: u32; + @group(0) @binding(0) var indexBuffer: atomic; struct SerializedLogData { @@ -175,8 +177,6 @@ describe('wgslGenerator with console.log', () => { @group(0) @binding(1) var dataBuffer: array; - var dataBlockIndex: u32; - var dataByteIndex: u32; fn nextByteIndex() -> u32 { @@ -257,6 +257,8 @@ describe('wgslGenerator with console.log', () => { return VertexOut(vec4f()); } + var dataBlockIndex: u32; + @group(0) @binding(0) var indexBuffer: atomic; struct SerializedLogData { @@ -266,8 +268,6 @@ describe('wgslGenerator with console.log', () => { @group(0) @binding(1) var dataBuffer: array; - var dataBlockIndex: u32; - var dataByteIndex: u32; fn nextByteIndex() -> u32 { @@ -317,7 +317,9 @@ describe('wgslGenerator with console.log', () => { const pipeline = root.createComputePipeline({ compute: fn }); expect(tgpu.resolve([pipeline])).toMatchInlineSnapshot(` - "@group(0) @binding(0) var indexBuffer: atomic; + "var dataBlockIndex: u32; + + @group(0) @binding(0) var indexBuffer: atomic; struct SerializedLogData { id: u32, @@ -326,8 +328,6 @@ describe('wgslGenerator with console.log', () => { @group(0) @binding(1) var dataBuffer: array; - var dataBlockIndex: u32; - var dataByteIndex: u32; fn nextByteIndex() -> u32 { @@ -375,7 +375,9 @@ describe('wgslGenerator with console.log', () => { }); expect(tgpu.resolve([pipeline])).toMatchInlineSnapshot(` - "@group(0) @binding(0) var indexBuffer: atomic; + "var dataBlockIndex: u32; + + @group(0) @binding(0) var indexBuffer: atomic; struct SerializedLogData { id: u32, @@ -384,8 +386,6 @@ describe('wgslGenerator with console.log', () => { @group(0) @binding(1) var dataBuffer: array; - var dataBlockIndex: u32; - var dataByteIndex: u32; fn nextByteIndex() -> u32 { @@ -453,7 +453,9 @@ describe('wgslGenerator with console.log', () => { }); expect(tgpu.resolve([pipeline])).toMatchInlineSnapshot(` - "@group(0) @binding(0) var indexBuffer: atomic; + "var dataBlockIndex: u32; + + @group(0) @binding(0) var indexBuffer: atomic; struct SerializedLogData { id: u32, @@ -462,8 +464,6 @@ describe('wgslGenerator with console.log', () => { @group(0) @binding(1) var dataBuffer: array; - var dataBlockIndex: u32; - var dataByteIndex: u32; fn nextByteIndex() -> u32 { @@ -539,6 +539,8 @@ describe('wgslGenerator with console.log', () => { data: array, } + var dataBlockIndex: u32; + @group(0) @binding(0) var indexBuffer: atomic; struct SerializedLogData { @@ -548,8 +550,6 @@ describe('wgslGenerator with console.log', () => { @group(0) @binding(1) var dataBuffer: array; - var dataBlockIndex: u32; - var dataByteIndex: u32; fn nextByteIndex() -> u32 { @@ -690,6 +690,8 @@ describe('wgslGenerator with console.log', () => { @group(0) @binding(1) var myUniform: vec2f; + var dataBlockIndex: u32; + @group(0) @binding(2) var indexBuffer: atomic; struct SerializedLogData { @@ -699,8 +701,6 @@ describe('wgslGenerator with console.log', () => { @group(0) @binding(3) var dataBuffer: array; - var dataBlockIndex: u32; - var dataByteIndex: u32; fn nextByteIndex() -> u32 { @@ -753,6 +753,8 @@ describe('wgslGenerator with console.log', () => { expect(tgpu.resolve([myPipeline.pipeline])).toMatchInlineSnapshot(` "@group(0) @binding(0) var sizeUniform: vec3u; + var dataBlockIndex: u32; + @group(0) @binding(1) var indexBuffer: atomic; struct SerializedLogData { @@ -762,8 +764,6 @@ describe('wgslGenerator with console.log', () => { @group(0) @binding(2) var dataBuffer: array; - var dataBlockIndex: u32; - var dataByteIndex: u32; fn log1serializer() { From e346c8e7d9d6f2fae2774181c7f79495ccd3e9d4 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Wed, 24 Jun 2026 11:41:15 +0200 Subject: [PATCH 26/44] Update declare tests --- packages/typegpu/tests/declare.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/typegpu/tests/declare.test.ts b/packages/typegpu/tests/declare.test.ts index 2b018c3192..bcaf64d09f 100644 --- a/packages/typegpu/tests/declare.test.ts +++ b/packages/typegpu/tests/declare.test.ts @@ -5,7 +5,7 @@ describe('tgpu.declare', () => { it('should inject provided declaration when resolving a function', () => { const declaration = tgpu['~unstable'].declare('@group(0) @binding(0) var val: f32;'); - const empty = tgpu.fn([])`() { /* do nothing */ }`.$uses({ declaration }); + const empty = tgpu.fn([])`declaration () { /* do nothing */ }`.$uses({ declaration }); expect(tgpu.resolve([empty])).toMatchInlineSnapshot(` "@group(0) @binding(0) var val: f32; @@ -33,7 +33,7 @@ struct Output { x: u32, }`); - const empty = tgpu.fn([])`() { /* do nothing */ }`.$uses({ decl1, decl2 }); + const empty = tgpu.fn([])`decl1 decl2 () { /* do nothing */ }`.$uses({ decl1, decl2 }); expect(tgpu.resolve([empty])).toMatchInlineSnapshot(` "@group(0) @binding(0) var val: f32; @@ -48,17 +48,17 @@ struct Output { it('should replace nested declarations', () => { const declaration = tgpu['~unstable'] - .declare('@group(0) @binding(0) var val: f32;') + .declare('@group(0) @binding(0) var val: f32; nested') .$uses({ nested: tgpu['~unstable'].declare('struct Output { x: u32 }'), }); - const empty = tgpu.fn([])`() { /* do nothing */ }`.$uses({ declaration }); + const empty = tgpu.fn([])`declaration () { /* do nothing */ }`.$uses({ declaration }); expect(tgpu.resolve([empty])).toMatchInlineSnapshot(` "struct Output { x: u32 } - @group(0) @binding(0) var val: f32; + @group(0) @binding(0) var val: f32; fn empty() { /* do nothing */ }" `); @@ -73,7 +73,7 @@ struct Output { .declare('@group(0) @binding(0) var val: Output;') .$uses({ Output }); - const empty = tgpu.fn([])`() { /* do nothing */ }`.$uses({ declaration }); + const empty = tgpu.fn([])`declaration () { /* do nothing */ }`.$uses({ declaration }); expect(tgpu.resolve([empty])).toMatchInlineSnapshot(` "struct Output { From 49743050b07ac307da04c1b1196dd3bbddde9088 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Wed, 24 Jun 2026 11:44:43 +0200 Subject: [PATCH 27/44] Remove warn test --- packages/typegpu/tests/resolve.test.ts | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/packages/typegpu/tests/resolve.test.ts b/packages/typegpu/tests/resolve.test.ts index 6e97b6bbea..4e215f25a4 100644 --- a/packages/typegpu/tests/resolve.test.ts +++ b/packages/typegpu/tests/resolve.test.ts @@ -495,25 +495,6 @@ describe('tgpu resolveWithContext', () => { expect(configSpy.mock.lastCall?.[0].bindings).toEqual([[colorSlot, v]]); }); - it('should warn when external WGSL is not used', () => { - using consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); - - tgpu.resolveWithContext({ - template: 'fn testFn() { return; }', - externals: { - ArraySchema: d.arrayOf(d.u32, 4), - JavaScriptObject: { field: d.vec2f() }, - }, - }); - - expect(consoleWarnSpy).toHaveBeenCalledWith( - "The external 'ArraySchema' wasn't used in the resolved template.", - ); - expect(consoleWarnSpy).toHaveBeenCalledWith( - "The external 'JavaScriptObject' wasn't used in the resolved template.", - ); - }); - it('should warn when external is neither wgsl nor an object', () => { using consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); From 65fcedf3032d607ed950f106d68a54b9d2923754 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Wed, 24 Jun 2026 12:03:01 +0200 Subject: [PATCH 28/44] Simplify regex --- .../typegpu/src/core/resolve/externals.ts | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/packages/typegpu/src/core/resolve/externals.ts b/packages/typegpu/src/core/resolve/externals.ts index 0e733c452d..8c04f2088b 100644 --- a/packages/typegpu/src/core/resolve/externals.ts +++ b/packages/typegpu/src/core/resolve/externals.ts @@ -66,15 +66,15 @@ export function addReturnTypeToExternals( } } -function boundedRegex(inner: RegExp) { - return new RegExp(`(? = new Map(); - const anyIdent = /([$_\p{XID_Start}][$\p{XID_Continue}]*)/u; - const initialIdents = new RegExp( - keys.map((key) => key.replaceAll('.', '\\.').replaceAll('$', '\\$')).join('|'), - 'u', - ); - const matcher = boundedRegex( - new RegExp(`(${initialIdents.source})(\\.${anyIdent.source})*`, 'ug'), - ); - - return wgsl.replaceAll(matcher, (match) => { + return wgsl.replaceAll(boundedPropChain, (match) => { const chain = match.split('.'); + if (!((chain.at(0) as string) in externalMap)) { + // this prop access does not start with an external + return match; + } + let currentItem: unknown = externalMap; let name: string | undefined = undefined; let suffix = ''; From 38a06439424b7b37cbc1d1f8b3b8d88008464498 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Wed, 24 Jun 2026 12:37:00 +0200 Subject: [PATCH 29/44] Update packages/typegpu/src/core/resolve/externals.ts Co-authored-by: pullfrog[bot] <226033991+pullfrog[bot]@users.noreply.github.com> --- packages/typegpu/src/core/resolve/externals.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typegpu/src/core/resolve/externals.ts b/packages/typegpu/src/core/resolve/externals.ts index 8c04f2088b..9213fc8551 100644 --- a/packages/typegpu/src/core/resolve/externals.ts +++ b/packages/typegpu/src/core/resolve/externals.ts @@ -95,7 +95,7 @@ export function replaceExternalsInWgsl( return wgsl.replaceAll(boundedPropChain, (match) => { const chain = match.split('.'); - if (!((chain.at(0) as string) in externalMap)) { + if (!Object.hasOwn(externalMap, chain.at(0) as string)) { // this prop access does not start with an external return match; } From f969d3786fa54315f7502902c3a6241956729173 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 26 Jun 2026 11:36:10 +0200 Subject: [PATCH 30/44] Simplify naming --- packages/typegpu/src/core/resolve/externals.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/typegpu/src/core/resolve/externals.ts b/packages/typegpu/src/core/resolve/externals.ts index cdb68e2507..04536d01d7 100644 --- a/packages/typegpu/src/core/resolve/externals.ts +++ b/packages/typegpu/src/core/resolve/externals.ts @@ -101,17 +101,20 @@ export function replaceExternalsInWgsl( } let currentItem: unknown = externalMap; - let name: string | undefined = undefined; let suffix = ''; for (const [i, elem] of chain.entries()) { currentItem = (currentItem as ExternalMap)[elem]; - name = elem; if (isResolvable(currentItem)) { suffix = chain .slice(i + 1) .map((s) => `.${s}`) .join(''); + + if (isNamable(currentItem) && getName(currentItem) === undefined) { + setName(currentItem, elem); + } + break; } @@ -123,10 +126,6 @@ export function replaceExternalsInWgsl( } } - if (isNamable(currentItem) && getName(currentItem) === undefined && name !== undefined) { - setName(currentItem, name); - } - let resolved = cache.get(currentItem); if (resolved === undefined) { resolved = ctx.resolve(currentItem).value; From 73dc847964adb18bdcaa8a5df32ccd3e5be7d9ad Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:28:51 +0200 Subject: [PATCH 31/44] Remove cache --- packages/typegpu/src/core/resolve/externals.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/packages/typegpu/src/core/resolve/externals.ts b/packages/typegpu/src/core/resolve/externals.ts index 04536d01d7..93764294f8 100644 --- a/packages/typegpu/src/core/resolve/externals.ts +++ b/packages/typegpu/src/core/resolve/externals.ts @@ -89,9 +89,6 @@ export function replaceExternalsInWgsl( return wgsl; } - // Avoid resolving the same item multiple times during one call. - const cache: Map = new Map(); - return wgsl.replaceAll(boundedPropChain, (match) => { const chain = match.split('.'); @@ -126,12 +123,6 @@ export function replaceExternalsInWgsl( } } - let resolved = cache.get(currentItem); - if (resolved === undefined) { - resolved = ctx.resolve(currentItem).value; - cache.set(currentItem, resolved); - } - - return resolved + suffix; + return ctx.resolve(currentItem).value + suffix; }); } From a5d2b0ab21aeadf8d9d9b8620fa975058b2dc2d9 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Thu, 2 Jul 2026 11:16:17 +0200 Subject: [PATCH 32/44] Copilot review fixes --- packages/typegpu/src/core/resolve/externals.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/typegpu/src/core/resolve/externals.ts b/packages/typegpu/src/core/resolve/externals.ts index 93764294f8..128055957c 100644 --- a/packages/typegpu/src/core/resolve/externals.ts +++ b/packages/typegpu/src/core/resolve/externals.ts @@ -68,7 +68,10 @@ export function addReturnTypeToExternals( const anyIdent = /([$_\p{XID_Start}][$\p{XID_Continue}]*)/u; // WGSL ident, modified to include $ const anyPropChain = new RegExp(`(${anyIdent.source})(\\.${anyIdent.source})*`, 'ug'); -const boundedPropChain = new RegExp(`(? key.includes('.')); + if (maybeInvalidKey) { + throw new Error(`External key '${maybeInvalidKey}' contains invalid character '.'`); + } + return wgsl.replaceAll(boundedPropChain, (match) => { const chain = match.split('.'); From 10e93dadd227e92307e7e2bf7289c662e688c3f2 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Mon, 6 Jul 2026 11:47:18 +0200 Subject: [PATCH 33/44] Add some regex tests --- .../typegpu/src/core/resolve/externals.ts | 4 +- .../typegpu/tests/internal/externals.test.ts | 62 ++++++++++++++++++- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/packages/typegpu/src/core/resolve/externals.ts b/packages/typegpu/src/core/resolve/externals.ts index 128055957c..00db5f7e85 100644 --- a/packages/typegpu/src/core/resolve/externals.ts +++ b/packages/typegpu/src/core/resolve/externals.ts @@ -66,9 +66,9 @@ export function addReturnTypeToExternals( } } -const anyIdent = /([$_\p{XID_Start}][$\p{XID_Continue}]*)/u; // WGSL ident, modified to include $ +export const anyIdent = /([$_\p{XID_Start}][$\p{XID_Continue}]*)/u; // WGSL ident, modified to include $ const anyPropChain = new RegExp(`(${anyIdent.source})(\\.${anyIdent.source})*`, 'ug'); -const boundedPropChain = new RegExp( +export const boundedPropChain = new RegExp( `(? { @@ -276,3 +281,58 @@ describe('external name collisions', () => { ); }); }); + +function matches(str: string, regex: RegExp) { + const globalRegex = regex.flags.includes('g') + ? regex + : new RegExp(regex.source, regex.flags + 'g'); + return [...str.matchAll(globalRegex)].map((m) => m[0]).join(' '); +} + +describe('regexes in externals', () => { + describe('anyIdent', () => { + it('correctly recognizes wgsl identifiers', () => { + expect(matches(' ident ', anyIdent)).toMatchInlineSnapshot(`"ident"`); + expect(matches(' _ident ', anyIdent)).toMatchInlineSnapshot(`"_ident"`); + expect(matches(' ident_1 ', anyIdent)).toMatchInlineSnapshot(`"ident_1"`); + expect(matches(' café_ඞඞඞ ', anyIdent)).toMatchInlineSnapshot(`"café_ඞඞඞ"`); + }); + + it('stops at non-ident characters', () => { + expect(matches(' ident;ident ', anyIdent)).toMatchInlineSnapshot(`"ident ident"`); + expect(matches(' ident.ident ', anyIdent)).toMatchInlineSnapshot(`"ident ident"`); + expect(matches(' ident)ident ', anyIdent)).toMatchInlineSnapshot(`"ident ident"`); + expect(matches(' ident+ident ', anyIdent)).toMatchInlineSnapshot(`"ident ident"`); + expect(matches(' ident/ident ', anyIdent)).toMatchInlineSnapshot(`"ident ident"`); + }); + + it('accepts dollar signs', () => { + expect(matches(' $ ', anyIdent)).toMatchInlineSnapshot(`"$"`); + expect(matches(' $ident ', anyIdent)).toMatchInlineSnapshot(`"$ident"`); + expect(matches(' ident$ident ', anyIdent)).toMatchInlineSnapshot(`"ident$ident"`); + expect(matches(' ident$$ident ', anyIdent)).toMatchInlineSnapshot(`"ident$$ident"`); + }); + }); + + describe('boundedPropChain', () => { + it('correctly recognizes prop chains', () => { + expect(matches(' p1 ', boundedPropChain)).toMatchInlineSnapshot(`"p1"`); + expect(matches(' p1.p2 ', boundedPropChain)).toMatchInlineSnapshot(`"p1.p2"`); + expect(matches(' p1.p2.p3.p4.p5 ', boundedPropChain)).toMatchInlineSnapshot( + `"p1.p2.p3.p4.p5"`, + ); + expect(matches(' p1.$ ', boundedPropChain)).toMatchInlineSnapshot(`"p1.$"`); + }); + + it('does not match non prop chains', () => { + expect(matches(' 1.0 ', boundedPropChain)).toMatchInlineSnapshot(`""`); + expect(matches(' .5f ', boundedPropChain)).toMatchInlineSnapshot(`""`); + expect(matches(' .prop ', boundedPropChain)).toMatchInlineSnapshot(`""`); + expect(matches(' p1 . p2 ', boundedPropChain)).toMatchInlineSnapshot(`"p1 p2"`); + expect(matches(' call().x ', boundedPropChain)).toMatchInlineSnapshot(`"call"`); + expect(matches(' arr[i].x ', boundedPropChain)).toMatchInlineSnapshot(`"arr i"`); + expect(matches(' (v1 + v2).x ', boundedPropChain)).toMatchInlineSnapshot(`"v1 v2"`); + expect(matches(' x,y ', boundedPropChain)).toMatchInlineSnapshot(`"x y"`); + }); + }); +}); From c6bc42746925236cb56ea8aa43b53d2ba6fc113e Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Thu, 9 Jul 2026 10:34:19 +0200 Subject: [PATCH 34/44] Add tests --- packages/typegpu/tests/slot.test.ts | 51 +++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/packages/typegpu/tests/slot.test.ts b/packages/typegpu/tests/slot.test.ts index 19ba0eb15f..b9b99d207a 100644 --- a/packages/typegpu/tests/slot.test.ts +++ b/packages/typegpu/tests/slot.test.ts @@ -423,4 +423,55 @@ describe('tgpu.slot', () => { }" `); }); + + it('allows comptime strings', () => { + const precisionSlot = tgpu.slot('f16'); + + const getFloat = () => { + 'use gpu'; + if (precisionSlot.$ === 'f32') { + return d.f32(); + } else { + return d.f16(); + } + }; + + expect(tgpu.resolve([getFloat])).toMatchInlineSnapshot(` + "fn getFloat() -> f16 { + { + return 0h; + } + }" + `); + }); + + it('disallows runtime strings in raw-wgsl implemented functions', () => { + const stringSlot = tgpu.slot('vec3f()'); + + const getVec = tgpu.fn([])`() => { + return stringSlot; + }`.$uses({ stringSlot }); + + expect(() => tgpu.resolve([getVec])).toThrowErrorMatchingInlineSnapshot(); + }); + + it('disallows runtime strings in TGSL implemented functions', () => { + const stringSlot = tgpu.slot('vec3f()'); + + const getVec = () => { + 'use gpu'; + const v = stringSlot.$; + return v; + }; + + expect(() => tgpu.resolve([getVec])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:getVec + - fn*:getVec(): 'const v = stringSlot.$' is invalid, cannot determine WGSL type of 'stringSlot.$' + ----- + - Try using or defining a schema that matches your desired value the most, and wrap the value with it: 'const v = Schema(stringSlot.$)' + -----] + `); + }); }); From 7ee8f2d58fcaf0bfca6b5f52e717302759825961 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Thu, 9 Jul 2026 11:17:17 +0200 Subject: [PATCH 35/44] Throw in raw-wgsl case --- .../typegpu/src/core/resolve/externals.ts | 5 ++ packages/typegpu/tests/slot.test.ts | 49 ++++++------------- 2 files changed, 21 insertions(+), 33 deletions(-) diff --git a/packages/typegpu/src/core/resolve/externals.ts b/packages/typegpu/src/core/resolve/externals.ts index 00db5f7e85..5d96bb18d8 100644 --- a/packages/typegpu/src/core/resolve/externals.ts +++ b/packages/typegpu/src/core/resolve/externals.ts @@ -3,6 +3,7 @@ import { isWgslStruct } from '../../data/wgslTypes.ts'; import { getName, hasTinyestMetadata, isNamable, setName } from '../../shared/meta.ts'; import { isWgsl, type ResolutionCtx } from '../../types.ts'; import type { FnExternals } from '../function/fnCore.ts'; +import { isSlot } from '../slot/slotTypes.ts'; /** * A key-value mapping where keys represent identifiers within shader code, @@ -131,6 +132,10 @@ export function replaceExternalsInWgsl( } } + if (isSlot(currentItem) && typeof currentItem.$ === 'string') { + throw new Error("Slots cannot be used for string injection. For that, use 'rawCodeSnippet'."); + } + return ctx.resolve(currentItem).value + suffix; }); } diff --git a/packages/typegpu/tests/slot.test.ts b/packages/typegpu/tests/slot.test.ts index b9b99d207a..48447f09c9 100644 --- a/packages/typegpu/tests/slot.test.ts +++ b/packages/typegpu/tests/slot.test.ts @@ -2,8 +2,8 @@ import { describe, expect } from 'vitest'; import { tgpu, d, std } from 'typegpu'; import { it } from 'typegpu-testing-utility'; -const RED = 'vec3f(1., 0., 0.)'; -const GREEN = 'vec3f(0., 1., 0.)'; +const RED = d.vec3f(1, 0, 0); +const GREEN = d.vec3f(0, 1, 0); describe('tgpu.slot', () => { it('resolves to default value if no value provided', () => { @@ -15,7 +15,7 @@ describe('tgpu.slot', () => { expect(tgpu.resolve([getColor])).toMatchInlineSnapshot(` "fn getColor() -> vec3f { - return vec3f(1., 0., 0.); + return vec3f(1, 0, 0); }" `); }); @@ -38,7 +38,7 @@ describe('tgpu.slot', () => { expect(tgpu.resolve([main])).toMatchInlineSnapshot(` "fn getColor() -> vec3f { - return vec3f(0., 1., 0.); + return vec3f(0, 1, 0); } fn main() { @@ -48,14 +48,14 @@ describe('tgpu.slot', () => { }); it('resolves to provided value', () => { - const colorSlot = tgpu.slot(); // no default + const colorSlot = tgpu.slot(); // no default const getColor = tgpu.fn([], d.vec3f)`() { return colorSlot; }`.$uses({ colorSlot }); // overriding to green - const getColorWithGreen = getColor.with(colorSlot, 'vec3f(0., 1., 0.)'); + const getColorWithGreen = getColor.with(colorSlot, d.vec3f(0, 1, 0)); const main = tgpu.fn([])`() { getColorWithGreen(); @@ -64,7 +64,7 @@ describe('tgpu.slot', () => { // should be green expect(tgpu.resolve([main])).toMatchInlineSnapshot(` "fn getColor() -> vec3f { - return vec3f(0., 1., 0.); + return vec3f(0, 1, 0); } fn main() { @@ -81,11 +81,10 @@ describe('tgpu.slot', () => { }`.$uses({ colorSlot }); expect(() => tgpu.resolve([getColor])).toThrowErrorMatchingInlineSnapshot(` - [Error: Resolution of the following tree failed: - - - - fn:getColor - - slot:color: Missing value for 'slot:color'] - `); + [Error: Resolution of the following tree failed: + - + - fn:getColor: Missing value for 'slot:color'] + `); }); it('prefers closer scope', () => { @@ -111,11 +110,11 @@ describe('tgpu.slot', () => { expect(tgpu.resolve([main])).toMatchInlineSnapshot(` "fn getColor() -> vec3f { - return vec3f(1., 0., 0.); + return vec3f(1, 0, 0); } fn getColor_1() -> vec3f { - return vec3f(0., 1., 0.); + return vec3f(0, 1, 0); } fn wrapper() { @@ -168,7 +167,7 @@ describe('tgpu.slot', () => { expect(tgpu.resolve([main])).toMatchInlineSnapshot(` "fn getSize() -> f32 { return 1; } - fn getColor() -> vec3f { return vec3f(1., 0., 0.); } + fn getColor() -> vec3f { return vec3f(1, 0, 0); } fn sizeAndColor() { getSize(); @@ -190,7 +189,7 @@ describe('tgpu.slot', () => { sizeAndColor_1(); } - fn getColor_1() -> vec3f { return vec3f(0., 1., 0.); } + fn getColor_1() -> vec3f { return vec3f(0, 1, 0); } fn sizeAndColor_2() { getSize(); @@ -452,26 +451,10 @@ describe('tgpu.slot', () => { return stringSlot; }`.$uses({ stringSlot }); - expect(() => tgpu.resolve([getVec])).toThrowErrorMatchingInlineSnapshot(); - }); - - it('disallows runtime strings in TGSL implemented functions', () => { - const stringSlot = tgpu.slot('vec3f()'); - - const getVec = () => { - 'use gpu'; - const v = stringSlot.$; - return v; - }; - expect(() => tgpu.resolve([getVec])).toThrowErrorMatchingInlineSnapshot(` [Error: Resolution of the following tree failed: - - - fn*:getVec - - fn*:getVec(): 'const v = stringSlot.$' is invalid, cannot determine WGSL type of 'stringSlot.$' - ----- - - Try using or defining a schema that matches your desired value the most, and wrap the value with it: 'const v = Schema(stringSlot.$)' - -----] + - fn:getVec: Slots cannot be used for string injection. For that, use 'rawCodeSnippet'.] `); }); }); From f09f85c7afdd9a336ea7425a8bb3b8dd021c3307 Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Thu, 9 Jul 2026 14:34:27 +0200 Subject: [PATCH 36/44] fix: Don't treat strings as resolvable --- .../typegpu/src/core/resolve/externals.ts | 4 +- packages/typegpu/src/core/resolve/stitch.ts | 4 +- packages/typegpu/src/core/slot/accessor.ts | 7 +--- packages/typegpu/src/core/valueProxyUtils.ts | 4 +- packages/typegpu/src/resolutionCtx.ts | 17 +++++--- .../typegpu/src/tgsl/generationHelpers.ts | 2 +- packages/typegpu/src/tgsl/wgslGenerator.ts | 39 +++++++++---------- packages/typegpu/src/types.ts | 1 - packages/typegpu/tests/slot.test.ts | 17 ++++++++ .../typegpu/tests/tgsl/wgslGenerator.test.ts | 33 ++++++++++++++++ 10 files changed, 88 insertions(+), 40 deletions(-) diff --git a/packages/typegpu/src/core/resolve/externals.ts b/packages/typegpu/src/core/resolve/externals.ts index 5d96bb18d8..1ca3d245ca 100644 --- a/packages/typegpu/src/core/resolve/externals.ts +++ b/packages/typegpu/src/core/resolve/externals.ts @@ -136,6 +136,8 @@ export function replaceExternalsInWgsl( throw new Error("Slots cannot be used for string injection. For that, use 'rawCodeSnippet'."); } - return ctx.resolve(currentItem).value + suffix; + return ( + (typeof currentItem === 'string' ? currentItem : ctx.resolve(currentItem).value) + suffix + ); }); } diff --git a/packages/typegpu/src/core/resolve/stitch.ts b/packages/typegpu/src/core/resolve/stitch.ts index 912c81d93a..b9664d8c3b 100644 --- a/packages/typegpu/src/core/resolve/stitch.ts +++ b/packages/typegpu/src/core/resolve/stitch.ts @@ -15,9 +15,7 @@ export function stitch( const ctx = getResolutionCtx() as ResolutionCtx; function resolveSnippet(maybeSnippet: Snippet | string | number) { - return isSnippet(maybeSnippet) - ? ctx.resolve(maybeSnippet.value, maybeSnippet.dataType).value - : maybeSnippet; + return isSnippet(maybeSnippet) ? ctx.resolveSnippet(maybeSnippet).value : maybeSnippet; } let result = ''; diff --git a/packages/typegpu/src/core/slot/accessor.ts b/packages/typegpu/src/core/slot/accessor.ts index 43f252f20d..b8d53a9768 100644 --- a/packages/typegpu/src/core/slot/accessor.ts +++ b/packages/typegpu/src/core/slot/accessor.ts @@ -151,12 +151,7 @@ abstract class AccessorBase< } [$resolve](ctx: ResolutionCtx): ResolvedSnippet { - const snippet = this.#createSnippet(); - return snip( - ctx.resolve(snippet.value, snippet.dataType).value, - snippet.dataType as T, - snippet.origin, - ); + return ctx.resolveSnippet(this.#createSnippet()); } } diff --git a/packages/typegpu/src/core/valueProxyUtils.ts b/packages/typegpu/src/core/valueProxyUtils.ts index ad401ea46e..819367f2ea 100644 --- a/packages/typegpu/src/core/valueProxyUtils.ts +++ b/packages/typegpu/src/core/valueProxyUtils.ts @@ -32,7 +32,7 @@ export const valueProxyHandler: ProxyHandler = return new Proxy( { [$internal]: true, - [$resolve]: (ctx) => ctx.resolve(accessed.value, accessed.dataType), + [$resolve]: (ctx) => ctx.resolveSnippet(accessed), [$ownSnippet]: accessed, toString: () => `${String(target)}[${prop}]`, }, @@ -49,7 +49,7 @@ export const valueProxyHandler: ProxyHandler = return new Proxy( { [$internal]: true, - [$resolve]: (ctx) => ctx.resolve(accessed.value, accessed.dataType), + [$resolve]: (ctx) => ctx.resolveSnippet(accessed), [$ownSnippet]: accessed, toString: () => `${String(target)}.${prop}`, }, diff --git a/packages/typegpu/src/resolutionCtx.ts b/packages/typegpu/src/resolutionCtx.ts index df357024a1..6b7a0e4e3f 100644 --- a/packages/typegpu/src/resolutionCtx.ts +++ b/packages/typegpu/src/resolutionCtx.ts @@ -958,6 +958,16 @@ export class ResolutionCtxImpl implements ResolutionCtx { } resolve(item: unknown, schema?: BaseData | UnknownData): ResolvedSnippet { + if (typeof item === 'string') { + if (!schema || schema === UnknownData) { + throw new Error( + `Strings cannot be injected into WGSL directly (tried to inject '${item}'). Look for TypeGPU APIs that cover your use-case, or resort to using tgpu['~unstable'].rawCodeSnippet for raw code injection.`, + ); + } + // Already resolved + return snip(item, schema, /* origin */ 'runtime'); + } + if ((isTgpuFn(item) || isShelllessImpl(item)) && !isProviding(item)) { // We skip providing functions to only perform the checks on slot-less functions. if ( @@ -1009,11 +1019,6 @@ export class ResolutionCtxImpl implements ResolutionCtx { return snip(item ? 'true' : 'false', bool, /* origin */ 'constant'); } - if (typeof item === 'string') { - // Already resolved - return snip(item, Void, /* origin */ 'runtime'); - } - if (schema && isWgslArray(schema)) { if (!Array.isArray(item)) { throw new WgslTypeError(`Cannot coerce ${item} into value of type '${schema}'`); @@ -1042,7 +1047,7 @@ export class ResolutionCtxImpl implements ResolutionCtx { throw new WgslTypeError( `Value ${safeStringify(item)} is not resolvable${ - schema ? ` to type ${safeStringify(schema)}` : '' + schema && schema !== UnknownData ? ` to type ${safeStringify(schema)}` : '' }`, ); } diff --git a/packages/typegpu/src/tgsl/generationHelpers.ts b/packages/typegpu/src/tgsl/generationHelpers.ts index cc645e63dd..de29bf0da7 100644 --- a/packages/typegpu/src/tgsl/generationHelpers.ts +++ b/packages/typegpu/src/tgsl/generationHelpers.ts @@ -167,7 +167,7 @@ export class ArrayExpression implements SelfResolvable { for (const elem of this.elements) { // We check if there are no references among the elements if (isAlias(elem) && !isNaturallyEphemeral(elem.dataType)) { - const snippetStr = ctx.resolve(elem.value, elem.dataType).value; + const snippetStr = ctx.resolveSnippet(elem).value; const snippetType = ctx.resolve(concretize(elem.dataType as BaseData)).value; throw new WgslTypeError( `'${snippetStr}' reference cannot be used in an array constructor.\n-----\nTry '${snippetType}(${snippetStr})' or 'arrayOf(${snippetType}, count)([...])' to copy the value instead.\n-----`, diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index 55d8ceb50e..3762e95da1 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -173,18 +173,17 @@ const unaryOpCodeToCodegen = { return snip(!argExpr.value, bool, 'constant'); } - const { value, dataType } = argExpr; - const argStr = ctx.resolve(value, dataType).value; + const argStr = ctx.resolveSnippet(argExpr).value; - if (wgsl.isBool(dataType)) { + if (wgsl.isBool(argExpr.dataType)) { return snip(`!${argStr}`, bool, 'runtime'); } - if (wgsl.isNumericSchema(dataType)) { + if (wgsl.isNumericSchema(argExpr.dataType)) { const resultStr = `!bool(${argStr})`; const nanGuardedStr = // abstractFloat will be resolved as comptime known value - dataType.type === 'f32' + argExpr.dataType.type === 'f32' ? `(((bitcast(${argStr}) & 0x7fffffff) > 0x7f800000) || ${resultStr})` - : dataType.type === 'f16' + : argExpr.dataType.type === 'f16' ? `(((bitcast(${argStr}) & 0x7fff) > 0x7c00) || ${resultStr})` : resultStr; @@ -363,7 +362,7 @@ ${this.ctx.pre}}`; // we can skip lhs const convRhs = tryConvertSnippet(this.ctx, rhsExpr, bool, false); - const rhsStr = this.ctx.resolve(convRhs.value, convRhs.dataType).value; + const rhsStr = this.ctx.resolveSnippet(convRhs).value; return snip(rhsStr, bool, 'runtime'); } @@ -452,8 +451,8 @@ ${this.ctx.pre}}`; ]; } - const lhsStr = this.ctx.resolve(convLhs.value, convLhs.dataType).value; - const rhsStr = this.ctx.resolve(convRhs.value, convRhs.dataType).value; + const lhsStr = this.ctx.resolveSnippet(convLhs).value; + const rhsStr = this.ctx.resolveSnippet(convRhs).value; const type = operatorToType(convLhs.dataType, op, convRhs.dataType); if (exprType === NODE.assignmentExpr) { @@ -498,7 +497,7 @@ ${this.ctx.pre}}`; return codegen(this.ctx, [argExpr]); } - const argStr = this.ctx.resolve(argExpr.value, argExpr.dataType).value; + const argStr = this.ctx.resolveSnippet(argExpr).value; const type = operatorToType(argExpr.dataType, op); // Result of an operation, so not a reference to anything @@ -583,7 +582,7 @@ ${this.ctx.pre}}`; // Either `Struct({ x: 1, y: 2 })`, or `Struct(otherStruct)`. // In both cases, we just let the argument resolve everything. return snip( - this.ctx.resolve(arg.value, callee.value).value, + this.ctx.resolveSnippet(arg).value, callee.value, // A new struct, so not a reference. /* origin */ 'runtime', @@ -623,7 +622,7 @@ ${this.ctx.pre}}`; // `d.arrayOf(...)(otherArr)`. // We just let the argument resolve everything. return snip( - this.ctx.resolve(arg.value, callee.value).value, + this.ctx.resolveSnippet(arg).value, callee.value, // A new array, so not a reference. /* origin */ 'runtime', @@ -1160,7 +1159,7 @@ Try 'return ${typeStr}(${str});' instead. this.ctx.defineVariable(rawId, snippet); const rhsSnippet = tryConvertSnippet(this.ctx, eq, definitionDataType, false); - const rhsStr = this.ctx.resolve(rhsSnippet.value, rhsSnippet.dataType).value; + const rhsStr = this.ctx.resolveSnippet(rhsSnippet).value; // Even though the user defined a 'let' (expecting it to be reassigned), the // reassignment might happen in a pruned branch, in which case we can generate @@ -1276,7 +1275,7 @@ Try 'return ${typeStr}(${str});' instead. this.ctx.defineVariable(rawId, snippet); const rhsSnippet = tryConvertSnippet(this.ctx, eq, definitionDataType, false); - const rhsStr = this.ctx.resolve(rhsSnippet.value, rhsSnippet.dataType).value; + const rhsStr = this.ctx.resolveSnippet(rhsSnippet).value; let emittedVarType: 'var' | 'let' | 'const' | `#VAR_${number}#`; if (varType === '') { @@ -1294,8 +1293,8 @@ Try 'return ${typeStr}(${str});' instead. protected _statement(statement: tinyest.Statement): string { if (typeof statement === 'string') { const id = this._identifier(statement); - const resolved = id.value && this.ctx.resolve(id.value).value; - // oxlint-disable-next-line typescript/no-base-to-string + const resolved = + id.value !== undefined && id.value !== null ? this.ctx.resolveSnippet(id).value : ''; return resolved ? `${this.ctx.pre}${resolved};` : ''; } @@ -1388,7 +1387,7 @@ ${this.ctx.pre}else ${alternate}`; try { const [_, condition, body] = statement; const condSnippet = this._typedExpression(condition, bool); - const conditionStr = this.ctx.resolve(condSnippet.value).value; + const conditionStr = this.ctx.resolveSnippet(condSnippet).value; const bodyStr = this._block(blockifySingleStatement(body)); return `${this.ctx.pre}while (${conditionStr}) ${bodyStr}`; @@ -1508,7 +1507,7 @@ ${this.ctx.pre}else ${alternate}`; // Post-update statement const [_, op, arg] = statement; const argExpr = this._expression(arg); - const argStr = this.ctx.resolve(argExpr.value, argExpr.dataType).value; + const argStr = this.ctx.resolveSnippet(argExpr).value; validateSnippetMutation(argExpr, statement); this.tryMarkModified(arg); @@ -1531,8 +1530,8 @@ ${this.ctx.pre}else ${alternate}`; } const expr = this._expression(statement); - const resolved = expr.value && this.ctx.resolve(expr.value).value; - // oxlint-disable-next-line typescript/no-base-to-string + const resolved = + expr.value !== undefined && expr.value !== null ? this.ctx.resolveSnippet(expr).value : ''; return resolved ? `${this.ctx.pre}${resolved};` : ''; } diff --git a/packages/typegpu/src/types.ts b/packages/typegpu/src/types.ts index 6f042f0510..d27ecb79f1 100644 --- a/packages/typegpu/src/types.ts +++ b/packages/typegpu/src/types.ts @@ -426,7 +426,6 @@ export function isWgsl(value: unknown): value is Wgsl { return ( typeof value === 'number' || typeof value === 'boolean' || - typeof value === 'string' || isSelfResolvable(value) || isWgslData(value) || isSlot(value) || diff --git a/packages/typegpu/tests/slot.test.ts b/packages/typegpu/tests/slot.test.ts index 48447f09c9..5a577055c1 100644 --- a/packages/typegpu/tests/slot.test.ts +++ b/packages/typegpu/tests/slot.test.ts @@ -457,4 +457,21 @@ describe('tgpu.slot', () => { - fn:getVec: Slots cannot be used for string injection. For that, use 'rawCodeSnippet'.] `); }); + + it(`doesn't allow arbitrary shader code to be injected`, () => { + const bar = tgpu.slot('// a comment'); + + function foo() { + 'use gpu'; + bar.$; + return 2; + } + + expect(() => tgpu.resolve([foo])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:foo + - fn*:foo(): Strings cannot be injected into WGSL directly (tried to inject '// a comment'). Look for TypeGPU APIs that cover your use-case, or resort to using tgpu['~unstable'].rawCodeSnippet for raw code injection.] + `); + }); }); diff --git a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts index 0e73ec78f6..1778fdb58b 100644 --- a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts +++ b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts @@ -2160,4 +2160,37 @@ describe('wgslGenerator', () => { `); }); }); + + it(`doesn't allow for code injection through string literals`, () => { + function main() { + 'use gpu'; + const some = 1 + 2; + ('pieceOfCode()'); + return some + 4; + } + + expect(() => tgpu.resolve([main])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:main + - fn*:main(): Strings cannot be injected into WGSL directly (tried to inject 'pieceOfCode()'). Look for TypeGPU APIs that cover your use-case, or resort to using tgpu['~unstable'].rawCodeSnippet for raw code injection.] + `); + }); + + it(`doesn't allow for code injection through external strings`, () => { + const pieceOfCode = 'pieceOfCode()'; + function main() { + 'use gpu'; + const some = 1 + 2; + pieceOfCode; + return some + 4; + } + + expect(() => tgpu.resolve([main])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:main + - fn*:main(): Strings cannot be injected into WGSL directly (tried to inject 'pieceOfCode()'). Look for TypeGPU APIs that cover your use-case, or resort to using tgpu['~unstable'].rawCodeSnippet for raw code injection.] + `); + }); }); From 049e4773a564d4e32f8fd6602a4bbc9635b4a053 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Thu, 9 Jul 2026 16:04:25 +0200 Subject: [PATCH 37/44] Fix the resolvable check --- packages/typegpu/src/core/resolve/externals.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typegpu/src/core/resolve/externals.ts b/packages/typegpu/src/core/resolve/externals.ts index 1ca3d245ca..ed1c25e42d 100644 --- a/packages/typegpu/src/core/resolve/externals.ts +++ b/packages/typegpu/src/core/resolve/externals.ts @@ -111,7 +111,7 @@ export function replaceExternalsInWgsl( for (const [i, elem] of chain.entries()) { currentItem = (currentItem as ExternalMap)[elem]; - if (isResolvable(currentItem)) { + if (isResolvable(currentItem) || typeof currentItem === 'string') { suffix = chain .slice(i + 1) .map((s) => `.${s}`) From 737c910103ee0726b7926a1496c41b0eb3fcf613 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Thu, 9 Jul 2026 16:11:22 +0200 Subject: [PATCH 38/44] Update tgsl tests --- .../typegpu/tests/tgsl/wgslGenerator.test.ts | 33 --------- packages/typegpu/tests/tgslFn.test.ts | 68 +++++++++++++++++++ 2 files changed, 68 insertions(+), 33 deletions(-) diff --git a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts index 1778fdb58b..0e73ec78f6 100644 --- a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts +++ b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts @@ -2160,37 +2160,4 @@ describe('wgslGenerator', () => { `); }); }); - - it(`doesn't allow for code injection through string literals`, () => { - function main() { - 'use gpu'; - const some = 1 + 2; - ('pieceOfCode()'); - return some + 4; - } - - expect(() => tgpu.resolve([main])).toThrowErrorMatchingInlineSnapshot(` - [Error: Resolution of the following tree failed: - - - - fn*:main - - fn*:main(): Strings cannot be injected into WGSL directly (tried to inject 'pieceOfCode()'). Look for TypeGPU APIs that cover your use-case, or resort to using tgpu['~unstable'].rawCodeSnippet for raw code injection.] - `); - }); - - it(`doesn't allow for code injection through external strings`, () => { - const pieceOfCode = 'pieceOfCode()'; - function main() { - 'use gpu'; - const some = 1 + 2; - pieceOfCode; - return some + 4; - } - - expect(() => tgpu.resolve([main])).toThrowErrorMatchingInlineSnapshot(` - [Error: Resolution of the following tree failed: - - - - fn*:main - - fn*:main(): Strings cannot be injected into WGSL directly (tried to inject 'pieceOfCode()'). Look for TypeGPU APIs that cover your use-case, or resort to using tgpu['~unstable'].rawCodeSnippet for raw code injection.] - `); - }); }); diff --git a/packages/typegpu/tests/tgslFn.test.ts b/packages/typegpu/tests/tgslFn.test.ts index 425a569505..e9e4dd45f8 100644 --- a/packages/typegpu/tests/tgslFn.test.ts +++ b/packages/typegpu/tests/tgslFn.test.ts @@ -1183,3 +1183,71 @@ describe('tgsl fn when using plugin', () => { `); }); }); + +describe('string injection', () => { + it('is forbidden directly', () => { + const fn = () => { + 'use gpu'; + const x = 1; + ('call()'); + }; + + expect(() => tgpu.resolve([fn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:fn + - fn*:fn(): Strings cannot be injected into WGSL directly (tried to inject 'call()'). Look for TypeGPU APIs that cover your use-case, or resort to using tgpu['~unstable'].rawCodeSnippet for raw code injection.] + `); + }); + + it('is forbidden via direct externals', () => { + const call = 'call()'; + + const fn = () => { + 'use gpu'; + const x = 1; + call; + }; + + expect(() => tgpu.resolve([fn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:fn + - fn*:fn(): Strings cannot be injected into WGSL directly (tried to inject 'call()'). Look for TypeGPU APIs that cover your use-case, or resort to using tgpu['~unstable'].rawCodeSnippet for raw code injection.] + `); + }); + + it('is forbidden via indirect externals', () => { + const call = ['call()']; + + const fn = () => { + 'use gpu'; + const x = 1; + call[0]; + }; + + expect(() => tgpu.resolve([fn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:fn + - fn*:fn(): Strings cannot be injected into WGSL directly (tried to inject 'call()'). Look for TypeGPU APIs that cover your use-case, or resort to using tgpu['~unstable'].rawCodeSnippet for raw code injection.] + `); + }); + + it('is forbidden via slots', () => { + const slot = tgpu.slot('call()'); + + const fn = () => { + 'use gpu'; + const x = 1; + slot.$; + }; + + expect(() => tgpu.resolve([fn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:fn + - fn*:fn(): Strings cannot be injected into WGSL directly (tried to inject 'call()'). Look for TypeGPU APIs that cover your use-case, or resort to using tgpu['~unstable'].rawCodeSnippet for raw code injection.] + `); + }); +}); From 44bab90922f34966b022a9c52a31a090573a19fa Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Thu, 9 Jul 2026 16:20:57 +0200 Subject: [PATCH 39/44] Move rawfn tests --- .../typegpu/src/core/resolve/externals.ts | 4 -- packages/typegpu/tests/rawFn.test.ts | 42 +++++++++++++++++++ packages/typegpu/tests/slot.test.ts | 34 +-------------- 3 files changed, 44 insertions(+), 36 deletions(-) diff --git a/packages/typegpu/src/core/resolve/externals.ts b/packages/typegpu/src/core/resolve/externals.ts index ed1c25e42d..777c4a216e 100644 --- a/packages/typegpu/src/core/resolve/externals.ts +++ b/packages/typegpu/src/core/resolve/externals.ts @@ -132,10 +132,6 @@ export function replaceExternalsInWgsl( } } - if (isSlot(currentItem) && typeof currentItem.$ === 'string') { - throw new Error("Slots cannot be used for string injection. For that, use 'rawCodeSnippet'."); - } - return ( (typeof currentItem === 'string' ? currentItem : ctx.resolve(currentItem).value) + suffix ); diff --git a/packages/typegpu/tests/rawFn.test.ts b/packages/typegpu/tests/rawFn.test.ts index d97d21c37c..eaf5170a26 100644 --- a/packages/typegpu/tests/rawFn.test.ts +++ b/packages/typegpu/tests/rawFn.test.ts @@ -594,3 +594,45 @@ describe('tgpu.computeFn with raw string WGSL implementation', () => { `); }); }); + +describe('string injection', () => { + it('is forbidden via direct externals', () => { + const fn = tgpu.fn([])`() { + ext; + };`.$uses({ ext: 'call()' }); + + expect(() => tgpu.resolve([fn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn:fn: Strings cannot be injected into WGSL directly (tried to inject 'call()'). Look for TypeGPU APIs that cover your use-case, or resort to using tgpu['~unstable'].rawCodeSnippet for raw code injection.] + `); + }); + + it('is forbidden via indirect externals', () => { + const fn = tgpu.fn([])`() { + ext.p; + };`.$uses({ ext: { p: 'call()' } }); + + expect(() => tgpu.resolve([fn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:fn + - fn*:fn(): Strings cannot be injected into WGSL directly (tried to inject 'call()'). Look for TypeGPU APIs that cover your use-case, or resort to using tgpu['~unstable'].rawCodeSnippet for raw code injection.] + `); + }); + + it('is forbidden via slots', () => { + const slot = tgpu.slot('call()'); + + const fn = tgpu.fn([])`() { + slot; + };`.$uses({ slot }); + + expect(() => tgpu.resolve([fn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn:fn + - slot:slot: Strings cannot be injected into WGSL directly (tried to inject 'call()'). Look for TypeGPU APIs that cover your use-case, or resort to using tgpu['~unstable'].rawCodeSnippet for raw code injection.] + `); + }); +}); diff --git a/packages/typegpu/tests/slot.test.ts b/packages/typegpu/tests/slot.test.ts index 5a577055c1..283d56dd9a 100644 --- a/packages/typegpu/tests/slot.test.ts +++ b/packages/typegpu/tests/slot.test.ts @@ -83,7 +83,8 @@ describe('tgpu.slot', () => { expect(() => tgpu.resolve([getColor])).toThrowErrorMatchingInlineSnapshot(` [Error: Resolution of the following tree failed: - - - fn:getColor: Missing value for 'slot:color'] + - fn:getColor + - slot:color: Missing value for 'slot:color'] `); }); @@ -443,35 +444,4 @@ describe('tgpu.slot', () => { }" `); }); - - it('disallows runtime strings in raw-wgsl implemented functions', () => { - const stringSlot = tgpu.slot('vec3f()'); - - const getVec = tgpu.fn([])`() => { - return stringSlot; - }`.$uses({ stringSlot }); - - expect(() => tgpu.resolve([getVec])).toThrowErrorMatchingInlineSnapshot(` - [Error: Resolution of the following tree failed: - - - - fn:getVec: Slots cannot be used for string injection. For that, use 'rawCodeSnippet'.] - `); - }); - - it(`doesn't allow arbitrary shader code to be injected`, () => { - const bar = tgpu.slot('// a comment'); - - function foo() { - 'use gpu'; - bar.$; - return 2; - } - - expect(() => tgpu.resolve([foo])).toThrowErrorMatchingInlineSnapshot(` - [Error: Resolution of the following tree failed: - - - - fn*:foo - - fn*:foo(): Strings cannot be injected into WGSL directly (tried to inject '// a comment'). Look for TypeGPU APIs that cover your use-case, or resort to using tgpu['~unstable'].rawCodeSnippet for raw code injection.] - `); - }); }); From a63b41f3abc8d54dfd16b92b2683017bb257a03a Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Thu, 9 Jul 2026 16:55:37 +0200 Subject: [PATCH 40/44] Add ResolvableString and forbid strings in raw fn --- packages/typegpu/src/core/function/fnCore.ts | 6 +++++- packages/typegpu/src/core/resolve/externals.ts | 4 +--- .../typegpu/src/core/resolve/resolvableString.ts | 15 +++++++++++++++ packages/typegpu/tests/rawFn.test.ts | 3 +-- 4 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 packages/typegpu/src/core/resolve/resolvableString.ts diff --git a/packages/typegpu/src/core/function/fnCore.ts b/packages/typegpu/src/core/function/fnCore.ts index 7dd753a5af..dbc9e1cffd 100644 --- a/packages/typegpu/src/core/function/fnCore.ts +++ b/packages/typegpu/src/core/function/fnCore.ts @@ -11,6 +11,7 @@ import { replaceExternalsInWgsl, mergeFunctionExternals, } from '../resolve/externals.ts'; +import { ResolvableString } from '../resolve/resolvableString.ts'; import { extractArgs } from './extractArgs.ts'; import type { Implementation, SeparatedEntryArgs } from './fnTypes.ts'; @@ -127,7 +128,10 @@ export function createFnCore( this.setExternals('args', { in: Object.fromEntries( - entryInput.positionalArgs.map((a) => [a.schemaKey, a.schemaKey]), + entryInput.positionalArgs.map((a) => [ + a.schemaKey, + new ResolvableString(a.schemaKey), + ]), ), }); } diff --git a/packages/typegpu/src/core/resolve/externals.ts b/packages/typegpu/src/core/resolve/externals.ts index 777c4a216e..a6576aacf5 100644 --- a/packages/typegpu/src/core/resolve/externals.ts +++ b/packages/typegpu/src/core/resolve/externals.ts @@ -132,8 +132,6 @@ export function replaceExternalsInWgsl( } } - return ( - (typeof currentItem === 'string' ? currentItem : ctx.resolve(currentItem).value) + suffix - ); + return ctx.resolve(currentItem).value + suffix; }); } diff --git a/packages/typegpu/src/core/resolve/resolvableString.ts b/packages/typegpu/src/core/resolve/resolvableString.ts new file mode 100644 index 0000000000..7be4b4a26e --- /dev/null +++ b/packages/typegpu/src/core/resolve/resolvableString.ts @@ -0,0 +1,15 @@ +import { snip, UnknownData } from '../../internal.ts'; +import { $internal, $resolve } from '../../shared/symbols.ts'; + +export class ResolvableString { + [$internal] = true; + #value; + + constructor(value: string) { + this.#value = value; + } + + [$resolve]() { + return snip(this.#value, UnknownData, 'runtime', true); + } +} diff --git a/packages/typegpu/tests/rawFn.test.ts b/packages/typegpu/tests/rawFn.test.ts index eaf5170a26..49b1fb0b73 100644 --- a/packages/typegpu/tests/rawFn.test.ts +++ b/packages/typegpu/tests/rawFn.test.ts @@ -616,8 +616,7 @@ describe('string injection', () => { expect(() => tgpu.resolve([fn])).toThrowErrorMatchingInlineSnapshot(` [Error: Resolution of the following tree failed: - - - fn*:fn - - fn*:fn(): Strings cannot be injected into WGSL directly (tried to inject 'call()'). Look for TypeGPU APIs that cover your use-case, or resort to using tgpu['~unstable'].rawCodeSnippet for raw code injection.] + - fn:fn: Strings cannot be injected into WGSL directly (tried to inject 'call()'). Look for TypeGPU APIs that cover your use-case, or resort to using tgpu['~unstable'].rawCodeSnippet for raw code injection.] `); }); From e4c47576c5ddc201034e01797da2ee8674eb9224 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Thu, 9 Jul 2026 17:04:24 +0200 Subject: [PATCH 41/44] lint & types --- packages/typegpu/src/core/resolve/externals.ts | 1 - packages/typegpu/tests/slot.test.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/typegpu/src/core/resolve/externals.ts b/packages/typegpu/src/core/resolve/externals.ts index a6576aacf5..822f77e3ff 100644 --- a/packages/typegpu/src/core/resolve/externals.ts +++ b/packages/typegpu/src/core/resolve/externals.ts @@ -3,7 +3,6 @@ import { isWgslStruct } from '../../data/wgslTypes.ts'; import { getName, hasTinyestMetadata, isNamable, setName } from '../../shared/meta.ts'; import { isWgsl, type ResolutionCtx } from '../../types.ts'; import type { FnExternals } from '../function/fnCore.ts'; -import { isSlot } from '../slot/slotTypes.ts'; /** * A key-value mapping where keys represent identifiers within shader code, diff --git a/packages/typegpu/tests/slot.test.ts b/packages/typegpu/tests/slot.test.ts index 283d56dd9a..9deb4ef4c0 100644 --- a/packages/typegpu/tests/slot.test.ts +++ b/packages/typegpu/tests/slot.test.ts @@ -89,7 +89,7 @@ describe('tgpu.slot', () => { }); it('prefers closer scope', () => { - const colorSlot = tgpu.slot(); // no default + const colorSlot = tgpu.slot(); // no default const getColor = tgpu.fn([], d.vec3f)`() -> vec3f { return colorSlot; From c0d313246f58015d0d0fd9b9fd5866bad1c7a1aa Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Thu, 9 Jul 2026 17:12:20 +0200 Subject: [PATCH 42/44] lint --- packages/typegpu/tests/slot.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typegpu/tests/slot.test.ts b/packages/typegpu/tests/slot.test.ts index 9deb4ef4c0..b8dc9283ec 100644 --- a/packages/typegpu/tests/slot.test.ts +++ b/packages/typegpu/tests/slot.test.ts @@ -131,7 +131,7 @@ describe('tgpu.slot', () => { it('reuses common nested functions', () => { const sizeSlot = tgpu.slot<1 | 100>(); - const colorSlot = tgpu.slot(); + const colorSlot = tgpu.slot(); const getSize = tgpu.fn([], d.f32)`() { return sizeSlot; }`.$uses({ sizeSlot }); From 34ae441b1836a8aa2d342e82e901638eeb68edb9 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Thu, 9 Jul 2026 17:19:15 +0200 Subject: [PATCH 43/44] Fix import --- packages/typegpu/src/core/resolve/resolvableString.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/typegpu/src/core/resolve/resolvableString.ts b/packages/typegpu/src/core/resolve/resolvableString.ts index 7be4b4a26e..38f33ddab2 100644 --- a/packages/typegpu/src/core/resolve/resolvableString.ts +++ b/packages/typegpu/src/core/resolve/resolvableString.ts @@ -1,4 +1,5 @@ -import { snip, UnknownData } from '../../internal.ts'; +import { UnknownData } from '../../data/dataTypes.ts'; +import { snip } from '../../data/snippet.ts'; import { $internal, $resolve } from '../../shared/symbols.ts'; export class ResolvableString { From 3d934c9073c8dd8363bf2dd980bc296ed5d2d847 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 10 Jul 2026 12:15:13 +0200 Subject: [PATCH 44/44] Review fixes --- packages/typegpu/src/resolutionCtx.ts | 4 +++- packages/typegpu/src/types.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/typegpu/src/resolutionCtx.ts b/packages/typegpu/src/resolutionCtx.ts index 6b7a0e4e3f..c59d177e1a 100644 --- a/packages/typegpu/src/resolutionCtx.ts +++ b/packages/typegpu/src/resolutionCtx.ts @@ -964,7 +964,9 @@ export class ResolutionCtxImpl implements ResolutionCtx { `Strings cannot be injected into WGSL directly (tried to inject '${item}'). Look for TypeGPU APIs that cover your use-case, or resort to using tgpu['~unstable'].rawCodeSnippet for raw code injection.`, ); } - // Already resolved + // For example: + // () => { 'use gpu'; const color = d.vec3f(); return color; } + // snip('color', d.vec3f) ^^^^^ return snip(item, schema, /* origin */ 'runtime'); } diff --git a/packages/typegpu/src/types.ts b/packages/typegpu/src/types.ts index d27ecb79f1..c5263b736e 100644 --- a/packages/typegpu/src/types.ts +++ b/packages/typegpu/src/types.ts @@ -72,7 +72,7 @@ export type ResolvableObject = | AnyData | ((...args: never[]) => unknown); -export type Wgsl = Eventual; +export type Wgsl = Eventual; export type TgpuShaderStage = 'compute' | 'vertex' | 'fragment';