From fed3aa22097db718ed11e8df2b9d111ba78e1e86 Mon Sep 17 00:00:00 2001 From: "pullfrog[bot]" <226033991+pullfrog[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 19:16:12 +0000 Subject: [PATCH 01/21] fix: improve runtime ternary support by refining side-effect detection - Mark function arguments as side-effect-free - Mark tgpu.const reads as side-effect-free - Propagate operand side-effects in pure binary operators - Add side-effect tracking to dualImpl (matching callableSchema) - Mark buffer reads (uniform/readonly/mutable) as side-effect-free - Add comprehensive ternary runtime tests from issue #2587 - Update existing ternary test for runtime select() generation --- packages/typegpu/src/core/buffer/bufferUsage.ts | 4 ++-- packages/typegpu/src/core/constant/tgpuConstant.ts | 2 +- packages/typegpu/src/core/function/dualImpl.ts | 9 +++++++-- packages/typegpu/src/resolutionCtx.ts | 2 +- packages/typegpu/src/tgsl/wgslGenerator.ts | 1 + packages/typegpu/tests/tgsl/ternaryOperator.test.ts | 10 +++++----- 6 files changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/typegpu/src/core/buffer/bufferUsage.ts b/packages/typegpu/src/core/buffer/bufferUsage.ts index 66adcdcbd1..52dc4acb0e 100644 --- a/packages/typegpu/src/core/buffer/bufferUsage.ts +++ b/packages/typegpu/src/core/buffer/bufferUsage.ts @@ -140,7 +140,7 @@ class TgpuFixedBufferImpl ctx.resolve(this), toString: () => `${this.usage}:${getName(this) ?? ''}.$`, @@ -262,7 +262,7 @@ export class TgpuLaidOutBufferImpl ctx.resolve(this), toString: () => `${this.usage}:${getName(this) ?? ''}.$`, diff --git a/packages/typegpu/src/core/constant/tgpuConstant.ts b/packages/typegpu/src/core/constant/tgpuConstant.ts index 723d9f63e5..b8d8458c70 100644 --- a/packages/typegpu/src/core/constant/tgpuConstant.ts +++ b/packages/typegpu/src/core/constant/tgpuConstant.ts @@ -128,7 +128,7 @@ class TgpuConstImpl implements TgpuConst, { [$internal]: true, get [$ownSnippet]() { - return snip(this, dataType, 'constant-immutable-def'); + return snip(this, dataType, 'constant-immutable-def', /* possibleSideEffects */ false); }, [$resolve]: (ctx) => ctx.resolve(this), toString: () => `const:${getName(this) ?? ''}.$`, diff --git a/packages/typegpu/src/core/function/dualImpl.ts b/packages/typegpu/src/core/function/dualImpl.ts index 45bcdb2c84..029813ef60 100644 --- a/packages/typegpu/src/core/function/dualImpl.ts +++ b/packages/typegpu/src/core/function/dualImpl.ts @@ -1,4 +1,4 @@ -import { type MapValueToSnippet, snip } from '../../data/snippet.ts'; +import { type MapValueToSnippet, noSideEffects, snip } from '../../data/snippet.ts'; import { setName } from '../../shared/meta.ts'; import { $gpuCallable } from '../../shared/symbols.ts'; import { tryConvertSnippet } from '../../tgsl/conversion.ts'; @@ -101,12 +101,17 @@ export function dualImpl(options: DualImplOptions): DualFn a.possibleSideEffects)) { + return noSideEffects(result); + } + return result; }, }; diff --git a/packages/typegpu/src/resolutionCtx.ts b/packages/typegpu/src/resolutionCtx.ts index f95399c67a..3eb58afd59 100644 --- a/packages/typegpu/src/resolutionCtx.ts +++ b/packages/typegpu/src/resolutionCtx.ts @@ -345,7 +345,7 @@ function createArgument( name, access: () => { used = true; - return snip(name, type, origin); + return snip(name, type, origin, /* possibleSideEffects */ false); }, decoratedType: type, get used() { diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index 5b517760b6..d691a50870 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -447,6 +447,7 @@ ${this.ctx.pre}}`; type, // Result of an operation, so not a reference to anything /* origin */ 'runtime', + exprType === NODE.assignmentExpr || lhsExpr.possibleSideEffects || rhsExpr.possibleSideEffects, ); } diff --git a/packages/typegpu/tests/tgsl/ternaryOperator.test.ts b/packages/typegpu/tests/tgsl/ternaryOperator.test.ts index 651fccb0dc..e13f525eab 100644 --- a/packages/typegpu/tests/tgsl/ternaryOperator.test.ts +++ b/packages/typegpu/tests/tgsl/ternaryOperator.test.ts @@ -181,7 +181,7 @@ describe('ternary operator', () => { `); }); - it('should throw when test is not comptime known', () => { + it('should generate select() for runtime condition with function params', () => { const myFn = tgpu.fn( [d.u32], d.u32, @@ -189,10 +189,10 @@ describe('ternary operator', () => { return n > 0 ? n : -n; }); - expect(() => tgpu.resolve([myFn])).toThrowErrorMatchingInlineSnapshot(` - [Error: Resolution of the following tree failed: - - - - fn:myFn: Ternary operator '(n > 0) ? n : (-n)' is invalid. For more complex branching, please use 'std.select' or if/else statements.] + expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(` + "fn myFn(n: u32) -> u32 { + return select(-(n), n, (n > 0u)); + }" `); }); }); From 89e4f5d7cd49e5e50284ab0d54156f6d422c9e9e Mon Sep 17 00:00:00 2001 From: "pullfrog[bot]" <226033991+pullfrog[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 19:17:25 +0000 Subject: [PATCH 02/21] fix: improve runtime ternary support by refining side-effect detection - Mark function arguments as side-effect-free - Mark tgpu.const reads as side-effect-free - Propagate operand side-effects in pure binary operators - Add side-effect tracking to dualImpl (matching callableSchema) - Mark buffer reads (uniform/readonly/mutable) as side-effect-free - Add comprehensive ternary runtime tests from issue #2587 - Update existing ternary test for runtime select() generation --- .../typegpu/tests/tgsl/ternaryRuntime.test.ts | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 packages/typegpu/tests/tgsl/ternaryRuntime.test.ts diff --git a/packages/typegpu/tests/tgsl/ternaryRuntime.test.ts b/packages/typegpu/tests/tgsl/ternaryRuntime.test.ts new file mode 100644 index 0000000000..34229ea73a --- /dev/null +++ b/packages/typegpu/tests/tgsl/ternaryRuntime.test.ts @@ -0,0 +1,141 @@ +import { describe, expect } from 'vitest'; +import { it } from 'typegpu-testing-utility'; +import tgpu, { d } from '../../src/index.js'; + +describe('runtime ternary operator', () => { + it('should handle subtraction in branches with function params', () => { + const myFn = tgpu.fn([d.u32, d.u32], d.u32)((b, w) => { + return (b > w) ? (b - w) : (w - b); + }); + + expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(` + "fn myFn(b: u32, w: u32) -> u32 { + return select((w - b), (b - w), (b > w)); + }" + `); + }); + + it('should handle const array indexing in branches', () => { + const RotLut2Gpu = tgpu.const(d.arrayOf(d.u32, 2), [10, 20]); + const RotLut3Gpu = tgpu.const(d.arrayOf(d.u32, 3), [30, 40, 50]); + + const myFn = tgpu.fn([d.u32, d.u32], d.u32)((r, bitU) => { + return (r === d.u32(2)) ? RotLut2Gpu.$[bitU] as number : RotLut3Gpu.$[bitU] as number; + }); + + expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(` + "const RotLut2Gpu: array = array(10u, 20u); + + const RotLut3Gpu: array = array(30u, 40u, 50u); + + fn myFn(r: u32, bitU: u32) -> u32 { + return select(RotLut3Gpu[bitU], RotLut2Gpu[bitU], (r == 2u)); + }" + `); + }); + + it('should handle nested runtime ternaries', () => { + const myFn = tgpu.fn([d.u32, d.u32, d.u32, d.u32], d.u32)((r, v1, v2, v3) => { + return (r === d.u32(1)) ? v1 : ((r === d.u32(2)) ? v2 : v3); + }); + + expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(` + "fn myFn(r: u32, v1: u32, v2: u32, v3: u32) -> u32 { + return select(select(v3, v2, (r == 2u)), v1, (r == 1u)); + }" + `); + }); + + it('should handle bit shift in branch with function param', () => { + const myFn = tgpu.fn([d.bool], d.u32)((isCustom) => { + return isCustom ? (d.u32(1) << d.u32(20)) : d.u32(0); + }); + + expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(` + "fn myFn(isCustom: bool) -> u32 { + return select(0u, (1u << 20u), isCustom); + }" + `); + }); + + it('should handle struct field access across ternaries', () => { + const Cw = d.struct({ + low: d.u32, + high: d.u32, + }); + + const myFn = tgpu.fn([d.bool, Cw, Cw], d.u32)((isCustom, customCw, stdCw) => { + return isCustom ? customCw.low : stdCw.low; + }); + + expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(` + "struct Cw { + low: u32, + high: u32, + } + + fn myFn(isCustom: bool, customCw: Cw, stdCw: Cw) -> u32 { + return select(stdCw.low, customCw.low, isCustom); + }" + `); + }); + + it('should handle buffer layout access in ternary branches', ({ root }) => { + const Cw = d.struct({ + low: d.u32, + high: d.u32, + }); + + const Layout = d.struct({ + codewords: d.arrayOf(Cw, 64), + }); + + const layout = root.createUniform(Layout, d.ref); + + const myFn = tgpu.fn([d.bool, d.u32], d.u32)((isCustom, cwIdx) => { + return isCustom ? layout.$.codewords[cwIdx]!.low : layout.$.codewords[cwIdx + d.u32(1)]!.low; + }); + + expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(` + "struct Cw { + low: u32, + high: u32, + } + + struct Layout { + codewords: array, + } + + @group(0) @binding(0) var layout_1: Layout; + + fn myFn(isCustom: bool, cwIdx: u32) -> u32 { + return select(layout_1.codewords[(cwIdx + 1u)].low, layout_1.codewords[cwIdx].low, isCustom); + }" + `); + }); + + it('should handle ternary with comparison and unary negation in branches', () => { + const myFn = tgpu.fn([d.u32], d.u32)((n) => { + return n > 0 ? n : -n; + }); + + expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(` + "fn myFn(n: u32) -> u32 { + return select(-(n), n, (n > 0u)); + }" + `); + }); + + it('should throw when a ternary branch contains an assignment', () => { + const myFn = tgpu.fn([d.i32], d.i32)((a) => { + let b = 0; + return a > 0 ? (b = a) : 0; + }); + + expect(() => tgpu.resolve([myFn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn:myFn: Ternary operator '(a > 0) ? (b = a) : 0' is invalid. For more complex branching, please use 'std.select' or if/else statements.] + `); + }); +}); From 59ebd23a199045834067149e8ed1e55dae5e27eb Mon Sep 17 00:00:00 2001 From: "pullfrog[bot]" <226033991+pullfrog[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 19:25:21 +0000 Subject: [PATCH 03/21] style: apply oxfmt formatting --- packages/typegpu/src/tgsl/wgslGenerator.ts | 4 +- .../typegpu/tests/tgsl/ternaryRuntime.test.ts | 48 ++++++++++++++----- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index d691a50870..77028a04fa 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -447,7 +447,9 @@ ${this.ctx.pre}}`; type, // Result of an operation, so not a reference to anything /* origin */ 'runtime', - exprType === NODE.assignmentExpr || lhsExpr.possibleSideEffects || rhsExpr.possibleSideEffects, + exprType === NODE.assignmentExpr || + lhsExpr.possibleSideEffects || + rhsExpr.possibleSideEffects, ); } diff --git a/packages/typegpu/tests/tgsl/ternaryRuntime.test.ts b/packages/typegpu/tests/tgsl/ternaryRuntime.test.ts index 34229ea73a..376a192f5f 100644 --- a/packages/typegpu/tests/tgsl/ternaryRuntime.test.ts +++ b/packages/typegpu/tests/tgsl/ternaryRuntime.test.ts @@ -4,8 +4,11 @@ import tgpu, { d } from '../../src/index.js'; describe('runtime ternary operator', () => { it('should handle subtraction in branches with function params', () => { - const myFn = tgpu.fn([d.u32, d.u32], d.u32)((b, w) => { - return (b > w) ? (b - w) : (w - b); + const myFn = tgpu.fn( + [d.u32, d.u32], + d.u32, + )((b, w) => { + return b > w ? b - w : w - b; }); expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(` @@ -19,8 +22,11 @@ describe('runtime ternary operator', () => { const RotLut2Gpu = tgpu.const(d.arrayOf(d.u32, 2), [10, 20]); const RotLut3Gpu = tgpu.const(d.arrayOf(d.u32, 3), [30, 40, 50]); - const myFn = tgpu.fn([d.u32, d.u32], d.u32)((r, bitU) => { - return (r === d.u32(2)) ? RotLut2Gpu.$[bitU] as number : RotLut3Gpu.$[bitU] as number; + const myFn = tgpu.fn( + [d.u32, d.u32], + d.u32, + )((r, bitU) => { + return r === d.u32(2) ? (RotLut2Gpu.$[bitU] as number) : (RotLut3Gpu.$[bitU] as number); }); expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(` @@ -35,8 +41,11 @@ describe('runtime ternary operator', () => { }); it('should handle nested runtime ternaries', () => { - const myFn = tgpu.fn([d.u32, d.u32, d.u32, d.u32], d.u32)((r, v1, v2, v3) => { - return (r === d.u32(1)) ? v1 : ((r === d.u32(2)) ? v2 : v3); + const myFn = tgpu.fn( + [d.u32, d.u32, d.u32, d.u32], + d.u32, + )((r, v1, v2, v3) => { + return r === d.u32(1) ? v1 : r === d.u32(2) ? v2 : v3; }); expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(` @@ -47,8 +56,11 @@ describe('runtime ternary operator', () => { }); it('should handle bit shift in branch with function param', () => { - const myFn = tgpu.fn([d.bool], d.u32)((isCustom) => { - return isCustom ? (d.u32(1) << d.u32(20)) : d.u32(0); + const myFn = tgpu.fn( + [d.bool], + d.u32, + )((isCustom) => { + return isCustom ? d.u32(1) << d.u32(20) : d.u32(0); }); expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(` @@ -64,7 +76,10 @@ describe('runtime ternary operator', () => { high: d.u32, }); - const myFn = tgpu.fn([d.bool, Cw, Cw], d.u32)((isCustom, customCw, stdCw) => { + const myFn = tgpu.fn( + [d.bool, Cw, Cw], + d.u32, + )((isCustom, customCw, stdCw) => { return isCustom ? customCw.low : stdCw.low; }); @@ -92,7 +107,10 @@ describe('runtime ternary operator', () => { const layout = root.createUniform(Layout, d.ref); - const myFn = tgpu.fn([d.bool, d.u32], d.u32)((isCustom, cwIdx) => { + const myFn = tgpu.fn( + [d.bool, d.u32], + d.u32, + )((isCustom, cwIdx) => { return isCustom ? layout.$.codewords[cwIdx]!.low : layout.$.codewords[cwIdx + d.u32(1)]!.low; }); @@ -115,7 +133,10 @@ describe('runtime ternary operator', () => { }); it('should handle ternary with comparison and unary negation in branches', () => { - const myFn = tgpu.fn([d.u32], d.u32)((n) => { + const myFn = tgpu.fn( + [d.u32], + d.u32, + )((n) => { return n > 0 ? n : -n; }); @@ -127,7 +148,10 @@ describe('runtime ternary operator', () => { }); it('should throw when a ternary branch contains an assignment', () => { - const myFn = tgpu.fn([d.i32], d.i32)((a) => { + const myFn = tgpu.fn( + [d.i32], + d.i32, + )((a) => { let b = 0; return a > 0 ? (b = a) : 0; }); From faee54533d77ad15db2a5fde49b9117e02f77b9d Mon Sep 17 00:00:00 2001 From: "pullfrog[bot]" <226033991+pullfrog[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 22:10:59 +0000 Subject: [PATCH 04/21] fix: add sideEffects prop to dualImpl for correct side-effect tracking --- packages/typegpu/src/core/function/dualImpl.ts | 8 +++++++- packages/typegpu/src/std/atomic.ts | 11 +++++++++++ packages/typegpu/src/std/texture.ts | 1 + 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/typegpu/src/core/function/dualImpl.ts b/packages/typegpu/src/core/function/dualImpl.ts index 029813ef60..c6cec0d9ff 100644 --- a/packages/typegpu/src/core/function/dualImpl.ts +++ b/packages/typegpu/src/core/function/dualImpl.ts @@ -29,6 +29,12 @@ interface DualImplOptions { */ readonly noComptime?: boolean | undefined; readonly ignoreImplicitCastWarning?: boolean | undefined; + /** + * Whether the function always has side effects. If `true`, the result always + * has `possibleSideEffects: true` regardless of argument side-effects. If + * `false` (default), the result has side effects only when any argument does. + */ + readonly sideEffects?: boolean | undefined; } export class MissingCpuImplError extends Error { @@ -108,7 +114,7 @@ export function dualImpl(options: DualImplOptions): DualFn a.possibleSideEffects)) { + if (!options.sideEffects && !args.some((a) => a.possibleSideEffects)) { return noSideEffects(result); } return result; diff --git a/packages/typegpu/src/std/atomic.ts b/packages/typegpu/src/std/atomic.ts index 59307693c0..0e7ec9d337 100644 --- a/packages/typegpu/src/std/atomic.ts +++ b/packages/typegpu/src/std/atomic.ts @@ -16,6 +16,7 @@ export const workgroupBarrier = dualImpl({ normalImpl: 'workgroupBarrier is a no-op outside of CODEGEN mode.', signature: { argTypes: [], returnType: Void }, codegenImpl: () => 'workgroupBarrier()', + sideEffects: true, }); export const storageBarrier = dualImpl({ @@ -23,6 +24,7 @@ export const storageBarrier = dualImpl({ normalImpl: 'storageBarrier is a no-op outside of CODEGEN mode.', signature: { argTypes: [], returnType: Void }, codegenImpl: () => 'storageBarrier()', + sideEffects: true, }); export const textureBarrier = dualImpl({ @@ -30,6 +32,7 @@ export const textureBarrier = dualImpl({ normalImpl: 'textureBarrier is a no-op outside of CODEGEN mode.', signature: { argTypes: [], returnType: Void }, codegenImpl: () => 'textureBarrier()', + sideEffects: true, }); const atomicNormalError = 'Atomic operations are not supported outside of CODEGEN mode.'; @@ -72,6 +75,7 @@ export const atomicStore = dualImpl<(a: T, value: number) = normalImpl: atomicNormalError, signature: atomicActionSignature, codegenImpl: (_ctx, [a, value]) => stitch`atomicStore(&${a}, ${value})`, + sideEffects: true, }); export const atomicAdd = dualImpl<(a: T, value: number) => number>({ @@ -79,6 +83,7 @@ export const atomicAdd = dualImpl<(a: T, value: number) => normalImpl: atomicNormalError, signature: atomicOpSignature, codegenImpl: (_ctx, [a, value]) => stitch`atomicAdd(&${a}, ${value})`, + sideEffects: true, }); export const atomicSub = dualImpl<(a: T, value: number) => number>({ @@ -86,6 +91,7 @@ export const atomicSub = dualImpl<(a: T, value: number) => normalImpl: atomicNormalError, signature: atomicOpSignature, codegenImpl: (_ctx, [a, value]) => stitch`atomicSub(&${a}, ${value})`, + sideEffects: true, }); export const atomicMax = dualImpl<(a: T, value: number) => number>({ @@ -93,6 +99,7 @@ export const atomicMax = dualImpl<(a: T, value: number) => normalImpl: atomicNormalError, signature: atomicOpSignature, codegenImpl: (_ctx, [a, value]) => stitch`atomicMax(&${a}, ${value})`, + sideEffects: true, }); export const atomicMin = dualImpl<(a: T, value: number) => number>({ @@ -100,6 +107,7 @@ export const atomicMin = dualImpl<(a: T, value: number) => normalImpl: atomicNormalError, signature: atomicOpSignature, codegenImpl: (_ctx, [a, value]) => stitch`atomicMin(&${a}, ${value})`, + sideEffects: true, }); export const atomicAnd = dualImpl<(a: T, value: number) => number>({ @@ -107,6 +115,7 @@ export const atomicAnd = dualImpl<(a: T, value: number) => normalImpl: atomicNormalError, signature: atomicOpSignature, codegenImpl: (_ctx, [a, value]) => stitch`atomicAnd(&${a}, ${value})`, + sideEffects: true, }); export const atomicOr = dualImpl<(a: T, value: number) => number>({ @@ -114,6 +123,7 @@ export const atomicOr = dualImpl<(a: T, value: number) => n normalImpl: atomicNormalError, signature: atomicOpSignature, codegenImpl: (_ctx, [a, value]) => stitch`atomicOr(&${a}, ${value})`, + sideEffects: true, }); export const atomicXor = dualImpl<(a: T, value: number) => number>({ @@ -121,4 +131,5 @@ export const atomicXor = dualImpl<(a: T, value: number) => normalImpl: atomicNormalError, signature: atomicOpSignature, codegenImpl: (_ctx, [a, value]) => stitch`atomicXor(&${a}, ${value})`, + sideEffects: true, }); diff --git a/packages/typegpu/src/std/texture.ts b/packages/typegpu/src/std/texture.ts index ec66b4d917..71c1a22f28 100644 --- a/packages/typegpu/src/std/texture.ts +++ b/packages/typegpu/src/std/texture.ts @@ -454,6 +454,7 @@ export const textureStore = dualImpl({ normalImpl: textureStoreCpu, codegenImpl: (_ctx, args) => stitch`textureStore(${args})`, signature: (...args) => ({ argTypes: args, returnType: Void }), + sideEffects: true, }); function textureDimensionsCpu(texture: T): number; From 5bc69206c47d9f5e9c22a5bad8a8be44e791cff8 Mon Sep 17 00:00:00 2001 From: "pullfrog[bot]" <226033991+pullfrog[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2026 09:38:14 +0000 Subject: [PATCH 05/21] make sideEffects required in DualImplOptions --- .../typegpu/src/core/function/dualImpl.ts | 4 +- packages/typegpu/src/core/function/tgpuFn.ts | 2 + packages/typegpu/src/data/matrix.ts | 5 ++ packages/typegpu/src/std/array.ts | 1 + packages/typegpu/src/std/atomic.ts | 1 + packages/typegpu/src/std/bitcast.ts | 2 + packages/typegpu/src/std/boolean.ts | 14 +++++ packages/typegpu/src/std/copy.ts | 1 + packages/typegpu/src/std/derivative.ts | 9 +++ packages/typegpu/src/std/discard.ts | 1 + packages/typegpu/src/std/matrix.ts | 5 ++ packages/typegpu/src/std/numeric.ts | 61 +++++++++++++++++++ packages/typegpu/src/std/operators.ts | 8 +++ packages/typegpu/src/std/packing.ts | 4 ++ packages/typegpu/src/std/subgroup.ts | 21 +++++++ packages/typegpu/src/std/texture.ts | 10 +++ .../typegpu/tests/internal/dualImpl.test.ts | 6 ++ 17 files changed, 153 insertions(+), 2 deletions(-) diff --git a/packages/typegpu/src/core/function/dualImpl.ts b/packages/typegpu/src/core/function/dualImpl.ts index c6cec0d9ff..d91ebec163 100644 --- a/packages/typegpu/src/core/function/dualImpl.ts +++ b/packages/typegpu/src/core/function/dualImpl.ts @@ -32,9 +32,9 @@ interface DualImplOptions { /** * Whether the function always has side effects. If `true`, the result always * has `possibleSideEffects: true` regardless of argument side-effects. If - * `false` (default), the result has side effects only when any argument does. + * `false`, the result has side effects only when any argument does. */ - readonly sideEffects?: boolean | undefined; + readonly sideEffects: boolean; } export class MissingCpuImplError extends Error { diff --git a/packages/typegpu/src/core/function/tgpuFn.ts b/packages/typegpu/src/core/function/tgpuFn.ts index d7e316e092..de297e19b1 100644 --- a/packages/typegpu/src/core/function/tgpuFn.ts +++ b/packages/typegpu/src/core/function/tgpuFn.ts @@ -238,6 +238,7 @@ function createFn( }), codegenImpl: (ctx, args) => ctx.withResetIndentLevel(() => stitch`${ctx.resolve(fn).value}(${args})`), + sideEffects: false, }); const fn = Object.assign(call, fnBase) as TgpuFn; @@ -297,6 +298,7 @@ function createBoundFunction( normalImpl: innerFn, codegenImpl: (ctx, args) => ctx.withResetIndentLevel(() => stitch`${ctx.resolve(fn).value}(${args})`), + sideEffects: false, }); const fn = Object.assign(call, fnBase) as TgpuFn; diff --git a/packages/typegpu/src/data/matrix.ts b/packages/typegpu/src/data/matrix.ts index b2fd9b3717..f7bb270a3d 100644 --- a/packages/typegpu/src/data/matrix.ts +++ b/packages/typegpu/src/data/matrix.ts @@ -588,6 +588,7 @@ export const translation4 = dualImpl({ }, codegenImpl: (_ctx, [v]) => stitch`mat4x4f(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, ${v}.x, ${v}.y, ${v}.z, 1)`, + sideEffects: false, }); /** @@ -610,6 +611,7 @@ export const scaling4 = dualImpl({ }, codegenImpl: (_ctx, [v]) => stitch`mat4x4f(${v}.x, 0, 0, 0, 0, ${v}.y, 0, 0, 0, 0, ${v}.z, 0, 0, 0, 0, 1)`, + sideEffects: false, }); /** @@ -632,6 +634,7 @@ export const rotationX4 = dualImpl({ }, codegenImpl: (_ctx, [a]) => stitch`mat4x4f(1, 0, 0, 0, 0, cos(${a}), sin(${a}), 0, 0, -sin(${a}), cos(${a}), 0, 0, 0, 0, 1)`, + sideEffects: false, }); /** @@ -654,6 +657,7 @@ export const rotationY4 = dualImpl({ }, codegenImpl: (_ctx, [a]) => stitch`mat4x4f(cos(${a}), 0, -sin(${a}), 0, 0, 1, 0, 0, sin(${a}), 0, cos(${a}), 0, 0, 0, 0, 1)`, + sideEffects: false, }); /** @@ -676,6 +680,7 @@ export const rotationZ4 = dualImpl({ }, codegenImpl: (_ctx, [a]) => stitch`mat4x4f(cos(${a}), sin(${a}), 0, 0, -sin(${a}), cos(${a}), 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)`, + sideEffects: false, }); // ---------- diff --git a/packages/typegpu/src/std/array.ts b/packages/typegpu/src/std/array.ts index f61b812d18..6fe62f6f55 100644 --- a/packages/typegpu/src/std/array.ts +++ b/packages/typegpu/src/std/array.ts @@ -22,4 +22,5 @@ export const arrayLength = dualImpl({ const length = sizeOfPointedToArray(a.dataType); return length > 0 ? `${length}` : stitch`arrayLength(${a})`; }, + sideEffects: false, }); diff --git a/packages/typegpu/src/std/atomic.ts b/packages/typegpu/src/std/atomic.ts index 0e7ec9d337..55a9513584 100644 --- a/packages/typegpu/src/std/atomic.ts +++ b/packages/typegpu/src/std/atomic.ts @@ -47,6 +47,7 @@ export const atomicLoad = dualImpl<(a: T) => number>({ return { argTypes: [a], returnType: a.inner }; }, codegenImpl: (_ctx, [a]) => stitch`atomicLoad(&${a})`, + sideEffects: false, }); const atomicActionSignature = (a: BaseData) => { diff --git a/packages/typegpu/src/std/bitcast.ts b/packages/typegpu/src/std/bitcast.ts index 4039858dd9..685d085710 100644 --- a/packages/typegpu/src/std/bitcast.ts +++ b/packages/typegpu/src/std/bitcast.ts @@ -35,6 +35,7 @@ export const bitcastU32toF32 = dualImpl({ : f32, }; }, + sideEffects: false, }); export type BitcastU32toI32Overload = ((value: number) => number) & @@ -64,4 +65,5 @@ export const bitcastU32toI32 = dualImpl({ : i32, }; }, + sideEffects: false, }); diff --git a/packages/typegpu/src/std/boolean.ts b/packages/typegpu/src/std/boolean.ts index fcb111bf3a..129d69fe66 100644 --- a/packages/typegpu/src/std/boolean.ts +++ b/packages/typegpu/src/std/boolean.ts @@ -65,6 +65,7 @@ export const allEq = dualImpl({ signature: (...argTypes) => ({ argTypes, returnType: bool }), normalImpl: (lhs: T, rhs: T) => cpuAll(cpuEq(lhs, rhs)), codegenImpl: (_ctx, [lhs, rhs]) => stitch`all(${lhs} == ${rhs})`, + sideEffects: false, }); const cpuEq = (lhs: T, rhs: T) => VectorOps.eq[lhs.kind](lhs, rhs); @@ -86,6 +87,7 @@ export const eq = dualImpl({ }), normalImpl: cpuEq, codegenImpl: (_ctx, [lhs, rhs]) => stitch`(${lhs} == ${rhs})`, + sideEffects: false, }); /** @@ -104,6 +106,7 @@ export const ne = dualImpl({ }), normalImpl: (lhs: T, rhs: T) => cpuNot(cpuEq(lhs, rhs)), codegenImpl: (_ctx, [lhs, rhs]) => stitch`(${lhs} != ${rhs})`, + sideEffects: false, }); const cpuLt = (lhs: T, rhs: T) => VectorOps.lt[lhs.kind](lhs, rhs); @@ -124,6 +127,7 @@ export const lt = dualImpl({ }), normalImpl: cpuLt, codegenImpl: (_ctx, [lhs, rhs]) => stitch`(${lhs} < ${rhs})`, + sideEffects: false, }); /** @@ -143,6 +147,7 @@ export const le = dualImpl({ normalImpl: (lhs: T, rhs: T) => cpuOr(cpuLt(lhs, rhs), cpuEq(lhs, rhs)), codegenImpl: (_ctx, [lhs, rhs]) => stitch`(${lhs} <= ${rhs})`, + sideEffects: false, }); /** @@ -162,6 +167,7 @@ export const gt = dualImpl({ normalImpl: (lhs: T, rhs: T) => cpuAnd(cpuNot(cpuLt(lhs, rhs)), cpuNot(cpuEq(lhs, rhs))), codegenImpl: (_ctx, [lhs, rhs]) => stitch`(${lhs} > ${rhs})`, + sideEffects: false, }); /** @@ -180,6 +186,7 @@ export const ge = dualImpl({ }), normalImpl: (lhs: T, rhs: T) => cpuNot(cpuLt(lhs, rhs)), codegenImpl: (_ctx, [lhs, rhs]) => stitch`(${lhs} >= ${rhs})`, + sideEffects: false, }); // logical ops @@ -260,6 +267,7 @@ export const not = dualImpl({ return 'false'; }, + sideEffects: false, }); const cpuOr = (lhs: T, rhs: T) => VectorOps.or[lhs.kind](lhs, rhs); @@ -275,6 +283,7 @@ export const or = dualImpl({ signature: (...argTypes) => ({ argTypes, returnType: argTypes[0] }), normalImpl: cpuOr, codegenImpl: (_ctx, [lhs, rhs]) => stitch`(${lhs} | ${rhs})`, + sideEffects: false, }); const cpuAnd = (lhs: T, rhs: T) => @@ -291,6 +300,7 @@ export const and = dualImpl({ signature: (...argTypes) => ({ argTypes, returnType: argTypes[0] }), normalImpl: cpuAnd, codegenImpl: (_ctx, [lhs, rhs]) => stitch`(${lhs} & ${rhs})`, + sideEffects: false, }); // logical aggregation @@ -308,6 +318,7 @@ export const all = dualImpl({ signature: (...argTypes) => ({ argTypes, returnType: bool }), normalImpl: cpuAll, codegenImpl: (_ctx, [value]) => stitch`all(${value})`, + sideEffects: false, }); /** @@ -321,6 +332,7 @@ export const any = dualImpl({ signature: (...argTypes) => ({ argTypes, returnType: bool }), normalImpl: (value: AnyBooleanVecInstance) => !cpuAll(cpuNot(value)), codegenImpl: (_ctx, [arg]) => stitch`any(${arg})`, + sideEffects: false, }); // other @@ -366,6 +378,7 @@ export const isCloseTo = dualImpl({ } return 'false'; }, + sideEffects: false, }); function cpuSelect(f: boolean, t: boolean, cond: boolean): boolean; @@ -448,4 +461,5 @@ export const select = dualImpl({ } return result; }, + sideEffects: false, }); diff --git a/packages/typegpu/src/std/copy.ts b/packages/typegpu/src/std/copy.ts index 348904d077..a3adc3eae2 100644 --- a/packages/typegpu/src/std/copy.ts +++ b/packages/typegpu/src/std/copy.ts @@ -31,4 +31,5 @@ export const copy = dualImpl({ codegenImpl(_ctx, [a]) { return stitch`${a}`; }, + sideEffects: false, }); diff --git a/packages/typegpu/src/std/derivative.ts b/packages/typegpu/src/std/derivative.ts index 8c8952b623..d57ba06491 100644 --- a/packages/typegpu/src/std/derivative.ts +++ b/packages/typegpu/src/std/derivative.ts @@ -12,6 +12,7 @@ export const dpdx = dualImpl({ normalImpl: derivativeNormalError, signature: (value) => ({ argTypes: [value], returnType: value }), codegenImpl: (_ctx, [value]) => stitch`dpdx(${value})`, + sideEffects: false, }); export const dpdxCoarse = dualImpl({ @@ -19,6 +20,7 @@ export const dpdxCoarse = dualImpl({ normalImpl: derivativeNormalError, signature: (value) => ({ argTypes: [value], returnType: value }), codegenImpl: (_ctx, [value]) => stitch`dpdxCoarse(${value})`, + sideEffects: false, }); export const dpdxFine = dualImpl({ @@ -26,6 +28,7 @@ export const dpdxFine = dualImpl({ normalImpl: derivativeNormalError, signature: (value) => ({ argTypes: [value], returnType: value }), codegenImpl: (_ctx, [value]) => stitch`dpdxFine(${value})`, + sideEffects: false, }); export const dpdy = dualImpl({ @@ -33,6 +36,7 @@ export const dpdy = dualImpl({ normalImpl: derivativeNormalError, signature: (value) => ({ argTypes: [value], returnType: value }), codegenImpl: (_ctx, [value]) => stitch`dpdy(${value})`, + sideEffects: false, }); export const dpdyCoarse = dualImpl({ @@ -40,6 +44,7 @@ export const dpdyCoarse = dualImpl({ normalImpl: derivativeNormalError, signature: (value) => ({ argTypes: [value], returnType: value }), codegenImpl: (_ctx, [value]) => stitch`dpdyCoarse(${value})`, + sideEffects: false, }); export const dpdyFine = dualImpl({ @@ -47,6 +52,7 @@ export const dpdyFine = dualImpl({ normalImpl: derivativeNormalError, signature: (value) => ({ argTypes: [value], returnType: value }), codegenImpl: (_ctx, [value]) => stitch`dpdyFine(${value})`, + sideEffects: false, }); export const fwidth = dualImpl({ @@ -54,6 +60,7 @@ export const fwidth = dualImpl({ normalImpl: derivativeNormalError, signature: (value) => ({ argTypes: [value], returnType: value }), codegenImpl: (_ctx, [value]) => stitch`fwidth(${value})`, + sideEffects: false, }); export const fwidthCoarse = dualImpl({ @@ -61,6 +68,7 @@ export const fwidthCoarse = dualImpl({ normalImpl: derivativeNormalError, signature: (value) => ({ argTypes: [value], returnType: value }), codegenImpl: (_ctx, [value]) => stitch`fwidthCoarse(${value})`, + sideEffects: false, }); export const fwidthFine = dualImpl({ @@ -68,4 +76,5 @@ export const fwidthFine = dualImpl({ normalImpl: derivativeNormalError, signature: (value) => ({ argTypes: [value], returnType: value }), codegenImpl: (_ctx, [value]) => stitch`fwidthFine(${value})`, + sideEffects: false, }); diff --git a/packages/typegpu/src/std/discard.ts b/packages/typegpu/src/std/discard.ts index 4c7339a8ac..1bf676d9f4 100644 --- a/packages/typegpu/src/std/discard.ts +++ b/packages/typegpu/src/std/discard.ts @@ -6,4 +6,5 @@ export const discard = dualImpl<() => never>({ normalImpl: '`discard` relies on GPU resources and cannot be executed outside of a draw call', signature: { argTypes: [], returnType: Void }, codegenImpl: () => 'discard;', + sideEffects: false, }); diff --git a/packages/typegpu/src/std/matrix.ts b/packages/typegpu/src/std/matrix.ts index b761c1c157..63ae19bb43 100644 --- a/packages/typegpu/src/std/matrix.ts +++ b/packages/typegpu/src/std/matrix.ts @@ -31,6 +31,7 @@ export const translate4 = dualImpl({ normalImpl: (matrix: m4x4f, vector: v3f) => mul(translation4(vector), matrix), signature: { argTypes: [mat4x4f, vec3f], returnType: mat4x4f }, codegenImpl: (ctx, [matrix, vector]) => stitch`(${gpuTranslation4(ctx, [vector])} * ${matrix})`, + sideEffects: false, }); /** @@ -44,6 +45,7 @@ export const scale4 = dualImpl({ normalImpl: (matrix: m4x4f, vector: v3f) => mul(scaling4(vector), matrix), signature: { argTypes: [mat4x4f, vec3f], returnType: mat4x4f }, codegenImpl: (ctx, [matrix, vector]) => stitch`(${gpuScaling4(ctx, [vector])} * ${matrix})`, + sideEffects: false, }); const rotateSignature = { argTypes: [mat4x4f, f32], returnType: mat4x4f }; @@ -59,6 +61,7 @@ export const rotateX4 = dualImpl({ normalImpl: (matrix: m4x4f, angle: number) => mul(rotationX4(angle), matrix), signature: rotateSignature, codegenImpl: (ctx, [matrix, angle]) => stitch`(${gpuRotationX4(ctx, [angle])} * ${matrix})`, + sideEffects: false, }); /** @@ -72,6 +75,7 @@ export const rotateY4 = dualImpl({ normalImpl: (matrix: m4x4f, angle: number) => mul(rotationY4(angle), matrix), signature: rotateSignature, codegenImpl: (ctx, [matrix, angle]) => stitch`(${gpuRotationY4(ctx, [angle])} * ${matrix})`, + sideEffects: false, }); /** @@ -85,4 +89,5 @@ export const rotateZ4 = dualImpl({ normalImpl: (matrix: m4x4f, angle: number) => mul(rotationZ4(angle), matrix), signature: rotateSignature, codegenImpl: (ctx, [matrix, angle]) => stitch`(${gpuRotationZ4(ctx, [angle])} * ${matrix})`, + sideEffects: false, }); diff --git a/packages/typegpu/src/std/numeric.ts b/packages/typegpu/src/std/numeric.ts index ed60310628..97c598ca73 100644 --- a/packages/typegpu/src/std/numeric.ts +++ b/packages/typegpu/src/std/numeric.ts @@ -123,6 +123,7 @@ export const abs = dualImpl({ signature: unaryIdentitySignature, normalImpl: cpuAbs, codegenImpl: (_ctx, [value]) => stitch`abs(${value})`, + sideEffects: false, }); function cpuAcos(value: number): number; @@ -139,6 +140,7 @@ export const acos = dualImpl({ signature: unifyRestrictedSignature(anyFloat), normalImpl: cpuAcos, codegenImpl: (_ctx, [value]) => stitch`acos(${value})`, + sideEffects: false, }); function cpuAcosh(value: number): number; @@ -155,6 +157,7 @@ export const acosh = dualImpl({ signature: unifyRestrictedSignature(anyFloat), normalImpl: cpuAcosh, codegenImpl: (_ctx, [value]) => stitch`acosh(${value})`, + sideEffects: false, }); function cpuAsin(value: number): number; @@ -171,6 +174,7 @@ export const asin = dualImpl({ signature: unifyRestrictedSignature(anyFloat), normalImpl: cpuAsin, codegenImpl: (_ctx, [value]) => stitch`asin(${value})`, + sideEffects: false, }); function cpuAsinh(value: number): number; @@ -187,6 +191,7 @@ export const asinh = dualImpl({ signature: unifyRestrictedSignature(anyFloat), normalImpl: cpuAsinh, codegenImpl: (_ctx, [value]) => stitch`asinh(${value})`, + sideEffects: false, }); function cpuAtan(value: number): number; @@ -203,6 +208,7 @@ export const atan = dualImpl({ signature: unifyRestrictedSignature(anyFloat), normalImpl: cpuAtan, codegenImpl: (_ctx, [value]) => stitch`atan(${value})`, + sideEffects: false, }); function cpuAtanh(value: number): number; @@ -219,6 +225,7 @@ export const atanh = dualImpl({ signature: unifyRestrictedSignature(anyFloat), normalImpl: cpuAtanh, codegenImpl: (_ctx, [value]) => stitch`atanh(${value})`, + sideEffects: false, }); function cpuAtan2(y: number, x: number): number; @@ -235,6 +242,7 @@ export const atan2 = dualImpl({ signature: unifyRestrictedSignature(anyFloat), normalImpl: cpuAtan2, codegenImpl: (_ctx, [y, x]) => stitch`atan2(${y}, ${x})`, + sideEffects: false, }); function cpuCeil(value: number): number; @@ -251,6 +259,7 @@ export const ceil = dualImpl({ signature: unifyRestrictedSignature(anyFloat), normalImpl: cpuCeil, codegenImpl: (_ctx, [value]) => stitch`ceil(${value})`, + sideEffects: false, }); function cpuClamp(value: number, low: number, high: number): number; @@ -267,6 +276,7 @@ export const clamp = dualImpl({ signature: variadicUnifySignature, normalImpl: cpuClamp, codegenImpl: (_ctx, [value, low, high]) => stitch`clamp(${value}, ${low}, ${high})`, + sideEffects: false, }); function cpuCos(value: number): number; @@ -283,6 +293,7 @@ export const cos = dualImpl({ signature: unifyRestrictedSignature(anyFloat), normalImpl: cpuCos, codegenImpl: (_ctx, [value]) => stitch`cos(${value})`, + sideEffects: false, }); function cpuCosh(value: number): number; @@ -299,6 +310,7 @@ export const cosh = dualImpl({ signature: unifyRestrictedSignature(anyFloat), normalImpl: cpuCosh, codegenImpl: (_ctx, [value]) => stitch`cosh(${value})`, + sideEffects: false, }); function cpuCountLeadingZeros(value: number): number; @@ -313,6 +325,7 @@ export const countLeadingZeros = dualImpl({ normalImpl: 'CPU implementation for countLeadingZeros not implemented yet. Please submit an issue at https://github.com/software-mansion/TypeGPU/issues', codegenImpl: (_ctx, [value]) => stitch`countLeadingZeros(${value})`, + sideEffects: false, }); function cpuCountOneBits(value: number): number; @@ -327,6 +340,7 @@ export const countOneBits = dualImpl({ normalImpl: 'CPU implementation for countOneBits not implemented yet. Please submit an issue at https://github.com/software-mansion/TypeGPU/issues', codegenImpl: (_ctx, [value]) => stitch`countOneBits(${value})`, + sideEffects: false, }); function cpuCountTrailingZeros(value: number): number; @@ -341,6 +355,7 @@ export const countTrailingZeros = dualImpl({ normalImpl: 'CPU implementation for countTrailingZeros not implemented yet. Please submit an issue at https://github.com/software-mansion/TypeGPU/issues', codegenImpl: (_ctx, [value]) => stitch`countTrailingZeros(${value})`, + sideEffects: false, }); export const cross = dualImpl({ @@ -348,6 +363,7 @@ export const cross = dualImpl({ signature: unifyRestrictedSignature([vec3f, vec3h]), normalImpl: (a: T, b: T): T => VectorOps.cross[a.kind](a, b), codegenImpl: (_ctx, [a, b]) => stitch`cross(${a}, ${b})`, + sideEffects: false, }); function cpuDegrees(value: number): number; @@ -366,6 +382,7 @@ export const degrees = dualImpl({ signature: unifyRestrictedSignature(anyFloat), normalImpl: cpuDegrees, codegenImpl: (_ctx, [value]) => stitch`degrees(${value})`, + sideEffects: false, }); export const determinant = dualImpl<(value: AnyMatInstance) => number>({ @@ -379,6 +396,7 @@ export const determinant = dualImpl<(value: AnyMatInstance) => number>({ normalImpl: 'CPU implementation for determinant not implemented yet. Please submit an issue at https://github.com/software-mansion/TypeGPU/issues', codegenImpl: (_ctx, [value]) => stitch`determinant(${value})`, + sideEffects: false, }); function cpuDistance(a: number, b: number): number; @@ -404,6 +422,7 @@ export const distance = dualImpl({ }, normalImpl: cpuDistance, codegenImpl: (_ctx, [a, b]) => stitch`distance(${a}, ${b})`, + sideEffects: false, }); export const dot = dualImpl({ @@ -414,6 +433,7 @@ export const dot = dualImpl({ }), normalImpl: (lhs: T, rhs: T): number => VectorOps.dot[lhs.kind](lhs, rhs), codegenImpl: (_ctx, [lhs, rhs]) => stitch`dot(${lhs}, ${rhs})`, + sideEffects: false, }); export const dot4U8Packed = dualImpl<(e1: number, e2: number) => number>({ @@ -422,6 +442,7 @@ export const dot4U8Packed = dualImpl<(e1: number, e2: number) => number>({ normalImpl: 'CPU implementation for dot4U8Packed not implemented yet. Please submit an issue at https://github.com/software-mansion/TypeGPU/issues', codegenImpl: (_ctx, [e1, e2]) => stitch`dot4U8Packed(${e1}, ${e2})`, + sideEffects: false, }); export const dot4I8Packed = dualImpl<(e1: number, e2: number) => number>({ @@ -430,6 +451,7 @@ export const dot4I8Packed = dualImpl<(e1: number, e2: number) => number>({ normalImpl: 'CPU implementation for dot4I8Packed not implemented yet. Please submit an issue at https://github.com/software-mansion/TypeGPU/issues', codegenImpl: (_ctx, [e1, e2]) => stitch`dot4I8Packed(${e1}, ${e2})`, + sideEffects: false, }); function cpuExp(value: number): number; @@ -446,6 +468,7 @@ export const exp = dualImpl({ signature: unifyRestrictedSignature(anyFloat), normalImpl: cpuExp, codegenImpl: (_ctx, [value]) => stitch`exp(${value})`, + sideEffects: false, }); function cpuExp2(value: number): number; @@ -462,6 +485,7 @@ export const exp2 = dualImpl({ signature: unifyRestrictedSignature(anyFloat), normalImpl: cpuExp2, codegenImpl: (_ctx, [value]) => stitch`exp2(${value})`, + sideEffects: false, }); function cpuExtractBits(e: number, offset: number, count: number): number; @@ -489,6 +513,7 @@ export const extractBits = dualImpl({ normalImpl: 'CPU implementation for extractBits not implemented yet. Please submit an issue at https://github.com/software-mansion/TypeGPU/issues', codegenImpl: (_ctx, [e, offset, count]) => stitch`extractBits(${e}, ${offset}, ${count})`, + sideEffects: false, }); export const faceForward = dualImpl<(e1: T, e2: T, e3: T) => T>({ @@ -497,6 +522,7 @@ export const faceForward = dualImpl<(e1: T, e2: T normalImpl: 'CPU implementation for faceForward not implemented yet. Please submit an issue at https://github.com/software-mansion/TypeGPU/issues', codegenImpl: (_ctx, [e1, e2, e3]) => stitch`faceForward(${e1}, ${e2}, ${e3})`, + sideEffects: false, }); function cpuFirstLeadingBit(value: number): number; @@ -511,6 +537,7 @@ export const firstLeadingBit = dualImpl({ normalImpl: 'CPU implementation for firstLeadingBit not implemented yet. Please submit an issue at https://github.com/software-mansion/TypeGPU/issues', codegenImpl: (_ctx, [value]) => stitch`firstLeadingBit(${value})`, + sideEffects: false, }); function cpuFirstTrailingBit(value: number): number; @@ -525,6 +552,7 @@ export const firstTrailingBit = dualImpl({ normalImpl: 'CPU implementation for firstTrailingBit not implemented yet. Please submit an issue at https://github.com/software-mansion/TypeGPU/issues', codegenImpl: (_ctx, [value]) => stitch`firstTrailingBit(${value})`, + sideEffects: false, }); function cpuFloor(value: number): number; @@ -541,6 +569,7 @@ export const floor = dualImpl({ signature: unifyRestrictedSignature(anyFloat), normalImpl: cpuFloor, codegenImpl: (_ctx, [arg]) => stitch`floor(${arg})`, + sideEffects: false, }); function cpuFma(e1: number, e2: number, e3: number): number; @@ -559,6 +588,7 @@ export const fma = dualImpl({ signature: unifyRestrictedSignature(anyFloat), normalImpl: cpuFma, codegenImpl: (_ctx, [e1, e2, e3]) => stitch`fma(${e1}, ${e2}, ${e3})`, + sideEffects: false, }); function cpuFract(value: number): number; @@ -575,6 +605,7 @@ export const fract = dualImpl({ signature: unifyRestrictedSignature(anyFloat), normalImpl: cpuFract, codegenImpl: (_ctx, [a]) => stitch`fract(${a})`, + sideEffects: false, }); const FrexpResults = { @@ -608,6 +639,7 @@ export const frexp = dualImpl({ return { argTypes: [value], returnType }; }, codegenImpl: (_ctx, [value]) => stitch`frexp(${value})`, + sideEffects: false, }); function cpuInsertBits(e: number, newbits: number, offset: number, count: number): number; @@ -642,6 +674,7 @@ export const insertBits = dualImpl({ 'CPU implementation for insertBits not implemented yet. Please submit an issue at https://github.com/software-mansion/TypeGPU/issues', codegenImpl: (_ctx, [e, newbits, offset, count]) => stitch`insertBits(${e}, ${newbits}, ${offset}, ${count})`, + sideEffects: false, }); function cpuInverseSqrt(value: number): number; @@ -660,6 +693,7 @@ export const inverseSqrt = dualImpl({ signature: unifyRestrictedSignature(anyFloat), normalImpl: cpuInverseSqrt, codegenImpl: (_ctx, [value]) => stitch`inverseSqrt(${value})`, + sideEffects: false, }); function cpuLdexp(e1: number, e2: number): number; @@ -700,6 +734,7 @@ export const ldexp = dualImpl({ normalImpl: 'CPU implementation for ldexp not implemented yet. Please submit an issue at https://github.com/software-mansion/TypeGPU/issues', codegenImpl: (_ctx, [e1, e2]) => stitch`ldexp(${e1}, ${e2})`, + sideEffects: false, }); function cpuLength(value: number): number; @@ -725,6 +760,7 @@ export const length = dualImpl({ }, normalImpl: cpuLength, codegenImpl: (_ctx, [arg]) => stitch`length(${arg})`, + sideEffects: false, }); function cpuLog(value: number): number; @@ -741,6 +777,7 @@ export const log = dualImpl({ signature: unifyRestrictedSignature(anyFloat), normalImpl: cpuLog, codegenImpl: (_ctx, [value]) => stitch`log(${value})`, + sideEffects: false, }); function cpuLog2(value: number): number; @@ -757,6 +794,7 @@ export const log2 = dualImpl({ signature: unifyRestrictedSignature(anyFloat), normalImpl: cpuLog2, codegenImpl: (_ctx, [value]) => stitch`log2(${value})`, + sideEffects: false, }); function cpuMax(a: number, b: number): number; @@ -778,6 +816,7 @@ export const max = dualImpl({ signature: variadicUnifySignature, normalImpl: variadicReduce(cpuMax) as VariadicOverload, codegenImpl: variadicStitch('max'), + sideEffects: false, }); function cpuMin(a: number, b: number): number; @@ -794,6 +833,7 @@ export const min = dualImpl({ signature: variadicUnifySignature, normalImpl: variadicReduce(cpuMin) as VariadicOverload, codegenImpl: variadicStitch('min'), + sideEffects: false, }); function cpuMix(e1: number, e2: number, e3: number): number; @@ -832,6 +872,7 @@ export const mix = dualImpl({ }, normalImpl: cpuMix, codegenImpl: (_ctx, [e1, e2, e3]) => stitch`mix(${e1}, ${e2}, ${e3})`, + sideEffects: false, }); const ModfResult = { @@ -874,6 +915,7 @@ export const modf: ModfOverload = dualImpl({ normalImpl: 'CPU implementation for modf not implemented yet. Please submit an issue at https://github.com/software-mansion/TypeGPU/issues', codegenImpl: (_ctx, [value]) => stitch`modf(${value})`, + sideEffects: false, }); export const normalize = dualImpl({ @@ -881,6 +923,7 @@ export const normalize = dualImpl({ signature: unifyRestrictedSignature(anyFloatVec), normalImpl: (v: T): T => VectorOps.normalize[v.kind](v), codegenImpl: (_ctx, [value]) => stitch`normalize(${value})`, + sideEffects: false, }); function powCpu(base: number, exponent: number): number; @@ -900,6 +943,7 @@ export const pow = dualImpl({ signature: unifyRestrictedSignature(anyFloat), normalImpl: powCpu, codegenImpl: (_ctx, [lhs, rhs]) => stitch`pow(${lhs}, ${rhs})`, + sideEffects: false, }); function cpuQuantizeToF16(value: number): number; function cpuQuantizeToF16(value: T): T; @@ -920,6 +964,7 @@ export const quantizeToF16 = dualImpl({ normalImpl: 'CPU implementation for quantizeToF16 not implemented yet. Please submit an issue at https://github.com/software-mansion/TypeGPU/issues', codegenImpl: (_ctx, [value]) => stitch`quantizeToF16(${value})`, + sideEffects: false, }); function cpuRadians(value: number): number; @@ -938,6 +983,7 @@ export const radians = dualImpl({ signature: unifyRestrictedSignature(anyFloat), normalImpl: cpuRadians, codegenImpl: (_ctx, [value]) => stitch`radians(${value})`, + sideEffects: false, }); export const reflect = dualImpl({ @@ -954,6 +1000,7 @@ export const reflect = dualImpl({ }, normalImpl: (e1: T, e2: T): T => sub(e1, mul(2 * dot(e2, e1), e2)), codegenImpl: (_ctx, [e1, e2]) => stitch`reflect(${e1}, ${e2})`, + sideEffects: false, }); export const refract = dualImpl<(e1: T, e2: T, e3: number) => T>({ @@ -965,6 +1012,7 @@ export const refract = dualImpl<(e1: T, e2: T, e3 argTypes: [e1, e2, isHalfPrecisionSchema(e1) ? f16 : f32], returnType: e1, }), + sideEffects: false, }); function cpuReverseBits(value: number): number; function cpuReverseBits(value: T): T; @@ -978,6 +1026,7 @@ export const reverseBits = dualImpl({ normalImpl: 'CPU implementation for reverseBits not implemented yet. Please submit an issue at https://github.com/software-mansion/TypeGPU/issues', codegenImpl: (_ctx, [value]) => stitch`reverseBits(${value})`, + sideEffects: false, }); function cpuRound(value: number): number; @@ -1003,6 +1052,7 @@ export const round = dualImpl({ signature: unifyRestrictedSignature(anyFloat), normalImpl: cpuRound, codegenImpl: (_ctx, [value]) => stitch`round(${value})`, + sideEffects: false, }); function cpuSaturate(value: number): number; @@ -1021,6 +1071,7 @@ export const saturate = dualImpl({ signature: unifyRestrictedSignature(anyFloat), normalImpl: cpuSaturate, codegenImpl: (_ctx, [value]) => stitch`saturate(${value})`, + sideEffects: false, }); function cpuSign(e: number): number; @@ -1044,6 +1095,7 @@ export const sign = dualImpl({ }, normalImpl: cpuSign, codegenImpl: (_ctx, [e]) => stitch`sign(${e})`, + sideEffects: false, }); function cpuSin(value: number): number; @@ -1060,6 +1112,7 @@ export const sin = dualImpl({ signature: unifyRestrictedSignature(anyFloat), normalImpl: cpuSin, codegenImpl: (_ctx, [value]) => stitch`sin(${value})`, + sideEffects: false, }); function cpuSinh(value: number): number; @@ -1078,6 +1131,7 @@ export const sinh = dualImpl({ signature: unifyRestrictedSignature(anyFloat), normalImpl: cpuSinh, codegenImpl: (_ctx, [value]) => stitch`sinh(${value})`, + sideEffects: false, }); function cpuSmoothstep(edge0: number, edge1: number, x: number): number; @@ -1098,6 +1152,7 @@ export const smoothstep = dualImpl({ signature: unifyRestrictedSignature(anyFloat), normalImpl: cpuSmoothstep, codegenImpl: (_ctx, [edge0, edge1, x]) => stitch`smoothstep(${edge0}, ${edge1}, ${x})`, + sideEffects: false, }); function cpuSqrt(value: number): number; @@ -1114,6 +1169,7 @@ export const sqrt = dualImpl({ signature: unifyRestrictedSignature(anyFloat), normalImpl: cpuSqrt, codegenImpl: (_ctx, [value]) => stitch`sqrt(${value})`, + sideEffects: false, }); function cpuStep(edge: number, x: number): number; @@ -1132,6 +1188,7 @@ export const step = dualImpl({ signature: unifyRestrictedSignature(anyFloat), normalImpl: cpuStep, codegenImpl: (_ctx, [edge, x]) => stitch`step(${edge}, ${x})`, + sideEffects: false, }); function cpuTan(value: number): number; @@ -1150,6 +1207,7 @@ export const tan = dualImpl({ signature: unifyRestrictedSignature(anyFloat), normalImpl: cpuTan, codegenImpl: (_ctx, [value]) => stitch`tan(${value})`, + sideEffects: false, }); function cpuTanh(value: number): number; @@ -1166,6 +1224,7 @@ export const tanh = dualImpl({ signature: unifyRestrictedSignature(anyFloat), normalImpl: cpuTanh, codegenImpl: (_ctx, [value]) => stitch`tanh(${value})`, + sideEffects: false, }); export const transpose = dualImpl<(e: T) => T>({ @@ -1174,6 +1233,7 @@ export const transpose = dualImpl<(e: T) => T>({ normalImpl: 'CPU implementation for transpose not implemented yet. Please submit an issue at https://github.com/software-mansion/TypeGPU/issues', codegenImpl: (_ctx, [e]) => stitch`transpose(${e})`, + sideEffects: false, }); function cpuTrunc(value: number): number; @@ -1188,4 +1248,5 @@ export const trunc = dualImpl({ normalImpl: 'CPU implementation for trunc not implemented yet. Please submit an issue at https://github.com/software-mansion/TypeGPU/issues', codegenImpl: (_ctx, [value]) => stitch`trunc(${value})`, + sideEffects: false, }); diff --git a/packages/typegpu/src/std/operators.ts b/packages/typegpu/src/std/operators.ts index 6052e25957..4a95e690af 100644 --- a/packages/typegpu/src/std/operators.ts +++ b/packages/typegpu/src/std/operators.ts @@ -117,6 +117,7 @@ export const add = dualImpl({ signature: binaryArithmeticSignature, normalImpl: cpuAdd, codegenImpl: (_ctx, [lhs, rhs]) => stitch`(${lhs} + ${rhs})`, + sideEffects: false, }); function cpuSub(lhs: number, rhs: number): number; // default subtraction @@ -144,6 +145,7 @@ export const sub = dualImpl({ signature: binaryArithmeticSignature, normalImpl: cpuSub, codegenImpl: (_ctx, [lhs, rhs]) => stitch`(${lhs} - ${rhs})`, + sideEffects: false, }); function cpuMul(lhs: number, rhs: number): number; // default multiplication @@ -195,6 +197,7 @@ export const mul = dualImpl({ signature: binaryMulSignature, normalImpl: cpuMul, codegenImpl: (_ctx, [lhs, rhs]) => stitch`(${lhs} * ${rhs})`, + sideEffects: false, }); function cpuDiv(lhs: number, rhs: number): number; // default js division @@ -225,6 +228,7 @@ export const div = dualImpl({ normalImpl: cpuDiv, codegenImpl: (_ctx, [lhs, rhs]) => stitch`(${lhs} / ${rhs})`, ignoreImplicitCastWarning: true, + sideEffects: false, }); type ModOverload = { @@ -263,6 +267,7 @@ export const mod = dualImpl({ throw new Error('Mod called with invalid arguments, expected types: number or vector.'); }) as ModOverload, codegenImpl: (_ctx, [lhs, rhs]) => stitch`(${lhs} % ${rhs})`, + sideEffects: false, }); function cpuNeg(value: number): number; @@ -282,6 +287,7 @@ export const neg = dualImpl({ }), normalImpl: cpuNeg, codegenImpl: (_ctx, [arg]) => stitch`-(${arg})`, + sideEffects: false, }); const anyConcreteInteger = [i32, u32, vec2i, vec3i, vec4i, vec2u, vec3u, vec4u] as BaseData[]; @@ -354,6 +360,7 @@ export const bitShiftLeft = dualImpl({ } return stitch`(${lhs} << ${rhs})`; }, + sideEffects: false, }); function cpuBitShiftRight(lhs: number, rhs: number): number; @@ -390,4 +397,5 @@ export const bitShiftRight = dualImpl({ } return stitch`(${lhs} >> ${rhs})`; }, + sideEffects: false, }); diff --git a/packages/typegpu/src/std/packing.ts b/packages/typegpu/src/std/packing.ts index 6bb163fb4d..f4f990e951 100644 --- a/packages/typegpu/src/std/packing.ts +++ b/packages/typegpu/src/std/packing.ts @@ -20,6 +20,7 @@ export const unpack2x16float = dualImpl({ }, signature: { argTypes: [u32], returnType: vec2f }, codegenImpl: (_ctx, [e]) => stitch`unpack2x16float(${e})`, + sideEffects: false, }); /** @@ -38,6 +39,7 @@ export const pack2x16float = dualImpl({ }, signature: { argTypes: [vec2f], returnType: u32 }, codegenImpl: (_ctx, [e]) => stitch`pack2x16float(${e})`, + sideEffects: false, }); /** @@ -60,6 +62,7 @@ export const unpack4x8unorm = dualImpl({ }, signature: { argTypes: [u32], returnType: vec4f }, codegenImpl: (_ctx, [e]) => stitch`unpack4x8unorm(${e})`, + sideEffects: false, }); /** @@ -80,4 +83,5 @@ export const pack4x8unorm = dualImpl({ }, signature: { argTypes: [vec4f], returnType: u32 }, codegenImpl: (_ctx, [e]) => stitch`pack4x8unorm(${e})`, + sideEffects: false, }); diff --git a/packages/typegpu/src/std/subgroup.ts b/packages/typegpu/src/std/subgroup.ts index f886490b07..83cb213944 100644 --- a/packages/typegpu/src/std/subgroup.ts +++ b/packages/typegpu/src/std/subgroup.ts @@ -42,6 +42,7 @@ export const subgroupAdd = dualImpl({ signature: (arg) => ({ argTypes: [arg], returnType: arg }), normalImpl: errorMessage, codegenImpl: (_ctx, [arg]) => stitch`subgroupAdd(${arg})`, + sideEffects: false, }); export const subgroupExclusiveAdd = dualImpl({ @@ -49,6 +50,7 @@ export const subgroupExclusiveAdd = dualImpl({ signature: (arg) => ({ argTypes: [arg], returnType: arg }), normalImpl: errorMessage, codegenImpl: (_ctx, [arg]) => stitch`subgroupExclusiveAdd(${arg})`, + sideEffects: false, }); export const subgroupInclusiveAdd = dualImpl({ @@ -56,6 +58,7 @@ export const subgroupInclusiveAdd = dualImpl({ signature: (arg) => ({ argTypes: [arg], returnType: arg }), normalImpl: errorMessage, codegenImpl: (_ctx, [arg]) => stitch`subgroupInclusiveAdd(${arg})`, + sideEffects: false, }); export const subgroupAll = dualImpl<(e: boolean) => boolean>({ @@ -63,6 +66,7 @@ export const subgroupAll = dualImpl<(e: boolean) => boolean>({ signature: { argTypes: [bool], returnType: bool }, normalImpl: errorMessage, codegenImpl: (_ctx, [e]) => stitch`subgroupAll(${e})`, + sideEffects: false, }); export const subgroupAnd = dualImpl({ @@ -70,6 +74,7 @@ export const subgroupAnd = dualImpl({ signature: (arg) => ({ argTypes: [arg], returnType: arg }), normalImpl: errorMessage, codegenImpl: (_ctx, [e]) => stitch`subgroupAnd(${e})`, + sideEffects: false, }); export const subgroupAny = dualImpl<(e: boolean) => boolean>({ @@ -77,6 +82,7 @@ export const subgroupAny = dualImpl<(e: boolean) => boolean>({ signature: { argTypes: [bool], returnType: bool }, normalImpl: errorMessage, codegenImpl: (_ctx, [e]) => stitch`subgroupAny(${e})`, + sideEffects: false, }); export const subgroupBallot = dualImpl<(e: boolean) => v4u>({ @@ -84,6 +90,7 @@ export const subgroupBallot = dualImpl<(e: boolean) => v4u>({ signature: { argTypes: [bool], returnType: vec4u }, normalImpl: errorMessage, codegenImpl: (_ctx, [e]) => stitch`subgroupBallot(${e})`, + sideEffects: false, }); export const subgroupBroadcast = dualImpl({ @@ -101,6 +108,7 @@ export const subgroupBroadcast = dualImpl({ }, normalImpl: errorMessage, codegenImpl: (_ctx, [e, index]) => stitch`subgroupBroadcast(${e}, ${index})`, + sideEffects: false, }); export const subgroupBroadcastFirst = dualImpl({ @@ -108,6 +116,7 @@ export const subgroupBroadcastFirst = dualImpl({ signature: (arg) => ({ argTypes: [arg], returnType: arg }), normalImpl: errorMessage, codegenImpl: (_ctx, [e]) => stitch`subgroupBroadcastFirst(${e})`, + sideEffects: false, }); export const subgroupElect = dualImpl<() => boolean>({ @@ -115,6 +124,7 @@ export const subgroupElect = dualImpl<() => boolean>({ signature: { argTypes: [], returnType: bool }, normalImpl: errorMessage, codegenImpl: () => stitch`subgroupElect()`, + sideEffects: false, }); export const subgroupMax = dualImpl({ @@ -122,6 +132,7 @@ export const subgroupMax = dualImpl({ signature: (arg) => ({ argTypes: [arg], returnType: arg }), normalImpl: errorMessage, codegenImpl: (_ctx, [arg]) => stitch`subgroupMax(${arg})`, + sideEffects: false, }); export const subgroupMin = dualImpl({ @@ -129,6 +140,7 @@ export const subgroupMin = dualImpl({ signature: (arg) => ({ argTypes: [arg], returnType: arg }), normalImpl: errorMessage, codegenImpl: (_ctx, [arg]) => stitch`subgroupMin(${arg})`, + sideEffects: false, }); export const subgroupMul = dualImpl({ @@ -136,6 +148,7 @@ export const subgroupMul = dualImpl({ signature: (arg) => ({ argTypes: [arg], returnType: arg }), normalImpl: errorMessage, codegenImpl: (_ctx, [arg]) => stitch`subgroupMul(${arg})`, + sideEffects: false, }); export const subgroupExclusiveMul = dualImpl({ @@ -143,6 +156,7 @@ export const subgroupExclusiveMul = dualImpl({ signature: (arg) => ({ argTypes: [arg], returnType: arg }), normalImpl: errorMessage, codegenImpl: (_ctx, [arg]) => stitch`subgroupExclusiveMul(${arg})`, + sideEffects: false, }); export const subgroupInclusiveMul = dualImpl({ @@ -150,6 +164,7 @@ export const subgroupInclusiveMul = dualImpl({ signature: (arg) => ({ argTypes: [arg], returnType: arg }), normalImpl: errorMessage, codegenImpl: (_ctx, [arg]) => stitch`subgroupInclusiveMul(${arg})`, + sideEffects: false, }); export const subgroupOr = dualImpl({ @@ -157,6 +172,7 @@ export const subgroupOr = dualImpl({ signature: (arg) => ({ argTypes: [arg], returnType: arg }), normalImpl: errorMessage, codegenImpl: (_ctx, [e]) => stitch`subgroupOr(${e})`, + sideEffects: false, }); export const subgroupShuffle = dualImpl({ @@ -174,6 +190,7 @@ export const subgroupShuffle = dualImpl({ }, normalImpl: errorMessage, codegenImpl: (_ctx, [e, index]) => stitch`subgroupShuffle(${e}, ${index})`, + sideEffects: false, }); export const subgroupShuffleDown = dualImpl({ @@ -189,6 +206,7 @@ export const subgroupShuffleDown = dualImpl({ }, normalImpl: errorMessage, codegenImpl: (_ctx, [e, delta]) => stitch`subgroupShuffleDown(${e}, ${delta})`, + sideEffects: false, }); export const subgroupShuffleUp = dualImpl({ @@ -204,6 +222,7 @@ export const subgroupShuffleUp = dualImpl({ }, normalImpl: errorMessage, codegenImpl: (_ctx, [e, delta]) => stitch`subgroupShuffleUp(${e}, ${delta})`, + sideEffects: false, }); export const subgroupShuffleXor = dualImpl({ @@ -219,6 +238,7 @@ export const subgroupShuffleXor = dualImpl({ }, normalImpl: errorMessage, codegenImpl: (_ctx, [e, mask]) => stitch`subgroupShuffleXor(${e}, ${mask})`, + sideEffects: false, }); export const subgroupXor = dualImpl({ @@ -226,4 +246,5 @@ export const subgroupXor = dualImpl({ signature: (arg) => ({ argTypes: [arg], returnType: arg }), normalImpl: errorMessage, codegenImpl: (_ctx, [e]) => stitch`subgroupXor(${e})`, + sideEffects: false, }); diff --git a/packages/typegpu/src/std/texture.ts b/packages/typegpu/src/std/texture.ts index 71c1a22f28..01c13fb2f1 100644 --- a/packages/typegpu/src/std/texture.ts +++ b/packages/typegpu/src/std/texture.ts @@ -122,6 +122,7 @@ export const textureSample = dualImpl({ returnType: isDepth ? f32 : vec4f, }; }, + sideEffects: false, }); function sampleBiasCpu( @@ -180,6 +181,7 @@ export const textureSampleBias = dualImpl({ argTypes: args as BaseData[], returnType: vec4f, }), + sideEffects: false, }); function sampleLevelCpu( @@ -301,6 +303,7 @@ export const textureSampleLevel = dualImpl({ returnType: isDepth ? f32 : vec4f, }; }, + sideEffects: false, }); type PrimitiveToLoadedType = { @@ -415,6 +418,7 @@ export const textureLoad = dualImpl({ returnType: dataType, }; }, + sideEffects: false, }); function textureStoreCpu( @@ -506,6 +510,7 @@ export const textureDimensions = dualImpl({ returnType: vec2u, }; }, + sideEffects: false, }); type Gather2dArgs = [ @@ -625,6 +630,7 @@ export const textureGather = dualImpl({ returnType: sampleTypeToVecType[(texture as WgslTexture).sampleType.type], }; }, + sideEffects: false, }); function textureSampleCompareCpu( @@ -689,6 +695,7 @@ export const textureSampleCompare = dualImpl({ argTypes: args, returnType: f32, }), + sideEffects: false, }); function textureSampleCompareLevelCpu( @@ -753,6 +760,7 @@ export const textureSampleCompareLevel = dualImpl({ argTypes: args, returnType: f32, }), + sideEffects: false, }); function textureSampleBaseClampToEdgeCpu( @@ -849,6 +857,7 @@ export const textureSampleGrad = dualImpl({ argTypes: args, returnType: vec4f, }), + sideEffects: false, }); export const textureSampleBaseClampToEdge = dualImpl({ @@ -859,4 +868,5 @@ export const textureSampleBaseClampToEdge = dualImpl({ argTypes: args, returnType: vec4f, }), + sideEffects: false, }); diff --git a/packages/typegpu/tests/internal/dualImpl.test.ts b/packages/typegpu/tests/internal/dualImpl.test.ts index 078b384868..0d0ba66090 100644 --- a/packages/typegpu/tests/internal/dualImpl.test.ts +++ b/packages/typegpu/tests/internal/dualImpl.test.ts @@ -10,6 +10,7 @@ describe('dualImpl', () => { signature: () => ({ argTypes: [], returnType: d.Void }), codegenImpl: () => 'code', name: 'myDualImpl', + sideEffects: false, }); expect(getName(dual)).toBe('myDualImpl'); }); @@ -20,6 +21,7 @@ describe('dualImpl', () => { signature: (snippet) => ({ argTypes: [snippet], returnType: snippet }), codegenImpl: (_ctx, [snippet]) => `(${snippet.value} + 3)`, name: 'myDualImpl', + sideEffects: false, }); const myFn = tgpu.fn([])(() => { @@ -39,6 +41,7 @@ describe('dualImpl', () => { signature: (snippet) => ({ argTypes: [snippet], returnType: snippet }), codegenImpl: (_ctx, [snippet]) => `fallback(${snippet.value})`, name: 'myDualImpl', + sideEffects: false, }); expect(() => dual(2)).toThrowErrorMatchingInlineSnapshot( @@ -56,6 +59,7 @@ describe('dualImpl', () => { signature: (snippet) => ({ argTypes: [snippet], returnType: snippet }), codegenImpl: (_ctx, [snippet]) => `fallback(${snippet.value})`, name: 'myDualImpl', + sideEffects: false, }); const myFn = tgpu.fn([])(() => { @@ -77,6 +81,7 @@ describe('dualImpl', () => { signature: (snippet) => ({ argTypes: [snippet], returnType: snippet }), codegenImpl: (_ctx, [snippet]) => `fallback(${snippet.value})`, name: 'myDualImpl', + sideEffects: false, }); const myFn = tgpu.fn([])(() => { @@ -99,6 +104,7 @@ describe('dualImpl', () => { signature: (snippet) => ({ argTypes: [snippet], returnType: snippet }), codegenImpl: (_ctx, [snippet]) => `(1 / ${snippet.value})`, name: 'myDualImpl', + sideEffects: false, }); const myFn = tgpu.fn([])(() => { From 6538f877a9d61c94bc6b06fd7477a8b9c822df51 Mon Sep 17 00:00:00 2001 From: "pullfrog[bot]" <226033991+pullfrog[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:14:28 +0000 Subject: [PATCH 06/21] chore: merge ternaryRuntime tests into ternaryOperator.test.ts --- .../tests/tgsl/ternaryOperator.test.ts | 145 +++++++++++++++ .../typegpu/tests/tgsl/ternaryRuntime.test.ts | 165 ------------------ 2 files changed, 145 insertions(+), 165 deletions(-) delete mode 100644 packages/typegpu/tests/tgsl/ternaryRuntime.test.ts diff --git a/packages/typegpu/tests/tgsl/ternaryOperator.test.ts b/packages/typegpu/tests/tgsl/ternaryOperator.test.ts index e13f525eab..0db4f05fca 100644 --- a/packages/typegpu/tests/tgsl/ternaryOperator.test.ts +++ b/packages/typegpu/tests/tgsl/ternaryOperator.test.ts @@ -195,4 +195,149 @@ describe('ternary operator', () => { }" `); }); + + it('should handle subtraction in branches with function params', () => { + const myFn = tgpu.fn( + [d.u32, d.u32], + d.u32, + )((b, w) => { + return b > w ? b - w : w - b; + }); + + expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(` + "fn myFn(b: u32, w: u32) -> u32 { + return select((w - b), (b - w), (b > w)); + }" + `); + }); + + it('should handle const array indexing in branches', () => { + const RotLut2Gpu = tgpu.const(d.arrayOf(d.u32, 2), [10, 20]); + const RotLut3Gpu = tgpu.const(d.arrayOf(d.u32, 3), [30, 40, 50]); + + const myFn = tgpu.fn( + [d.u32, d.u32], + d.u32, + )((r, bitU) => { + return r === d.u32(2) ? (RotLut2Gpu.$[bitU] as number) : (RotLut3Gpu.$[bitU] as number); + }); + + expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(` + "const RotLut2Gpu: array = array(10u, 20u); + + const RotLut3Gpu: array = array(30u, 40u, 50u); + + fn myFn(r: u32, bitU: u32) -> u32 { + return select(RotLut3Gpu[bitU], RotLut2Gpu[bitU], (r == 2u)); + }" + `); + }); + + it('should handle nested runtime ternaries', () => { + const myFn = tgpu.fn( + [d.u32, d.u32, d.u32, d.u32], + d.u32, + )((r, v1, v2, v3) => { + return r === d.u32(1) ? v1 : r === d.u32(2) ? v2 : v3; + }); + + expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(` + "fn myFn(r: u32, v1: u32, v2: u32, v3: u32) -> u32 { + return select(select(v3, v2, (r == 2u)), v1, (r == 1u)); + }" + `); + }); + + it('should handle bit shift in branch with function param', () => { + const myFn = tgpu.fn( + [d.bool], + d.u32, + )((isCustom) => { + return isCustom ? d.u32(1) << d.u32(20) : d.u32(0); + }); + + expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(` + "fn myFn(isCustom: bool) -> u32 { + return select(0u, (1u << 20u), isCustom); + }" + `); + }); + + it('should handle struct field access across ternaries', () => { + const Cw = d.struct({ + low: d.u32, + high: d.u32, + }); + + const myFn = tgpu.fn( + [d.bool, Cw, Cw], + d.u32, + )((isCustom, customCw, stdCw) => { + return isCustom ? customCw.low : stdCw.low; + }); + + expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(` + "struct Cw { + low: u32, + high: u32, + } + + fn myFn(isCustom: bool, customCw: Cw, stdCw: Cw) -> u32 { + return select(stdCw.low, customCw.low, isCustom); + }" + `); + }); + + it('should handle buffer layout access in ternary branches', ({ root }) => { + const Cw = d.struct({ + low: d.u32, + high: d.u32, + }); + + const Layout = d.struct({ + codewords: d.arrayOf(Cw, 64), + }); + + const layout = root.createUniform(Layout, d.ref); + + const myFn = tgpu.fn( + [d.bool, d.u32], + d.u32, + )((isCustom, cwIdx) => { + return isCustom ? layout.$.codewords[cwIdx]!.low : layout.$.codewords[cwIdx + d.u32(1)]!.low; + }); + + expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(` + "struct Cw { + low: u32, + high: u32, + } + + struct Layout { + codewords: array, + } + + @group(0) @binding(0) var layout_1: Layout; + + fn myFn(isCustom: bool, cwIdx: u32) -> u32 { + return select(layout_1.codewords[(cwIdx + 1u)].low, layout_1.codewords[cwIdx].low, isCustom); + }" + `); + }); + + it('should throw when a ternary branch contains an assignment', () => { + const myFn = tgpu.fn( + [d.i32], + d.i32, + )((a) => { + let b = 0; + return a > 0 ? (b = a) : 0; + }); + + expect(() => tgpu.resolve([myFn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn:myFn: Ternary operator '(a > 0) ? (b = a) : 0' is invalid. For more complex branching, please use 'std.select' or if/else statements.] + `); + }); }); diff --git a/packages/typegpu/tests/tgsl/ternaryRuntime.test.ts b/packages/typegpu/tests/tgsl/ternaryRuntime.test.ts deleted file mode 100644 index 376a192f5f..0000000000 --- a/packages/typegpu/tests/tgsl/ternaryRuntime.test.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { describe, expect } from 'vitest'; -import { it } from 'typegpu-testing-utility'; -import tgpu, { d } from '../../src/index.js'; - -describe('runtime ternary operator', () => { - it('should handle subtraction in branches with function params', () => { - const myFn = tgpu.fn( - [d.u32, d.u32], - d.u32, - )((b, w) => { - return b > w ? b - w : w - b; - }); - - expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(` - "fn myFn(b: u32, w: u32) -> u32 { - return select((w - b), (b - w), (b > w)); - }" - `); - }); - - it('should handle const array indexing in branches', () => { - const RotLut2Gpu = tgpu.const(d.arrayOf(d.u32, 2), [10, 20]); - const RotLut3Gpu = tgpu.const(d.arrayOf(d.u32, 3), [30, 40, 50]); - - const myFn = tgpu.fn( - [d.u32, d.u32], - d.u32, - )((r, bitU) => { - return r === d.u32(2) ? (RotLut2Gpu.$[bitU] as number) : (RotLut3Gpu.$[bitU] as number); - }); - - expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(` - "const RotLut2Gpu: array = array(10u, 20u); - - const RotLut3Gpu: array = array(30u, 40u, 50u); - - fn myFn(r: u32, bitU: u32) -> u32 { - return select(RotLut3Gpu[bitU], RotLut2Gpu[bitU], (r == 2u)); - }" - `); - }); - - it('should handle nested runtime ternaries', () => { - const myFn = tgpu.fn( - [d.u32, d.u32, d.u32, d.u32], - d.u32, - )((r, v1, v2, v3) => { - return r === d.u32(1) ? v1 : r === d.u32(2) ? v2 : v3; - }); - - expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(` - "fn myFn(r: u32, v1: u32, v2: u32, v3: u32) -> u32 { - return select(select(v3, v2, (r == 2u)), v1, (r == 1u)); - }" - `); - }); - - it('should handle bit shift in branch with function param', () => { - const myFn = tgpu.fn( - [d.bool], - d.u32, - )((isCustom) => { - return isCustom ? d.u32(1) << d.u32(20) : d.u32(0); - }); - - expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(` - "fn myFn(isCustom: bool) -> u32 { - return select(0u, (1u << 20u), isCustom); - }" - `); - }); - - it('should handle struct field access across ternaries', () => { - const Cw = d.struct({ - low: d.u32, - high: d.u32, - }); - - const myFn = tgpu.fn( - [d.bool, Cw, Cw], - d.u32, - )((isCustom, customCw, stdCw) => { - return isCustom ? customCw.low : stdCw.low; - }); - - expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(` - "struct Cw { - low: u32, - high: u32, - } - - fn myFn(isCustom: bool, customCw: Cw, stdCw: Cw) -> u32 { - return select(stdCw.low, customCw.low, isCustom); - }" - `); - }); - - it('should handle buffer layout access in ternary branches', ({ root }) => { - const Cw = d.struct({ - low: d.u32, - high: d.u32, - }); - - const Layout = d.struct({ - codewords: d.arrayOf(Cw, 64), - }); - - const layout = root.createUniform(Layout, d.ref); - - const myFn = tgpu.fn( - [d.bool, d.u32], - d.u32, - )((isCustom, cwIdx) => { - return isCustom ? layout.$.codewords[cwIdx]!.low : layout.$.codewords[cwIdx + d.u32(1)]!.low; - }); - - expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(` - "struct Cw { - low: u32, - high: u32, - } - - struct Layout { - codewords: array, - } - - @group(0) @binding(0) var layout_1: Layout; - - fn myFn(isCustom: bool, cwIdx: u32) -> u32 { - return select(layout_1.codewords[(cwIdx + 1u)].low, layout_1.codewords[cwIdx].low, isCustom); - }" - `); - }); - - it('should handle ternary with comparison and unary negation in branches', () => { - const myFn = tgpu.fn( - [d.u32], - d.u32, - )((n) => { - return n > 0 ? n : -n; - }); - - expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(` - "fn myFn(n: u32) -> u32 { - return select(-(n), n, (n > 0u)); - }" - `); - }); - - it('should throw when a ternary branch contains an assignment', () => { - const myFn = tgpu.fn( - [d.i32], - d.i32, - )((a) => { - let b = 0; - return a > 0 ? (b = a) : 0; - }); - - expect(() => tgpu.resolve([myFn])).toThrowErrorMatchingInlineSnapshot(` - [Error: Resolution of the following tree failed: - - - - fn:myFn: Ternary operator '(a > 0) ? (b = a) : 0' is invalid. For more complex branching, please use 'std.select' or if/else statements.] - `); - }); -}); From 0d6d65620df1b4ff0b57d14f97bef42ef56b4af8 Mon Sep 17 00:00:00 2001 From: "pullfrog[bot]" <226033991+pullfrog[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:23:35 +0000 Subject: [PATCH 07/21] =?UTF-8?q?chore:=20address=20review=20feedback=20?= =?UTF-8?q?=E2=80=94=20use=20snip=20param=20for=20side-effects,=20fix=20di?= =?UTF-8?q?scard=20sideEffects?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/typegpu/src/core/function/dualImpl.ts | 12 +++++------- packages/typegpu/src/std/discard.ts | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/typegpu/src/core/function/dualImpl.ts b/packages/typegpu/src/core/function/dualImpl.ts index d91ebec163..7dbe44cdc9 100644 --- a/packages/typegpu/src/core/function/dualImpl.ts +++ b/packages/typegpu/src/core/function/dualImpl.ts @@ -1,4 +1,4 @@ -import { type MapValueToSnippet, noSideEffects, snip } from '../../data/snippet.ts'; +import { type MapValueToSnippet, snip } from '../../data/snippet.ts'; import { setName } from '../../shared/meta.ts'; import { $gpuCallable } from '../../shared/symbols.ts'; import { tryConvertSnippet } from '../../tgsl/conversion.ts'; @@ -107,17 +107,15 @@ export function dualImpl(options: DualImplOptions): DualFn a.possibleSideEffects); + + return snip( options.codegenImpl(ctx, converted), concretize(returnType), // Functions give up ownership of their return value /* origin */ 'runtime', + possibleSideEffects, ); - - if (!options.sideEffects && !args.some((a) => a.possibleSideEffects)) { - return noSideEffects(result); - } - return result; }, }; diff --git a/packages/typegpu/src/std/discard.ts b/packages/typegpu/src/std/discard.ts index 1bf676d9f4..66347d9ff8 100644 --- a/packages/typegpu/src/std/discard.ts +++ b/packages/typegpu/src/std/discard.ts @@ -6,5 +6,5 @@ export const discard = dualImpl<() => never>({ normalImpl: '`discard` relies on GPU resources and cannot be executed outside of a draw call', signature: { argTypes: [], returnType: Void }, codegenImpl: () => 'discard;', - sideEffects: false, + sideEffects: true, }); From 3b6467e2bf3d29040a62020771653542db1c44ba Mon Sep 17 00:00:00 2001 From: "pullfrog[bot]" <226033991+pullfrog[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2026 13:52:51 +0000 Subject: [PATCH 08/21] fix: user-defined functions should assume side-effects are possible --- packages/typegpu/src/core/function/tgpuFn.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/typegpu/src/core/function/tgpuFn.ts b/packages/typegpu/src/core/function/tgpuFn.ts index de297e19b1..31b664f155 100644 --- a/packages/typegpu/src/core/function/tgpuFn.ts +++ b/packages/typegpu/src/core/function/tgpuFn.ts @@ -238,7 +238,7 @@ function createFn( }), codegenImpl: (ctx, args) => ctx.withResetIndentLevel(() => stitch`${ctx.resolve(fn).value}(${args})`), - sideEffects: false, + sideEffects: true, }); const fn = Object.assign(call, fnBase) as TgpuFn; @@ -298,7 +298,7 @@ function createBoundFunction( normalImpl: innerFn, codegenImpl: (ctx, args) => ctx.withResetIndentLevel(() => stitch`${ctx.resolve(fn).value}(${args})`), - sideEffects: false, + sideEffects: true, }); const fn = Object.assign(call, fnBase) as TgpuFn; From 2dca0236ea8bd355dcf3a6050b31d3763041f85a Mon Sep 17 00:00:00 2001 From: "pullfrog[bot]" <226033991+pullfrog[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:08:09 +0000 Subject: [PATCH 09/21] chore: clarify docs for sideEffects and possibleSideEffects --- .../typegpu/src/core/function/dualImpl.ts | 17 ++++++---- packages/typegpu/src/data/snippet.ts | 33 +++++++++++++++---- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/packages/typegpu/src/core/function/dualImpl.ts b/packages/typegpu/src/core/function/dualImpl.ts index 4ffc428914..4234b9e604 100644 --- a/packages/typegpu/src/core/function/dualImpl.ts +++ b/packages/typegpu/src/core/function/dualImpl.ts @@ -33,12 +33,17 @@ interface DualImplOptions { * Whether calling this function is a side-effect in itself, irrespective of * its arguments. Examples: * - * - `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. + * - `workgroupBarrier()` → `true` — the barrier synchronizes threads. + * - `discard` → `true` — it discards the fragment. + * - `sin(x)`, `atomicLoad(p)`, `abs(x)` → `false` — these are purely + * value-producing; the call itself has no observable effect beyond the + * returned value. + * + * When `true`, every call produces a snippet whose `possibleSideEffects` is + * `true`, regardless of whether the arguments have side-effects. This + * prevents the call from being placed in a ternary branch compiled to + * `select()`, because `select()` unconditionally evaluates both branches — + * a conditional side-effect would execute unconditionally. * * When `false`, the result inherits side-effects from its arguments: it * only has `possibleSideEffects: true` if at least one argument does. diff --git a/packages/typegpu/src/data/snippet.ts b/packages/typegpu/src/data/snippet.ts index 5457cfa940..963014bf02 100644 --- a/packages/typegpu/src/data/snippet.ts +++ b/packages/typegpu/src/data/snippet.ts @@ -93,14 +93,20 @@ export interface Snippet { readonly dataType: BaseData | UnknownData; readonly origin: Origin; /** - * A snippet has possible side effects either if we're sure that it has side - * effects, or if our systems are unable to reliably determine that it - * doesn't have side effects. + * 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). * - * A snippet has side-effects if executing the WGSL code within and not using - * the return value is not equivalent to just not executing the WGSL code at - * all, with a margin of executing a few more instructions. For example, code - * that mutates memory, or synchronizes threads, has side effects. + * 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. + * + * This is **not** the same as "impure" in the functional-programming sense. + * A call like `atomicLoad(p)` is not referentially transparent (it reads + * mutable state), but producing its WGSL expression has no observable side + * effects — the read itself does not modify program state. That is why + * `atomicLoad` has `sideEffects: false` in its `DualImplOptions`. */ readonly possibleSideEffects: boolean; } @@ -139,6 +145,14 @@ export function isSnippetNumeric(snippet: Snippet) { return isNumericSchema(snippet.dataType); } +/** + * Create a snippet. + * + * @param possibleSideEffects — whether generating this snippet produces + * observable side-effects in WGSL. Defaults to `true` (safe/conservative). + * Set to `false` when you know the expression is side-effect-free, e.g. + * reading a function parameter, accessing a constant, or any pure builtin. + */ export function snip( value: string, dataType: BaseData, @@ -189,6 +203,11 @@ export function withSideEffects(possibleSideEffects: boolean, snippet: Snippet): return new SnippetImpl(snippet.value, snippet.dataType, snippet.origin, possibleSideEffects); } +/** + * Returns a copy of the snippet marked as having no side-effects. + * Use when you know the produced WGSL expression is observationally pure + * (e.g. reading a parameter, accessing a constant, or a pure builtin like `sin`). + */ export function noSideEffects(snippet: ResolvedSnippet): ResolvedSnippet; export function noSideEffects(snippet: Snippet): Snippet; export function noSideEffects(snippet: Snippet): Snippet { From 96cd096d4927b9a6519622eda2e803cfcbf169b9 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Mon, 6 Jul 2026 11:46:02 +0200 Subject: [PATCH 10/21] work --- .../typegpu/src/core/function/dualImpl.ts | 1 + .../core/rawCodeSnippet/tgpuRawCodeSnippet.ts | 17 ++++-- packages/typegpu/src/core/sampler/sampler.ts | 4 +- packages/typegpu/src/core/slot/accessor.ts | 17 +++++- .../src/core/texture/externalTexture.ts | 2 +- packages/typegpu/src/core/texture/texture.ts | 4 +- .../typegpu/src/core/unroll/tgpuUnroll.ts | 10 +++- .../typegpu/src/core/variable/tgpuVariable.ts | 2 +- packages/typegpu/src/data/ref.ts | 22 +++++--- packages/typegpu/src/resolutionCtx.ts | 3 +- packages/typegpu/src/std/boolean.ts | 2 +- packages/typegpu/src/tgsl/conversion.ts | 18 +++++-- packages/typegpu/src/tgsl/forOfUtils.ts | 18 +++---- .../typegpu/src/tgsl/generationHelpers.ts | 19 +++---- packages/typegpu/src/tgsl/wgslGenerator.ts | 54 ++++++++++--------- packages/typegpu/tests/utils/parseResolved.ts | 7 ++- 16 files changed, 126 insertions(+), 74 deletions(-) diff --git a/packages/typegpu/src/core/function/dualImpl.ts b/packages/typegpu/src/core/function/dualImpl.ts index 4234b9e604..f6482b15b3 100644 --- a/packages/typegpu/src/core/function/dualImpl.ts +++ b/packages/typegpu/src/core/function/dualImpl.ts @@ -108,6 +108,7 @@ export function dualImpl(options: DualImplOptions): DualFn( 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 +91,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 +123,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 +133,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..9fd134e0e6 100644 --- a/packages/typegpu/src/core/slot/accessor.ts +++ b/packages/typegpu/src/core/slot/accessor.ts @@ -7,6 +7,7 @@ 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), ); } @@ -156,6 +168,7 @@ abstract class AccessorBase< ctx.resolve(snippet.value, snippet.dataType).value, snippet.dataType as T, snippet.origin, + snippet.possibleSideEffects, ); } } 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 ac15188d00..08dbdec395 100644 --- a/packages/typegpu/src/core/texture/texture.ts +++ b/packages/typegpu/src/core/texture/texture.ts @@ -571,7 +571,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()}.$`, @@ -666,7 +666,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..852409851a 100644 --- a/packages/typegpu/src/core/unroll/tgpuUnroll.ts +++ b/packages/typegpu/src/core/unroll/tgpuUnroll.ts @@ -20,7 +20,8 @@ export class UnrollableIterable implements SelfResolvable { } [$resolve](_ctx: ResolutionCtx): ResolvedSnippet { - return snip(stitch`${this.snippet}`, this.snippet.dataType as BaseData, this.snippet.origin); + const { dataType, origin, possibleSideEffects } = this.snippet; + return snip(stitch`${this.snippet}`, dataType as BaseData, origin, possibleSideEffects); } } @@ -95,7 +96,12 @@ export const unroll = (() => { impl[$internal] = true; impl[$gpuCallable] = { call(_ctx, [value]) { - return snip(new UnrollableIterable(value), value.dataType, value.origin); + return snip( + new UnrollableIterable(value), + value.dataType, + value.origin, + value.possibleSideEffects, + ); }, }; 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..e18d41f336 100644 --- a/packages/typegpu/src/data/ref.ts +++ b/packages/typegpu/src/data/ref.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 snip(value.value, explicitFrom(value.dataType), value.origin, false); } /** @@ -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,14 @@ 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, false); } } @@ -194,8 +199,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.value.snippet.possibleSideEffects, + ); } - return snip(stitch`(*${snippet})`, innerType, snippet.origin); + return snip(stitch`(*${snippet})`, innerType, snippet.origin, false); } diff --git a/packages/typegpu/src/resolutionCtx.ts b/packages/typegpu/src/resolutionCtx.ts index df357024a1..95e355406f 100644 --- a/packages/typegpu/src/resolutionCtx.ts +++ b/packages/typegpu/src/resolutionCtx.ts @@ -1006,7 +1006,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') { @@ -1052,6 +1052,7 @@ export class ResolutionCtxImpl implements ResolutionCtx { this.resolve(snippet.value, snippet.dataType).value, snippet.dataType, snippet.origin, + snippet.possibleSideEffects, ) as ResolvedSnippet; } 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 9c7d6d4373..e96de59232 100644 --- a/packages/typegpu/src/tgsl/conversion.ts +++ b/packages/typegpu/src/tgsl/conversion.ts @@ -256,7 +256,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': { @@ -368,18 +373,23 @@ 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 snip( + stitch`${snip(value, target, origin, possibleSideEffects)}`, + 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..9f3541cacb 100644 --- a/packages/typegpu/src/tgsl/generationHelpers.ts +++ b/packages/typegpu/src/tgsl/generationHelpers.ts @@ -55,7 +55,12 @@ 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 snip( + snippet.value, + concretize(snippet.dataType as AnyWgslData), + snippet.origin, + snippet.possibleSideEffects, + ); } export function concretizeSnippets(args: Snippet[]): Snippet[] { @@ -121,18 +126,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..02b15b2714 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -31,7 +31,6 @@ import { coerceToSnippet, concretize, type GenerationCtx, - numericLiteralToSnippet, } from './generationHelpers.ts'; import { accessIndex } from './accessIndex.ts'; import { accessProp } from './accessProp.ts'; @@ -163,35 +162,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', argExpr.possibleSideEffects); }, } satisfies Partial unknown>>; @@ -262,9 +259,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 +286,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 +329,7 @@ ${this.ctx.pre}}`; } if (typeof expression === 'boolean') { - return snip(expression, bool, /* origin */ 'constant'); + return snip(expression, bool, /* origin */ 'constant', false); } if ( @@ -348,7 +346,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 +356,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); @@ -540,8 +538,8 @@ ${this.ctx.pre}}`; // Numeric Literal const type = typeof expression[1] === 'string' - ? numericLiteralToSnippet(parseNumericString(expression[1])) - : numericLiteralToSnippet(expression[1]); + ? coerceToSnippet(parseNumericString(expression[1])) + : coerceToSnippet(expression[1]); invariant(type, `Expected ${stringifyNode(expression)} to be valid numeric literal`); return type; } @@ -575,6 +573,7 @@ ${this.ctx.pre}}`; callee.value, // A new struct, so not a reference. /* origin */ 'runtime', + /* possibleSideEffects */ false, ); } @@ -587,6 +586,7 @@ ${this.ctx.pre}}`; callee.value, // A new struct, so not a reference. /* origin */ 'runtime', + arg.possibleSideEffects, ); } @@ -604,6 +604,7 @@ ${this.ctx.pre}}`; callee.value, // A new array, so not a reference. /* origin */ 'runtime', + /* possibleSideEffects */ false, ); } @@ -617,6 +618,7 @@ ${this.ctx.pre}}`; stitch`${this.ctx.resolve(callee.value).value}(${arg.value.elements})`, arg.dataType, /* origin */ 'runtime', + arg.possibleSideEffects, ); } @@ -627,6 +629,7 @@ ${this.ctx.pre}}`; callee.value, // A new array, so not a reference. /* origin */ 'runtime', + arg.possibleSideEffects, ); } @@ -851,7 +854,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) { @@ -1154,8 +1162,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 +1277,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); diff --git a/packages/typegpu/tests/utils/parseResolved.ts b/packages/typegpu/tests/utils/parseResolved.ts index 1c69c2dad7..2ab0cbd692 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]); } From 86600545b8b17a5f1f3839e5f5fc515469a23038 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Mon, 6 Jul 2026 16:41:26 +0200 Subject: [PATCH 11/21] work + tests + docs --- .../src/content/docs/apis/utils.mdx | 15 +- .../core/rawCodeSnippet/tgpuRawCodeSnippet.ts | 3 +- packages/typegpu/src/tgsl/wgslGenerator.ts | 46 +- .../typegpu/tests/tgsl/sideEffects.test.ts | 546 +++++++++++++++++- 4 files changed, 576 insertions(+), 34 deletions(-) diff --git a/apps/typegpu-docs/src/content/docs/apis/utils.mdx b/apps/typegpu-docs/src/content/docs/apis/utils.mdx index 830cf107ee..013137e372 100644 --- a/apps/typegpu-docs/src/content/docs/apis/utils.mdx +++ b/apps/typegpu-docs/src/content/docs/apis/utils.mdx @@ -141,7 +141,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 condition can be a runtime value. ::: ## *tgpu.rawCodeSnippet* @@ -157,7 +158,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'; @@ -187,6 +188,16 @@ If what the expression is a direct reference to an existing value (e.g. a unifor storage binding, ...), then choose from `'uniform'`, `'mutable'`, `'readonly'`, `'workgroup'`, `'private'` or `'handle'` depending on the address space of the referred value. +### `possibleSideEffects` +It is an indicator, 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. + ## *console.log* Yes, you read that correctly, TypeGPU implements logging to the console on the GPU! diff --git a/packages/typegpu/src/core/rawCodeSnippet/tgpuRawCodeSnippet.ts b/packages/typegpu/src/core/rawCodeSnippet/tgpuRawCodeSnippet.ts index a537fd4b4a..bfd8019e00 100644 --- a/packages/typegpu/src/core/rawCodeSnippet/tgpuRawCodeSnippet.ts +++ b/packages/typegpu/src/core/rawCodeSnippet/tgpuRawCodeSnippet.ts @@ -41,6 +41,7 @@ export type RawCodeSnippetOrigin = Exclude< * @param expression The code snippet that will be injected in place of `foo.$` * @param type The type of the expression * @param [origin='runtime'] Where the value originates from. + * @param [possibleSideEffects=true] 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). * * **-- Which origin to choose?** * @@ -59,7 +60,7 @@ export type RawCodeSnippetOrigin = Exclude< * // 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 = () => { * 'use gpu'; diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index 02b15b2714..648cc7adfb 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -500,7 +500,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) { @@ -549,7 +549,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)) { @@ -573,7 +578,7 @@ ${this.ctx.pre}}`; callee.value, // A new struct, so not a reference. /* origin */ 'runtime', - /* possibleSideEffects */ false, + false, ); } @@ -604,7 +609,7 @@ ${this.ctx.pre}}`; callee.value, // A new array, so not a reference. /* origin */ 'runtime', - /* possibleSideEffects */ false, + false, ); } @@ -808,6 +813,7 @@ ${this.ctx.pre}}`; stitch`${this.ctx.resolve(structType).value}(${convertedSnippets})`, structType, /* origin */ 'runtime', + convertedSnippets.some((s) => s.possibleSideEffects), ); } @@ -892,7 +898,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) { @@ -1000,10 +1006,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 { @@ -1014,13 +1030,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(); @@ -1030,12 +1046,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 { @@ -1473,7 +1489,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(); @@ -1493,7 +1509,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/tgsl/sideEffects.test.ts b/packages/typegpu/tests/tgsl/sideEffects.test.ts index 1e20d622b3..a36ce8e051 100644 --- a/packages/typegpu/tests/tgsl/sideEffects.test.ts +++ b/packages/typegpu/tests/tgsl/sideEffects.test.ts @@ -1,10 +1,51 @@ -import { tgpu, d } from 'typegpu'; +import { tgpu, d, std } from 'typegpu'; import { test } from 'typegpu-testing-utility'; +import { describe, expect } from 'vitest'; import { expectSideEffects } from '../utils/parseResolved.ts'; -import { describe } from 'vitest'; +import { isBeingTranspiled } from '../../src/std/environment.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 +53,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('bind group buffer slots', () => { + 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 +127,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 access 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 access 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 references', () => { + 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(true); + }); + test('variables', () => { expectSideEffects(() => { 'use gpu'; @@ -41,6 +254,12 @@ 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', () => { @@ -51,6 +270,22 @@ describe('code without side-effects', () => { }).toEqual(false); }); + test('indexed vectors', () => { + expectSideEffects(() => { + 'use gpu'; + const v = d.vec3f(); + return v[0]; + }).toEqual(false); + }); + + test('indexed matrix columns', () => { + expectSideEffects(() => { + 'use gpu'; + const m = d.mat2x2f(); + return m.columns[0]; + }).toEqual(false); + }); + test('vectors created from indexed arrays', () => { expectSideEffects(() => { 'use gpu'; @@ -59,6 +294,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 +310,302 @@ 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 UnknownData datatypes convertion', () => { + 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'; - const foo = Foo(); - return foo.prop; + 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 false', () => { + 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 !isBeingTranspiled() || flag; + }).toEqual(false); + + expectSideEffects(() => { + 'use gpu'; + return isBeingTranspiled() || impureBool(); + }).toEqual(false); + + expectSideEffects(() => { + 'use gpu'; + return !isBeingTranspiled() && impureBool(); + }).toEqual(false); + }); + + test('logical short-circuit with pure rhs', () => { + expectSideEffects(() => { + 'use gpu'; + const flag = true; + return !isBeingTranspiled() || flag; + }).toEqual(false); + + expectSideEffects(() => { + 'use gpu'; + const flag = true; + return isBeingTranspiled() && flag; + }).toEqual(false); + }); + + test('comptime-folded comparisons', () => { + const x = 1; + expectSideEffects(() => { + 'use gpu'; + 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 of 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 branches', () => { + expectSideEffects(() => { + 'use gpu'; + const flag = false; + return flag ? 1 : 2; }).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); + }); + + test('impure accessor call', () => { + const accessor = tgpu.accessor(d.f32, impureInt); + expectSideEffects(() => { + 'use gpu'; + return accessor.$; + }).toEqual(true); + }); + + test('unroll 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); + }); - function getNextValue() { + 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('struct field access on impure struct', () => { + expectSideEffects(() => { + 'use gpu'; + return impureStruct().pos; + }).toEqual(true); + }); + + test('same-type conversion with impure value', () => { + expectSideEffects(() => { 'use gpu'; - return next.$; - } + return d.u32(impureInt()); + }).toEqual(true); + }); + + test('logical not of impure value', () => { + expectSideEffects(() => { + 'use gpu'; + return !impureStruct(); + }).toEqual(true); + }); + + test('logical short-circuit with impure rhs', () => { + expectSideEffects(() => { + 'use gpu'; + return !isBeingTranspiled() || impureBool(); + }).toEqual(true); + + expectSideEffects(() => { + 'use gpu'; + return 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 the 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); }); }); From e9c7882aebeb9c7e43e4642d7b7b4bfbb4c30f3a Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Mon, 6 Jul 2026 16:44:12 +0200 Subject: [PATCH 12/21] test fix --- packages/typegpu/tests/tgsl/sideEffects.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typegpu/tests/tgsl/sideEffects.test.ts b/packages/typegpu/tests/tgsl/sideEffects.test.ts index a36ce8e051..860bb631bb 100644 --- a/packages/typegpu/tests/tgsl/sideEffects.test.ts +++ b/packages/typegpu/tests/tgsl/sideEffects.test.ts @@ -239,7 +239,7 @@ describe('code without side-effects', () => { 'use gpu'; const v = impureVec(); return d.ref(v); - }).toEqual(true); + }).toEqual(false); }); test('variables', () => { From 5c290276109ebf61de49ad016f17d94c846ca828 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Tue, 7 Jul 2026 12:39:28 +0200 Subject: [PATCH 13/21] docs update --- .../typegpu/src/core/function/dualImpl.ts | 5 ++--- packages/typegpu/src/data/snippet.ts | 12 ++++++----- packages/typegpu/src/tgsl/wgslGenerator.ts | 5 +++-- .../typegpu/tests/tgsl/sideEffects.test.ts | 21 +++++++++++-------- packages/typegpu/tests/utils/parseResolved.ts | 2 +- 5 files changed, 25 insertions(+), 20 deletions(-) diff --git a/packages/typegpu/src/core/function/dualImpl.ts b/packages/typegpu/src/core/function/dualImpl.ts index f6482b15b3..a3a1bdb2f8 100644 --- a/packages/typegpu/src/core/function/dualImpl.ts +++ b/packages/typegpu/src/core/function/dualImpl.ts @@ -35,9 +35,8 @@ interface DualImplOptions { * * - `workgroupBarrier()` → `true` — the barrier synchronizes threads. * - `discard` → `true` — it discards the fragment. - * - `sin(x)`, `atomicLoad(p)`, `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 `true`, every call produces a snippet whose `possibleSideEffects` is * `true`, regardless of whether the arguments have side-effects. This diff --git a/packages/typegpu/src/data/snippet.ts b/packages/typegpu/src/data/snippet.ts index 963014bf02..3a299a0178 100644 --- a/packages/typegpu/src/data/snippet.ts +++ b/packages/typegpu/src/data/snippet.ts @@ -102,11 +102,13 @@ export interface Snippet { * both branches unconditionally — a side-effect meant to be conditional * would execute regardless of the condition. * - * This is **not** the same as "impure" in the functional-programming sense. - * A call like `atomicLoad(p)` is not referentially transparent (it reads - * mutable state), but producing its WGSL expression has no observable side - * effects — the read itself does not modify program state. That is why - * `atomicLoad` has `sideEffects: false` in its `DualImplOptions`. + * This is not the same as "impure" in the functional-programming sense. + * Reading a mutable storage buffer is not referentially transparent (another + * thread may have written to it), yet producing its WGSL expression has no + * observable side-effect — the read itself does not modify program state, so + * such a snippet has `possibleSideEffects: false`. Conversely, `atomicLoad(p)` + * does count as a side-effect because atomic operations + * may synchronize threads through memory ordering. */ readonly possibleSideEffects: boolean; } diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index 648cc7adfb..fd9c51455b 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -30,6 +30,7 @@ import { ArrayExpression, coerceToSnippet, concretize, + numericLiteralToSnippet, type GenerationCtx, } from './generationHelpers.ts'; import { accessIndex } from './accessIndex.ts'; @@ -538,8 +539,8 @@ ${this.ctx.pre}}`; // Numeric Literal const type = typeof expression[1] === 'string' - ? coerceToSnippet(parseNumericString(expression[1])) - : coerceToSnippet(expression[1]); + ? numericLiteralToSnippet(parseNumericString(expression[1])) + : numericLiteralToSnippet(expression[1]); invariant(type, `Expected ${stringifyNode(expression)} to be valid numeric literal`); return type; } diff --git a/packages/typegpu/tests/tgsl/sideEffects.test.ts b/packages/typegpu/tests/tgsl/sideEffects.test.ts index 860bb631bb..6da8a92a49 100644 --- a/packages/typegpu/tests/tgsl/sideEffects.test.ts +++ b/packages/typegpu/tests/tgsl/sideEffects.test.ts @@ -6,6 +6,9 @@ import { isBeingTranspiled } from '../../src/std/environment.ts'; const Boid = d.struct({ pos: d.vec3f }); +// These are pure functions (they return constants), but for now we +// assume any user-defined function may have side-effects, so calling one +// yields a snippet with `possibleSideEffects: true`. const impureVec = () => { 'use gpu'; return d.vec3f(6, 6, 6); @@ -84,7 +87,7 @@ describe('code without side-effects', () => { }).toEqual(false); }); - test('bind group buffer slots', () => { + test('bound buffer reads', () => { const layout = tgpu.bindGroupLayout({ uniform: { uniform: d.f32 }, readonly: { storage: d.f32, access: 'readonly' }, @@ -144,7 +147,7 @@ describe('code without side-effects', () => { }).toEqual(false); }); - test('buffer access from accessor', ({ root }) => { + 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); @@ -209,7 +212,7 @@ describe('code without side-effects', () => { }).toEqual(false); }); - test('workgroup and private var references', () => { + test('workgroup and private var reads', () => { const w = tgpu.workgroupVar(d.u32); const p = tgpu.privateVar(d.u32, 2); @@ -332,7 +335,7 @@ describe('code without side-effects', () => { }).toEqual(false); }); - test('snippet with UnknownData datatypes convertion', () => { + test('snippet with the UnknownData datatype conversion', () => { const boidSlot = tgpu.slot(Boid()); expectSideEffects( @@ -362,7 +365,7 @@ describe('code without side-effects', () => { }).toEqual(false); }); - test('boolean literal false', () => { + test('boolean literals', () => { expectSideEffects(() => { 'use gpu'; return false; @@ -437,7 +440,7 @@ describe('code without side-effects', () => { }).toEqual(false); }); - test('unary expression of pure value', () => { + test('unary expression with pure value', () => { expectSideEffects(() => { 'use gpu'; return ~5; @@ -466,7 +469,7 @@ describe('code without side-effects', () => { }).toEqual(false); }); - test('runtime ternary with pure branches', () => { + test('runtime ternary with pure condition', () => { expectSideEffects(() => { 'use gpu'; const flag = false; @@ -491,7 +494,7 @@ describe('code with side-effects', () => { }).toEqual(true); }); - test('unroll with impure element', () => { + test('unroll over array with impure element', () => { expectSideEffects(() => { 'use gpu'; return tgpu.unroll([1, impureInt(), 3]); @@ -520,7 +523,7 @@ describe('code with side-effects', () => { }).toEqual(true); }); - test('struct field access on impure struct', () => { + test('prop access on impure struct', () => { expectSideEffects(() => { 'use gpu'; return impureStruct().pos; diff --git a/packages/typegpu/tests/utils/parseResolved.ts b/packages/typegpu/tests/utils/parseResolved.ts index 2ab0cbd692..bd2476cbf0 100644 --- a/packages/typegpu/tests/utils/parseResolved.ts +++ b/packages/typegpu/tests/utils/parseResolved.ts @@ -74,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); } From bbb2dfcdbdb970bf3d78a94c3cb89b464f5b4d8a Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Tue, 7 Jul 2026 12:49:44 +0200 Subject: [PATCH 14/21] final polish --- apps/typegpu-docs/src/content/docs/apis/utils.mdx | 4 ++-- packages/typegpu/src/tgsl/wgslGenerator.ts | 2 +- packages/typegpu/tests/tgsl/sideEffects.test.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/typegpu-docs/src/content/docs/apis/utils.mdx b/apps/typegpu-docs/src/content/docs/apis/utils.mdx index a2c704d3b0..819069d345 100644 --- a/apps/typegpu-docs/src/content/docs/apis/utils.mdx +++ b/apps/typegpu-docs/src/content/docs/apis/utils.mdx @@ -89,7 +89,7 @@ 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. -However, if both branches are known to have no side-effects, then condition can be a runtime value. +However, if both branches are known to have no side-effects, then the condition can be a runtime value. ::: ## *console.log* @@ -549,7 +549,7 @@ storage binding, ...), then choose from `'uniform'`, `'mutable'`, `'readonly'`, `'private'` or `'handle'` depending on the address space of the referred value. ### `possibleSideEffects` -The fourth optional parameter `possibleSideEffects` is an indicator, whether generating this snippet may produce a WGSL expression with +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). diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index fd9c51455b..25b18db645 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -183,7 +183,7 @@ const unaryOpCodeToCodegen = { const resultStr = `!bool(${argStr})`; const nanGuardedStr = // abstractFloat will be resolved as comptime known value dataType.type === 'f32' || dataType.type === 'f16' - ? `(((bitcast(${dataType.type === 'f16' ? `f32(${argStr})` : argStr}) << 1u) - 1u) < 0xff000000)` + ? `(((bitcast(${dataType.type === 'f16' ? `f32(${argStr})` : argStr}) << 1u) - 1u) >= 0xff000000)` : resultStr; return snip(nanGuardedStr, bool, 'runtime', argExpr.possibleSideEffects); diff --git a/packages/typegpu/tests/tgsl/sideEffects.test.ts b/packages/typegpu/tests/tgsl/sideEffects.test.ts index 6da8a92a49..9e5d0b7dd5 100644 --- a/packages/typegpu/tests/tgsl/sideEffects.test.ts +++ b/packages/typegpu/tests/tgsl/sideEffects.test.ts @@ -578,7 +578,7 @@ describe('code with side-effects', () => { }).toEqual(true); }); - test('coercion of the object with an impure field to the struct type', () => { + test('coercion of an object with an impure field to the struct type', () => { expectSideEffects( tgpu.fn( [], From 6d5ee169e9ac50d1013737253b1c97a6a6c6b8b1 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Tue, 7 Jul 2026 12:51:25 +0200 Subject: [PATCH 15/21] relict of pullfrog --- packages/typegpu/src/core/function/dualImpl.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/typegpu/src/core/function/dualImpl.ts b/packages/typegpu/src/core/function/dualImpl.ts index a3a1bdb2f8..564feb5d6b 100644 --- a/packages/typegpu/src/core/function/dualImpl.ts +++ b/packages/typegpu/src/core/function/dualImpl.ts @@ -33,17 +33,11 @@ interface DualImplOptions { * Whether calling this function is a side-effect in itself, irrespective of * its arguments. Examples: * - * - `workgroupBarrier()` → `true` — the barrier synchronizes threads. - * - `discard` → `true` — it discards the fragment. - * - `sin(x)`, `abs(x)` → `false` — these are purely value-producing; the + * - `discard` -> `true` - it discards the fragment. + * - `workgroupBarrier()` -> `true` - the barrier synchronizes threads. + * - `sin(x)`, `abs(x)` -> `false` - these are purely value-producing; the * call itself has no observable effect beyond the returned value. * - * When `true`, every call produces a snippet whose `possibleSideEffects` is - * `true`, regardless of whether the arguments have side-effects. This - * prevents the call from being placed in a ternary branch compiled to - * `select()`, because `select()` unconditionally evaluates both branches — - * a conditional side-effect would execute unconditionally. - * * When `false`, the result inherits side-effects from its arguments: it * only has `possibleSideEffects: true` if at least one argument does. */ From 731cb19f12bce785568a3b545f98838b493062cf Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Tue, 7 Jul 2026 13:29:40 +0200 Subject: [PATCH 16/21] review changes --- packages/typegpu/src/tgsl/wgslGenerator.ts | 2 +- .../typegpu/tests/tgsl/sideEffects.test.ts | 38 +++++++++++-------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index 25b18db645..55a91146ee 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -189,7 +189,7 @@ const unaryOpCodeToCodegen = { return snip(nanGuardedStr, bool, 'runtime', argExpr.possibleSideEffects); } - return snip(false, bool, 'constant', argExpr.possibleSideEffects); + return snip(false, bool, 'constant', false); }, } satisfies Partial unknown>>; diff --git a/packages/typegpu/tests/tgsl/sideEffects.test.ts b/packages/typegpu/tests/tgsl/sideEffects.test.ts index 9e5d0b7dd5..af4a7291dc 100644 --- a/packages/typegpu/tests/tgsl/sideEffects.test.ts +++ b/packages/typegpu/tests/tgsl/sideEffects.test.ts @@ -1,8 +1,7 @@ import { tgpu, d, std } from 'typegpu'; import { test } from 'typegpu-testing-utility'; -import { describe, expect } from 'vitest'; +import { describe } from 'vitest'; import { expectSideEffects } from '../utils/parseResolved.ts'; -import { isBeingTranspiled } from '../../src/std/environment.ts'; const Boid = d.struct({ pos: d.vec3f }); @@ -265,7 +264,7 @@ describe('code without side-effects', () => { }).toEqual(false); }); - test('indexed arrays', () => { + test('indexed array', () => { expectSideEffects(() => { 'use gpu'; const hello = [1, 2, 3]; @@ -273,7 +272,7 @@ describe('code without side-effects', () => { }).toEqual(false); }); - test('indexed vectors', () => { + test('indexed vector', () => { expectSideEffects(() => { 'use gpu'; const v = d.vec3f(); @@ -281,7 +280,7 @@ describe('code without side-effects', () => { }).toEqual(false); }); - test('indexed matrix columns', () => { + test('indexed matrix column', () => { expectSideEffects(() => { 'use gpu'; const m = d.mat2x2f(); @@ -289,7 +288,7 @@ describe('code without side-effects', () => { }).toEqual(false); }); - test('vectors created from indexed arrays', () => { + test('vector created from indexed array', () => { expectSideEffects(() => { 'use gpu'; const arr = [1, 2, 3]; @@ -365,7 +364,7 @@ describe('code without side-effects', () => { }).toEqual(false); }); - test('boolean literals', () => { + test('boolean literal', () => { expectSideEffects(() => { 'use gpu'; return false; @@ -388,17 +387,17 @@ describe('code without side-effects', () => { const flag = true; expectSideEffects(() => { 'use gpu'; - return !isBeingTranspiled() || flag; + return !std.isBeingTranspiled() || flag; }).toEqual(false); expectSideEffects(() => { 'use gpu'; - return isBeingTranspiled() || impureBool(); + return std.isBeingTranspiled() || impureBool(); }).toEqual(false); expectSideEffects(() => { 'use gpu'; - return !isBeingTranspiled() && impureBool(); + return !std.isBeingTranspiled() && impureBool(); }).toEqual(false); }); @@ -406,17 +405,17 @@ describe('code without side-effects', () => { expectSideEffects(() => { 'use gpu'; const flag = true; - return !isBeingTranspiled() || flag; + return !std.isBeingTranspiled() || flag; }).toEqual(false); expectSideEffects(() => { 'use gpu'; const flag = true; - return isBeingTranspiled() && flag; + return std.isBeingTranspiled() && flag; }).toEqual(false); }); - test('comptime-folded comparisons', () => { + test('comptime-folded comparison', () => { const x = 1; expectSideEffects(() => { 'use gpu'; @@ -476,6 +475,13 @@ describe('code without side-effects', () => { 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', () => { @@ -540,19 +546,19 @@ describe('code with side-effects', () => { test('logical not of impure value', () => { expectSideEffects(() => { 'use gpu'; - return !impureStruct(); + return !impureInt(); }).toEqual(true); }); test('logical short-circuit with impure rhs', () => { expectSideEffects(() => { 'use gpu'; - return !isBeingTranspiled() || impureBool(); + return !std.isBeingTranspiled() || impureBool(); }).toEqual(true); expectSideEffects(() => { 'use gpu'; - return isBeingTranspiled() && impureBool(); + return std.isBeingTranspiled() && impureBool(); }).toEqual(true); }); From 1bc02b309d987366e1e2ca1a871bd63d8ab06f2e Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Tue, 7 Jul 2026 13:45:56 +0200 Subject: [PATCH 17/21] more pull frog relicts --- packages/typegpu/src/data/snippet.ts | 35 ++++--------------- .../typegpu/tests/tgsl/sideEffects.test.ts | 3 -- 2 files changed, 7 insertions(+), 31 deletions(-) diff --git a/packages/typegpu/src/data/snippet.ts b/packages/typegpu/src/data/snippet.ts index 3a299a0178..5457cfa940 100644 --- a/packages/typegpu/src/data/snippet.ts +++ b/packages/typegpu/src/data/snippet.ts @@ -93,22 +93,14 @@ export interface Snippet { readonly dataType: BaseData | UnknownData; readonly origin: Origin; /** - * 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). + * A snippet has possible side effects either if we're sure that it has side + * effects, or if our systems are unable to reliably determine that it + * doesn't have side effects. * - * 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. - * - * This is not the same as "impure" in the functional-programming sense. - * Reading a mutable storage buffer is not referentially transparent (another - * thread may have written to it), yet producing its WGSL expression has no - * observable side-effect — the read itself does not modify program state, so - * such a snippet has `possibleSideEffects: false`. Conversely, `atomicLoad(p)` - * does count as a side-effect because atomic operations - * may synchronize threads through memory ordering. + * A snippet has side-effects if executing the WGSL code within and not using + * the return value is not equivalent to just not executing the WGSL code at + * all, with a margin of executing a few more instructions. For example, code + * that mutates memory, or synchronizes threads, has side effects. */ readonly possibleSideEffects: boolean; } @@ -147,14 +139,6 @@ export function isSnippetNumeric(snippet: Snippet) { return isNumericSchema(snippet.dataType); } -/** - * Create a snippet. - * - * @param possibleSideEffects — whether generating this snippet produces - * observable side-effects in WGSL. Defaults to `true` (safe/conservative). - * Set to `false` when you know the expression is side-effect-free, e.g. - * reading a function parameter, accessing a constant, or any pure builtin. - */ export function snip( value: string, dataType: BaseData, @@ -205,11 +189,6 @@ export function withSideEffects(possibleSideEffects: boolean, snippet: Snippet): return new SnippetImpl(snippet.value, snippet.dataType, snippet.origin, possibleSideEffects); } -/** - * Returns a copy of the snippet marked as having no side-effects. - * Use when you know the produced WGSL expression is observationally pure - * (e.g. reading a parameter, accessing a constant, or a pure builtin like `sin`). - */ export function noSideEffects(snippet: ResolvedSnippet): ResolvedSnippet; export function noSideEffects(snippet: Snippet): Snippet; export function noSideEffects(snippet: Snippet): Snippet { diff --git a/packages/typegpu/tests/tgsl/sideEffects.test.ts b/packages/typegpu/tests/tgsl/sideEffects.test.ts index af4a7291dc..55e203b096 100644 --- a/packages/typegpu/tests/tgsl/sideEffects.test.ts +++ b/packages/typegpu/tests/tgsl/sideEffects.test.ts @@ -5,9 +5,6 @@ import { expectSideEffects } from '../utils/parseResolved.ts'; const Boid = d.struct({ pos: d.vec3f }); -// These are pure functions (they return constants), but for now we -// assume any user-defined function may have side-effects, so calling one -// yields a snippet with `possibleSideEffects: true`. const impureVec = () => { 'use gpu'; return d.vec3f(6, 6, 6); From c6ee8ecc0ff241aca53da2207109d612a276949a Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Tue, 7 Jul 2026 15:47:11 +0200 Subject: [PATCH 18/21] accessor dualFn test --- packages/typegpu/tests/accessor.test.ts | 17 +++++++++++++++++ packages/typegpu/tests/tgsl/sideEffects.test.ts | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) 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 55e203b096..33151acb44 100644 --- a/packages/typegpu/tests/tgsl/sideEffects.test.ts +++ b/packages/typegpu/tests/tgsl/sideEffects.test.ts @@ -154,7 +154,7 @@ describe('code without side-effects', () => { }).toEqual(false); }); - test('pure function access from accessor', () => { + test('pure function call from accessor', () => { const accessor = tgpu.accessor(d.bool, std.subgroupElect); expectSideEffects(() => { From ad575e85f7777c8518756e797fd4a13a7b2395ed Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Tue, 7 Jul 2026 18:25:31 +0200 Subject: [PATCH 19/21] review changes --- packages/typegpu/src/core/slot/accessor.ts | 10 ++------- .../typegpu/src/core/unroll/tgpuUnroll.ts | 16 ++++---------- packages/typegpu/src/data/ref.ts | 13 ++++++++---- packages/typegpu/src/data/snippet.ts | 6 ++++++ packages/typegpu/src/resolutionCtx.ts | 21 ++++++++----------- packages/typegpu/src/tgsl/conversion.ts | 8 +------ .../typegpu/src/tgsl/generationHelpers.ts | 9 ++------ 7 files changed, 33 insertions(+), 50 deletions(-) diff --git a/packages/typegpu/src/core/slot/accessor.ts b/packages/typegpu/src/core/slot/accessor.ts index 9fd134e0e6..2c98b33646 100644 --- a/packages/typegpu/src/core/slot/accessor.ts +++ b/packages/typegpu/src/core/slot/accessor.ts @@ -1,6 +1,6 @@ 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'; @@ -94,7 +94,6 @@ abstract class AccessorBase< * @returns A snippet representing the accessor. */ #createSnippet() { - // oxlint-disable-next-line typescript/no-non-null-assertion -- it's there const ctx = getResolutionCtx()!; let value = getGpuValueRecursively(ctx.unwrap(this.slot)); @@ -164,12 +163,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, - snippet.possibleSideEffects, - ); + return withValue(ctx.resolve(snippet.value, snippet.dataType).value, snippet); } } diff --git a/packages/typegpu/src/core/unroll/tgpuUnroll.ts b/packages/typegpu/src/core/unroll/tgpuUnroll.ts index 852409851a..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,9 +17,8 @@ export class UnrollableIterable implements SelfResolvable { this.snippet = snippet; } - [$resolve](_ctx: ResolutionCtx): ResolvedSnippet { - const { dataType, origin, possibleSideEffects } = this.snippet; - return snip(stitch`${this.snippet}`, dataType as BaseData, origin, possibleSideEffects); + [$resolve](ctx: ResolutionCtx): ResolvedSnippet { + return ctx.resolveSnippet(this.snippet); } } @@ -96,12 +93,7 @@ export const unroll = (() => { impl[$internal] = true; impl[$gpuCallable] = { call(_ctx, [value]) { - return snip( - new UnrollableIterable(value), - value.dataType, - value.origin, - value.possibleSideEffects, - ); + return withValue(new UnrollableIterable(value), value); }, }; diff --git a/packages/typegpu/src/data/ref.ts b/packages/typegpu/src/data/ref.ts index e18d41f336..306f03711d 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, false); + return withDataType(explicitFrom(value.dataType), value); } /** @@ -187,7 +187,12 @@ export class RefOperator implements SelfResolvable { if (!this.#ptrType) { throw new Error(stitch`Cannot take a reference of ${this.snippet}`); } - return snip(stitch`(&${this.snippet})`, this.#ptrType, this.snippet.origin, false); + return snip( + stitch`(&${this.snippet})`, + this.#ptrType, + this.snippet.origin, + this.snippet.possibleSideEffects, + ); } } @@ -203,7 +208,7 @@ export function derefSnippet(snippet: Snippet): Snippet { stitch`${snippet.value.snippet}`, innerType, snippet.origin, - snippet.value.snippet.possibleSideEffects, + 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 95e355406f..c1ef5fe1b9 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'; @@ -1048,12 +1054,7 @@ export class ResolutionCtxImpl implements ResolutionCtx { } resolveSnippet(snippet: Snippet): ResolvedSnippet { - return snip( - this.resolve(snippet.value, snippet.dataType).value, - snippet.dataType, - snippet.origin, - snippet.possibleSideEffects, - ) as ResolvedSnippet; + return withValue(this.resolve(snippet.value, snippet.dataType).value, snippet); } pushMode(mode: ExecState) { @@ -1117,11 +1118,7 @@ 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], - ), + ctx.fixedBindings.map((binding, idx) => [String(idx), binding.resource] as [string, any]), ), ), ] as [number, TgpuBindGroup]; diff --git a/packages/typegpu/src/tgsl/conversion.ts b/packages/typegpu/src/tgsl/conversion.ts index e96de59232..d09c1f2a96 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'; @@ -384,12 +383,7 @@ export function tryConvertSnippet( if (dataType === UnknownData) { // Commit unknown to the expected type. - return snip( - stitch`${snip(value, target, origin, possibleSideEffects)}`, - target, - origin, - possibleSideEffects, - ); + return ctx.resolveSnippet(snip(value, target, origin, possibleSideEffects)); } } diff --git a/packages/typegpu/src/tgsl/generationHelpers.ts b/packages/typegpu/src/tgsl/generationHelpers.ts index 9f3541cacb..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,12 +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, - snippet.possibleSideEffects, - ); + return withDataType(concretize(snippet.dataType as AnyWgslData), snippet); } export function concretizeSnippets(args: Snippet[]): Snippet[] { From 81a30b1c8194ca3edf44138cf94db9f3519ee10e Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Tue, 7 Jul 2026 18:50:38 +0200 Subject: [PATCH 20/21] oxlint --- packages/typegpu/src/core/slot/accessor.ts | 1 + packages/typegpu/src/resolutionCtx.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/typegpu/src/core/slot/accessor.ts b/packages/typegpu/src/core/slot/accessor.ts index 2c98b33646..d7be6df538 100644 --- a/packages/typegpu/src/core/slot/accessor.ts +++ b/packages/typegpu/src/core/slot/accessor.ts @@ -94,6 +94,7 @@ abstract class AccessorBase< * @returns A snippet representing the accessor. */ #createSnippet() { + // oxlint-disable-next-line typescript/no-non-null-assertion -- it's there const ctx = getResolutionCtx()!; let value = getGpuValueRecursively(ctx.unwrap(this.slot)); diff --git a/packages/typegpu/src/resolutionCtx.ts b/packages/typegpu/src/resolutionCtx.ts index c1ef5fe1b9..e2d1d59c8d 100644 --- a/packages/typegpu/src/resolutionCtx.ts +++ b/packages/typegpu/src/resolutionCtx.ts @@ -1118,6 +1118,7 @@ export function resolve(item: Wgsl, options: ResolutionCtxImplOptions): Resoluti new TgpuBindGroupImpl( catchallLayout, Object.fromEntries( + // oxlint-disable-next-line typescript/no-explicit-any -- it's fine ctx.fixedBindings.map((binding, idx) => [String(idx), binding.resource] as [string, any]), ), ), From df3997a695e8368e6487b7e6c1dfd3da419ba019 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Wed, 8 Jul 2026 16:35:39 +0200 Subject: [PATCH 21/21] review changes --- packages/typegpu/src/core/slot/accessor.ts | 2 +- packages/typegpu/src/data/ref.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/typegpu/src/core/slot/accessor.ts b/packages/typegpu/src/core/slot/accessor.ts index d7be6df538..18b170df9f 100644 --- a/packages/typegpu/src/core/slot/accessor.ts +++ b/packages/typegpu/src/core/slot/accessor.ts @@ -164,7 +164,7 @@ abstract class AccessorBase< [$resolve](ctx: ResolutionCtx): ResolvedSnippet { const snippet = this.#createSnippet(); - return withValue(ctx.resolve(snippet.value, snippet.dataType).value, snippet); + return ctx.resolveSnippet(snippet); } } diff --git a/packages/typegpu/src/data/ref.ts b/packages/typegpu/src/data/ref.ts index 306f03711d..77dac4395a 100644 --- a/packages/typegpu/src/data/ref.ts +++ b/packages/typegpu/src/data/ref.ts @@ -212,5 +212,5 @@ export function derefSnippet(snippet: Snippet): Snippet { ); } - return snip(stitch`(*${snippet})`, innerType, snippet.origin, false); + return snip(stitch`(*${snippet})`, innerType, snippet.origin, snippet.possibleSideEffects); }