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/fnCore.ts b/packages/typegpu/src/core/function/fnCore.ts index 6c24352dff..5e57839ccb 100644 --- a/packages/typegpu/src/core/function/fnCore.ts +++ b/packages/typegpu/src/core/function/fnCore.ts @@ -224,7 +224,7 @@ export function createFnCore( } const [body, sourceMap] = stripSourceMap(ast.body) as [Block, SourceMap]; if (pluginData.filename !== undefined) { - ctx.addSourceMap(sourceMap, pluginData.filename); + ctx.addSourceMap(this, body, sourceMap, pluginData.filename); } // If an entrypoint implementation has a second argument, it represents the output schema. 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..6e08803cc1 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); diff --git a/packages/typegpu/src/errors.ts b/packages/typegpu/src/errors.ts index eb6beed88f..c87b83b99f 100644 --- a/packages/typegpu/src/errors.ts +++ b/packages/typegpu/src/errors.ts @@ -7,7 +7,9 @@ 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 { CombinedSourceMap, Source } from './types.ts'; +import { $internal } from './shared/symbols.ts'; +import type { FnCore } from './core/function/fnCore.ts'; const prefix = 'Invariant failed'; @@ -40,6 +42,43 @@ export function invariant( throw new Error(value); } +function getFnSource(item: unknown, sourceMap?: CombinedSourceMap): Source | undefined { + const core = (item as { [$internal]?: { fnCore?: FnCore } } | undefined)?.[$internal]?.fnCore; + return core !== undefined ? sourceMap?.get(core) : undefined; +} + +function stringifySource(source: Source): string { + // Plugin uses zero-indexed columns, + // while editors usually interpret columns as one-indexed. + return `${source.file}:${source.location[0]}:${source.location[1] + 1}`; +} + +function constructMessage( + cause: unknown, + trace: unknown[], + node?: tinyest.AnyNode, + sourceMap?: CombinedSourceMap, +) { + 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 < entries.length - 1) { + const source = getFnSource(ancestor, sourceMap); + return `${base}${INCLUDE_SOURCE && source !== undefined ? ` (defined at ${stringifySource(source)})` : ''}`; + } else { + const source = node !== undefined ? sourceMap?.get(node) : undefined; + 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 @@ -50,40 +89,34 @@ export function invariant( export class ResolutionError extends Error { readonly cause: unknown; readonly trace: unknown[]; - readonly location: ErrorSource | undefined; - - constructor(cause: unknown, trace: unknown[], source?: ErrorSource) { - let entries = trace.map( - (ancestor) => `- ${hasTinyestMetadata(ancestor) ? `fn*:${getName(ancestor)}` : ancestor}`, - ); + readonly node: tinyest.AnyNode | undefined; + readonly sourceMap: CombinedSourceMap | undefined; - // 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}`); + constructor( + cause: unknown, + trace: unknown[], + node?: tinyest.AnyNode, + sourceMap?: CombinedSourceMap, + ) { + super(constructMessage(cause, trace, node, sourceMap)); this.cause = cause; this.trace = trace; - this.location = source; + this.node = node; + this.sourceMap = sourceMap; // Set the prototype explicitly. Object.setPrototypeOf(this, ResolutionError.prototype); } + get source(): Source | undefined { + return this.node ? this.sourceMap?.get(this.node) : undefined; + } + appendToTrace(ancestor: unknown): ResolutionError { const newTrace = [ancestor, ...this.trace]; - return new ResolutionError(this.cause, newTrace, this.location); + return new ResolutionError(this.cause, newTrace, this.node, this.sourceMap); } } diff --git a/packages/typegpu/src/resolutionCtx.ts b/packages/typegpu/src/resolutionCtx.ts index 0530ab3e7c..1ea0aed494 100644 --- a/packages/typegpu/src/resolutionCtx.ts +++ b/packages/typegpu/src/resolutionCtx.ts @@ -59,7 +59,7 @@ import type { import { CodegenState, isSelfResolvable, NormalState, type FunctionArgument } from './types.ts'; import type { WgslEnableExtension } from './wgslExtensions.ts'; import { getName, hasTinyestMetadata, isNamable, setName } from './shared/meta.ts'; -import { FuncParameterType, type SourceMap } from 'tinyest'; +import { FuncParameterType, type Block, type SourceMap } from 'tinyest'; import { accessProp } from './tgsl/accessProp.ts'; import { createIoSchema } from './core/function/ioSchema.ts'; import { isShelllessImpl } from './core/function/shelllessImpl.ts'; @@ -69,6 +69,7 @@ import { AutoStruct } from './data/autoStruct.ts'; import { EntryInputRouter } from './core/function/entryInputRouter.ts'; import { validateIdentifier, sanitizePrimer, bannedTokens } from './nameUtils.ts'; import { minify } from './minify.ts'; +import type { FnCore } from './core/function/fnCore.ts'; /** * Inserted into bind group entry definitions that belong @@ -609,13 +610,15 @@ export class ResolutionCtxImpl implements ResolutionCtx { this._itemStateStack.clearBlockExternals(); } - addSourceMap(map: SourceMap, file: string) { - map.forEach((value, key) => { - const [line, column] = value; - // Plugin uses zero-indexed columns, - // while editors usually interpret columns as one-indexed. - this.sourceMap.set(key, { file, location: [line, column + 1] }); - }); + addSourceMap(core: FnCore, body: Block, map: SourceMap, file: string) { + map.forEach((value, key) => this.sourceMap.set(key, { file, location: value })); + const maybeSource = map.get(body); + if (maybeSource) { + this.sourceMap.set(core, { + file: file, + location: maybeSource, + }); + } } generateLog(op: SupportedLogOp, args: Snippet[]): Snippet { diff --git a/packages/typegpu/src/tgsl/fail.ts b/packages/typegpu/src/tgsl/fail.ts index 310f5a93ad..428e502c1e 100644 --- a/packages/typegpu/src/tgsl/fail.ts +++ b/packages/typegpu/src/tgsl/fail.ts @@ -21,7 +21,6 @@ export function fail( const ctx = getResolutionCtx(); const node = isSnippet(item) ? getSourceNode(item) : item; - const source = node !== undefined ? ctx?.sourceMap.get(node) : undefined; - throw new ResolutionError(message, [], source); + throw new ResolutionError(message, [], node, ctx?.sourceMap); } diff --git a/packages/typegpu/src/types.ts b/packages/typegpu/src/types.ts index e6f0028f3b..0cd7eceb01 100644 --- a/packages/typegpu/src/types.ts +++ b/packages/typegpu/src/types.ts @@ -50,6 +50,7 @@ import type { StorageFlag } from './extension.ts'; import type { TgpuBufferBinding } from './core/buffer/bufferBinding.ts'; import type { ShelllessRepository } from './tgsl/shellless.ts'; import type { SupportedLogOp } from './tgsl/consoleLog/types.ts'; +import type { FnCore } from './core/function/fnCore.ts'; export type ResolvableObject = | SelfResolvable @@ -268,8 +269,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, @@ -424,7 +425,7 @@ export interface ResolutionCtx { defineVariable(id: string, snippet: Snippet): void; setBlockExternals(externals: Record): void; clearBlockExternals(): void; - addSourceMap(map: SourceMap, file: string): void; + addSourceMap(core: FnCore, body: Block, map: SourceMap, file: string): void; /** * Types that are used in `return` statements are diff --git a/packages/typegpu/tests/sourceMaps.test.ts b/packages/typegpu/tests/sourceMaps.test.ts index c48d4239b7..8c628e0be6 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(); @@ -39,8 +93,15 @@ 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.replace(/\/.*packages/, '') : path; + path !== undefined ? path.replaceAll(/\/\S*packages/g, '') : path; describe('source mapped errors', () => { it('includes the source in the message', () => { @@ -49,7 +110,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. @@ -60,13 +121,13 @@ describe('source mapped errors', () => { it('includes the source as a property', () => { const err = expectError(() => tgpu.resolve([fn1])) as ResolutionError; - expect(stripRoot(err.location?.file)).toMatchInlineSnapshot( + expect(stripRoot(err.source?.file)).toMatchInlineSnapshot( `"/typegpu/tests/sourceMaps.test.ts"`, ); - expect(err.location?.location).toMatchInlineSnapshot(` + expect(err.source?.location).toMatchInlineSnapshot(` [ - 24, - 3, + 27, + 2, ] `); }); @@ -104,4 +165,125 @@ 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:20) + - fn:shelled (defined at /typegpu/tests/sourceMaps.test.ts:47:35) + - fn*:generic() (defined at /typegpu/tests/sourceMaps.test.ts:42:31) + - 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:75) + - 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:41) + - 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:81) + - 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:31) + - 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:65) + - 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:33) + - 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 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:30) + - fn*:fn0() (at /typegpu/tests/sourceMaps.test.ts:21:11): + 'a++' is invalid because update is only allowed as a statement." + `); + }); });