Skip to content
Open
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
fed3aa2
fix: improve runtime ternary support by refining side-effect detection
pullfrog[bot] Jun 15, 2026
89e4f5d
fix: improve runtime ternary support by refining side-effect detection
pullfrog[bot] Jun 15, 2026
59ebd23
style: apply oxfmt formatting
pullfrog[bot] Jun 15, 2026
faee545
fix: add sideEffects prop to dualImpl for correct side-effect tracking
pullfrog[bot] Jun 15, 2026
5bc6920
make sideEffects required in DualImplOptions
pullfrog[bot] Jun 16, 2026
6538f87
chore: merge ternaryRuntime tests into ternaryOperator.test.ts
pullfrog[bot] Jun 16, 2026
0d6d656
chore: address review feedback — use snip param for side-effects, fix…
pullfrog[bot] Jun 16, 2026
3b6467e
fix: user-defined functions should assume side-effects are possible
pullfrog[bot] Jun 16, 2026
57a384a
comptime comparisons: cleanup + tests
cieplypolar Jun 19, 2026
870342c
strict runtime relational operators + tests
cieplypolar Jun 23, 2026
f5b4984
&& and || strict types
cieplypolar Jun 23, 2026
41fe48d
ternary condition to bool
cieplypolar Jun 23, 2026
7b9def7
Merge branch 'main' into fix/unify-test-in-ternary
cieplypolar Jun 23, 2026
c797858
Merge branch 'main' into pullfrog/2587-improve-ternary-side-effect-de…
cieplypolar Jul 6, 2026
2dca023
chore: clarify docs for sideEffects and possibleSideEffects
pullfrog[bot] Jun 18, 2026
96cd096
work
cieplypolar Jul 6, 2026
8660054
work + tests + docs
cieplypolar Jul 6, 2026
e9c7882
test fix
cieplypolar Jul 6, 2026
5c29027
docs update
cieplypolar Jul 7, 2026
a878f01
Merge branch 'main' into fix/possible-side-effects-flow
cieplypolar Jul 7, 2026
bbb2dfc
final polish
cieplypolar Jul 7, 2026
6d5ee16
relict of pullfrog
cieplypolar Jul 7, 2026
731cb19
review changes
cieplypolar Jul 7, 2026
1bc02b3
more pull frog relicts
cieplypolar Jul 7, 2026
c6ee8ec
accessor dualFn test
cieplypolar Jul 7, 2026
ad575e8
review changes
cieplypolar Jul 7, 2026
81a30b1
oxlint
cieplypolar Jul 7, 2026
b58883a
Merge branch 'main' into fix/possible-side-effects-flow
cieplypolar Jul 8, 2026
f62da06
Merge branch 'main' into fix/possible-side-effects-flow
cieplypolar Jul 8, 2026
df3997a
review changes
cieplypolar Jul 8, 2026
f5a7400
Merge branch 'main' into fix/unify-test-in-ternary
cieplypolar Jul 8, 2026
23d4955
nr style
cieplypolar Jul 7, 2026
36dd367
review fixes
cieplypolar Jul 8, 2026
f91c49e
review changes
cieplypolar Jul 14, 2026
14ca77f
review fixes
cieplypolar Jul 14, 2026
4e94946
Merge branch 'fix/possible-side-effects-flow' into fix/unify-test-in-…
cieplypolar Jul 14, 2026
ffdb6f6
handling undefined
cieplypolar Jul 14, 2026
53db4a4
small
cieplypolar Jul 14, 2026
67fd71e
small
cieplypolar Jul 15, 2026
a6688d2
Merge branch 'main' into fix/unify-test-in-ternary
cieplypolar Jul 15, 2026
29819e6
review fixes
cieplypolar Jul 15, 2026
40c861c
Merge branch 'main' into fix/unify-test-in-ternary
cieplypolar Jul 16, 2026
145fbda
Merge branch 'main' into fix/unify-test-in-ternary
cieplypolar Jul 20, 2026
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
131 changes: 98 additions & 33 deletions packages/typegpu/src/tgsl/wgslGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { $gpuCallable, $internal, $providing, isMarkedInternal } from '../shared
import { safeStringify } from '../shared/stringify.ts';
import { pow } from '../std/numeric.ts';
import { add, div, mul, neg, sub } from '../std/operators.ts';
import { eq, ne, lt, le, gt, ge } from '../std/boolean.ts';

import {
isGPUCallable,
isKnownAtComptime,
Expand Down Expand Up @@ -80,11 +82,17 @@ const parenthesizedOps = [
'|',
'^',
'&',
'&&',
'||',
];

const binaryLogicalOps = ['&&', '||', '==', '!=', '===', '!==', '<', '<=', '>', '>='];
const binaryRelationalOpToStdMap: Record<string, string> = {
'===': eq.toString(),
'!==': ne.toString(),
'<': lt.toString(),
'<=': le.toString(),
'>': gt.toString(),
'>=': ge.toString(),
};

const bitShiftOps: string[] = ['<<', '>>', '<<=', '>>='];

Expand Down Expand Up @@ -332,39 +340,75 @@ ${this.ctx.pre}}`;
return snip(expression, bool, /* origin */ 'constant', false);
}

if (
expression[0] === NODE.logicalExpr ||
expression[0] === NODE.binaryExpr ||
expression[0] === NODE.assignmentExpr
) {
// Logical/Binary/Assignment Expression
const [exprType, lhs, op, rhs] = expression;
if (expression[0] === NODE.logicalExpr) {
const [_, lhs, op, rhs] = expression;
const lhsExpr = this._expression(lhs);

// Short Circuit Evaluation
if ((op === '||' || op === '&&') && isKnownAtComptime(lhsExpr)) {
if (isKnownAtComptime(lhsExpr)) {
const castToBool = wgsl.isBool(this.ctx.expectedType);
const evalRhs = op === '&&' ? lhsExpr.value : !lhsExpr.value;

if (!evalRhs) {
return snip(op === '||', bool, 'constant', false);
return castToBool
? snip(op === '||', bool, 'constant', false)
: coerceToSnippet(lhsExpr.value);
}

const rhsExpr = this._expression(rhs);

if (rhsExpr.dataType === UnknownData) {
throw new WgslTypeError(`Right-hand side of '${op}' is of unknown type`);
if (isKnownAtComptime(rhsExpr)) {
return castToBool
? snip(!!rhsExpr.value, bool, 'constant', false)
: coerceToSnippet(rhsExpr.value);
}

if (isKnownAtComptime(rhsExpr)) {
return snip(!!rhsExpr.value, bool, 'constant', false);
if (rhsExpr.dataType === UnknownData) {
throw new WgslTypeError(`Right-hand side of '${op}' is of unknown type`);
}

// we can skip lhs
const convRhs = tryConvertSnippet(this.ctx, rhsExpr, bool, false);
const rhsStr = this.ctx.resolveSnippet(convRhs).value;
return snip(rhsStr, bool, 'runtime', convRhs.possibleSideEffects);
return rhsExpr;
}

const rhsExpr = this._expression(rhs);

// they are not known at comptime
if (lhsExpr.dataType === UnknownData) {
throw new WgslTypeError(`Left-hand side of '${op}' is of unknown type`);
}

if (!isKnownAtComptime(rhsExpr) && rhsExpr.dataType === UnknownData) {
throw new WgslTypeError(`Right-hand side of '${op}' is of unknown type`);
}

const [convLhs, convRhs] = convertToCommonType(this.ctx, [lhsExpr, rhsExpr], [bool]) ?? [
lhsExpr,
rhsExpr,
];

if (!wgsl.isBool(convLhs.dataType) || !wgsl.isBool(convRhs.dataType)) {
Comment thread
cieplypolar marked this conversation as resolved.
throw new WgslTypeError(
`Logical expression '${op}' requires boolean operands. Got '${String(convLhs.dataType)}' and '${String(convRhs.dataType)}'.`,
);
}
Comment thread
cieplypolar marked this conversation as resolved.

const lhsStr = this.ctx.resolve(convLhs.value, convLhs.dataType).value;
const rhsStr = this.ctx.resolve(convRhs.value, convRhs.dataType).value;

// hardcoded parentheses - operators not present in `parenthesizedOps`
return snip(
`(${lhsStr} ${op} ${rhsStr})`,
bool,
'runtime',
lhsExpr.possibleSideEffects || rhsExpr.possibleSideEffects,
);
}

if (expression[0] === NODE.binaryExpr || expression[0] === NODE.assignmentExpr) {
// Binary/Assignment Expression
const [exprType, lhs, op, rhs] = expression;
const lhsExpr = this._expression(lhs);
const rhsExpr = this._expression(rhs);

if (rhsExpr.value instanceof RefOperator) {
Expand All @@ -381,24 +425,26 @@ ${this.ctx.pre}}`;
throw new Error('Please use the !== operator instead of !=');
}

if (op === '===' && isKnownAtComptime(lhsExpr) && isKnownAtComptime(rhsExpr)) {
return snip(lhsExpr.value === rhsExpr.value, bool, 'constant', false);
}

if (op === '!==' && isKnownAtComptime(lhsExpr) && isKnownAtComptime(rhsExpr)) {
return snip(lhsExpr.value !== rhsExpr.value, bool, 'constant', false);
}

if (
(op === '<' || op === '<=' || op === '>' || op === '>=') &&
isKnownAtComptime(lhsExpr) &&
isKnownAtComptime(rhsExpr)
) {
const stdBinaryRelationalOp = binaryRelationalOpToStdMap[op];
if (stdBinaryRelationalOp && isKnownAtComptime(lhsExpr) && isKnownAtComptime(rhsExpr)) {
const left = lhsExpr.value;
const right = rhsExpr.value;

switch (op) {
case '===':
return snip(left === right, bool, 'constant', false);
case '!==':
return snip(left !== right, bool, 'constant', false);
}

if (typeof left !== 'number' || typeof right !== 'number') {
const bothVectors = wgsl.isVec(lhsExpr.dataType) && wgsl.isVec(rhsExpr.dataType);
throw new WgslTypeError(
`Inequality comparison '${op}' requires numeric operands, got '${typeof left}' and '${typeof right}'`,
`Comparison '${op}' requires numeric operands.${
bothVectors
? ` For component-wise comparison, use 'std.${stdBinaryRelationalOp}'.`
: ''
}`,
);
}

Expand Down Expand Up @@ -467,6 +513,24 @@ ${this.ctx.pre}}`;
}
}

if (stdBinaryRelationalOp) {
const equalityCheck = ['===', '!=='].includes(op);
const correctOperandTypes =
(wgsl.isNumericSchema(convLhs.dataType) && wgsl.isNumericSchema(convRhs.dataType)) ||
(equalityCheck && wgsl.isBool(convLhs.dataType) && wgsl.isBool(convRhs.dataType));

if (!correctOperandTypes) {
const bothVectors = wgsl.isVec(convLhs.dataType) && wgsl.isVec(convRhs.dataType);
throw new WgslTypeError(
`Comparison '${op}' requires numeric${equalityCheck ? ' or boolean' : ''} operands. Got '${String(convLhs.dataType)}' and '${String(convRhs.dataType)}'.${
bothVectors
? ` For component-wise comparison, use 'std.${stdBinaryRelationalOp}'.`
: ''
}`,
);
}
}

return snip(
parenthesizedOps.includes(op)
? `(${lhsStr} ${OP_MAP[op] ?? op} ${rhsStr})`
Expand Down Expand Up @@ -876,6 +940,7 @@ ${this.ctx.pre}}`;
if (isKnownAtComptime(test)) {
return test.value ? this._expression(consequentNode) : this._expression(alternativeNode);
} else {
const convertedTest = tryConvertSnippet(this.ctx, test, bool, false);
const consequent = this._expression(consequentNode);
const alternative = this._expression(alternativeNode);
const [con, alt] =
Expand All @@ -888,7 +953,7 @@ ${this.ctx.pre}}`;
}

return snip(
stitch`select(${alt}, ${con}, ${test})`,
stitch`select(${alt}, ${con}, ${convertedTest})`,
con.dataType,
'runtime',
// this select has side-effects only if the condition has side-effects
Expand Down
Loading
Loading