Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
15 changes: 13 additions & 2 deletions packages/typegpu/src/core/function/dualImpl.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -29,6 +29,12 @@ interface DualImplOptions<T extends AnyFn> {
*/
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.
*/
readonly sideEffects: boolean;
}

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

return snip(
const result = snip(
options.codegenImpl(ctx, converted),
concretize(returnType),
// Functions give up ownership of their return value
/* origin */ 'runtime',
);

if (!options.sideEffects && !args.some((a) => a.possibleSideEffects)) {
return noSideEffects(result);
}
return result;
Comment thread
pullfrog[bot] marked this conversation as resolved.
Outdated
},
};

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: false,
Comment thread
pullfrog[bot] marked this conversation as resolved.
Outdated
});

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: false,
Comment thread
pullfrog[bot] marked this conversation as resolved.
Outdated
});

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
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
Member

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,
});
14 changes: 14 additions & 0 deletions packages/typegpu/src/std/boolean.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export const allEq = dualImpl({
signature: (...argTypes) => ({ argTypes, returnType: bool }),
normalImpl: <T extends AnyVecInstance>(lhs: T, rhs: T) => cpuAll(cpuEq(lhs, rhs)),
codegenImpl: (_ctx, [lhs, rhs]) => stitch`all(${lhs} == ${rhs})`,
sideEffects: false,
});

const cpuEq = <T extends AnyVecInstance>(lhs: T, rhs: T) => VectorOps.eq[lhs.kind](lhs, rhs);
Expand All @@ -86,6 +87,7 @@ export const eq = dualImpl({
}),
normalImpl: cpuEq,
codegenImpl: (_ctx, [lhs, rhs]) => stitch`(${lhs} == ${rhs})`,
sideEffects: false,
});

/**
Expand All @@ -104,6 +106,7 @@ export const ne = dualImpl({
}),
normalImpl: <T extends AnyVecInstance>(lhs: T, rhs: T) => cpuNot(cpuEq(lhs, rhs)),
codegenImpl: (_ctx, [lhs, rhs]) => stitch`(${lhs} != ${rhs})`,
sideEffects: false,
});

const cpuLt = <T extends AnyNumericVecInstance>(lhs: T, rhs: T) => VectorOps.lt[lhs.kind](lhs, rhs);
Expand All @@ -124,6 +127,7 @@ export const lt = dualImpl({
}),
normalImpl: cpuLt,
codegenImpl: (_ctx, [lhs, rhs]) => stitch`(${lhs} < ${rhs})`,
sideEffects: false,
});

/**
Expand All @@ -143,6 +147,7 @@ export const le = dualImpl({
normalImpl: <T extends AnyNumericVecInstance>(lhs: T, rhs: T) =>
cpuOr(cpuLt(lhs, rhs), cpuEq(lhs, rhs)),
codegenImpl: (_ctx, [lhs, rhs]) => stitch`(${lhs} <= ${rhs})`,
sideEffects: false,
});

/**
Expand All @@ -162,6 +167,7 @@ export const gt = dualImpl({
normalImpl: <T extends AnyNumericVecInstance>(lhs: T, rhs: T) =>
cpuAnd(cpuNot(cpuLt(lhs, rhs)), cpuNot(cpuEq(lhs, rhs))),
codegenImpl: (_ctx, [lhs, rhs]) => stitch`(${lhs} > ${rhs})`,
sideEffects: false,
});

/**
Expand All @@ -180,6 +186,7 @@ export const ge = dualImpl({
}),
normalImpl: <T extends AnyNumericVecInstance>(lhs: T, rhs: T) => cpuNot(cpuLt(lhs, rhs)),
codegenImpl: (_ctx, [lhs, rhs]) => stitch`(${lhs} >= ${rhs})`,
sideEffects: false,
});

// logical ops
Expand Down Expand Up @@ -260,6 +267,7 @@ export const not = dualImpl({

return 'false';
},
sideEffects: false,
});

const cpuOr = <T extends AnyBooleanVecInstance>(lhs: T, rhs: T) => VectorOps.or[lhs.kind](lhs, rhs);
Expand All @@ -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 = <T extends AnyBooleanVecInstance>(lhs: T, rhs: T) =>
Expand All @@ -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
Expand All @@ -308,6 +318,7 @@ export const all = dualImpl({
signature: (...argTypes) => ({ argTypes, returnType: bool }),
normalImpl: cpuAll,
codegenImpl: (_ctx, [value]) => stitch`all(${value})`,
sideEffects: false,
});

/**
Expand All @@ -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
Expand Down Expand Up @@ -366,6 +378,7 @@ export const isCloseTo = dualImpl({
}
return 'false';
},
sideEffects: false,
});

function cpuSelect(f: boolean, t: boolean, cond: boolean): boolean;
Expand Down Expand Up @@ -448,4 +461,5 @@ export const select = dualImpl({
}
return result;
},
sideEffects: false,
});
1 change: 1 addition & 0 deletions packages/typegpu/src/std/copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@ export const copy = dualImpl({
codegenImpl(_ctx, [a]) {
return stitch`${a}`;
},
sideEffects: false,
});
Loading
Loading