Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/typegpu/src/core/buffer/bufferUsage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ class TgpuFixedBufferImpl<TData extends BaseData, TUsage extends BindableBufferU
{
[$internal]: true,
get [$ownSnippet]() {
return snip(this, dataType, usage);
return snip(this, dataType, usage, /* possibleSideEffects */ false);
},
[$resolve]: (ctx) => ctx.resolve(this),
toString: () => `${this.usage}:${getName(this) ?? '<unnamed>'}.$`,
Expand Down Expand Up @@ -262,7 +262,7 @@ export class TgpuLaidOutBufferImpl<TData extends BaseData, TUsage extends Bindab
{
[$internal]: true,
get [$ownSnippet]() {
return snip(this, schema, usage);
return snip(this, schema, usage, /* possibleSideEffects */ false);
},
[$resolve]: (ctx) => ctx.resolve(this),
toString: () => `${this.usage}:${getName(this) ?? '<unnamed>'}.$`,
Expand Down
2 changes: 1 addition & 1 deletion packages/typegpu/src/core/constant/tgpuConstant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ class TgpuConstImpl<TDataType extends BaseData> implements TgpuConst<TDataType>,
{
[$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) ?? '<unnamed>'}.$`,
Expand Down
23 changes: 23 additions & 0 deletions packages/typegpu/src/core/function/dualImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,26 @@ interface DualImplOptions<T extends AnyFn> {
*/
readonly noComptime?: boolean | undefined;
readonly ignoreImplicitCastWarning?: boolean | undefined;
/**
* 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;
}

export class MissingCpuImplError extends Error {
Expand Down Expand Up @@ -101,11 +121,14 @@ export function dualImpl<T extends AnyFn>(options: DualImplOptions<T>): DualFn<T
}
}

const possibleSideEffects = options.sideEffects || args.some((a) => a.possibleSideEffects);

return snip(
options.codegenImpl(ctx, converted),
concretize(returnType),
// Functions give up ownership of their return value
/* origin */ 'runtime',
possibleSideEffects,
);
},
};
Expand Down
2 changes: 2 additions & 0 deletions packages/typegpu/src/core/function/tgpuFn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ function createFn<ImplSchema extends AnyFn>(
}),
codegenImpl: (ctx, args) =>
ctx.withResetIndentLevel(() => stitch`${ctx.resolve(fn).value}(${args})`),
sideEffects: true,
});

const fn = Object.assign(call, fnBase) as TgpuFn<ImplSchema>;
Expand Down Expand Up @@ -297,6 +298,7 @@ function createBoundFunction<ImplSchema extends AnyFn>(
normalImpl: innerFn,
codegenImpl: (ctx, args) =>
ctx.withResetIndentLevel(() => stitch`${ctx.resolve(fn).value}(${args})`),
sideEffects: true,
});

const fn = Object.assign(call, fnBase) as TgpuFn<ImplSchema>;
Expand Down
5 changes: 5 additions & 0 deletions packages/typegpu/src/data/matrix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});

/**
Expand All @@ -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,
});

/**
Expand All @@ -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,
});

/**
Expand All @@ -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,
});

/**
Expand All @@ -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,
});

// ----------
Expand Down
29 changes: 29 additions & 0 deletions packages/typegpu/src/data/snippet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion packages/typegpu/src/resolutionCtx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
1 change: 1 addition & 0 deletions packages/typegpu/src/std/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ export const arrayLength = dualImpl({
const length = sizeOfPointedToArray(a.dataType);
return length > 0 ? `${length}` : stitch`arrayLength(${a})`;
},
sideEffects: false,
});
12 changes: 12 additions & 0 deletions packages/typegpu/src/std/atomic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,23 @@ export const workgroupBarrier = dualImpl({
normalImpl: 'workgroupBarrier is a no-op outside of CODEGEN mode.',
signature: { argTypes: [], returnType: Void },
codegenImpl: () => 'workgroupBarrier()',
sideEffects: true,
Comment thread
pullfrog[bot] marked this conversation as resolved.
});

export const storageBarrier = dualImpl({
name: 'storageBarrier',
normalImpl: 'storageBarrier is a no-op outside of CODEGEN mode.',
signature: { argTypes: [], returnType: Void },
codegenImpl: () => 'storageBarrier()',
sideEffects: true,
});

export const textureBarrier = dualImpl({
name: 'textureBarrier',
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.';
Expand All @@ -44,6 +47,7 @@ export const atomicLoad = dualImpl<<T extends AnyAtomic>(a: T) => number>({
return { argTypes: [a], returnType: a.inner };
},
codegenImpl: (_ctx, [a]) => stitch`atomicLoad(&${a})`,
sideEffects: false,

@aleksanderkatan aleksanderkatan Jun 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't get how workgroupBarrier has side effects, and atomicLoad does not.
I guess this comes down to what definition we impose (currently, I see no explanation of that in code).

Based on the main use case of this being the ternary operator, I suggest sideEffects -> prunable change (if pruning it makes no difference, we might as well call it in WGSL).

What !sideEffects pure prunable
tgpu.const true true true
atomicStore() false false false
atomicLoad() <difficult to interpret> false (even though it's fine in a ternary) true
workgroupBarrier() <difficult to interpret> true (even though it's not fine in a ternary) false

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The definition that distinguishes them: sideEffects answers "does calling this function itself produce an observable side-effect beyond the returned value?" workgroupBarrier() does (synchronizes threads), so sideEffects: true. atomicLoad() doesn't — reading from memory is impure (not referentially transparent) but the read itself doesn't modify program state. The docs I just pushed to dualImpl.ts and snippet.ts spell this out with examples.

As for renaming — I considered prunable but it inverts the sense (sideEffects: false → prunable), which would require flipping every call site. Also sideEffects maps well to the concept: "does this function have side effects?" is the natural question when declaring a builtin. Happy to consider alternatives though.

Pullfrog  | View workflow run | via Pullfrog | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

});

const atomicActionSignature = (a: BaseData) => {
Expand Down Expand Up @@ -72,53 +76,61 @@ export const atomicStore = dualImpl<<T extends AnyAtomic>(a: T, value: number) =
normalImpl: atomicNormalError,
signature: atomicActionSignature,
codegenImpl: (_ctx, [a, value]) => stitch`atomicStore(&${a}, ${value})`,
sideEffects: true,
});

export const atomicAdd = dualImpl<<T extends AnyAtomic>(a: T, value: number) => number>({
name: 'atomicAdd',
normalImpl: atomicNormalError,
signature: atomicOpSignature,
codegenImpl: (_ctx, [a, value]) => stitch`atomicAdd(&${a}, ${value})`,
sideEffects: true,
});

export const atomicSub = dualImpl<<T extends AnyAtomic>(a: T, value: number) => number>({
name: 'atomicSub',
normalImpl: atomicNormalError,
signature: atomicOpSignature,
codegenImpl: (_ctx, [a, value]) => stitch`atomicSub(&${a}, ${value})`,
sideEffects: true,
});

export const atomicMax = dualImpl<<T extends AnyAtomic>(a: T, value: number) => number>({
name: 'atomicMax',
normalImpl: atomicNormalError,
signature: atomicOpSignature,
codegenImpl: (_ctx, [a, value]) => stitch`atomicMax(&${a}, ${value})`,
sideEffects: true,
});

export const atomicMin = dualImpl<<T extends AnyAtomic>(a: T, value: number) => number>({
name: 'atomicMin',
normalImpl: atomicNormalError,
signature: atomicOpSignature,
codegenImpl: (_ctx, [a, value]) => stitch`atomicMin(&${a}, ${value})`,
sideEffects: true,
});

export const atomicAnd = dualImpl<<T extends AnyAtomic>(a: T, value: number) => number>({
name: 'atomicAnd',
normalImpl: atomicNormalError,
signature: atomicOpSignature,
codegenImpl: (_ctx, [a, value]) => stitch`atomicAnd(&${a}, ${value})`,
sideEffects: true,
});

export const atomicOr = dualImpl<<T extends AnyAtomic>(a: T, value: number) => number>({
name: 'atomicOr',
normalImpl: atomicNormalError,
signature: atomicOpSignature,
codegenImpl: (_ctx, [a, value]) => stitch`atomicOr(&${a}, ${value})`,
sideEffects: true,
});

export const atomicXor = dualImpl<<T extends AnyAtomic>(a: T, value: number) => number>({
name: 'atomicXor',
normalImpl: atomicNormalError,
signature: atomicOpSignature,
codegenImpl: (_ctx, [a, value]) => stitch`atomicXor(&${a}, ${value})`,
sideEffects: true,
});
2 changes: 2 additions & 0 deletions packages/typegpu/src/std/bitcast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const bitcastU32toF32 = dualImpl({
: f32,
};
},
sideEffects: false,
});

export type BitcastU32toI32Overload = ((value: number) => number) &
Expand Down Expand Up @@ -64,4 +65,5 @@ export const bitcastU32toI32 = dualImpl({
: i32,
};
},
sideEffects: false,
});
Loading
Loading