diff --git a/apps/typegpu-docs/src/content/docs/apis/utils.mdx b/apps/typegpu-docs/src/content/docs/apis/utils.mdx index 79e99e52fb..819069d345 100644 --- a/apps/typegpu-docs/src/content/docs/apis/utils.mdx +++ b/apps/typegpu-docs/src/content/docs/apis/utils.mdx @@ -88,7 +88,8 @@ const myFunction = tgpu.fn([])(() => { :::caution Ternary operator's condition must be a comptime-known value. -This restriction is due to WGSL having no ternary operator equivalent. +This restriction is due to WGSL having no ternary operator equivalent. +However, if both branches are known to have no side-effects, then the condition can be a runtime value. ::: ## *console.log* @@ -517,7 +518,7 @@ import { tgpu, d } from 'typegpu'; // final shader bundle, but we cannot // refer to it in any other way. const existingGlobal = tgpu['~unstable'] - .rawCodeSnippet('EXISTING_GLOBAL', d.f32, 'constant'); + .rawCodeSnippet('EXISTING_GLOBAL', d.f32, 'constant', false); const foo = tgpu.fn([], d.f32)(() => { 'use gpu'; @@ -546,3 +547,13 @@ optimizations. If what the expression is a direct reference to an existing value (e.g. a uniform, a storage binding, ...), then choose from `'uniform'`, `'mutable'`, `'readonly'`, `'workgroup'`, `'private'` or `'handle'` depending on the address space of the referred value. + +### `possibleSideEffects` +The fourth optional parameter `possibleSideEffects` indicates, whether generating this snippet may produce a WGSL expression with +observable side-effects (e.g. calling a barrier, discarding a fragment, +or writing to memory). + +Snippets with `possibleSideEffects: true` cannot appear in ternary +branches that get compiled to `select()`, because `select()` evaluates +both branches unconditionally - a side-effect meant to be conditional +would execute regardless of the condition. diff --git a/packages/typegpu/src/core/function/dualImpl.ts b/packages/typegpu/src/core/function/dualImpl.ts index 4ffc428914..564feb5d6b 100644 --- a/packages/typegpu/src/core/function/dualImpl.ts +++ b/packages/typegpu/src/core/function/dualImpl.ts @@ -35,10 +35,8 @@ interface DualImplOptions { * * - `discard` -> `true` - it discards the fragment. * - `workgroupBarrier()` -> `true` - the barrier synchronizes threads. - * - `atomicLoad(p)` -> `true` - atomic operations may synchronize threads - * through memory ordering. - * - `sin(x)`, `abs(x)` -> `false` - these are purely value-producing; the call - * itself has no observable effect beyond the returned value. + * - `sin(x)`, `abs(x)` -> `false` - these are purely value-producing; the + * call itself has no observable effect beyond the returned value. * * When `false`, the result inherits side-effects from its arguments: it * only has `possibleSideEffects: true` if at least one argument does. @@ -103,6 +101,7 @@ export function dualImpl(options: DualImplOptions): DualFn { * 'use gpu'; @@ -76,8 +77,9 @@ export function rawCodeSnippet( expression: string, type: TDataType, origin: RawCodeSnippetOrigin | undefined = 'runtime', + possibleSideEffects: boolean | undefined = true, ): TgpuRawCodeSnippet { - return new TgpuRawCodeSnippetImpl(expression, type, origin); + return new TgpuRawCodeSnippetImpl(expression, type, origin, possibleSideEffects); } // -------------- @@ -90,14 +92,21 @@ class TgpuRawCodeSnippetImpl readonly [$internal]: true; readonly dataType: TDataType; readonly origin: RawCodeSnippetOrigin; + readonly possibleSideEffects: boolean; #expression: string; #externals: ExternalMap | undefined; - constructor(expression: string, type: TDataType, origin: RawCodeSnippetOrigin) { + constructor( + expression: string, + type: TDataType, + origin: RawCodeSnippetOrigin, + possibleSideEffects: boolean, + ) { this[$internal] = true; this.dataType = type; this.origin = origin; + this.possibleSideEffects = possibleSideEffects; this.#expression = expression; } @@ -115,7 +124,7 @@ class TgpuRawCodeSnippetImpl [$resolve](ctx: ResolutionCtx): ResolvedSnippet { const replacedExpression = replaceExternalsInWgsl(ctx, this.#externals ?? {}, this.#expression); - return snip(replacedExpression, this.dataType, this.origin); + return snip(replacedExpression, this.dataType, this.origin, this.possibleSideEffects); } toString() { @@ -125,12 +134,13 @@ class TgpuRawCodeSnippetImpl get [$gpuValueOf](): InferGPU { const dataType = this.dataType; const origin = this.origin; + const possibleSideEffects = this.possibleSideEffects; return new Proxy( { [$internal]: true, get [$ownSnippet]() { - return snip(this, dataType, origin); + return snip(this, dataType, origin, possibleSideEffects); }, [$resolve]: (ctx) => ctx.resolve(this), toString: () => `raw(${String(this.dataType)}): "${this.#expression}".$`, diff --git a/packages/typegpu/src/core/sampler/sampler.ts b/packages/typegpu/src/core/sampler/sampler.ts index f5b20dd37e..7bc48a9825 100644 --- a/packages/typegpu/src/core/sampler/sampler.ts +++ b/packages/typegpu/src/core/sampler/sampler.ts @@ -121,7 +121,7 @@ export class TgpuLaidOutSamplerImpl< { [$internal]: true, get [$ownSnippet]() { - return snip(this, schema, /* origin */ 'handle'); + return snip(this, schema, /* origin */ 'handle', false); }, [$resolve]: (ctx) => ctx.resolve(this), toString: () => `${this.toString()}.$`, @@ -212,7 +212,7 @@ class TgpuFixedSamplerImpl { [$internal]: true, get [$ownSnippet]() { - return snip(this, schema, /* origin */ 'handle'); + return snip(this, schema, /* origin */ 'handle', false); }, [$resolve]: (ctx) => ctx.resolve(this), toString: () => `${this.toString()}.$`, diff --git a/packages/typegpu/src/core/slot/accessor.ts b/packages/typegpu/src/core/slot/accessor.ts index 43f252f20d..18b170df9f 100644 --- a/packages/typegpu/src/core/slot/accessor.ts +++ b/packages/typegpu/src/core/slot/accessor.ts @@ -1,12 +1,13 @@ import { type AnyData, isData } from '../../data/dataTypes.ts'; import { schemaCallWrapper } from '../../data/schemaCallWrapper.ts'; -import { isSnippet, type ResolvedSnippet, snip } from '../../data/snippet.ts'; +import { isSnippet, type ResolvedSnippet, snip, withValue } from '../../data/snippet.ts'; import type { BaseData } from '../../data/wgslTypes.ts'; import { getResolutionCtx, inCodegenMode } from '../../execMode.ts'; import { getName, hasTinyestMetadata, setName } from '../../shared/meta.ts'; import type { InferGPU } from '../../shared/repr.ts'; import { $getNameForward, + $gpuCallable, $gpuValueOf, $internal, $ownSnippet, @@ -15,6 +16,7 @@ import { import type { UnwrapRuntimeConstructor } from '../../tgpuBindGroupLayout.ts'; import { getOwnSnippet, + isGPUCallable, NormalState, type ResolutionCtx, type SelfResolvable, @@ -96,7 +98,12 @@ abstract class AccessorBase< const ctx = getResolutionCtx()!; let value = getGpuValueRecursively(ctx.unwrap(this.slot)); - while (typeof value === 'function' && !isTgpuFn(value) && !hasTinyestMetadata(value)) { + while ( + typeof value === 'function' && + !isTgpuFn(value) && + !isGPUCallable(value) && + !hasTinyestMetadata(value) + ) { // Not a GPU function, so has to be a resource accessor (ran in codegen mode) or comptime value = value(); if (isSnippet(value)) { @@ -109,9 +116,14 @@ abstract class AccessorBase< return ownSnippet; } + if (isGPUCallable(value)) { + return value[$gpuCallable].call(ctx, []); + } + if (isTgpuFn(value) || hasTinyestMetadata(value)) { + const fn = ctx.resolve(value); return ctx.withResetIndentLevel(() => - snip(`${ctx.resolve(value).value}()`, this.schema, /* origin */ 'runtime'), + snip(`${fn.value}()`, this.schema, /* origin */ 'runtime', fn.possibleSideEffects), ); } @@ -152,11 +164,7 @@ abstract class AccessorBase< [$resolve](ctx: ResolutionCtx): ResolvedSnippet { const snippet = this.#createSnippet(); - return snip( - ctx.resolve(snippet.value, snippet.dataType).value, - snippet.dataType as T, - snippet.origin, - ); + return ctx.resolveSnippet(snippet); } } diff --git a/packages/typegpu/src/core/texture/externalTexture.ts b/packages/typegpu/src/core/texture/externalTexture.ts index 78ae4cd582..bb2b5f82b3 100644 --- a/packages/typegpu/src/core/texture/externalTexture.ts +++ b/packages/typegpu/src/core/texture/externalTexture.ts @@ -56,7 +56,7 @@ export class TgpuExternalTextureImpl implements TgpuExternalTexture, SelfResolva { [$internal]: true, get [$ownSnippet]() { - return snip(this, schema, 'handle'); + return snip(this, schema, 'handle', false); }, [$resolve]: (ctx) => ctx.resolve(this), toString: () => `textureExternal:${getName(this) ?? ''}.$`, diff --git a/packages/typegpu/src/core/texture/texture.ts b/packages/typegpu/src/core/texture/texture.ts index e724d9af68..113df11021 100644 --- a/packages/typegpu/src/core/texture/texture.ts +++ b/packages/typegpu/src/core/texture/texture.ts @@ -610,7 +610,7 @@ class TgpuFixedTextureViewImpl { [$internal]: true, get [$ownSnippet]() { - return snip(this, schema, /* origin */ 'handle'); + return snip(this, schema, /* origin */ 'handle', false); }, [$resolve]: (ctx) => ctx.resolve(this), toString: () => `${this.toString()}.$`, @@ -705,7 +705,7 @@ export class TgpuLaidOutTextureViewImpl ctx.resolve(this), toString: () => `${this.toString()}.$`, diff --git a/packages/typegpu/src/core/unroll/tgpuUnroll.ts b/packages/typegpu/src/core/unroll/tgpuUnroll.ts index 6d3da4bd6d..8fecb81791 100644 --- a/packages/typegpu/src/core/unroll/tgpuUnroll.ts +++ b/packages/typegpu/src/core/unroll/tgpuUnroll.ts @@ -1,10 +1,8 @@ -import { stitch } from '../resolve/stitch.ts'; import { $gpuCallable, $internal, $resolve } from '../../shared/symbols.ts'; import { setName } from '../../shared/meta.ts'; import type { DualFn } from '../../types.ts'; -import { type ResolvedSnippet, snip, type Snippet } from '../../data/snippet.ts'; +import { type ResolvedSnippet, type Snippet, withValue } from '../../data/snippet.ts'; import type { ResolutionCtx, SelfResolvable } from '../../types.ts'; -import type { BaseData } from '../../data/wgslTypes.ts'; /** * The result of calling `tgpu.unroll(...)`. The code responsible for @@ -19,8 +17,8 @@ export class UnrollableIterable implements SelfResolvable { this.snippet = snippet; } - [$resolve](_ctx: ResolutionCtx): ResolvedSnippet { - return snip(stitch`${this.snippet}`, this.snippet.dataType as BaseData, this.snippet.origin); + [$resolve](ctx: ResolutionCtx): ResolvedSnippet { + return ctx.resolveSnippet(this.snippet); } } @@ -95,7 +93,7 @@ export const unroll = (() => { impl[$internal] = true; impl[$gpuCallable] = { call(_ctx, [value]) { - return snip(new UnrollableIterable(value), value.dataType, value.origin); + return withValue(new UnrollableIterable(value), value); }, }; diff --git a/packages/typegpu/src/core/variable/tgpuVariable.ts b/packages/typegpu/src/core/variable/tgpuVariable.ts index e5d39b4aa2..c6d0644795 100644 --- a/packages/typegpu/src/core/variable/tgpuVariable.ts +++ b/packages/typegpu/src/core/variable/tgpuVariable.ts @@ -112,7 +112,7 @@ class TgpuVarImpl { [$internal]: true, get [$ownSnippet]() { - return snip(this, dataType, origin); + return snip(this, dataType, origin, false); }, [$resolve]: (ctx) => ctx.resolve(this), toString: () => `var:${getName(this) ?? ''}.$`, diff --git a/packages/typegpu/src/data/ref.ts b/packages/typegpu/src/data/ref.ts index fa51b7ee22..77dac4395a 100644 --- a/packages/typegpu/src/data/ref.ts +++ b/packages/typegpu/src/data/ref.ts @@ -5,7 +5,7 @@ import { $gpuCallable, $internal, $ownSnippet, $resolve } from '../shared/symbol import type { DualFn, SelfResolvable } from '../types.ts'; import { UnknownData } from './dataTypes.ts'; import { createPtrFromOrigin, explicitFrom } from './ptr.ts'; -import { isAlias, type ResolvedSnippet, snip, type Snippet } from './snippet.ts'; +import { isAlias, type ResolvedSnippet, snip, type Snippet, withDataType } from './snippet.ts'; import { isNaturallyEphemeral, isPtr, type Ptr, type StorableData } from './wgslTypes.ts'; // ---------- @@ -77,7 +77,7 @@ export const _ref = (() => { if (isPtr(value.dataType)) { // This can happen if we take a reference of an *implicit* pointer, one // made by assigning a reference to a `const`. - return snip(value.value, explicitFrom(value.dataType), value.origin); + return withDataType(explicitFrom(value.dataType), value); } /** @@ -90,7 +90,12 @@ export const _ref = (() => { * ``` */ const ptrType = createPtrFromOrigin(value.origin, value.dataType as StorableData); - return snip(new RefOperator(value, ptrType), ptrType ?? UnknownData, /* origin */ 'runtime'); + return snip( + new RefOperator(value, ptrType), + ptrType ?? UnknownData, + /* origin */ 'runtime', + value.possibleSideEffects, + ); }, }; @@ -175,14 +180,19 @@ export class RefOperator implements SelfResolvable { if (!this.#ptrType) { throw new Error(stitch`Cannot take a reference of ${this.snippet}`); } - return snip(this, this.#ptrType, this.snippet.origin); + return snip(this, this.#ptrType, this.snippet.origin, this.snippet.possibleSideEffects); } [$resolve](): ResolvedSnippet { if (!this.#ptrType) { throw new Error(stitch`Cannot take a reference of ${this.snippet}`); } - return snip(stitch`(&${this.snippet})`, this.#ptrType, this.snippet.origin); + return snip( + stitch`(&${this.snippet})`, + this.#ptrType, + this.snippet.origin, + this.snippet.possibleSideEffects, + ); } } @@ -194,8 +204,13 @@ export function derefSnippet(snippet: Snippet): Snippet { const innerType = snippet.dataType.inner; if (snippet.value instanceof RefOperator) { - return snip(stitch`${snippet.value.snippet}`, innerType, snippet.origin); + return snip( + stitch`${snippet.value.snippet}`, + innerType, + snippet.origin, + snippet.possibleSideEffects, + ); } - return snip(stitch`(*${snippet})`, innerType, snippet.origin); + return snip(stitch`(*${snippet})`, innerType, snippet.origin, snippet.possibleSideEffects); } diff --git a/packages/typegpu/src/data/snippet.ts b/packages/typegpu/src/data/snippet.ts index 5457cfa940..35db60b5f0 100644 --- a/packages/typegpu/src/data/snippet.ts +++ b/packages/typegpu/src/data/snippet.ts @@ -180,6 +180,12 @@ export function withDataType(dataType: BaseData | UnknownData, snippet: Snippet) return new SnippetImpl(snippet.value, dataType, snippet.origin, snippet.possibleSideEffects); } +export function withValue(value: string, snippet: Snippet): ResolvedSnippet; +export function withValue(value: unknown, snippet: Snippet): Snippet; +export function withValue(value: unknown, snippet: Snippet): Snippet { + return new SnippetImpl(value, snippet.dataType, snippet.origin, snippet.possibleSideEffects); +} + export function withSideEffects( possibleSideEffects: boolean, snippet: ResolvedSnippet, diff --git a/packages/typegpu/src/resolutionCtx.ts b/packages/typegpu/src/resolutionCtx.ts index df357024a1..e2d1d59c8d 100644 --- a/packages/typegpu/src/resolutionCtx.ts +++ b/packages/typegpu/src/resolutionCtx.ts @@ -12,7 +12,13 @@ import { } from './core/slot/slotTypes.ts'; import { isData, UnknownData } from './data/dataTypes.ts'; import { bool } from './data/numeric.ts'; -import { type Origin, type ResolvedSnippet, snip, type Snippet } from './data/snippet.ts'; +import { + type Origin, + type ResolvedSnippet, + snip, + type Snippet, + withValue, +} from './data/snippet.ts'; import { type BaseData, isPtr, isWgslArray, isWgslStruct, Void } from './data/wgslTypes.ts'; import { invariant, MissingSlotValueError, ResolutionError, WgslTypeError } from './errors.ts'; import { provideCtx, topLevelState } from './execMode.ts'; @@ -1006,7 +1012,7 @@ export class ResolutionCtxImpl implements ResolutionCtx { } if (typeof item === 'boolean') { - return snip(item ? 'true' : 'false', bool, /* origin */ 'constant'); + return snip(item ? 'true' : 'false', bool, /* origin */ 'constant', false); } if (typeof item === 'string') { @@ -1048,11 +1054,7 @@ export class ResolutionCtxImpl implements ResolutionCtx { } resolveSnippet(snippet: Snippet): ResolvedSnippet { - return snip( - this.resolve(snippet.value, snippet.dataType).value, - snippet.dataType, - snippet.origin, - ) as ResolvedSnippet; + return withValue(this.resolve(snippet.value, snippet.dataType).value, snippet); } pushMode(mode: ExecState) { @@ -1116,11 +1118,8 @@ export function resolve(item: Wgsl, options: ResolutionCtxImplOptions): Resoluti new TgpuBindGroupImpl( catchallLayout, Object.fromEntries( - ctx.fixedBindings.map( - (binding, idx) => - // oxlint-disable-next-line typescript/no-explicit-any -- it's fine - [String(idx), binding.resource] as [string, any], - ), + // oxlint-disable-next-line typescript/no-explicit-any -- it's fine + ctx.fixedBindings.map((binding, idx) => [String(idx), binding.resource] as [string, any]), ), ), ] as [number, TgpuBindGroup]; diff --git a/packages/typegpu/src/std/boolean.ts b/packages/typegpu/src/std/boolean.ts index 129d69fe66..8d6d673f93 100644 --- a/packages/typegpu/src/std/boolean.ts +++ b/packages/typegpu/src/std/boolean.ts @@ -367,7 +367,7 @@ export const isCloseTo = dualImpl({ return false; }, // GPU implementation - codegenImpl: (_ctx, [lhs, rhs, precision = snip(0.01, f32, /* origin */ 'constant')]) => { + codegenImpl: (_ctx, [lhs, rhs, precision = snip(0.01, f32, /* origin */ 'constant', false)]) => { if (isSnippetNumeric(lhs) && isSnippetNumeric(rhs)) { return stitch`(abs(f32(${lhs}) - f32(${rhs})) <= ${precision})`; } diff --git a/packages/typegpu/src/tgsl/conversion.ts b/packages/typegpu/src/tgsl/conversion.ts index 71758f1c1e..e88a634dbb 100644 --- a/packages/typegpu/src/tgsl/conversion.ts +++ b/packages/typegpu/src/tgsl/conversion.ts @@ -1,4 +1,3 @@ -import { stitch } from '../core/resolve/stitch.ts'; import { UnknownData } from '../data/dataTypes.ts'; import { undecorate } from '../data/dataTypes.ts'; import { derefSnippet, RefOperator } from '../data/ref.ts'; @@ -256,7 +255,12 @@ function applyActionToSnippet( switch (action.action) { case 'ref': - return snip(new RefOperator(snippet, targetType as Ptr), targetType, snippet.origin); + return snip( + new RefOperator(snippet, targetType as Ptr), + targetType, + snippet.origin, + snippet.possibleSideEffects, + ); case 'deref': return derefSnippet(snippet); case 'cast': { @@ -394,18 +398,18 @@ export function tryConvertSnippet( ): Snippet { const targets = Array.isArray(targetDataTypes) ? targetDataTypes : [targetDataTypes]; - const { value, dataType, origin } = snippet; + const { value, dataType, origin, possibleSideEffects } = snippet; if (targets.length === 1) { const target = targets[0] as AnyWgslData; if (target === dataType) { - return snip(value, target, origin); + return snip(value, target, origin, possibleSideEffects); } if (dataType === UnknownData) { // Commit unknown to the expected type. - return snip(stitch`${snip(value, target, origin)}`, target, origin); + return ctx.resolveSnippet(snip(value, target, origin, possibleSideEffects)); } } diff --git a/packages/typegpu/src/tgsl/forOfUtils.ts b/packages/typegpu/src/tgsl/forOfUtils.ts index cb541d5fd7..767202cd7a 100644 --- a/packages/typegpu/src/tgsl/forOfUtils.ts +++ b/packages/typegpu/src/tgsl/forOfUtils.ts @@ -72,9 +72,9 @@ export function getRangeSnippets( const dataType = [start, end, step].every((v) => v >= 0) ? u32 : i32; return { - start: snip(start, dataType, 'constant'), - end: snip(end, dataType, 'constant'), - step: snip(step, dataType, 'constant'), + start: snip(start, dataType, 'constant', false), + end: snip(end, dataType, 'constant', false), + step: snip(step, dataType, 'constant', false), comparison: step < 0 ? '>' : '<', }; } @@ -89,8 +89,8 @@ You can wrap iterable with \`tgpu.unroll(...)\`. If iterable is known at comptim } const defaults = { - start: snip(0, u32, 'constant'), - step: snip(1, u32, 'constant'), + start: snip(0, u32, 'constant', false), + step: snip(1, u32, 'constant', false), comparison: '<' as const, }; @@ -99,7 +99,7 @@ You can wrap iterable with \`tgpu.unroll(...)\`. If iterable is known at comptim ...defaults, end: dataType.elementCount > 0 - ? snip(dataType.elementCount, u32, 'constant') + ? snip(dataType.elementCount, u32, 'constant', false) : arrayLength[$gpuCallable].call(ctx, [iterableSnippet]), }; } @@ -107,7 +107,7 @@ You can wrap iterable with \`tgpu.unroll(...)\`. If iterable is known at comptim if (wgsl.isVec(dataType)) { return { ...defaults, - end: snip(dataType.componentCount, u32, 'constant'), + end: snip(dataType.componentCount, u32, 'constant', false), }; } @@ -115,14 +115,14 @@ You can wrap iterable with \`tgpu.unroll(...)\`. If iterable is known at comptim if (Array.isArray(value)) { return { ...defaults, - end: snip(value.length, u32, 'constant'), + end: snip(value.length, u32, 'constant', false), }; } if (value instanceof ArrayExpression) { return { ...defaults, - end: snip(value.elements.length, u32, 'constant'), + end: snip(value.elements.length, u32, 'constant', false), }; } } diff --git a/packages/typegpu/src/tgsl/generationHelpers.ts b/packages/typegpu/src/tgsl/generationHelpers.ts index cc645e63dd..4c5903b439 100644 --- a/packages/typegpu/src/tgsl/generationHelpers.ts +++ b/packages/typegpu/src/tgsl/generationHelpers.ts @@ -1,7 +1,7 @@ import { UnknownData } from '../data/dataTypes.ts'; import { abstractFloat, abstractInt, bool, f32, i32 } from '../data/numeric.ts'; import { isRef } from '../data/ref.ts'; -import { isAlias, isSnippet, snip } from '../data/snippet.ts'; +import { isAlias, isSnippet, snip, withDataType } from '../data/snippet.ts'; import type { ResolvedSnippet, Snippet } from '../data/snippet.ts'; import { type AnyWgslData, @@ -55,7 +55,7 @@ export function concretize(type: T): T | F32 | I32 { } export function concretizeSnippet(snippet: Snippet): Snippet { - return snip(snippet.value, concretize(snippet.dataType as AnyWgslData), snippet.origin); + return withDataType(concretize(snippet.dataType as AnyWgslData), snippet); } export function concretizeSnippets(args: Snippet[]): Snippet[] { @@ -121,18 +121,6 @@ export function coerceToSnippet(value: unknown): Snippet { ); } - if ( - typeof value === 'string' || - typeof value === 'function' || - typeof value === 'object' || - typeof value === 'symbol' || - typeof value === 'undefined' || - value === null - ) { - // Nothing representable in WGSL as-is, so unknown - return snip(value, UnknownData, /* origin */ 'constant', /* possibleSideEffects */ false); - } - if (typeof value === 'number') { return numericLiteralToSnippet(value); } diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index 55d8ceb50e..55a91146ee 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -30,8 +30,8 @@ import { ArrayExpression, coerceToSnippet, concretize, - type GenerationCtx, numericLiteralToSnippet, + type GenerationCtx, } from './generationHelpers.ts'; import { accessIndex } from './accessIndex.ts'; import { accessProp } from './accessProp.ts'; @@ -163,35 +163,33 @@ function operatorToType< const unaryOpCodeToCodegen = { '-': neg[$gpuCallable].call.bind(neg), - void: () => snip(undefined, wgsl.Void, 'constant'), + void: () => snip(undefined, wgsl.Void, 'constant', false), '!': (ctx: GenerationCtx, [argExpr]: Snippet[]) => { if (argExpr === undefined) { throw new Error('The unary operator `!` expects 1 argument, but 0 were provided.'); } if (isKnownAtComptime(argExpr)) { - return snip(!argExpr.value, bool, 'constant'); + return snip(!argExpr.value, bool, 'constant', false); } const { value, dataType } = argExpr; const argStr = ctx.resolve(value, dataType).value; if (wgsl.isBool(dataType)) { - return snip(`!${argStr}`, bool, 'runtime'); + return snip(`!${argStr}`, bool, 'runtime', argExpr.possibleSideEffects); } if (wgsl.isNumericSchema(dataType)) { const resultStr = `!bool(${argStr})`; const nanGuardedStr = // abstractFloat will be resolved as comptime known value - dataType.type === 'f32' - ? `(((bitcast(${argStr}) & 0x7fffffff) > 0x7f800000) || ${resultStr})` - : dataType.type === 'f16' - ? `(((bitcast(${argStr}) & 0x7fff) > 0x7c00) || ${resultStr})` - : resultStr; + dataType.type === 'f32' || dataType.type === 'f16' + ? `(((bitcast(${dataType.type === 'f16' ? `f32(${argStr})` : argStr}) << 1u) - 1u) >= 0xff000000)` + : resultStr; - return snip(nanGuardedStr, bool, 'runtime'); + return snip(nanGuardedStr, bool, 'runtime', argExpr.possibleSideEffects); } - return snip(false, bool, 'constant'); + return snip(false, bool, 'constant', false); }, } satisfies Partial unknown>>; @@ -262,9 +260,10 @@ ${this.ctx.pre}}`; const varName = this.ctx.makeUniqueIdentifier(id, 'block'); const ptrType = ptrFn(dataType); const snippet = snip( - new RefOperator(snip(varName, dataType, 'function'), ptrType), + new RefOperator(snip(varName, dataType, 'function', false), ptrType), ptrType, 'function', + false, ); this.ctx.defineVariable(id, snippet); return varName; @@ -288,7 +287,7 @@ ${this.ctx.pre}}`; throw new Error('Cannot resolve an empty identifier'); } if (id === 'undefined') { - return snip(undefined, wgsl.Void, 'constant'); + return snip(undefined, wgsl.Void, 'constant', false); } const res = this.ctx.getById(id); @@ -331,7 +330,7 @@ ${this.ctx.pre}}`; } if (typeof expression === 'boolean') { - return snip(expression, bool, /* origin */ 'constant'); + return snip(expression, bool, /* origin */ 'constant', false); } if ( @@ -348,7 +347,7 @@ ${this.ctx.pre}}`; const evalRhs = op === '&&' ? lhsExpr.value : !lhsExpr.value; if (!evalRhs) { - return snip(op === '||', bool, 'constant'); + return snip(op === '||', bool, 'constant', false); } const rhsExpr = this._expression(rhs); @@ -358,13 +357,13 @@ ${this.ctx.pre}}`; } if (isKnownAtComptime(rhsExpr)) { - return snip(!!rhsExpr.value, bool, 'constant'); + return snip(!!rhsExpr.value, bool, 'constant', false); } // we can skip lhs const convRhs = tryConvertSnippet(this.ctx, rhsExpr, bool, false); const rhsStr = this.ctx.resolve(convRhs.value, convRhs.dataType).value; - return snip(rhsStr, bool, 'runtime'); + return snip(rhsStr, bool, 'runtime', convRhs.possibleSideEffects); } const rhsExpr = this._expression(rhs); @@ -502,7 +501,7 @@ ${this.ctx.pre}}`; const type = operatorToType(argExpr.dataType, op); // Result of an operation, so not a reference to anything - return snip(`${op}${argStr}`, type, /* origin */ 'runtime'); + return snip(`${op}${argStr}`, type, /* origin */ 'runtime', argExpr.possibleSideEffects); } if (expression[0] === NODE.memberAccess) { @@ -551,7 +550,12 @@ ${this.ctx.pre}}`; const [_, calleeNode, argNodes] = expression; const _callee = this._expression(calleeNode); const callee = mathToStd.has(_callee.value as AnyFn) - ? snip(mathToStd.get(_callee.value as AnyFn) as DualFn, UnknownData, 'runtime') + ? snip( + mathToStd.get(_callee.value as AnyFn) as DualFn, + UnknownData, + 'runtime', + _callee.possibleSideEffects, + ) : _callee; if (supportedLogOps().includes(callee.value as AnyFn)) { @@ -575,6 +579,7 @@ ${this.ctx.pre}}`; callee.value, // A new struct, so not a reference. /* origin */ 'runtime', + false, ); } @@ -587,6 +592,7 @@ ${this.ctx.pre}}`; callee.value, // A new struct, so not a reference. /* origin */ 'runtime', + arg.possibleSideEffects, ); } @@ -604,6 +610,7 @@ ${this.ctx.pre}}`; callee.value, // A new array, so not a reference. /* origin */ 'runtime', + false, ); } @@ -617,6 +624,7 @@ ${this.ctx.pre}}`; stitch`${this.ctx.resolve(callee.value).value}(${arg.value.elements})`, arg.dataType, /* origin */ 'runtime', + arg.possibleSideEffects, ); } @@ -627,6 +635,7 @@ ${this.ctx.pre}}`; callee.value, // A new array, so not a reference. /* origin */ 'runtime', + arg.possibleSideEffects, ); } @@ -805,6 +814,7 @@ ${this.ctx.pre}}`; stitch`${this.ctx.resolve(structType).value}(${convertedSnippets})`, structType, /* origin */ 'runtime', + convertedSnippets.some((s) => s.possibleSideEffects), ); } @@ -851,7 +861,12 @@ ${this.ctx.pre}}`; const arrayType = arrayOf(elemType as wgsl.AnyWgslData, values.length); - return snip(new ArrayExpression(arrayType, values), arrayType, /* origin */ 'runtime'); + return snip( + new ArrayExpression(arrayType, values), + arrayType, + /* origin */ 'runtime', + values.some((v) => v.possibleSideEffects), + ); } if (expression[0] === NODE.conditionalExpr) { @@ -884,7 +899,7 @@ ${this.ctx.pre}}`; } if (expression[0] === NODE.stringLiteral) { - return snip(expression[1], UnknownData, /* origin */ 'constant'); + return snip(expression[1], UnknownData, /* origin */ 'constant', false); } if (expression[0] === NODE.preUpdate) { @@ -992,10 +1007,20 @@ ${this.ctx.pre}}`; if (args.length === 1 && args[0]?.dataType === schema) { // Already of the desired type, e.g. `bool(false)` or `vec3f(vec3f(1, 2, 3))` // We can make this snippet ephemeral, as we know it will be deep copied in JS - return snip(stitch`${args[0]}`, schema, fallthroughCopyOrigin(args[0].origin)); + return snip( + stitch`${args[0]}`, + schema, + fallthroughCopyOrigin(args[0].origin), + args[0].possibleSideEffects, + ); } // Creating a 'runtime' snippet, since it's instantiating a new value - return snip(stitch`${this.ctx.resolve(schema).value}(${args})`, schema, 'runtime'); + return snip( + stitch`${this.ctx.resolve(schema).value}(${args})`, + schema, + 'runtime', + args.some((s) => s.possibleSideEffects), + ); } public numericLiteral(value: number, schema: wgsl.BaseData): ResolvedSnippet { @@ -1006,13 +1031,13 @@ ${this.ctx.pre}}`; } if (schema.type === 'abstractInt') { - return snip(`${value}`, schema, /* origin */ 'constant'); + return snip(`${value}`, schema, /* origin */ 'constant', false); } if (schema.type === 'u32') { - return snip(`${value}u`, schema, /* origin */ 'constant'); + return snip(`${value}u`, schema, /* origin */ 'constant', false); } if (schema.type === 'i32') { - return snip(`${value}i`, schema, /* origin */ 'constant'); + return snip(`${value}i`, schema, /* origin */ 'constant', false); } const exp = value.toExponential(); @@ -1022,12 +1047,12 @@ ${this.ctx.pre}}`; // Just picking the shorter one const base = exp.length < decimal.length ? exp : decimal; if (schema.type === 'f32') { - return snip(`${base}f`, schema, /* origin */ 'constant'); + return snip(`${base}f`, schema, /* origin */ 'constant', false); } if (schema.type === 'f16') { - return snip(`${base}h`, schema, /* origin */ 'constant'); + return snip(`${base}h`, schema, /* origin */ 'constant', false); } - return snip(base, schema, /* origin */ 'constant'); + return snip(base, schema, /* origin */ 'constant', false); } protected _return(statement: tinyest.Return): string { @@ -1154,8 +1179,7 @@ Try 'return ${typeStr}(${str});' instead. this.ctx.makeUniqueIdentifier(rawId, 'block'), concreteType, /* origin */ 'local-def', - // Accessing variable declarations is side-effect free. - /* possibleSideEffects */ false, + false, ); this.ctx.defineVariable(rawId, snippet); @@ -1270,8 +1294,7 @@ Try 'return ${typeStr}(${str});' instead. this.ctx.makeUniqueIdentifier(rawId, 'block'), concreteType, /* origin */ varOrigin, - // Accessing variable declarations is side-effect free. - /* possibleSideEffects */ false, + false, ); this.ctx.defineVariable(rawId, snippet); @@ -1467,7 +1490,7 @@ ${this.ctx.pre}else ${alternate}`; if (isTgpuRange(iterableSnippet.value)) { bodyStr = this._block(blockified, { - [originalLoopVarName]: snip(index, range.start.dataType, 'runtime'), // range.start, .end , .step have the same dataType + [originalLoopVarName]: snip(index, range.start.dataType, 'runtime', false), // range.start, .end , .step have the same dataType }); } else { this.ctx.indent(); @@ -1487,7 +1510,7 @@ ${this.ctx.pre}else ${alternate}`; )};`; bodyStr = `{\n${loopVarDeclStr}\n${this._blockStatement(blockified, { - [originalLoopVarName]: snip(loopVarName, elementType, elementSnippet.origin), + [originalLoopVarName]: snip(loopVarName, elementType, elementSnippet.origin, false), })}\n`; this.ctx.dedent(); bodyStr += `${this.ctx.pre}}`; diff --git a/packages/typegpu/tests/accessor.test.ts b/packages/typegpu/tests/accessor.test.ts index feb5ba4f51..5cb0cadf6c 100644 --- a/packages/typegpu/tests/accessor.test.ts +++ b/packages/typegpu/tests/accessor.test.ts @@ -533,4 +533,21 @@ describe('tgpu.accessor', () => { }" `); }); + + it('allows zero-argument dualFn', () => { + const accessor = tgpu.accessor(d.bool, std.subgroupElect); + + expect( + tgpu.resolve([ + () => { + 'use gpu'; + return accessor.$; + }, + ]), + ).toMatchInlineSnapshot(` + "fn item() -> bool { + return subgroupElect(); + }" + `); + }); }); diff --git a/packages/typegpu/tests/tgsl/sideEffects.test.ts b/packages/typegpu/tests/tgsl/sideEffects.test.ts index 1e20d622b3..33151acb44 100644 --- a/packages/typegpu/tests/tgsl/sideEffects.test.ts +++ b/packages/typegpu/tests/tgsl/sideEffects.test.ts @@ -1,10 +1,50 @@ -import { tgpu, d } from 'typegpu'; +import { tgpu, d, std } from 'typegpu'; import { test } from 'typegpu-testing-utility'; -import { expectSideEffects } from '../utils/parseResolved.ts'; import { describe } from 'vitest'; +import { expectSideEffects } from '../utils/parseResolved.ts'; + +const Boid = d.struct({ pos: d.vec3f }); + +const impureVec = () => { + 'use gpu'; + return d.vec3f(6, 6, 6); +}; +const impureInt = () => { + 'use gpu'; + return 666; +}; +const impureMat = () => { + 'use gpu'; + return d.mat2x2f(6, 6, 6, 6); +}; +const impureStruct = () => { + 'use gpu'; + return Boid(); +}; +const impureBool = () => { + 'use gpu'; + return true; +}; describe('code without side-effects', () => { - test('scalar literals', () => { + test('numeric literals', () => { + expectSideEffects(() => { + 'use gpu'; + return 1.7e308; + }).toEqual(false); + + expectSideEffects(() => { + 'use gpu'; + return 1; + }).toEqual(false); + + expectSideEffects(() => { + 'use gpu'; + return 1.5; + }).toEqual(false); + }); + + test('slot-bound scalar values', () => { const returnSlot = tgpu.slot(); const fn = tgpu.fn(() => { 'use gpu'; @@ -12,9 +52,66 @@ describe('code without side-effects', () => { }); expectSideEffects(fn.with(returnSlot, 0)).toEqual(false); - expectSideEffects(fn.with(returnSlot, 1.5)).toEqual(false); - expectSideEffects(fn.with(returnSlot, -100)).toEqual(false); expectSideEffects(fn.with(returnSlot, false)).toEqual(false); + expectSideEffects(fn.with(returnSlot, d.vec3f())).toEqual(false); + expectSideEffects(fn.with(returnSlot, d.mat2x2f())).toEqual(false); + expectSideEffects(fn.with(returnSlot, Boid())).toEqual(false); + }); + + test('buffer usage reads', ({ root }) => { + const uniform = root.createUniform(d.f32); + const readonly = root.createReadonly(d.f32); + const mutable = root.createMutable(d.f32); + + const buffer = root.createBuffer(d.f32).$usage('storage').as('mutable'); + + tgpu.resolve([buffer]); + + expectSideEffects(() => { + 'use gpu'; + return uniform.$; + }).toEqual(false); + + expectSideEffects(() => { + 'use gpu'; + return readonly.$; + }).toEqual(false); + + expectSideEffects(() => { + 'use gpu'; + return mutable.$; + }).toEqual(false); + }); + + test('bound buffer reads', () => { + const layout = tgpu.bindGroupLayout({ + uniform: { uniform: d.f32 }, + readonly: { storage: d.f32, access: 'readonly' }, + mutable: { storage: d.f32, access: 'mutable' }, + }); + + expectSideEffects(() => { + 'use gpu'; + return layout.$.uniform; + }).toEqual(false); + + expectSideEffects(() => { + 'use gpu'; + return layout.$.readonly; + }).toEqual(false); + + expectSideEffects(() => { + 'use gpu'; + return layout.$.mutable; + }).toEqual(false); + }); + + test('tgpu.const read', () => { + const c = tgpu.const(d.u32, 42); + expectSideEffects(() => { + 'use gpu'; + return c.$; + }).toEqual(false); }); test('vectors created from literals', () => { @@ -29,6 +126,121 @@ describe('code without side-effects', () => { }).toEqual(false); }); + test('sampler access', ({ root }) => { + const layout = tgpu.bindGroupLayout({ + s: { sampler: 'filtering' }, + }); + const s = root.createSampler({}); + + expectSideEffects(() => { + 'use gpu'; + return s.$; + }).toEqual(false); + + expectSideEffects(() => { + 'use gpu'; + return layout.$.s; + }).toEqual(false); + }); + + test('buffer read from accessor', ({ root }) => { + const Boid = d.struct({ pos: d.vec3f }); + const buffer = root.createUniform(Boid); + const accessor = tgpu.accessor(d.f32, () => buffer.$.pos.y); + + expectSideEffects(() => { + 'use gpu'; + return accessor.$; + }).toEqual(false); + }); + + test('pure function call from accessor', () => { + const accessor = tgpu.accessor(d.bool, std.subgroupElect); + + expectSideEffects(() => { + 'use gpu'; + return accessor.$; + }).toEqual(false); + }); + + test('external texture access', () => { + const layout = tgpu.bindGroupLayout({ + tex: { externalTexture: d.textureExternal() }, + }); + + expectSideEffects(() => { + 'use gpu'; + return layout.$.tex; + }).toEqual(false); + }); + + test('fixed texture access', ({ root }) => { + const texture = root + .createTexture({ + size: [256, 256], + format: 'rgba8unorm', + }) + .$usage('sampled'); + + const sampledView = texture.createView(); + + expectSideEffects(() => { + 'use gpu'; + return sampledView.$; + }).toEqual(false); + }); + + test('bound texture access', () => { + const layout = tgpu.bindGroupLayout({ + tex: { texture: d.texture2d() }, + }); + + expectSideEffects(() => { + 'use gpu'; + return layout.$.tex; + }).toEqual(false); + }); + + test('unroll over pure array', () => { + expectSideEffects(() => { + 'use gpu'; + return tgpu.unroll([1, 2, 3]); + }).toEqual(false); + }); + + test('workgroup and private var reads', () => { + const w = tgpu.workgroupVar(d.u32); + const p = tgpu.privateVar(d.u32, 2); + + expectSideEffects(() => { + 'use gpu'; + return [w.$, p.$]; // all of the elements of array need to be pure + }).toEqual(false); + }); + + test('creating ref of pure value', () => { + expectSideEffects(() => { + 'use gpu'; + return d.ref(d.vec3f()); + }).toEqual(false); + }); + + test('creating ref from implicit pointer of pure value', () => { + expectSideEffects(() => { + 'use gpu'; + const v = d.vec3f(); + return d.ref(v); + }).toEqual(false); + }); + + test('creating ref from implicit pointer of impure value', () => { + expectSideEffects(() => { + 'use gpu'; + const v = impureVec(); + return d.ref(v); + }).toEqual(false); + }); + test('variables', () => { expectSideEffects(() => { 'use gpu'; @@ -41,9 +253,15 @@ describe('code without side-effects', () => { const hello = [1, 2, 3]; return hello; }).toEqual(false); + + expectSideEffects(() => { + 'use gpu'; + const hello = impureInt(); + return hello; + }).toEqual(false); }); - test('indexed arrays', () => { + test('indexed array', () => { expectSideEffects(() => { 'use gpu'; const hello = [1, 2, 3]; @@ -51,7 +269,23 @@ describe('code without side-effects', () => { }).toEqual(false); }); - test('vectors created from indexed arrays', () => { + test('indexed vector', () => { + expectSideEffects(() => { + 'use gpu'; + const v = d.vec3f(); + return v[0]; + }).toEqual(false); + }); + + test('indexed matrix column', () => { + expectSideEffects(() => { + 'use gpu'; + const m = d.mat2x2f(); + return m.columns[0]; + }).toEqual(false); + }); + + test('vector created from indexed array', () => { expectSideEffects(() => { 'use gpu'; const arr = [1, 2, 3]; @@ -59,6 +293,13 @@ describe('code without side-effects', () => { }).toEqual(false); }); + test('vector swizzle of pure value', () => { + expectSideEffects(() => { + 'use gpu'; + return d.vec3f(1, 2, 3).xy; + }).toEqual(false); + }); + test('vector from accessor', () => { const v = tgpu.accessor(d.vec3f, d.vec3f()); expectSideEffects(() => { @@ -68,30 +309,309 @@ describe('code without side-effects', () => { }); test('prop access', () => { - const Foo = d.struct({ - prop: d.f32, - }); + expectSideEffects(() => { + 'use gpu'; + const foo = Boid(); + return foo.pos; + }).toEqual(false); + }); + + test('vector kind property', () => { + expectSideEffects(() => { + 'use gpu'; + return d.vec3f().kind; + }).toEqual(false); + }); + + test('same-type conversion with pure value', () => { + expectSideEffects(() => { + 'use gpu'; + const v = d.u32(); + return d.u32(v); + }).toEqual(false); + }); + + test('snippet with the UnknownData datatype conversion', () => { + const boidSlot = tgpu.slot(Boid()); + + expectSideEffects( + tgpu.fn( + [], + Boid, + )(() => { + 'use gpu'; + return boidSlot.$; + }), + ).toEqual(false); + }); + + test('logical not of pure value', () => { + const flag = false; + expectSideEffects(() => { + 'use gpu'; + return !flag; + }).toEqual(false); + }); + + test('deref of ref wrapping impure value', () => { + expectSideEffects(() => { + 'use gpu'; + const v = d.ref(impureVec()); + return v.$; + }).toEqual(false); + }); + + test('boolean literal', () => { + expectSideEffects(() => { + 'use gpu'; + return false; + }).toEqual(false); + }); + + test('comptime equality comparison', () => { + expectSideEffects(() => { + 'use gpu'; + return std.getTargetShaderLanguage() === 'wgsl'; + }).toEqual(false); + + expectSideEffects(() => { + 'use gpu'; + return std.getTargetShaderLanguage() !== 'wgsl'; + }).toEqual(false); + }); + + test('logical short-circuit with comptime folding', () => { + const flag = true; + expectSideEffects(() => { + 'use gpu'; + return !std.isBeingTranspiled() || flag; + }).toEqual(false); + + expectSideEffects(() => { + 'use gpu'; + return std.isBeingTranspiled() || impureBool(); + }).toEqual(false); + + expectSideEffects(() => { + 'use gpu'; + return !std.isBeingTranspiled() && impureBool(); + }).toEqual(false); + }); + + test('logical short-circuit with pure rhs', () => { + expectSideEffects(() => { + 'use gpu'; + const flag = true; + return !std.isBeingTranspiled() || flag; + }).toEqual(false); + + expectSideEffects(() => { + 'use gpu'; + const flag = true; + return std.isBeingTranspiled() && flag; + }).toEqual(false); + }); + test('comptime-folded comparison', () => { + const x = 1; expectSideEffects(() => { 'use gpu'; - const foo = Foo(); - return foo.prop; + return x > 0; + }).toEqual(false); + }); + + test('binary comparison of pure values', () => { + const x = 1; + expectSideEffects(() => { + 'use gpu'; + const y = impureInt(); + return x > y; + }).toEqual(false); + }); + + test('comptime-folded dualImpl call', () => { + expectSideEffects(() => { + 'use gpu'; + return 1 + 1; + }).toEqual(false); + }); + + test('unary expression with pure value', () => { + expectSideEffects(() => { + 'use gpu'; + return ~5; + }).toEqual(false); + }); + + test('struct constructor with no args', () => { + expectSideEffects(() => { + 'use gpu'; + return Boid(); + }).toEqual(false); + }); + + test('struct copy from pure source', () => { + expectSideEffects(() => { + 'use gpu'; + const src = Boid(); + return Boid(src); + }).toEqual(false); + }); + + test('comptime ternary', () => { + expectSideEffects(() => { + 'use gpu'; + return true ? 1 : 2; + }).toEqual(false); + }); + + test('runtime ternary with pure condition', () => { + expectSideEffects(() => { + 'use gpu'; + const flag = false; + return flag ? 1 : 2; + }).toEqual(false); + }); + + test('logical not of impure value of complex datatype', () => { + expectSideEffects(() => { + 'use gpu'; + return !impureStruct(); }).toEqual(false); }); }); describe('code with side-effects', () => { - test('vectors created from side-effectful components', () => { - const next = tgpu.privateVar(d.f32); + test('vector constructor with impure components', () => { + expectSideEffects(() => { + 'use gpu'; + return d.vec3f(impureInt()); + }).toEqual(true); + }); - function getNextValue() { + test('impure accessor call', () => { + const accessor = tgpu.accessor(d.f32, impureInt); + expectSideEffects(() => { 'use gpu'; - return next.$; - } + return accessor.$; + }).toEqual(true); + }); + + test('unroll over array with impure element', () => { + expectSideEffects(() => { + 'use gpu'; + return tgpu.unroll([1, impureInt(), 3]); + }).toEqual(true); + }); + + test('creating ref of impure value', () => { + expectSideEffects(() => { + 'use gpu'; + return d.ref(impureVec()); + }).toEqual(true); + }); + + test('matrix column access on impure matrix', () => { + expectSideEffects(() => { + 'use gpu'; + return impureMat().columns[0]; + }).toEqual(true); + }); + + test('matrix column access with impure index', () => { + expectSideEffects(() => { + 'use gpu'; + const m = d.mat2x2f(); + return m.columns[impureInt()]; + }).toEqual(true); + }); + + test('prop access on impure struct', () => { + expectSideEffects(() => { + 'use gpu'; + return impureStruct().pos; + }).toEqual(true); + }); + + test('same-type conversion with impure value', () => { + expectSideEffects(() => { + 'use gpu'; + return d.u32(impureInt()); + }).toEqual(true); + }); + + test('logical not of impure value', () => { + expectSideEffects(() => { + 'use gpu'; + return !impureInt(); + }).toEqual(true); + }); + + test('logical short-circuit with impure rhs', () => { + expectSideEffects(() => { + 'use gpu'; + return !std.isBeingTranspiled() || impureBool(); + }).toEqual(true); + + expectSideEffects(() => { + 'use gpu'; + return std.isBeingTranspiled() && impureBool(); + }).toEqual(true); + }); + + test('binary comparison with impure rhs', () => { + expectSideEffects(() => { + 'use gpu'; + const y = impureInt(); + return y > impureInt(); + }).toEqual(true); + }); + + test('unary expression of impure value', () => { + expectSideEffects(() => { + 'use gpu'; + return ~impureInt(); + }).toEqual(true); + }); + + test('struct copy from impure source', () => { + expectSideEffects(() => { + 'use gpu'; + return Boid(impureStruct()); + }).toEqual(true); + }); + + test('coercion of an object with an impure field to the struct type', () => { + expectSideEffects( + tgpu.fn( + [], + Boid, + )(() => { + return { pos: impureVec() }; + }), + ).toEqual(true); + }); + + test('vector swizzle of impure value', () => { + expectSideEffects(() => { + 'use gpu'; + return d.vec3f(impureVec()).xy; + }).toEqual(true); + }); + + test('runtime ternary with impure condition', () => { + expectSideEffects(() => { + 'use gpu'; + return impureBool() ? 1 : 2; + }).toEqual(true); + }); + test('array index with impure index', () => { + const Arr = d.arrayOf(d.f32, 3); + const arr = tgpu.accessor(Arr, Arr([1, 2, 3])); expectSideEffects(() => { 'use gpu'; - return d.vec3f(getNextValue(), getNextValue(), getNextValue()); + return arr.$[impureInt()]; }).toEqual(true); }); }); diff --git a/packages/typegpu/tests/utils/parseResolved.ts b/packages/typegpu/tests/utils/parseResolved.ts index 1c69c2dad7..bd2476cbf0 100644 --- a/packages/typegpu/tests/utils/parseResolved.ts +++ b/packages/typegpu/tests/utils/parseResolved.ts @@ -34,10 +34,13 @@ class ExtractingGenerator extends WgslGenerator { if (this.returnedSnippet) { throw new Error('Cannot inspect multiple return values'); } - if (!statement[1]) { + if (statement[1] === undefined) { throw new Error('Cannot inspect if nothing is returned'); } - this.returnedSnippet = this._expression(statement[1]); + const expectedReturnType = this.ctx.topFunctionReturnType; + this.returnedSnippet = expectedReturnType + ? this._typedExpression(statement[1], expectedReturnType) + : this._expression(statement[1]); return super._return([NODE.return]); } @@ -71,6 +74,6 @@ export function expectDataTypeOf( return expect(extractSnippetFromFn(cb).dataType); } -export function expectSideEffects(cb: () => unknown): Assertion { +export function expectSideEffects(cb: TgpuFn | (() => unknown)): Assertion { return expect(extractSnippetFromFn(cb).possibleSideEffects); }