diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index 711ab807c9..1ad3cd1106 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -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, @@ -80,11 +82,17 @@ const parenthesizedOps = [ '|', '^', '&', - '&&', - '||', ]; const binaryLogicalOps = ['&&', '||', '==', '!=', '===', '!==', '<', '<=', '>', '>=']; +const binaryRelationalOpToStdMap: Record = { + '===': eq.toString(), + '!==': ne.toString(), + '<': lt.toString(), + '<=': le.toString(), + '>': gt.toString(), + '>=': ge.toString(), +}; const bitShiftOps: string[] = ['<<', '>>', '<<=', '>>=']; @@ -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)) { + throw new WgslTypeError( + `Logical expression '${op}' requires boolean operands. Got '${String(convLhs.dataType)}' and '${String(convRhs.dataType)}'.`, + ); } + 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) { @@ -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}'.` + : '' + }`, ); } @@ -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})` @@ -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] = @@ -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 diff --git a/packages/typegpu/tests/tgsl/binaryLogicalOps.test.ts b/packages/typegpu/tests/tgsl/binaryLogicalOps.test.ts new file mode 100644 index 0000000000..b9852304a4 --- /dev/null +++ b/packages/typegpu/tests/tgsl/binaryLogicalOps.test.ts @@ -0,0 +1,701 @@ +import { expect, describe, beforeEach } from 'vitest'; +import { it } from 'typegpu-testing-utility'; +import { tgpu, d } from 'typegpu'; + +describe('binaryLogicalOps', () => { + const Boid = d.struct({ pos: d.vec3f }); + const BoidOnSteroids = d.struct({ pos: d.vec3f, strength: d.f32 }); + + describe('relational', () => { + describe('comptime', () => { + it('handles numeric', () => { + const x = 7 as number; + const y = 8 as number; + + const f = () => { + 'use gpu'; + let r = true; + r = x === y; + r = x !== y; + r = x < y; + r = x <= y; + r = x > y; + r = x >= y; + }; + + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "fn f() { + var r = true; + r = false; + r = true; + r = true; + r = true; + r = false; + r = false; + }" + `); + }); + + it('equality comparison handles non numeric operands', () => { + const x = Boid(); + const y = BoidOnSteroids(); + + const eq = () => { + 'use gpu'; + const _r = x === y; + }; + const ne = () => { + 'use gpu'; + const _r = x !== y; + }; + + expect(tgpu.resolve([eq])).toMatchInlineSnapshot(` + "fn eq() { + const _r = false; + }" + `); + expect(tgpu.resolve([ne])).toMatchInlineSnapshot(` + "fn ne() { + const _r = true; + }" + `); + }); + + it('throws when both operands are not numeric', () => { + const x = Boid(); + const y = BoidOnSteroids(); + + const eq = () => { + 'use gpu'; + const _r = x === y; + }; + const ne = () => { + 'use gpu'; + const _r = x !== y; + }; + const lt = () => { + 'use gpu'; + const _r = x < y; + }; + const le = () => { + 'use gpu'; + const _r = x <= y; + }; + const gt = () => { + 'use gpu'; + const _r = x > y; + }; + const ge = () => { + 'use gpu'; + const _r = x >= y; + }; + + expect(() => tgpu.resolve([lt])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:lt + - fn*:lt(): Comparison '<' requires numeric operands.] + `); + expect(() => tgpu.resolve([le])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:le + - fn*:le(): Comparison '<=' requires numeric operands.] + `); + expect(() => tgpu.resolve([gt])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:gt + - fn*:gt(): Comparison '>' requires numeric operands.] + `); + expect(() => tgpu.resolve([ge])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:ge + - fn*:ge(): Comparison '>=' requires numeric operands.] + `); + }); + + it('when both operands are vectors suggests std function', () => { + const x = d.vec3f(); + const y = x; + + const f = () => { + 'use gpu'; + return x >= y; + }; + + expect(() => tgpu.resolve([f])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:f + - fn*:f(): Comparison '>=' requires numeric operands. For component-wise comparison, use 'std.ge'.] + `); + }); + }); + + describe('runtime', () => { + it('handles numeric', () => { + const x = 7 as number; + + const f = tgpu.fn([d.i32])((y) => { + 'use gpu'; + let r = true; + r = x === y; + r = x !== y; + r = x < y; + r = x <= y; + r = x > y; + r = x >= y; + }); + + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "fn f(y: i32) { + var r = true; + r = (7i == y); + r = (7i != y); + r = (7i < y); + r = (7i <= y); + r = (7i > y); + r = (7i >= y); + }" + `); + }); + + it('equality comparison handles boolean operands', () => { + const a = false; + const cAccessor = tgpu.accessor(d.bool, () => true); + const f = tgpu.fn([d.bool])((b) => { + 'use gpu'; + let r = true; + r = cAccessor.$ === b; + r = a !== b; + }); + + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "fn f(b: bool) { + var r = true; + r = (true == b); + r = (false != b); + }" + `); + }); + + it('throws when both operands are not numeric', () => { + const xAccessor = tgpu.accessor(Boid, () => Boid()); + + const eq = tgpu.fn([BoidOnSteroids])((y) => { + 'use gpu'; + const _r = xAccessor.$ === Boid(y); + }); + const ne = tgpu.fn([BoidOnSteroids])((y) => { + 'use gpu'; + const _r = xAccessor.$ !== Boid(y); + }); + const lt = tgpu.fn([BoidOnSteroids])((y) => { + 'use gpu'; + const _r = xAccessor.$ < Boid(y); + }); + const le = tgpu.fn([BoidOnSteroids])((y) => { + 'use gpu'; + const _r = xAccessor.$ <= Boid(y); + }); + const gt = tgpu.fn([BoidOnSteroids])((y) => { + 'use gpu'; + const _r = xAccessor.$ > Boid(y); + }); + const ge = tgpu.fn([BoidOnSteroids])((y) => { + 'use gpu'; + const _r = xAccessor.$ >= Boid(y); + }); + + expect(() => tgpu.resolve([eq])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn:eq: Comparison '===' requires numeric or boolean operands. Got 'struct:Boid' and 'struct:Boid'.] + `); + expect(() => tgpu.resolve([ne])).toThrowErrorMatchingInlineSnapshot( + ` + [Error: Resolution of the following tree failed: + - + - fn:ne: Comparison '!==' requires numeric or boolean operands. Got 'struct:Boid' and 'struct:Boid'.] + `, + ); + expect(() => tgpu.resolve([lt])).toThrowErrorMatchingInlineSnapshot( + ` + [Error: Resolution of the following tree failed: + - + - fn:lt: Comparison '<' requires numeric operands. Got 'struct:Boid' and 'struct:Boid'.] + `, + ); + expect(() => tgpu.resolve([le])).toThrowErrorMatchingInlineSnapshot( + ` + [Error: Resolution of the following tree failed: + - + - fn:le: Comparison '<=' requires numeric operands. Got 'struct:Boid' and 'struct:Boid'.] + `, + ); + expect(() => tgpu.resolve([gt])).toThrowErrorMatchingInlineSnapshot( + ` + [Error: Resolution of the following tree failed: + - + - fn:gt: Comparison '>' requires numeric operands. Got 'struct:Boid' and 'struct:Boid'.] + `, + ); + expect(() => tgpu.resolve([ge])).toThrowErrorMatchingInlineSnapshot( + ` + [Error: Resolution of the following tree failed: + - + - fn:ge: Comparison '>=' requires numeric operands. Got 'struct:Boid' and 'struct:Boid'.] + `, + ); + }); + + it('when both operands are vectors suggests std function', () => { + const x = d.vec3f(); + + const f = tgpu.fn([d.vec3f])((y) => { + 'use gpu'; + return x === y; + }); + + expect(() => tgpu.resolve([f])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn:f: Comparison '===' requires numeric or boolean operands. Got 'vec3f' and 'vec3f'. For component-wise comparison, use 'std.eq'.] + `); + }); + }); + }); + + describe('operator && runtime operands', () => { + it('handles boolean operands', () => { + const and = tgpu.fn( + [d.bool, d.bool], + d.bool, + )((x, y) => { + 'use gpu'; + return x && y; + }); + + expect(tgpu.resolve([and])).toMatchInlineSnapshot(` + "fn and(x: bool, y: bool) -> bool { + return (x && y); + }" + `); + }); + + it('handles ref', () => { + const and = () => { + 'use gpu'; + const a = d.ref(false); + const b = d.ref(true); + return !!(a.$ && b.$); + }; + + expect(tgpu.resolve([and])).toMatchInlineSnapshot(` + "fn and() -> bool { + var a = false; + var b = true; + return !!(a && b); + }" + `); + }); + + it('throws when both operands are not boolean', () => { + const and = tgpu.fn( + [d.u32, Boid], + d.bool, + )((x, y) => { + 'use gpu'; + return !!(x && y); + }); + + expect(() => tgpu.resolve([and])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn:and: Logical expression '&&' requires boolean operands. Got 'u32' and 'struct:Boid'.] + `); + }); + }); + + describe('operator || runtime operands', () => { + it('handles boolean operands', () => { + const or = tgpu.fn( + [d.bool, d.bool], + d.bool, + )((x, y) => { + 'use gpu'; + return x || y; + }); + + expect(tgpu.resolve([or])).toMatchInlineSnapshot(` + "fn or(x: bool, y: bool) -> bool { + return (x || y); + }" + `); + }); + + it('handles ref', () => { + const or = () => { + 'use gpu'; + const a = d.ref(false); + const b = d.ref(true); + return !!(a.$ || b.$); + }; + + expect(tgpu.resolve([or])).toMatchInlineSnapshot(` + "fn or() -> bool { + var a = false; + var b = true; + return !!(a || b); + }" + `); + }); + + it('throws when both operands are not boolean', () => { + const or = tgpu.fn( + [d.u32, Boid], + d.bool, + )((x, y) => { + 'use gpu'; + return !!(x || y); + }); + + expect(() => tgpu.resolve([or])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn:or: Logical expression '||' requires boolean operands. Got 'u32' and 'struct:Boid'.] + `); + }); + }); + + describe('short-circuit evaluation', () => { + const state = { counter: 0, result: true }; + const getTrackedBool = tgpu.comptime(() => { + state.counter++; + return state.result; + }); + beforeEach(() => { + state.counter = 0; + state.result = true; + }); + + describe('inside condition', () => { + it('handles ||', () => { + const f = () => { + 'use gpu'; + let res = -1; + // oxlint-disable-next-line(no-constant-binary-expression) -- part of the test + if (true || getTrackedBool()) { + res = 1; + } + return res; + }; + + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "fn f() -> i32 { + var res = -1; + { + res = 1i; + } + return res; + }" + `); + expect(state.counter).toBe(0); + }); + + it('handles chained ||', () => { + state.result = false; + + const f = () => { + 'use gpu'; + let res = -1; + + if ( + // oxlint-disable-next-line(no-constant-binary-expression) -- part of the test + getTrackedBool() || + true || + getTrackedBool() || + getTrackedBool() || + getTrackedBool() + ) { + res = 1; + } + return res; + }; + + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "fn f() -> i32 { + var res = -1; + { + res = 1i; + } + return res; + }" + `); + expect(state.counter).toEqual(1); + }); + + it('skips false lhs', () => { + const f = tgpu.fn( + [d.bool], + d.i32, + )((b) => { + 'use gpu'; + let res = -1; + // oxlint-disable-next-line(no-constant-binary-expression) -- part of the test + if (false || b) { + res = 1; + } + return res; + }); + + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "fn f(b: bool) -> i32 { + var res = -1; + if (b) { + res = 1i; + } + return res; + }" + `); + }); + + it('throws when rhs cannot be converted to boolean', () => { + const b = false; + const f = tgpu.fn( + [d.vec3f], + d.i32, + )((v) => { + 'use gpu'; + let res = -1; + if (b || v) { + res = 1; + } + return res; + }); + + expect(() => tgpu.resolve([f])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn:f: Cannot convert value of type 'vec3f' to any of the target types: [bool]] + `); + }); + + it('handles &&', () => { + const f = () => { + 'use gpu'; + let res = -1; + // oxlint-disable-next-line(no-constant-binary-expression) -- part of the test + if (false && getTrackedBool()) { + res = 1; + } + return res; + }; + + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "fn f() -> i32 { + let res = -1; + return res; + }" + `); + expect(state.counter).toBe(0); + }); + + it('handles chained &&', () => { + const f = () => { + 'use gpu'; + let res = -1; + if ( + // oxlint-disable-next-line(no-constant-binary-expression) -- part of the test + getTrackedBool() && + false && + getTrackedBool() && + getTrackedBool() && + getTrackedBool() + ) { + res = 1; + } + return res; + }; + + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "fn f() -> i32 { + let res = -1; + return res; + }" + `); + expect(state.counter).toBe(1); + }); + + it('skips true lhs', () => { + const f = tgpu.fn( + [d.bool], + d.i32, + )((b) => { + 'use gpu'; + let res = -1; + // oxlint-disable-next-line(no-constant-binary-expression) -- part of the test + if (true && b) { + res = 1; + } + return res; + }); + + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "fn f(b: bool) -> i32 { + var res = -1; + if (b) { + res = 1i; + } + return res; + }" + `); + }); + + it('throws when rhs cannot be converted to boolean', () => { + const b = true; + const f = tgpu.fn( + [d.vec3f], + d.i32, + )((v) => { + 'use gpu'; + let res = -1; + if (b && v) { + res = 1; + } + return res; + }); + + expect(() => tgpu.resolve([f])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn:f: Cannot convert value of type 'vec3f' to any of the target types: [bool]] + `); + }); + + it('handles mixed operators', () => { + const f = () => { + 'use gpu'; + let res = -1; + // oxlint-disable-next-line(no-constant-binary-expression) -- part of the test + if (true || (getTrackedBool() && getTrackedBool())) { + res = 1; + } + return res; + }; + + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "fn f() -> i32 { + var res = -1; + { + res = 1i; + } + return res; + }" + `); + expect(state.counter).toBe(0); + }); + }); + describe('outside condition', () => { + it('operator && returns comptime lhs if it is false', () => { + const x = 0; + const v = d.vec3f(); + + const f = () => { + 'use gpu'; + const y = x && v; + }; + + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "fn f() { + const y = 0; + }" + `); + }); + + it('operator && returns comptime rhs if lhs is true', () => { + const x = 1; + const v = d.vec3f(); + + const f = () => { + 'use gpu'; + const y = x && v; + }; + + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "fn f() { + let y = vec3f(); + }" + `); + }); + + it('operator && returns runtime rhs if lhs is true', () => { + const x = 1; + + const f = () => { + 'use gpu'; + const v = d.vec3f(); + const y = x && v; + }; + + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "fn f() { + let v = vec3f(); + let y = (&v); + }" + `); + }); + + it('operator || returns comptime lhs if it is true', () => { + const x = 1; + const v = d.vec3f(); + + const f = () => { + 'use gpu'; + const y = x || v; + }; + + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "fn f() { + const y = 1; + }" + `); + }); + + it('operator || returns comptime rhs if lhs is false', () => { + const x = 0; + const v = d.vec3f(); + + const f = () => { + 'use gpu'; + const y = x || v; + }; + + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "fn f() { + let y = vec3f(); + }" + `); + }); + + it('operator || returns runtime rhs if lhs is false', () => { + const x = 0; + + const f = () => { + 'use gpu'; + const v = d.vec3f(); + const y = x || v; + }; + + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "fn f() { + let v = vec3f(); + let y = (&v); + }" + `); + }); + }); + }); +}); diff --git a/packages/typegpu/tests/tgsl/sideEffects.test.ts b/packages/typegpu/tests/tgsl/sideEffects.test.ts index e6ca250fbc..eed9425a3b 100644 --- a/packages/typegpu/tests/tgsl/sideEffects.test.ts +++ b/packages/typegpu/tests/tgsl/sideEffects.test.ts @@ -486,6 +486,22 @@ describe('code without side-effects', () => { return !impureStruct(); }).toEqual(false); }); + + test('operators && and || with pure runtime operands', () => { + expectSideEffects(() => { + 'use gpu'; + const b1 = false; + const b2 = true; + return b1 && b2; + }).toEqual(false); + + expectSideEffects(() => { + 'use gpu'; + const b1 = false; + const b2 = true; + return b1 || b2; + }).toEqual(false); + }); }); describe('code with side-effects', () => { @@ -621,4 +637,18 @@ describe('code with side-effects', () => { return arr.$[impureInt()]; }).toEqual(true); }); + + test('operators && and || with impure runtime operand', () => { + expectSideEffects(() => { + 'use gpu'; + const b1 = false; + return b1 && impureBool(); + }).toEqual(true); + + expectSideEffects(() => { + 'use gpu'; + const b1 = false; + return impureBool() || b1; + }).toEqual(true); + }); }); diff --git a/packages/typegpu/tests/tgsl/ternaryOperator.test.ts b/packages/typegpu/tests/tgsl/ternaryOperator.test.ts index f6fa6bf61b..796d277dac 100644 --- a/packages/typegpu/tests/tgsl/ternaryOperator.test.ts +++ b/packages/typegpu/tests/tgsl/ternaryOperator.test.ts @@ -181,6 +181,21 @@ describe('ternary operator', () => { `); }); + it('should throw when condition cannot be converted to bool', () => { + const myFn = tgpu.fn( + [d.vec3f, d.u32], + d.u32, + )((v, n) => { + return v ? n : n + 1; + }); + + expect(() => tgpu.resolve([myFn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn:myFn: Cannot convert value of type 'vec3f' to any of the target types: [bool]] + `); + }); + it('should generate select() for runtime condition with function params', () => { const myFn = tgpu.fn( [d.i32], diff --git a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts index 828f40646c..48af1fa33e 100644 --- a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts +++ b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts @@ -1986,203 +1986,4 @@ describe('wgslGenerator', () => { - fn:fn3: Value 'NaN' (abstractFloat) cannot be resolved due to WGSL's Finite Math Assumption (see: https://www.w3.org/TR/WGSL/#finite-math-assumption). This value might be a result of a comptime-evaluated operation.] `); }); - - describe('short-circuit evaluation', () => { - const state = { - counter: 0, - result: true, - }; - - const getTrackedBool = tgpu.comptime(() => { - state.counter++; - return state.result; - }); - - beforeEach(() => { - state.counter = 0; - state.result = true; - }); - - it('handles `||`', () => { - const f = () => { - 'use gpu'; - let res = -1; - // oxlint-disable-next-line(no-constant-binary-expression) -- part of the test - if (true || getTrackedBool()) { - res = 1; - } - return res; - }; - - expect(tgpu.resolve([f])).toMatchInlineSnapshot(` - "fn f() -> i32 { - var res = -1; - { - res = 1i; - } - return res; - }" - `); - expect(state.counter).toBe(0); - }); - - it('handles `&&`', () => { - const f = () => { - 'use gpu'; - let res = -1; - // oxlint-disable-next-line(no-constant-binary-expression) -- part of the test - if (false && getTrackedBool()) { - res = 1; - } - return res; - }; - - expect(tgpu.resolve([f])).toMatchInlineSnapshot(` - "fn f() -> i32 { - let res = -1; - return res; - }" - `); - expect(state.counter).toBe(0); - }); - - it('handles chained `||`', () => { - state.result = false; - - const f = () => { - 'use gpu'; - let res = -1; - // oxlint-disable-next-line(no-constant-binary-expression) -- part of the test - if (getTrackedBool() || true || getTrackedBool() || getTrackedBool() || getTrackedBool()) { - res = 1; - } - return res; - }; - - expect(tgpu.resolve([f])).toMatchInlineSnapshot(` - "fn f() -> i32 { - var res = -1; - { - res = 1i; - } - return res; - }" - `); - expect(state.counter).toEqual(1); - }); - - it('handles chained `&&`', () => { - const f = () => { - 'use gpu'; - let res = -1; - // oxlint-disable-next-line(no-constant-binary-expression) -- part of the test - if (getTrackedBool() && false && getTrackedBool() && getTrackedBool() && getTrackedBool()) { - res = 1; - } - return res; - }; - - expect(tgpu.resolve([f])).toMatchInlineSnapshot(` - "fn f() -> i32 { - let res = -1; - return res; - }" - `); - expect(state.counter).toBe(1); - }); - - it('handles mixed logical operators', () => { - const f = () => { - 'use gpu'; - let res = -1; - // oxlint-disable-next-line(no-constant-binary-expression) -- part of the test - if (true || (getTrackedBool() && getTrackedBool())) { - res = 1; - } - return res; - }; - - expect(tgpu.resolve([f])).toMatchInlineSnapshot(` - "fn f() -> i32 { - var res = -1; - { - res = 1i; - } - return res; - }" - `); - expect(state.counter).toBe(0); - }); - - it('skips lhs if known at compile time', () => { - const f1 = tgpu.fn( - [d.bool], - d.i32, - )((b) => { - 'use gpu'; - let res = -1; - // oxlint-disable-next-line(no-constant-binary-expression) -- part of the test - if (false || b) { - res = 1; - } - return res; - }); - - const f2 = tgpu.fn( - [d.bool], - d.i32, - )((b) => { - 'use gpu'; - let res = -1; - // oxlint-disable-next-line(no-constant-binary-expression) -- part of the test - if (true && b) { - res = 1; - } - return res; - }); - - expect(tgpu.resolve([f1, f2])).toMatchInlineSnapshot(` - "fn f1(b: bool) -> i32 { - var res = -1; - if (b) { - res = 1i; - } - return res; - } - - fn f2(b: bool) -> i32 { - var res = -1; - if (b) { - res = 1i; - } - return res; - }" - `); - }); - }); - - it('allows a for-loop variable to reuse an external name without shadowing it after the loop', () => { - const size = tgpu.privateVar(d.u32, 4); - - function foo() { - 'use gpu'; - let acc = d.u32(0); - for (let size = d.u32(0); size < 3; size++) { - acc += size; - } - return acc + size.$; - } - - expect(tgpu.resolve([foo])).toMatchInlineSnapshot(` - "var size: u32 = 4u; - - fn foo() -> u32 { - var acc = 0u; - for (var size = 0u; (size < 3u); size++) { - acc += size; - } - return (acc + size); - }" - `); - }); });