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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] =?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/11] 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 97f345540dabd94f8489323410e8e854f7470f12 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/11] chore: clarify docs for sideEffects and possibleSideEffects --- .../typegpu/src/core/function/dualImpl.ts | 20 +++++++++++-- packages/typegpu/src/data/snippet.ts | 29 +++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/packages/typegpu/src/core/function/dualImpl.ts b/packages/typegpu/src/core/function/dualImpl.ts index 7dbe44cdc9..4234b9e604 100644 --- a/packages/typegpu/src/core/function/dualImpl.ts +++ b/packages/typegpu/src/core/function/dualImpl.ts @@ -30,9 +30,23 @@ 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`, the result has side effects only when any argument does. + * 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)`, `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. */ readonly sideEffects: boolean; } diff --git a/packages/typegpu/src/data/snippet.ts b/packages/typegpu/src/data/snippet.ts index 8088abbe90..963014bf02 100644 --- a/packages/typegpu/src/data/snippet.ts +++ b/packages/typegpu/src/data/snippet.ts @@ -92,6 +92,22 @@ 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). + * + * 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; } @@ -129,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, @@ -179,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 c7de770c8bb4f49f6a108a7ec0691f6901b78787 Mon Sep 17 00:00:00 2001 From: "pullfrog[bot]" <226033991+pullfrog[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 11:09:01 +0000 Subject: [PATCH 10/11] chore: clarify possibleSideEffects docs and treat atomicLoad as side-effectful --- .../typegpu/src/core/function/dualImpl.ts | 9 ++--- packages/typegpu/src/data/snippet.ts | 34 +++++-------------- packages/typegpu/src/std/atomic.ts | 2 +- 3 files changed, 15 insertions(+), 30 deletions(-) diff --git a/packages/typegpu/src/core/function/dualImpl.ts b/packages/typegpu/src/core/function/dualImpl.ts index 4234b9e604..f1c369b23d 100644 --- a/packages/typegpu/src/core/function/dualImpl.ts +++ b/packages/typegpu/src/core/function/dualImpl.ts @@ -33,11 +33,12 @@ 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)`, `atomicLoad(p)`, `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. + * - `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. * * 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..edf3ecd86b 100644 --- a/packages/typegpu/src/data/snippet.ts +++ b/packages/typegpu/src/data/snippet.ts @@ -93,20 +93,17 @@ 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. + * 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. * - * 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 information is currently used to determine if a ternary expression + * can be safely translated into a select() call. */ readonly possibleSideEffects: boolean; } @@ -145,14 +142,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, @@ -203,11 +192,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/src/std/atomic.ts b/packages/typegpu/src/std/atomic.ts index 55a9513584..46841a0cc0 100644 --- a/packages/typegpu/src/std/atomic.ts +++ b/packages/typegpu/src/std/atomic.ts @@ -47,7 +47,7 @@ export const atomicLoad = dualImpl<(a: T) => number>({ return { argTypes: [a], returnType: a.inner }; }, codegenImpl: (_ctx, [a]) => stitch`atomicLoad(&${a})`, - sideEffects: false, + sideEffects: true, }); const atomicActionSignature = (a: BaseData) => { From 32154c9cabcff02e492a542611d1d9d5d738e248 Mon Sep 17 00:00:00 2001 From: "pullfrog[bot]" <226033991+pullfrog[bot]@users.noreply.github.com> Date: Thu, 25 Jun 2026 10:19:56 +0000 Subject: [PATCH 11/11] chore: address review feedback - remove irrelevant docs and use i32 in ternary test --- packages/typegpu/src/core/function/dualImpl.ts | 14 ++++---------- packages/typegpu/src/data/snippet.ts | 3 --- .../typegpu/tests/tgsl/ternaryOperator.test.ts | 8 ++++---- 3 files changed, 8 insertions(+), 17 deletions(-) diff --git a/packages/typegpu/src/core/function/dualImpl.ts b/packages/typegpu/src/core/function/dualImpl.ts index f1c369b23d..4ffc428914 100644 --- a/packages/typegpu/src/core/function/dualImpl.ts +++ b/packages/typegpu/src/core/function/dualImpl.ts @@ -33,19 +33,13 @@ 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 + * - `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 + * - `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. */ diff --git a/packages/typegpu/src/data/snippet.ts b/packages/typegpu/src/data/snippet.ts index edf3ecd86b..5457cfa940 100644 --- a/packages/typegpu/src/data/snippet.ts +++ b/packages/typegpu/src/data/snippet.ts @@ -101,9 +101,6 @@ export interface Snippet { * 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. - * - * This information is currently used to determine if a ternary expression - * can be safely translated into a select() call. */ readonly possibleSideEffects: boolean; } diff --git a/packages/typegpu/tests/tgsl/ternaryOperator.test.ts b/packages/typegpu/tests/tgsl/ternaryOperator.test.ts index 0db4f05fca..23b2ffbcf2 100644 --- a/packages/typegpu/tests/tgsl/ternaryOperator.test.ts +++ b/packages/typegpu/tests/tgsl/ternaryOperator.test.ts @@ -183,15 +183,15 @@ describe('ternary operator', () => { it('should generate select() for runtime condition with function params', () => { const myFn = tgpu.fn( - [d.u32], - d.u32, + [d.i32], + d.i32, )((n) => { return n > 0 ? n : -n; }); expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(` - "fn myFn(n: u32) -> u32 { - return select(-(n), n, (n > 0u)); + "fn myFn(n: i32) -> i32 { + return select(-(n), n, (n > 0i)); }" `); });