From 4b4a5b7b56f75fe94acf45dd038a66ceb1ee690c Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 30 Oct 2026 14:47:20 +0100 Subject: [PATCH 1/7] test: Add trace history tests --- packages/typegpu/tests/sourceMaps.test.ts | 171 +++++++++++++++++++++- 1 file changed, 167 insertions(+), 4 deletions(-) diff --git a/packages/typegpu/tests/sourceMaps.test.ts b/packages/typegpu/tests/sourceMaps.test.ts index c48d4239b7..08f7270898 100644 --- a/packages/typegpu/tests/sourceMaps.test.ts +++ b/packages/typegpu/tests/sourceMaps.test.ts @@ -1,5 +1,6 @@ -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, vi } from 'vitest'; import { d, ResolutionError, tgpu } from 'typegpu'; +import { it } from 'typegpu-testing-utility'; vi.mock('../src/shared/env.ts', () => ({ INCLUDE_SOURCE: true, @@ -12,6 +13,8 @@ vi.mock('../dist/shared/env.js', () => ({ TEST: true, })); +// Functions are all defined here so that the resulting source maps are +// less likely to be impacted by new tests, formatting changes etc. const fn0 = () => { 'use gpu'; let a = 0; @@ -30,6 +33,57 @@ const fn2 = () => { let b = a; }; +const shellless = () => { + 'use gpu'; + let a = 0; + let b = a++; +}; + +const generic = tgpu.fn(() => { + 'use gpu'; + shellless(); +}); + +const shelled = tgpu.fn([])(() => { + generic(); +}); + +const main = () => { + 'use gpu'; + shelled(); +}; + +const shelledCompute = tgpu.computeFn({ workgroupSize: [1, 1, 1] })(() => { + fn0(); +}); + +const shelllessCompute = (x: number) => { + 'use gpu'; + fn0(); +}; + +const shelledVertex = tgpu.vertexFn({ out: { pos: d.builtin.position } })(() => { + fn0(); + return { pos: d.vec4f() }; +}); + +const shelllessVertex = () => { + 'use gpu'; + fn0(); + return { $position: d.vec4f() }; +}; + +const shelledFragment = tgpu.fragmentFn({ out: d.vec4f })(() => { + fn0(); + return d.vec4f(); +}); + +const shelllessFragment = () => { + 'use gpu'; + fn0(); + return d.vec4f(); +}; + function expectError(cb: () => void) { try { cb(); @@ -40,7 +94,7 @@ function expectError(cb: () => void) { } const stripRoot = (path: string | undefined) => - path !== undefined ? path.replace(/\/.*packages/, '') : path; + path !== undefined ? path.replaceAll(/\/\S*packages/g, '') : path; describe('source mapped errors', () => { it('includes the source in the message', () => { @@ -49,7 +103,7 @@ describe('source mapped errors', () => { expect(stripRoot(err.message)).toMatchInlineSnapshot(` "Resolution of the following tree failed: - - - fn:fn1 (at /typegpu/tests/sourceMaps.test.ts:24:3): + - fn:fn1 (at /typegpu/tests/sourceMaps.test.ts:27:3): 'b = a' is invalid, because references cannot be assigned. ----- Try 'b = vec3f(a)' to copy the value instead. @@ -65,7 +119,7 @@ describe('source mapped errors', () => { ); expect(err.location?.location).toMatchInlineSnapshot(` [ - 24, + 27, 3, ] `); @@ -104,4 +158,113 @@ describe('source mapped errors', () => { -----" `); }); + + it('includes sources for the call history', () => { + const err = expectError(() => tgpu.resolve([main])) as ResolutionError; + + expect(stripRoot(err.message)).toMatchInlineSnapshot(` + "Resolution of the following tree failed: + - + - fn*:main + - fn*:main() (defined at /typegpu/tests/sourceMaps.test.ts:51:19) + - fn:shelled (defined at /typegpu/tests/sourceMaps.test.ts:47:34) + - fn*:generic() (defined at /typegpu/tests/sourceMaps.test.ts:42:30) + - fn*:shellless() (at /typegpu/tests/sourceMaps.test.ts:39:11): + 'a++' is invalid because update is only allowed as a statement." + `); + }); + + it('includes source for shelled compute', ({ root }) => { + const pipeline = root.createComputePipeline({ compute: shelledCompute }); + + const err = expectError(() => pipeline.dispatchWorkgroups(1)) as ResolutionError; + + expect(stripRoot(err.message)).toMatchInlineSnapshot(` + "Resolution of the following tree failed: + - computePipelineCore + - computeFn:shelledCompute (defined at /typegpu/tests/sourceMaps.test.ts:56:74) + - fn*:fn0() (at /typegpu/tests/sourceMaps.test.ts:21:11): + 'a++' is invalid because update is only allowed as a statement." + `); + }); + + it('includes source for shellless compute', ({ root }) => { + const pipeline = root.createGuardedComputePipeline(shelllessCompute); + + const err = expectError(() => pipeline.dispatchThreads(1)) as ResolutionError; + + expect(stripRoot(err.message)).toMatchInlineSnapshot(` + "Resolution of the following tree failed: + - computePipelineCore + - computeFn:mainCompute + - fn:shelllessCompute (defined at /typegpu/tests/sourceMaps.test.ts:60:40) + - fn*:fn0() (at /typegpu/tests/sourceMaps.test.ts:21:11): + 'a++' is invalid because update is only allowed as a statement." + `); + }); + + it('includes source for shelled vertex', ({ root }) => { + const err = expectError(() => tgpu.resolve([shelledVertex])) as ResolutionError; + + expect(stripRoot(err.message)).toMatchInlineSnapshot(` + "Resolution of the following tree failed: + - + - vertexFn:shelledVertex (defined at /typegpu/tests/sourceMaps.test.ts:65:80) + - fn*:fn0() (at /typegpu/tests/sourceMaps.test.ts:21:11): + 'a++' is invalid because update is only allowed as a statement." + `); + }); + + it('includes source for shellless vertex', ({ root }) => { + const pipeline = root.createRenderPipeline({ + vertex: shelllessVertex, + fragment: () => d.vec4f(), + }); + + const err = expectError(() => tgpu.resolve([pipeline])) as ResolutionError; + + expect(stripRoot(err.message)).toMatchInlineSnapshot(` + "Resolution of the following tree failed: + - + - renderPipeline:pipeline + - renderPipelineCore + - autoVertexFn (defined at /typegpu/tests/sourceMaps.test.ts:70:30) + - fn*:fn0() (at /typegpu/tests/sourceMaps.test.ts:21:11): + 'a++' is invalid because update is only allowed as a statement." + `); + }); + + it('includes source for shelled fragment', ({ root }) => { + const err = expectError(() => tgpu.resolve([shelledFragment])) as ResolutionError; + + expect(stripRoot(err.message)).toMatchInlineSnapshot(` + "Resolution of the following tree failed: + - + - fragmentFn:shelledFragment (defined at /typegpu/tests/sourceMaps.test.ts:76:64) + - fn*:fn0() (at /typegpu/tests/sourceMaps.test.ts:21:11): + 'a++' is invalid because update is only allowed as a statement." + `); + }); + + it('includes source for shellless fragment', ({ root }) => { + const pipeline = root.createRenderPipeline({ + vertex: () => { + 'use gpu'; + return { $position: d.vec4f() }; + }, + fragment: shelllessFragment, + }); + + const err = expectError(() => tgpu.resolve([pipeline])) as ResolutionError; + + expect(stripRoot(err.message)).toMatchInlineSnapshot(` + "Resolution of the following tree failed: + - + - renderPipeline:pipeline + - renderPipelineCore + - autoFragmentFn (defined at /typegpu/tests/sourceMaps.test.ts:81:32) + - fn*:fn0() (at /typegpu/tests/sourceMaps.test.ts:21:11): + 'a++' is invalid because update is only allowed as a statement." + `); + }); }); From 2ac6f21be9ecfc244a4977e72b65c732cf0dd3b5 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 30 Oct 2026 15:12:07 +0100 Subject: [PATCH 2/7] refactor: Keep fnCore on $internal --- packages/typegpu/src/core/function/autoIO.ts | 16 ++++++---------- .../typegpu/src/core/function/shelllessImpl.ts | 5 +++-- .../typegpu/src/core/function/tgpuComputeFn.ts | 5 ++--- packages/typegpu/src/core/function/tgpuFn.ts | 5 +++-- .../typegpu/src/core/function/tgpuFragmentFn.ts | 5 ++--- .../typegpu/src/core/function/tgpuVertexFn.ts | 5 ++--- 6 files changed, 18 insertions(+), 23 deletions(-) diff --git a/packages/typegpu/src/core/function/autoIO.ts b/packages/typegpu/src/core/function/autoIO.ts index 0cbf1c477b..36437ad5d2 100644 --- a/packages/typegpu/src/core/function/autoIO.ts +++ b/packages/typegpu/src/core/function/autoIO.ts @@ -69,10 +69,9 @@ type AutoFragmentFnImpl = ( */ export class AutoFragmentFn implements SelfResolvable { // Prototype properties - declare [$internal]: true; declare resourceType: 'auto-fragment-fn'; - #core: FnCore; + readonly [$internal]: { fnCore: FnCore }; autoIn: AutoStruct; autoOut: AutoStruct; @@ -85,7 +84,7 @@ export class AutoFragmentFn implements SelfResolvable { if (!getName(impl)) { setName(impl, 'fragmentFn'); } - this.#core = createFnCore(impl, 'fragment'); + this[$internal] = { fnCore: createFnCore(impl, 'fragment') }; this.autoIn = new AutoStruct( { ...builtinFragmentIn, ...varyings }, undefined, @@ -103,12 +102,11 @@ export class AutoFragmentFn implements SelfResolvable { [$resolve](ctx: ResolutionCtx): ResolvedSnippet { return ctx.withSlots([[shaderStageSlot, 'fragment']], () => - this.#core.resolve(ctx, [this.autoIn], this.autoOut), + this[$internal].fnCore.resolve(ctx, [this.autoIn], this.autoOut), ); } } -AutoFragmentFn.prototype[$internal] = true; AutoFragmentFn.prototype.resourceType = 'auto-fragment-fn'; type AutoVertexFnImpl = ( @@ -120,10 +118,9 @@ type AutoVertexFnImpl = ( */ export class AutoVertexFn implements SelfResolvable { // Prototype properties - declare [$internal]: true; declare resourceType: 'auto-vertex-fn'; - #core: FnCore; + readonly [$internal]: { fnCore: FnCore }; autoIn: AutoStruct; autoOut: AutoStruct; @@ -136,7 +133,7 @@ export class AutoVertexFn implements SelfResolvable { if (!getName(impl)) { setName(impl, 'vertexFn'); } - this.#core = createFnCore(impl, 'vertex'); + this[$internal] = { fnCore: createFnCore(impl, 'vertex') }; this.autoIn = new AutoStruct({ ...builtinVertexIn, ...attribs }, undefined); setName(this.autoIn, 'VertexIn'); this.autoOut = new AutoStruct( @@ -154,10 +151,9 @@ export class AutoVertexFn implements SelfResolvable { [$resolve](ctx: ResolutionCtx): ResolvedSnippet { return ctx.withSlots([[shaderStageSlot, 'vertex']], () => - this.#core.resolve(ctx, [this.autoIn], this.autoOut), + this[$internal].fnCore.resolve(ctx, [this.autoIn], this.autoOut), ); } } -AutoVertexFn.prototype[$internal] = true; AutoVertexFn.prototype.resourceType = 'auto-vertex-fn'; diff --git a/packages/typegpu/src/core/function/shelllessImpl.ts b/packages/typegpu/src/core/function/shelllessImpl.ts index c85daff24b..657d48a6a6 100644 --- a/packages/typegpu/src/core/function/shelllessImpl.ts +++ b/packages/typegpu/src/core/function/shelllessImpl.ts @@ -3,7 +3,7 @@ import type { BaseData } from '../../data/wgslTypes.ts'; import { getName } from '../../shared/meta.ts'; import { $getNameForward, $internal, $resolve } from '../../shared/symbols.ts'; import type { ResolutionCtx, SelfResolvable } from '../../types.ts'; -import { createFnCore } from './fnCore.ts'; +import { createFnCore, type FnCore } from './fnCore.ts'; /** * Shell-less functions are possible because we can infer the signature based solely on the context @@ -24,6 +24,7 @@ import { createFnCore } from './fnCore.ts'; export interface ShelllessImpl extends SelfResolvable { readonly resourceType: 'shellless-impl'; readonly argTypes: BaseData[]; + readonly [$internal]: { fnCore: FnCore }; readonly [$getNameForward]: unknown; } @@ -38,7 +39,7 @@ export function createShelllessImpl( const core = createFnCore(implementation, 'normal'); return { - [$internal]: true, + [$internal]: { fnCore: core }, [$getNameForward]: core, resourceType: 'shellless-impl' as const, argTypes, diff --git a/packages/typegpu/src/core/function/tgpuComputeFn.ts b/packages/typegpu/src/core/function/tgpuComputeFn.ts index f7118526a6..078cf8de0a 100644 --- a/packages/typegpu/src/core/function/tgpuComputeFn.ts +++ b/packages/typegpu/src/core/function/tgpuComputeFn.ts @@ -47,7 +47,7 @@ export interface TgpuComputeFn< // oxlint-disable-next-line typescript/no-explicit-any -- to allow assigning any compute fn to TgpuComputeFn (non-generic) type ComputeIn extends IORecord = any, > extends TgpuNamable { - readonly [$internal]: true; + readonly [$internal]: { fnCore: FnCore }; readonly shell: TgpuComputeFnShellHeader; $uses(dependencyMap: Record): this; @@ -110,7 +110,6 @@ function createComputeFn>( ): TgpuComputeFn { type This = TgpuComputeFn & SelfResolvable & { - [$internal]: true; [$getNameForward]: FnCore; }; @@ -124,7 +123,7 @@ function createComputeFn>( return this; }, - [$internal]: true, + [$internal]: { fnCore: core }, [$getNameForward]: core, $name(newLabel: string): This { setName(this, newLabel); diff --git a/packages/typegpu/src/core/function/tgpuFn.ts b/packages/typegpu/src/core/function/tgpuFn.ts index c4b58b182a..f51f049eee 100644 --- a/packages/typegpu/src/core/function/tgpuFn.ts +++ b/packages/typegpu/src/core/function/tgpuFn.ts @@ -69,6 +69,7 @@ export type TgpuFnShell = Tgpu interface TgpuFnBase extends TgpuNamable, Withable> { [$internal]: { implementation: Implementation; + fnCore: FnCore; }; readonly resourceType: 'function'; readonly shell: TgpuFnShellHeader< @@ -212,7 +213,7 @@ function createFn( const fnBase = { shell, resourceType: 'function' as const, - [$internal]: { implementation }, + [$internal]: { implementation, fnCore: core }, $uses(newExternals: Record) { core.setExternals('userProvided', newExternals); @@ -303,7 +304,7 @@ function createBoundFunction( const fnBase: This = { resourceType: 'function', shell: innerFn.shell, - [$internal]: { implementation: innerFn[$internal].implementation }, + [$internal]: innerFn[$internal], [$providing]: { inner: innerFn, pairs }, $uses(newExternals) { diff --git a/packages/typegpu/src/core/function/tgpuFragmentFn.ts b/packages/typegpu/src/core/function/tgpuFragmentFn.ts index e6d3b8091f..def53e94c8 100644 --- a/packages/typegpu/src/core/function/tgpuFragmentFn.ts +++ b/packages/typegpu/src/core/function/tgpuFragmentFn.ts @@ -110,7 +110,7 @@ export interface TgpuFragmentFn< in Varying extends TgpuFragmentFn.In = Record, out Output extends TgpuFragmentFn.Out = TgpuFragmentFn.Out, > extends TgpuNamable { - readonly [$internal]: true; + readonly [$internal]: { fnCore: FnCore }; readonly shell: TgpuFragmentFnShellHeader; readonly outputType: IOLayoutToSchema; @@ -182,7 +182,6 @@ function createFragmentFn( ): TgpuFragmentFn { type This = TgpuFragmentFn & SelfResolvable & { - [$internal]: true; [$getNameForward]: FnCore; }; @@ -201,7 +200,7 @@ function createFragmentFn( return this; }, - [$internal]: true, + [$internal]: { fnCore: core }, [$getNameForward]: core, $name(newLabel: string): This { setName(this, newLabel); diff --git a/packages/typegpu/src/core/function/tgpuVertexFn.ts b/packages/typegpu/src/core/function/tgpuVertexFn.ts index e0d2d4c388..0095f9189e 100644 --- a/packages/typegpu/src/core/function/tgpuVertexFn.ts +++ b/packages/typegpu/src/core/function/tgpuVertexFn.ts @@ -81,7 +81,7 @@ export interface TgpuVertexFn< in VertexIn extends TgpuVertexFn.In = Record, out VertexOut extends TgpuVertexFn.Out = TgpuVertexFn.Out, > extends TgpuNamable { - readonly [$internal]: true; + readonly [$internal]: { fnCore: FnCore }; readonly shell: TgpuVertexFnShellHeader; $uses(dependencyMap: Record): this; } @@ -159,7 +159,6 @@ function createVertexFn( ): TgpuVertexFn { type This = TgpuVertexFn & SelfResolvable & { - [$internal]: true; [$getNameForward]: FnCore; }; @@ -174,7 +173,7 @@ function createVertexFn( return this; }, - [$internal]: true, + [$internal]: { fnCore: core }, [$getNameForward]: core, $name(newLabel: string): This { setName(this, newLabel); From 18310475c9ac8c040b415132e5f9149886995b85 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 30 Oct 2026 15:17:44 +0100 Subject: [PATCH 3/7] feat: Save source on fnCore --- packages/typegpu/src/core/function/fnCore.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/typegpu/src/core/function/fnCore.ts b/packages/typegpu/src/core/function/fnCore.ts index 6c24352dff..49ecfcfb2a 100644 --- a/packages/typegpu/src/core/function/fnCore.ts +++ b/packages/typegpu/src/core/function/fnCore.ts @@ -6,7 +6,7 @@ import { type BaseData, isWgslData, isWgslStruct, Void } from '../../data/wgslTy import { validateIdentifier } from '../../nameUtils.ts'; import { getFunctionMetadata, getName } from '../../shared/meta.ts'; import { $getNameForward } from '../../shared/symbols.ts'; -import type { ResolutionCtx, ShaderStage } from '../../types.ts'; +import type { ErrorSource, ResolutionCtx, ShaderStage } from '../../types.ts'; import { type ExternalMap, replaceExternalsInWgsl, @@ -56,6 +56,8 @@ export interface FnCore { */ entryInput?: SeparatedEntryArgs, ): ResolvedSnippet; + /** Points to the implementation's body. */ + source: ErrorSource | undefined; } export function createFnCore( @@ -90,6 +92,8 @@ export function createFnCore( } }, + source: undefined as undefined | ErrorSource, + resolve( ctx: ResolutionCtx, argTypes: BaseData[], @@ -225,6 +229,13 @@ export function createFnCore( const [body, sourceMap] = stripSourceMap(ast.body) as [Block, SourceMap]; if (pluginData.filename !== undefined) { ctx.addSourceMap(sourceMap, pluginData.filename); + const maybeSource = sourceMap.get(body); + if (maybeSource) { + this.source = { + file: pluginData.filename, + location: maybeSource, + }; + } } // If an entrypoint implementation has a second argument, it represents the output schema. From 1e5ee52a7bfcb4a01316ab9c64703b3217033a6f Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 30 Oct 2026 15:53:11 +0100 Subject: [PATCH 4/7] feat: Include trace sources in errors --- packages/typegpu/src/errors.ts | 63 ++++++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 18 deletions(-) diff --git a/packages/typegpu/src/errors.ts b/packages/typegpu/src/errors.ts index eb6beed88f..7ea28bc2af 100644 --- a/packages/typegpu/src/errors.ts +++ b/packages/typegpu/src/errors.ts @@ -8,6 +8,13 @@ import type { TgpuBindGroupLayout } from './tgpuBindGroupLayout.ts'; import type * as tinyest from 'tinyest'; import { stringifyNode } from './shared/tseynit.ts'; import type { ErrorSource } from './types.ts'; +import { $internal } from './shared/symbols.ts'; +import type { TgpuFn } from 'typegpu'; +import type { TgpuVertexFn } from 'typegpu'; +import type { TgpuFragmentFn } from 'typegpu'; +import type { TgpuComputeFn } from 'typegpu'; +import type { ShelllessImpl } from './core/function/shelllessImpl.ts'; +import type { AutoFragmentFn, AutoVertexFn } from './core/function/autoIO.ts'; const prefix = 'Invariant failed'; @@ -40,6 +47,43 @@ export function invariant( throw new Error(value); } +function getFnSource(item: unknown): ErrorSource | undefined { + const typed = item as + | TgpuFn + | TgpuVertexFn + | TgpuFragmentFn + | TgpuComputeFn + | ShelllessImpl + | AutoVertexFn + | AutoFragmentFn + | undefined; + return typed?.[$internal]?.fnCore?.source; +} + +function stringifySource(source: ErrorSource): string { + return `${source.file}:${source.location[0]}:${source.location[1]}`; +} + +function constructMessage(cause: unknown, trace: unknown[], source?: ErrorSource) { + const causeString = + cause && typeof cause === 'object' && 'message' in cause ? cause.message : cause; + + const entries = trace.length > 20 ? [...trace.slice(0, 11), '...', ...trace.slice(-10)] : trace; + const traceString = entries + .map((ancestor, idx) => { + const base = `- ${hasTinyestMetadata(ancestor) ? `fn*:${getName(ancestor)}` : ancestor}`; + if (idx < trace.length - 1) { + const maybeSource = getFnSource(ancestor); + return `${base}${INCLUDE_SOURCE && maybeSource !== undefined ? ` (defined at ${stringifySource(maybeSource)})` : ''}`; + } else { + return `${base}${INCLUDE_SOURCE && source !== undefined ? ` (at ${stringifySource(source)}):\n` : ': '}`; + } + }) + .join('\n'); + + return `Resolution of the following tree failed:\n${traceString}${causeString}`; +} + /** * An error that happens during resolution of WGSL code. * Contains a trace of all ancestor resolvables in @@ -53,24 +97,7 @@ export class ResolutionError extends Error { readonly location: ErrorSource | undefined; constructor(cause: unknown, trace: unknown[], source?: ErrorSource) { - let entries = trace.map( - (ancestor) => `- ${hasTinyestMetadata(ancestor) ? `fn*:${getName(ancestor)}` : ancestor}`, - ); - - // Showing only the root and leaf nodes. - if (entries.length > 20) { - entries = [...entries.slice(0, 11), '...', ...entries.slice(-10)]; - } - - const sourceString = `${entries.join('\n')}${ - INCLUDE_SOURCE && source !== undefined - ? ` (at ${source.file}:${source.location[0]}:${source.location[1]}):\n` - : `: ` - }`; - const causeString = - cause && typeof cause === 'object' && 'message' in cause ? cause.message : cause; - - super(`Resolution of the following tree failed:\n${sourceString}${causeString}`); + super(constructMessage(cause, trace, source)); this.cause = cause; this.trace = trace; From 12ab8a10099846b0693c40f65ffcc0d678a4de70 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 30 Oct 2026 15:55:27 +0100 Subject: [PATCH 5/7] test: Add a test for bound fn --- packages/typegpu/tests/sourceMaps.test.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/typegpu/tests/sourceMaps.test.ts b/packages/typegpu/tests/sourceMaps.test.ts index 08f7270898..e9c0376215 100644 --- a/packages/typegpu/tests/sourceMaps.test.ts +++ b/packages/typegpu/tests/sourceMaps.test.ts @@ -93,6 +93,13 @@ function expectError(cb: () => void) { throw new Error('Expected callback to throw.'); } +const slot = tgpu.slot(1); +const fn = tgpu.fn([])(() => { + const a = slot.$; + fn0(); +}); +const bound = fn.with(slot, 2); + const stripRoot = (path: string | undefined) => path !== undefined ? path.replaceAll(/\/\S*packages/g, '') : path; @@ -267,4 +274,16 @@ describe('source mapped errors', () => { 'a++' is invalid because update is only allowed as a statement." `); }); + + it('includes source for bound fn', () => { + const err = expectError(() => tgpu.resolve([bound])) as ResolutionError; + + expect(stripRoot(err.message)).toMatchInlineSnapshot(` + "Resolution of the following tree failed: + - + - fn:fn (defined at /typegpu/tests/sourceMaps.test.ts:97:29) + - fn*:fn0() (at /typegpu/tests/sourceMaps.test.ts:21:11): + 'a++' is invalid because update is only allowed as a statement." + `); + }); }); From f3e77119bb6caf82019215a0724a0d51451523eb Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 30 Oct 2026 15:57:44 +0100 Subject: [PATCH 6/7] refactor: ErrorSource -> Source --- packages/typegpu/src/core/function/fnCore.ts | 6 +++--- packages/typegpu/src/errors.ts | 12 ++++++------ packages/typegpu/src/types.ts | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/typegpu/src/core/function/fnCore.ts b/packages/typegpu/src/core/function/fnCore.ts index 49ecfcfb2a..8edc1617e2 100644 --- a/packages/typegpu/src/core/function/fnCore.ts +++ b/packages/typegpu/src/core/function/fnCore.ts @@ -6,7 +6,7 @@ import { type BaseData, isWgslData, isWgslStruct, Void } from '../../data/wgslTy import { validateIdentifier } from '../../nameUtils.ts'; import { getFunctionMetadata, getName } from '../../shared/meta.ts'; import { $getNameForward } from '../../shared/symbols.ts'; -import type { ErrorSource, ResolutionCtx, ShaderStage } from '../../types.ts'; +import type { Source, ResolutionCtx, ShaderStage } from '../../types.ts'; import { type ExternalMap, replaceExternalsInWgsl, @@ -57,7 +57,7 @@ export interface FnCore { entryInput?: SeparatedEntryArgs, ): ResolvedSnippet; /** Points to the implementation's body. */ - source: ErrorSource | undefined; + source: Source | undefined; } export function createFnCore( @@ -92,7 +92,7 @@ export function createFnCore( } }, - source: undefined as undefined | ErrorSource, + source: undefined as undefined | Source, resolve( ctx: ResolutionCtx, diff --git a/packages/typegpu/src/errors.ts b/packages/typegpu/src/errors.ts index 7ea28bc2af..f4d23ca688 100644 --- a/packages/typegpu/src/errors.ts +++ b/packages/typegpu/src/errors.ts @@ -7,7 +7,7 @@ import { DEV, INCLUDE_SOURCE, TEST } from './shared/env.ts'; import type { TgpuBindGroupLayout } from './tgpuBindGroupLayout.ts'; import type * as tinyest from 'tinyest'; import { stringifyNode } from './shared/tseynit.ts'; -import type { ErrorSource } from './types.ts'; +import type { Source } from './types.ts'; import { $internal } from './shared/symbols.ts'; import type { TgpuFn } from 'typegpu'; import type { TgpuVertexFn } from 'typegpu'; @@ -47,7 +47,7 @@ export function invariant( throw new Error(value); } -function getFnSource(item: unknown): ErrorSource | undefined { +function getFnSource(item: unknown): Source | undefined { const typed = item as | TgpuFn | TgpuVertexFn @@ -60,11 +60,11 @@ function getFnSource(item: unknown): ErrorSource | undefined { return typed?.[$internal]?.fnCore?.source; } -function stringifySource(source: ErrorSource): string { +function stringifySource(source: Source): string { return `${source.file}:${source.location[0]}:${source.location[1]}`; } -function constructMessage(cause: unknown, trace: unknown[], source?: ErrorSource) { +function constructMessage(cause: unknown, trace: unknown[], source?: Source) { const causeString = cause && typeof cause === 'object' && 'message' in cause ? cause.message : cause; @@ -94,9 +94,9 @@ function constructMessage(cause: unknown, trace: unknown[], source?: ErrorSource export class ResolutionError extends Error { readonly cause: unknown; readonly trace: unknown[]; - readonly location: ErrorSource | undefined; + readonly location: Source | undefined; - constructor(cause: unknown, trace: unknown[], source?: ErrorSource) { + constructor(cause: unknown, trace: unknown[], source?: Source) { super(constructMessage(cause, trace, source)); this.cause = cause; diff --git a/packages/typegpu/src/types.ts b/packages/typegpu/src/types.ts index e6f0028f3b..d7062c8ed1 100644 --- a/packages/typegpu/src/types.ts +++ b/packages/typegpu/src/types.ts @@ -268,8 +268,8 @@ export type ExecState = NormalState | CodegenState | SimulationState; type MapKey = SourceMap extends Map ? K : never; type MapValue = SourceMap extends Map ? V : never; -export type ErrorSource = { file: string | undefined; location: MapValue }; -export type CombinedSourceMap = Map; +export type Source = { file: string | undefined; location: MapValue }; +export type CombinedSourceMap = Map; /** * Passed into each resolvable item. All items in a tree share a resolution ctx, From e4d46392663dafe13057544ce2001f2185b59590 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 30 Oct 2026 17:28:26 +0100 Subject: [PATCH 7/7] Review fixes --- packages/typegpu/src/core/function/fnCore.ts | 2 +- packages/typegpu/src/core/function/tgpuFn.ts | 2 +- packages/typegpu/src/errors.ts | 19 +++---------------- 3 files changed, 5 insertions(+), 18 deletions(-) diff --git a/packages/typegpu/src/core/function/fnCore.ts b/packages/typegpu/src/core/function/fnCore.ts index 8edc1617e2..c8f2b09b20 100644 --- a/packages/typegpu/src/core/function/fnCore.ts +++ b/packages/typegpu/src/core/function/fnCore.ts @@ -56,7 +56,7 @@ export interface FnCore { */ entryInput?: SeparatedEntryArgs, ): ResolvedSnippet; - /** Points to the implementation's body. */ + /** Points to the implementation's body. Filled in during resolution. */ source: Source | undefined; } diff --git a/packages/typegpu/src/core/function/tgpuFn.ts b/packages/typegpu/src/core/function/tgpuFn.ts index f51f049eee..6e08803cc1 100644 --- a/packages/typegpu/src/core/function/tgpuFn.ts +++ b/packages/typegpu/src/core/function/tgpuFn.ts @@ -304,7 +304,7 @@ function createBoundFunction( const fnBase: This = { resourceType: 'function', shell: innerFn.shell, - [$internal]: innerFn[$internal], + [$internal]: { ...innerFn[$internal] }, [$providing]: { inner: innerFn, pairs }, $uses(newExternals) { diff --git a/packages/typegpu/src/errors.ts b/packages/typegpu/src/errors.ts index f4d23ca688..9157f410d0 100644 --- a/packages/typegpu/src/errors.ts +++ b/packages/typegpu/src/errors.ts @@ -9,12 +9,7 @@ import type * as tinyest from 'tinyest'; import { stringifyNode } from './shared/tseynit.ts'; import type { Source } from './types.ts'; import { $internal } from './shared/symbols.ts'; -import type { TgpuFn } from 'typegpu'; -import type { TgpuVertexFn } from 'typegpu'; -import type { TgpuFragmentFn } from 'typegpu'; -import type { TgpuComputeFn } from 'typegpu'; -import type { ShelllessImpl } from './core/function/shelllessImpl.ts'; -import type { AutoFragmentFn, AutoVertexFn } from './core/function/autoIO.ts'; +import type { FnCore } from './core/function/fnCore.ts'; const prefix = 'Invariant failed'; @@ -48,15 +43,7 @@ export function invariant( } function getFnSource(item: unknown): Source | undefined { - const typed = item as - | TgpuFn - | TgpuVertexFn - | TgpuFragmentFn - | TgpuComputeFn - | ShelllessImpl - | AutoVertexFn - | AutoFragmentFn - | undefined; + const typed = item as { [$internal]?: { fnCore?: FnCore } } | undefined; return typed?.[$internal]?.fnCore?.source; } @@ -72,7 +59,7 @@ function constructMessage(cause: unknown, trace: unknown[], source?: Source) { const traceString = entries .map((ancestor, idx) => { const base = `- ${hasTinyestMetadata(ancestor) ? `fn*:${getName(ancestor)}` : ancestor}`; - if (idx < trace.length - 1) { + if (idx < entries.length - 1) { const maybeSource = getFnSource(ancestor); return `${base}${INCLUDE_SOURCE && maybeSource !== undefined ? ` (defined at ${stringifySource(maybeSource)})` : ''}`; } else {