diff --git a/apps/typegpu-docs/src/examples/simulation/fluid-double-buffering/index.ts b/apps/typegpu-docs/src/examples/simulation/fluid-double-buffering/index.ts index 47be22856..5893c4a34 100644 --- a/apps/typegpu-docs/src/examples/simulation/fluid-double-buffering/index.ts +++ b/apps/typegpu-docs/src/examples/simulation/fluid-double-buffering/index.ts @@ -454,7 +454,6 @@ function makePipelines( outputGridMutable: TgpuBufferMutable, ) { const initWorldPipeline = root['~unstable'] - .with(inputGridSlot, outputGridMutable) .with(outputGridSlot, outputGridMutable) .createGuardedComputePipeline((xu, yu) => { 'use gpu'; diff --git a/packages/typegpu/src/core/function/tgpuFn.ts b/packages/typegpu/src/core/function/tgpuFn.ts index 4c5a3cc93..8983881c4 100644 --- a/packages/typegpu/src/core/function/tgpuFn.ts +++ b/packages/typegpu/src/core/function/tgpuFn.ts @@ -289,7 +289,7 @@ function createBoundFunction( slot: TgpuSlot | TgpuAccessor, value: unknown, ): TgpuFn { - return createBoundFunction(fn, [ + return createBoundFunction(innerFn, [ ...pairs, [isAccessor(slot) ? slot.slot : slot, value], ]); diff --git a/packages/typegpu/src/resolutionCtx.ts b/packages/typegpu/src/resolutionCtx.ts index 5b1bb0c26..4eaf848de 100644 --- a/packages/typegpu/src/resolutionCtx.ts +++ b/packages/typegpu/src/resolutionCtx.ts @@ -65,11 +65,12 @@ import type { ItemLayer, ItemStateStack, ResolutionCtx, + StackLayer, Wgsl, } from './types.ts'; import { CodegenState, isSelfResolvable, NormalState } from './types.ts'; import type { WgslExtension } from './wgslExtensions.ts'; -import { hasTinyestMetadata } from './shared/meta.ts'; +import { getName, hasTinyestMetadata } from './shared/meta.ts'; /** * Inserted into bind group entry definitions that belong @@ -90,23 +91,8 @@ export type ResolutionCtxImplOptions = { readonly namespace: Namespace; }; -type SlotBindingLayer = { - type: 'slotBinding'; - bindingMap: WeakMap, unknown>; -}; - -type BlockScopeLayer = { - type: 'blockScope'; - declarations: Map; -}; - class ItemStateStackImpl implements ItemStateStack { - private _stack: ( - | ItemLayer - | SlotBindingLayer - | FunctionScopeLayer - | BlockScopeLayer - )[] = []; + private _stack: StackLayer[] = []; private _itemDepth = 0; get itemDepth(): number { @@ -133,21 +119,14 @@ class ItemStateStackImpl implements ItemStateStack { }); } - popItem() { - this.pop('item'); - } - pushSlotBindings(pairs: SlotValuePair[]) { this._stack.push({ type: 'slotBinding', bindingMap: new WeakMap(pairs), + usedSet: new WeakSet(), }); } - popSlotBindings() { - this.pop('slotBinding'); - } - pushFunctionScope( args: Snippet[], argAliases: Record, @@ -167,10 +146,6 @@ class ItemStateStackImpl implements ItemStateStack { return scope; } - popFunctionScope() { - this.pop('functionScope'); - } - pushBlockScope() { this._stack.push({ type: 'blockScope', @@ -178,20 +153,19 @@ class ItemStateStackImpl implements ItemStateStack { }); } - popBlockScope() { - this.pop('blockScope'); - } - - pop(type?: (typeof this._stack)[number]['type']) { + pop(type: T): Extract; + pop(): StackLayer | undefined; + pop(type?: StackLayer['type']) { const layer = this._stack[this._stack.length - 1]; if (!layer || (type && layer.type !== type)) { throw new Error(`Internal error, expected a ${type} layer to be on top.`); } - this._stack.pop(); + const poppedValue = this._stack.pop(); if (type === 'item') { this._itemDepth--; } + return poppedValue; } readSlot(slot: TgpuSlot): T | undefined { @@ -204,6 +178,7 @@ class ItemStateStackImpl implements ItemStateStack { const boundValue = layer.bindingMap.get(slot); if (boundValue !== undefined) { + layer.usedSet.add(slot); return boundValue as T; } } else if ( @@ -433,7 +408,7 @@ export class ResolutionCtxImpl implements ResolutionCtx { } popBlockScope() { - this._itemStateStack.popBlockScope(); + this._itemStateStack.pop('blockScope'); } generateLog(op: string, args: Snippet[]): Snippet { @@ -487,7 +462,7 @@ export class ResolutionCtxImpl implements ResolutionCtx { returnType, }; } finally { - this._itemStateStack.popFunctionScope(); + this._itemStateStack.pop('functionScope'); } } @@ -537,7 +512,16 @@ export class ResolutionCtxImpl implements ResolutionCtx { try { return callback(); } finally { - this._itemStateStack.popSlotBindings(); + const usedSlots = this._itemStateStack.pop('slotBinding').usedSet; + pairs.forEach((pair) => { + if (!usedSlots.has(pair[0])) { + console.warn( + `Slot '${getName(pair[0])}' with value '${ + pair[1] + }' was provided in a 'with' method despite not being utilized during resolution. Please verify that this slot was intended for use and that, in case of WGSL-implemented functions, it is properly declared with the '$uses' method.`, + ); + } + }); } } @@ -624,7 +608,7 @@ export class ResolutionCtxImpl implements ResolutionCtx { throw new ResolutionError(err, [derived]); } finally { - this._itemStateStack.popItem(); + this._itemStateStack.pop('item'); } } @@ -695,7 +679,7 @@ export class ResolutionCtxImpl implements ResolutionCtx { throw new ResolutionError(err, [item]); } finally { - this._itemStateStack.popItem(); + this._itemStateStack.pop('item'); } } diff --git a/packages/typegpu/src/types.ts b/packages/typegpu/src/types.ts index 377e1905c..64de7e6e4 100644 --- a/packages/typegpu/src/types.ts +++ b/packages/typegpu/src/types.ts @@ -107,15 +107,30 @@ export type FunctionScopeLayer = { reportedReturnTypes: Set; }; +export type SlotBindingLayer = { + type: 'slotBinding'; + bindingMap: WeakMap, unknown>; + usedSet: WeakSet>; +}; + +export type BlockScopeLayer = { + type: 'blockScope'; + declarations: Map; +}; + +export type StackLayer = + | ItemLayer + | SlotBindingLayer + | FunctionScopeLayer + | BlockScopeLayer; + export interface ItemStateStack { readonly itemDepth: number; readonly topItem: ItemLayer; readonly topFunctionScope: FunctionScopeLayer | undefined; pushItem(): void; - popItem(): void; pushSlotBindings(pairs: SlotValuePair[]): void; - popSlotBindings(): void; pushFunctionScope( args: Snippet[], argAliases: Record, @@ -126,10 +141,11 @@ export interface ItemStateStack { returnType: AnyData | undefined, externalMap: Record, ): FunctionScopeLayer; - popFunctionScope(): void; pushBlockScope(): void; - popBlockScope(): void; - pop(type?: 'functionScope' | 'blockScope' | 'slotBinding' | 'item'): void; + + pop(type: T): Extract; + pop(): StackLayer | undefined; + readSlot(slot: TgpuSlot): T | undefined; getSnippetById(id: string): Snippet | undefined; defineBlockVariable(id: string, snippet: Snippet): void; diff --git a/packages/typegpu/tests/slot.test.ts b/packages/typegpu/tests/slot.test.ts index a9a70bee8..1e5237e87 100644 --- a/packages/typegpu/tests/slot.test.ts +++ b/packages/typegpu/tests/slot.test.ts @@ -1,7 +1,7 @@ -import { describe, expect } from 'vitest'; +import { describe, expect, vi } from 'vitest'; import * as d from '../src/data/index.ts'; -import * as std from '../src/std/index.ts'; import tgpu from '../src/index.ts'; +import * as std from '../src/std/index.ts'; import { it } from './utils/extendedIt.ts'; import { asWgsl } from './utils/parseResolved.ts'; @@ -363,4 +363,183 @@ describe('tgpu.slot', () => { }" `); }); + + it('warns when the slot is unused in WGSL-implemented functions', () => { + using warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + const colorSlot = tgpu.slot(); + + const getColor = tgpu.fn([], d.vec3f)`() { return vec3f(); }`; + + const main = getColor.with(colorSlot, d.vec3f(0, 1, 0)); + + tgpu.resolve({ externals: { main } }); + + expect(warnSpy).toHaveBeenCalledWith( + "Slot 'colorSlot' with value 'vec3f(0, 1, 0)' was provided in a 'with' method despite not being utilized during resolution. Please verify that this slot was intended for use and that, in case of WGSL-implemented functions, it is properly declared with the '$uses' method.", + ); + }); + + it('warns when the slot is unused in TGSL-implemented functions', () => { + using warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + const colorSlot = tgpu.slot(); + + const getColor = tgpu.fn([], d.vec3f)(() => { + return d.vec3f(); + }); + + const main = getColor.with(colorSlot, d.vec3f(0, 1, 0)); + + tgpu.resolve({ externals: { main } }); + + expect(warnSpy).toHaveBeenCalledWith( + "Slot 'colorSlot' with value 'vec3f(0, 1, 0)' was provided in a 'with' method despite not being utilized during resolution. Please verify that this slot was intended for use and that, in case of WGSL-implemented functions, it is properly declared with the '$uses' method.", + ); + }); + + it('warns when the slot is unused in WGSL-implemented pipeline', ({ root }) => { + using warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + const colorSlot = tgpu.slot(); + + const computeFn = tgpu['~unstable'].computeFn({ + workgroupSize: [1, 1, 1], + in: { gid: d.builtin.globalInvocationId }, + })`{ }`; + + const pipeline = root['~unstable'] + .with(colorSlot, d.vec3f(1, 0, 1)) + .withCompute(computeFn) + .createPipeline(); + + tgpu.resolve({ externals: { pipeline } }); + + expect(warnSpy).toHaveBeenCalledWith( + "Slot 'colorSlot' with value 'vec3f(1, 0, 1)' was provided in a 'with' method despite not being utilized during resolution. Please verify that this slot was intended for use and that, in case of WGSL-implemented functions, it is properly declared with the '$uses' method.", + ); + }); + + it('warns when the slot is unused in TGSL-implemented pipeline', ({ root }) => { + using warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + const colorSlot = tgpu.slot(); + + const computeFn = tgpu['~unstable'].computeFn({ + workgroupSize: [1, 1, 1], + in: { gid: d.builtin.globalInvocationId }, + })(() => {}); + + const pipeline = root['~unstable'] + .with(colorSlot, d.vec3f(1, 0, 1)) + .withCompute(computeFn) + .createPipeline(); + + tgpu.resolve({ externals: { pipeline } }); + + expect(warnSpy).toHaveBeenCalledWith( + "Slot 'colorSlot' with value 'vec3f(1, 0, 1)' was provided in a 'with' method despite not being utilized during resolution. Please verify that this slot was intended for use and that, in case of WGSL-implemented functions, it is properly declared with the '$uses' method.", + ); + }); + + it('distinguishes different slot usages', () => { + using warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + const colorSlot = tgpu.slot(); + + const getColorUsingSlot = tgpu + .fn([], d.vec3f)(() => { + return colorSlot.$; + }) + .with(colorSlot, d.vec3f(0, 1, 0)); + + const getColor = tgpu + .fn([], d.vec3f)(() => { + return d.vec3f(); + }) + .with(colorSlot, d.vec3f(2, 1, 0)); + + const main = tgpu.fn([])(() => { + getColorUsingSlot(); + getColor(); + }); + tgpu.resolve({ externals: { main } }); + + expect(warnSpy).toHaveBeenCalledWith( + "Slot 'colorSlot' with value 'vec3f(2, 1, 0)' was provided in a 'with' method despite not being utilized during resolution. Please verify that this slot was intended for use and that, in case of WGSL-implemented functions, it is properly declared with the '$uses' method.", + ); + }); + + it('does not warn in nested case', () => { + using warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + const sizeSlot = tgpu.slot(); + + const getSize = tgpu.fn([], d.f32)(() => sizeSlot.$); + + const main = tgpu.fn([], d.f32)(() => getSize()).with(sizeSlot, 1); + + tgpu.resolve({ externals: { main } }); + + expect(warnSpy).toHaveBeenCalledTimes(0); + }); + + it('warns exactly as many times as there are unused slots', () => { + using warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + const sizeSlot = tgpu.slot(); + const colorSlot = tgpu.slot(); + const shapeSlot = tgpu.slot<0 | 1 | 2>(); + + const getSize = tgpu.fn([], d.f32)`() { return sizeSlot; }` + .$uses({ sizeSlot }) + .with(sizeSlot, 1) + .with(colorSlot, RED) + .with(shapeSlot, 2); + + tgpu.resolve({ externals: { getSize } }); + + expect(warnSpy).toHaveBeenCalledTimes(2); + }); + + it('does not warn when default value is used', () => { + using warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + const sizeSlot = tgpu.slot(7); + + const getSize = tgpu.fn([], d.f32)`() { return sizeSlot; }` + .$uses({ sizeSlot }); + + tgpu.resolve({ externals: { getSize } }); + + expect(warnSpy).toHaveBeenCalledTimes(0); + }); + + it('includes slot bindings in toString', () => { + const firstSlot = tgpu.slot(); + const secondSlot = tgpu.slot(); + const thirdSlot = tgpu.slot(); + + const getSize = tgpu.fn([], d.f32)(() => + firstSlot.$ + secondSlot.$ + thirdSlot.$ + ) + .with(firstSlot, 1) + .with(secondSlot, 2) + .with(thirdSlot, 3); + + expect(getSize.toString()).toMatchInlineSnapshot( + `"fn:getSize[firstSlot=1, secondSlot=2, thirdSlot=3]"`, + ); + }); + + it('safe stringifies in toString', () => { + const slot = tgpu.slot(); + + const getSize = tgpu.fn([], d.f32)(() => slot.$.x) + .with(slot, d.vec4f(1, 2, 3, 4)); + + expect(getSize.toString()).toMatchInlineSnapshot( + `"fn:getSize[slot=vec4f(1, 2, 3, 4)]"`, + ); + }); }); diff --git a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts index 627ea920f..6c9baf706 100644 --- a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts +++ b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts @@ -320,7 +320,7 @@ describe('wgslGenerator', () => { const res2 = wgslGenerator.expression( (astInfo.ast?.body[1][1] as tinyest.Const)[2], ); - ctx[$internal].itemStateStack.popBlockScope(); + ctx[$internal].itemStateStack.pop('blockScope'); expect(res2.dataType).toStrictEqual(d.vec4f); @@ -335,7 +335,7 @@ describe('wgslGenerator', () => { const res4 = wgslGenerator.expression( astInfo.ast?.body[1][2] as tinyest.Expression, ); - ctx[$internal].itemStateStack.popBlockScope(); + ctx[$internal].itemStateStack.pop('blockScope'); expect(res3.dataType).toStrictEqual(d.atomic(d.u32)); expect(res4.dataType).toStrictEqual(Void);