From e9af65ef78ff3116a9cdf927bbbe0fb5eecd42c3 Mon Sep 17 00:00:00 2001 From: Aleksander Katan Date: Thu, 23 Oct 2025 11:14:55 +0200 Subject: [PATCH 01/11] Add tests --- packages/typegpu/tests/slot.test.ts | 66 ++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/packages/typegpu/tests/slot.test.ts b/packages/typegpu/tests/slot.test.ts index a9a70bee84..261733f846 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,66 @@ 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(); }`; + + getColor.with(colorSlot, d.vec3f(0, 1, 0)); + + expect(warnSpy).toHaveBeenCalledWith(); + }); + + 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(); + }); + + getColor.with(colorSlot, d.vec3f(0, 1, 0)); + + expect(warnSpy).toHaveBeenCalledWith(); + }); + + 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 }, + })`{ }`; + + root['~unstable'] + .with(colorSlot, d.vec3f(1, 0, 1)) + .withCompute(computeFn) + .createPipeline(); + + expect(warnSpy).toHaveBeenCalledWith(); + }); + + 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 }, + })(() => {}); + + root['~unstable'] + .with(colorSlot, d.vec3f(1, 0, 1)) + .withCompute(computeFn) + .createPipeline(); + + expect(warnSpy).toHaveBeenCalledWith(); + }); }); From 14e34af91bd749f760b6f2bd7f1b80b66c898d46 Mon Sep 17 00:00:00 2001 From: Aleksander Katan Date: Thu, 23 Oct 2025 12:49:58 +0200 Subject: [PATCH 02/11] Add a semi-working implementation --- packages/typegpu/src/resolutionCtx.ts | 11 +++++- packages/typegpu/tests/slot.test.ts | 56 +++++++++++++++++++++++---- 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/packages/typegpu/src/resolutionCtx.ts b/packages/typegpu/src/resolutionCtx.ts index a25252031f..f274410c7e 100644 --- a/packages/typegpu/src/resolutionCtx.ts +++ b/packages/typegpu/src/resolutionCtx.ts @@ -69,7 +69,7 @@ import type { } 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 @@ -521,9 +521,11 @@ export class ResolutionCtxImpl implements ResolutionCtx { }; } + private usedSlots: WeakSet> = new WeakSet(); readSlot(slot: TgpuSlot): T { const value = this._itemStateStack.readSlot(slot); + this.usedSlots.add(slot); if (value === undefined) { throw new MissingSlotValueError(slot); } @@ -538,6 +540,13 @@ export class ResolutionCtxImpl implements ResolutionCtx { return callback(); } finally { this._itemStateStack.popSlotBindings(); + pairs.forEach((pair) => { + !this.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 in the '$uses' array of the relevant .`, + ); + }); } } diff --git a/packages/typegpu/tests/slot.test.ts b/packages/typegpu/tests/slot.test.ts index 261733f846..e6f2076038 100644 --- a/packages/typegpu/tests/slot.test.ts +++ b/packages/typegpu/tests/slot.test.ts @@ -371,9 +371,13 @@ describe('tgpu.slot', () => { const getColor = tgpu.fn([], d.vec3f)`() { return vec3f(); }`; - getColor.with(colorSlot, d.vec3f(0, 1, 0)); + const main = getColor.with(colorSlot, d.vec3f(0, 1, 0)); - expect(warnSpy).toHaveBeenCalledWith(); + 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 in the '$uses' array of the relevant .", + ); }); it('warns when the slot is unused in TGSL-implemented functions', () => { @@ -385,9 +389,13 @@ describe('tgpu.slot', () => { return d.vec3f(); }); - getColor.with(colorSlot, d.vec3f(0, 1, 0)); + const main = getColor.with(colorSlot, d.vec3f(0, 1, 0)); - expect(warnSpy).toHaveBeenCalledWith(); + 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 in the '$uses' array of the relevant .", + ); }); it('warns when the slot is unused in WGSL-implemented pipeline', ({ root }) => { @@ -400,12 +408,16 @@ describe('tgpu.slot', () => { in: { gid: d.builtin.globalInvocationId }, })`{ }`; - root['~unstable'] + const pipeline = root['~unstable'] .with(colorSlot, d.vec3f(1, 0, 1)) .withCompute(computeFn) .createPipeline(); - expect(warnSpy).toHaveBeenCalledWith(); + 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 in the '$uses' array of the relevant .", + ); }); it('warns when the slot is unused in TGSL-implemented pipeline', ({ root }) => { @@ -418,11 +430,41 @@ describe('tgpu.slot', () => { in: { gid: d.builtin.globalInvocationId }, })(() => {}); - root['~unstable'] + 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 in the '$uses' array of the relevant .", + ); + }); + + 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(); }); }); From 7d8b4690231f2fd094bcb96f10f3c119319c9b13 Mon Sep 17 00:00:00 2001 From: Aleksander Katan Date: Thu, 23 Oct 2025 13:40:56 +0200 Subject: [PATCH 03/11] Push the slot usage down to the stack --- packages/typegpu/src/resolutionCtx.ts | 17 ++++++++++------- packages/typegpu/tests/slot.test.ts | 4 +++- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/typegpu/src/resolutionCtx.ts b/packages/typegpu/src/resolutionCtx.ts index f274410c7e..b37bcdf5d1 100644 --- a/packages/typegpu/src/resolutionCtx.ts +++ b/packages/typegpu/src/resolutionCtx.ts @@ -93,6 +93,7 @@ export type ResolutionCtxImplOptions = { type SlotBindingLayer = { type: 'slotBinding'; bindingMap: WeakMap, unknown>; + usedSet: WeakSet>; }; type BlockScopeLayer = { @@ -141,11 +142,13 @@ class ItemStateStackImpl implements ItemStateStack { this._stack.push({ type: 'slotBinding', bindingMap: new WeakMap(pairs), + usedSet: new WeakSet(), }); } - popSlotBindings() { - this.pop('slotBinding'); + popSlotBindings(): WeakSet> { + const slotBindings = this.pop('slotBinding') as SlotBindingLayer; + return slotBindings.usedSet; } pushFunctionScope( @@ -188,10 +191,11 @@ class ItemStateStackImpl implements ItemStateStack { 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 { @@ -202,6 +206,7 @@ class ItemStateStackImpl implements ItemStateStack { layer.usedSlots.add(slot); } else if (layer?.type === 'slotBinding') { const boundValue = layer.bindingMap.get(slot); + layer.usedSet.add(slot); if (boundValue !== undefined) { return boundValue as T; @@ -521,11 +526,9 @@ export class ResolutionCtxImpl implements ResolutionCtx { }; } - private usedSlots: WeakSet> = new WeakSet(); readSlot(slot: TgpuSlot): T { const value = this._itemStateStack.readSlot(slot); - this.usedSlots.add(slot); if (value === undefined) { throw new MissingSlotValueError(slot); } @@ -539,9 +542,9 @@ export class ResolutionCtxImpl implements ResolutionCtx { try { return callback(); } finally { - this._itemStateStack.popSlotBindings(); + const usedSlots = this._itemStateStack.popSlotBindings(); pairs.forEach((pair) => { - !this.usedSlots.has(pair[0]) && console.warn( + !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 in the '$uses' array of the relevant .`, diff --git a/packages/typegpu/tests/slot.test.ts b/packages/typegpu/tests/slot.test.ts index e6f2076038..5658c85160 100644 --- a/packages/typegpu/tests/slot.test.ts +++ b/packages/typegpu/tests/slot.test.ts @@ -465,6 +465,8 @@ describe('tgpu.slot', () => { }); tgpu.resolve({ externals: { main } }); - expect(warnSpy).toHaveBeenCalledWith(); + 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 in the '$uses' array of the relevant .", + ); }); }); From dc99c35c8cbb7d75e3c7fd597d251f1d7b1a6a90 Mon Sep 17 00:00:00 2001 From: Aleksander Katan Date: Thu, 23 Oct 2025 13:46:56 +0200 Subject: [PATCH 04/11] Remove unused slot from double-buffering --- .../src/examples/simulation/fluid-double-buffering/index.ts | 1 - 1 file changed, 1 deletion(-) 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 e26cb79e6e..35adce8ad3 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 initWorldAction = root['~unstable'] - .with(inputGridSlot, outputGridMutable) .with(outputGridSlot, outputGridMutable) .prepareDispatch((xu, yu) => { 'use gpu'; From 2cb06ebca4245c7b07b16ba5b676d4bfa648269d Mon Sep 17 00:00:00 2001 From: Aleksander Katan Date: Thu, 23 Oct 2025 13:51:24 +0200 Subject: [PATCH 05/11] Fix the message --- packages/typegpu/src/resolutionCtx.ts | 2 +- packages/typegpu/tests/slot.test.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/typegpu/src/resolutionCtx.ts b/packages/typegpu/src/resolutionCtx.ts index b37bcdf5d1..5b3d348cff 100644 --- a/packages/typegpu/src/resolutionCtx.ts +++ b/packages/typegpu/src/resolutionCtx.ts @@ -547,7 +547,7 @@ export class ResolutionCtxImpl implements ResolutionCtx { !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 in the '$uses' array of the relevant .`, + }' 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.`, ); }); } diff --git a/packages/typegpu/tests/slot.test.ts b/packages/typegpu/tests/slot.test.ts index 5658c85160..81bdbf2aa4 100644 --- a/packages/typegpu/tests/slot.test.ts +++ b/packages/typegpu/tests/slot.test.ts @@ -376,7 +376,7 @@ describe('tgpu.slot', () => { 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 in the '$uses' array of the relevant .", + "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.", ); }); @@ -394,7 +394,7 @@ describe('tgpu.slot', () => { 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 in the '$uses' array of the relevant .", + "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.", ); }); @@ -416,7 +416,7 @@ describe('tgpu.slot', () => { 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 in the '$uses' array of the relevant .", + "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.", ); }); @@ -438,7 +438,7 @@ describe('tgpu.slot', () => { 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 in the '$uses' array of the relevant .", + "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.", ); }); @@ -466,7 +466,7 @@ describe('tgpu.slot', () => { 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 in the '$uses' array of the relevant .", + "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.", ); }); }); From a298ddbe8c7ecaf42bc4a03c3671fd539ab2a2cf Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Thu, 23 Oct 2025 13:53:38 +0200 Subject: [PATCH 06/11] Update packages/typegpu/src/resolutionCtx.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/typegpu/src/resolutionCtx.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/typegpu/src/resolutionCtx.ts b/packages/typegpu/src/resolutionCtx.ts index 5b3d348cff..9fc4aa84d0 100644 --- a/packages/typegpu/src/resolutionCtx.ts +++ b/packages/typegpu/src/resolutionCtx.ts @@ -544,11 +544,13 @@ export class ResolutionCtxImpl implements ResolutionCtx { } finally { const usedSlots = this._itemStateStack.popSlotBindings(); pairs.forEach((pair) => { - !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.`, - ); + 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.`, + ); + } }); } } From 512637a8535c71ff8bee96717124859bffd54142 Mon Sep 17 00:00:00 2001 From: Aleksander Katan Date: Thu, 23 Oct 2025 16:34:12 +0200 Subject: [PATCH 07/11] Fix repeated logs (and repeated slot refills) --- packages/typegpu/src/core/function/tgpuFn.ts | 1 - packages/typegpu/src/resolutionCtx.ts | 2 +- packages/typegpu/tests/slot.test.ts | 45 ++++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/packages/typegpu/src/core/function/tgpuFn.ts b/packages/typegpu/src/core/function/tgpuFn.ts index 4c5a3cc937..c1bc85d0a6 100644 --- a/packages/typegpu/src/core/function/tgpuFn.ts +++ b/packages/typegpu/src/core/function/tgpuFn.ts @@ -290,7 +290,6 @@ function createBoundFunction( value: unknown, ): TgpuFn { return createBoundFunction(fn, [ - ...pairs, [isAccessor(slot) ? slot.slot : slot, value], ]); }, diff --git a/packages/typegpu/src/resolutionCtx.ts b/packages/typegpu/src/resolutionCtx.ts index 9fc4aa84d0..3365882fec 100644 --- a/packages/typegpu/src/resolutionCtx.ts +++ b/packages/typegpu/src/resolutionCtx.ts @@ -206,9 +206,9 @@ class ItemStateStackImpl implements ItemStateStack { layer.usedSlots.add(slot); } else if (layer?.type === 'slotBinding') { const boundValue = layer.bindingMap.get(slot); - layer.usedSet.add(slot); if (boundValue !== undefined) { + layer.usedSet.add(slot); return boundValue as T; } } else if ( diff --git a/packages/typegpu/tests/slot.test.ts b/packages/typegpu/tests/slot.test.ts index 81bdbf2aa4..8174ca342d 100644 --- a/packages/typegpu/tests/slot.test.ts +++ b/packages/typegpu/tests/slot.test.ts @@ -469,4 +469,49 @@ describe('tgpu.slot', () => { "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); + }); }); From 52ccf524f876edfcf2384f4617dcae2b3aeb17c2 Mon Sep 17 00:00:00 2001 From: Aleksander Katan Date: Fri, 24 Oct 2025 16:24:23 +0200 Subject: [PATCH 08/11] Move types to, well, types.ts --- packages/typegpu/src/resolutionCtx.ts | 20 +++----------------- packages/typegpu/src/types.ts | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/packages/typegpu/src/resolutionCtx.ts b/packages/typegpu/src/resolutionCtx.ts index 3365882fec..f509654a70 100644 --- a/packages/typegpu/src/resolutionCtx.ts +++ b/packages/typegpu/src/resolutionCtx.ts @@ -65,6 +65,8 @@ import type { ItemLayer, ItemStateStack, ResolutionCtx, + SlotBindingLayer, + StackLayer, Wgsl, } from './types.ts'; import { CodegenState, isSelfResolvable, NormalState } from './types.ts'; @@ -90,24 +92,8 @@ export type ResolutionCtxImplOptions = { readonly namespace: Namespace; }; -type SlotBindingLayer = { - type: 'slotBinding'; - bindingMap: WeakMap, unknown>; - usedSet: WeakSet>; -}; - -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 { diff --git a/packages/typegpu/src/types.ts b/packages/typegpu/src/types.ts index 377e1905cb..d32418b134 100644 --- a/packages/typegpu/src/types.ts +++ b/packages/typegpu/src/types.ts @@ -107,6 +107,23 @@ 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; From e6ddefd8d5318fe187e68387c8b3c871a3da9ea1 Mon Sep 17 00:00:00 2001 From: Aleksander Katan Date: Fri, 24 Oct 2025 16:52:12 +0200 Subject: [PATCH 09/11] Merge pop methods into one --- packages/typegpu/src/resolutionCtx.ts | 32 +++++-------------- packages/typegpu/src/types.ts | 9 +++--- .../typegpu/tests/tgsl/wgslGenerator.test.ts | 4 +-- 3 files changed, 14 insertions(+), 31 deletions(-) diff --git a/packages/typegpu/src/resolutionCtx.ts b/packages/typegpu/src/resolutionCtx.ts index f509654a70..85f2345c10 100644 --- a/packages/typegpu/src/resolutionCtx.ts +++ b/packages/typegpu/src/resolutionCtx.ts @@ -65,7 +65,6 @@ import type { ItemLayer, ItemStateStack, ResolutionCtx, - SlotBindingLayer, StackLayer, Wgsl, } from './types.ts'; @@ -120,10 +119,6 @@ class ItemStateStackImpl implements ItemStateStack { }); } - popItem() { - this.pop('item'); - } - pushSlotBindings(pairs: SlotValuePair[]) { this._stack.push({ type: 'slotBinding', @@ -132,11 +127,6 @@ class ItemStateStackImpl implements ItemStateStack { }); } - popSlotBindings(): WeakSet> { - const slotBindings = this.pop('slotBinding') as SlotBindingLayer; - return slotBindings.usedSet; - } - pushFunctionScope( args: Snippet[], argAliases: Record, @@ -156,10 +146,6 @@ class ItemStateStackImpl implements ItemStateStack { return scope; } - popFunctionScope() { - this.pop('functionScope'); - } - pushBlockScope() { this._stack.push({ type: 'blockScope', @@ -167,11 +153,9 @@ 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.`); @@ -424,7 +408,7 @@ export class ResolutionCtxImpl implements ResolutionCtx { } popBlockScope() { - this._itemStateStack.popBlockScope(); + this._itemStateStack.pop('blockScope'); } generateLog(args: Snippet[]): Snippet { @@ -478,7 +462,7 @@ export class ResolutionCtxImpl implements ResolutionCtx { returnType, }; } finally { - this._itemStateStack.popFunctionScope(); + this._itemStateStack.pop('functionScope'); } } @@ -528,7 +512,7 @@ export class ResolutionCtxImpl implements ResolutionCtx { try { return callback(); } finally { - const usedSlots = this._itemStateStack.popSlotBindings(); + const usedSlots = this._itemStateStack.pop('slotBinding').usedSet; pairs.forEach((pair) => { if (!usedSlots.has(pair[0])) { console.warn( @@ -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 d32418b134..64de7e6e46 100644 --- a/packages/typegpu/src/types.ts +++ b/packages/typegpu/src/types.ts @@ -130,9 +130,7 @@ export interface ItemStateStack { readonly topFunctionScope: FunctionScopeLayer | undefined; pushItem(): void; - popItem(): void; pushSlotBindings(pairs: SlotValuePair[]): void; - popSlotBindings(): void; pushFunctionScope( args: Snippet[], argAliases: Record, @@ -143,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/tgsl/wgslGenerator.test.ts b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts index 627ea920f7..6c9baf7064 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); From 614c03cf315c0a280b3aa4e86532e272cab07511 Mon Sep 17 00:00:00 2001 From: Aleksander Katan Date: Tue, 28 Oct 2025 16:31:13 +0100 Subject: [PATCH 10/11] Change functions to keep all of their slots and `innerFn` instead of one slot and `fn` --- packages/typegpu/src/core/function/tgpuFn.ts | 3 ++- packages/typegpu/tests/slot.test.ts | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/typegpu/src/core/function/tgpuFn.ts b/packages/typegpu/src/core/function/tgpuFn.ts index c1bc85d0a6..8983881c42 100644 --- a/packages/typegpu/src/core/function/tgpuFn.ts +++ b/packages/typegpu/src/core/function/tgpuFn.ts @@ -289,7 +289,8 @@ 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/tests/slot.test.ts b/packages/typegpu/tests/slot.test.ts index 8174ca342d..55f0c74eb0 100644 --- a/packages/typegpu/tests/slot.test.ts +++ b/packages/typegpu/tests/slot.test.ts @@ -514,4 +514,21 @@ describe('tgpu.slot', () => { 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]"`, + ); + }); }); From d86ee0432f1719672c973c9ef556114a78bf8cee Mon Sep 17 00:00:00 2001 From: Aleksander Katan Date: Tue, 28 Oct 2025 16:32:47 +0100 Subject: [PATCH 11/11] Add a test for nice object stringify --- packages/typegpu/tests/slot.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/typegpu/tests/slot.test.ts b/packages/typegpu/tests/slot.test.ts index 55f0c74eb0..1e5237e873 100644 --- a/packages/typegpu/tests/slot.test.ts +++ b/packages/typegpu/tests/slot.test.ts @@ -531,4 +531,15 @@ describe('tgpu.slot', () => { `"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)]"`, + ); + }); });