Skip to content

Commit a8e4c49

Browse files
authored
impr: Short-circuit evaluation for operators && and || (#2361)
1 parent 711297b commit a8e4c49

3 files changed

Lines changed: 323 additions & 21 deletions

File tree

packages/typegpu/src/tgsl/wgslGenerator.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,31 @@ ${this.ctx.pre}}`;
352352
// Logical/Binary/Assignment Expression
353353
const [exprType, lhs, op, rhs] = expression;
354354
const lhsExpr = this._expression(lhs);
355+
356+
// Short Circuit Evaluation
357+
if ((op === '||' || op === '&&') && isKnownAtComptime(lhsExpr)) {
358+
const evalRhs = op === '&&' ? lhsExpr.value : !lhsExpr.value;
359+
360+
if (!evalRhs) {
361+
return snip(op === '||', bool, 'constant');
362+
}
363+
364+
const rhsExpr = this._expression(rhs);
365+
366+
if (rhsExpr.dataType === UnknownData) {
367+
throw new WgslTypeError(`Right-hand side of '${op}' is of unknown type`);
368+
}
369+
370+
if (isKnownAtComptime(rhsExpr)) {
371+
return snip(!!rhsExpr.value, bool, 'constant');
372+
}
373+
374+
// we can skip lhs
375+
const convRhs = tryConvertSnippet(this.ctx, rhsExpr, bool, false);
376+
const rhsStr = this.ctx.resolve(convRhs.value, convRhs.dataType).value;
377+
return snip(rhsStr, bool, 'runtime');
378+
}
379+
355380
const rhsExpr = this._expression(rhs);
356381

357382
if (rhsExpr.value instanceof RefOperator) {

packages/typegpu/tests/std/boolean/not.test.ts

Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,6 @@ describe('not', () => {
121121

122122
expect(tgpu.resolve([f])).toMatchInlineSnapshot(`
123123
"fn f() -> i32 {
124-
if (((false && true) && false)) {
125-
return 1;
126-
}
127124
return -1;
128125
}"
129126
`);
@@ -153,4 +150,110 @@ describe('not', () => {
153150
}"
154151
`);
155152
});
153+
154+
it('converts numeric vectors to boolean vectors and negates component-wise', () => {
155+
expect(not(d.vec2f(0.0, 1.0))).toStrictEqual(d.vec2b(true, false));
156+
expect(not(d.vec3i(0, 5, -1))).toStrictEqual(d.vec3b(true, false, false));
157+
expect(not(d.vec4u(0, 0, 1, 0))).toStrictEqual(d.vec4b(true, true, false, true));
158+
expect(not(d.vec4h(0, 3.14, 0, -2.5))).toStrictEqual(d.vec4b(true, false, true, false));
159+
});
160+
161+
it('negates truthiness check', () => {
162+
const s = {};
163+
expect(not(null)).toBe(true);
164+
expect(not(undefined)).toBe(true);
165+
expect(not(s)).toBe(false);
166+
});
167+
168+
it('mimics WGSL behavior on NaN', () => {
169+
expect(not(NaN)).toBe(false);
170+
});
171+
172+
it('generates correct WGSL on a boolean runtime-known argument', () => {
173+
const testFn = tgpu.fn(
174+
[d.bool],
175+
d.bool,
176+
)((v) => {
177+
return not(v);
178+
});
179+
expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(`
180+
"fn testFn(v: bool) -> bool {
181+
return !v;
182+
}"
183+
`);
184+
});
185+
186+
it('generates correct WGSL on a numeric runtime-known argument', () => {
187+
const testFn = tgpu.fn(
188+
[d.i32],
189+
d.bool,
190+
)((v) => {
191+
return not(v);
192+
});
193+
expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(`
194+
"fn testFn(v: i32) -> bool {
195+
return !bool(v);
196+
}"
197+
`);
198+
});
199+
200+
it('generates correct WGSL on a boolean vector runtime-known argument', () => {
201+
const testFn = tgpu.fn(
202+
[d.vec3b],
203+
d.vec3b,
204+
)((v) => {
205+
return not(v);
206+
});
207+
expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(`
208+
"fn testFn(v: vec3<bool>) -> vec3<bool> {
209+
return !(v);
210+
}"
211+
`);
212+
});
213+
214+
it('generates correct WGSL on a numeric vector runtime-known argument', () => {
215+
const testFn = tgpu.fn(
216+
[d.vec3f],
217+
d.vec3b,
218+
)((v) => {
219+
return not(v);
220+
});
221+
expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(`
222+
"fn testFn(v: vec3f) -> vec3<bool> {
223+
return !(vec3<bool>(v));
224+
}"
225+
`);
226+
});
227+
228+
it('generates correct WGSL on a numeric vector comptime-known argument', () => {
229+
const f = () => {
230+
'use gpu';
231+
const v = not(d.vec4f(Infinity, -Infinity, 0, NaN));
232+
};
233+
234+
expect(tgpu.resolve([f])).toMatchInlineSnapshot(`
235+
"fn f() {
236+
var v = vec4<bool>(false, false, true, false);
237+
}"
238+
`);
239+
});
240+
241+
it('evaluates at compile time for comptime-known arguments', () => {
242+
const getN = tgpu.comptime(() => 42);
243+
const slot = tgpu.slot<{ a?: number }>({});
244+
245+
const f = () => {
246+
'use gpu';
247+
if (not(getN()) && not(slot.$.a) && not(d.vec4f(1, 8, 8, 2)).x) {
248+
return 1;
249+
}
250+
return -1;
251+
};
252+
253+
expect(tgpu.resolve([f])).toMatchInlineSnapshot(`
254+
"fn f() -> i32 {
255+
return -1;
256+
}"
257+
`);
258+
});
156259
});

packages/typegpu/tests/tgsl/wgslGenerator.test.ts

Lines changed: 192 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2139,13 +2139,13 @@ describe('wgslGenerator', () => {
21392139
};
21402140

21412141
expect(tgpu.resolve([f])).toMatchInlineSnapshot(`
2142-
"fn f() -> i32 {
2143-
if ((true || false)) {
2144-
return 1;
2145-
}
2146-
return -1;
2147-
}"
2148-
`);
2142+
"fn f() -> i32 {
2143+
{
2144+
return 1;
2145+
}
2146+
return -1;
2147+
}"
2148+
`);
21492149
});
21502150

21512151
it('handles unary operator `!` on operands from slots and accessors', () => {
@@ -2166,13 +2166,13 @@ describe('wgslGenerator', () => {
21662166
};
21672167

21682168
expect(tgpu.resolve([f])).toMatchInlineSnapshot(`
2169-
"fn f() -> i32 {
2170-
if ((true && true)) {
2171-
return 1;
2172-
}
2173-
return -1;
2174-
}"
2175-
`);
2169+
"fn f() -> i32 {
2170+
{
2171+
return 1;
2172+
}
2173+
return -1;
2174+
}"
2175+
`);
21762176
});
21772177

21782178
it('handles chained unary operators `!`', () => {
@@ -2185,10 +2185,10 @@ describe('wgslGenerator', () => {
21852185
});
21862186

21872187
expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(`
2188-
"fn testFn(n: i32) -> bool {
2189-
return (true || !!!bool(n));
2190-
}"
2191-
`);
2188+
"fn testFn(n: i32) -> bool {
2189+
return true;
2190+
}"
2191+
`);
21922192
});
21932193

21942194
it('handles unary operator `!` on complex comptime-known operand', () => {
@@ -2209,4 +2209,178 @@ describe('wgslGenerator', () => {
22092209
}"
22102210
`);
22112211
});
2212+
2213+
describe('short-circuit evaluation', () => {
2214+
const state = {
2215+
counter: 0,
2216+
result: true,
2217+
};
2218+
2219+
const getTrackedBool = tgpu.comptime(() => {
2220+
state.counter++;
2221+
return state.result;
2222+
});
2223+
2224+
beforeEach(() => {
2225+
state.counter = 0;
2226+
state.result = true;
2227+
});
2228+
2229+
it('handles `||`', () => {
2230+
const f = () => {
2231+
'use gpu';
2232+
let res = -1;
2233+
// oxlint-disable-next-line(no-constant-binary-expression) -- part of the test
2234+
if (true || getTrackedBool()) {
2235+
res = 1;
2236+
}
2237+
return res;
2238+
};
2239+
2240+
expect(tgpu.resolve([f])).toMatchInlineSnapshot(`
2241+
"fn f() -> i32 {
2242+
var res = -1;
2243+
{
2244+
res = 1i;
2245+
}
2246+
return res;
2247+
}"
2248+
`);
2249+
expect(state.counter).toBe(0);
2250+
});
2251+
2252+
it('handles `&&`', () => {
2253+
const f = () => {
2254+
'use gpu';
2255+
let res = -1;
2256+
// oxlint-disable-next-line(no-constant-binary-expression) -- part of the test
2257+
if (false && getTrackedBool()) {
2258+
res = 1;
2259+
}
2260+
return res;
2261+
};
2262+
2263+
expect(tgpu.resolve([f])).toMatchInlineSnapshot(`
2264+
"fn f() -> i32 {
2265+
var res = -1;
2266+
return res;
2267+
}"
2268+
`);
2269+
expect(state.counter).toBe(0);
2270+
});
2271+
2272+
it('handles chained `||`', () => {
2273+
state.result = false;
2274+
2275+
const f = () => {
2276+
'use gpu';
2277+
let res = -1;
2278+
// oxlint-disable-next-line(no-constant-binary-expression) -- part of the test
2279+
if (getTrackedBool() || true || getTrackedBool() || getTrackedBool() || getTrackedBool()) {
2280+
res = 1;
2281+
}
2282+
return res;
2283+
};
2284+
2285+
expect(tgpu.resolve([f])).toMatchInlineSnapshot(`
2286+
"fn f() -> i32 {
2287+
var res = -1;
2288+
{
2289+
res = 1i;
2290+
}
2291+
return res;
2292+
}"
2293+
`);
2294+
expect(state.counter).toEqual(1);
2295+
});
2296+
2297+
it('handles chained `&&`', () => {
2298+
const f = () => {
2299+
'use gpu';
2300+
let res = -1;
2301+
// oxlint-disable-next-line(no-constant-binary-expression) -- part of the test
2302+
if (getTrackedBool() && false && getTrackedBool() && getTrackedBool() && getTrackedBool()) {
2303+
res = 1;
2304+
}
2305+
return res;
2306+
};
2307+
2308+
expect(tgpu.resolve([f])).toMatchInlineSnapshot(`
2309+
"fn f() -> i32 {
2310+
var res = -1;
2311+
return res;
2312+
}"
2313+
`);
2314+
expect(state.counter).toBe(1);
2315+
});
2316+
2317+
it('handles mixed logical operators', () => {
2318+
const f = () => {
2319+
'use gpu';
2320+
let res = -1;
2321+
// oxlint-disable-next-line(no-constant-binary-expression) -- part of the test
2322+
if (true || (getTrackedBool() && getTrackedBool())) {
2323+
res = 1;
2324+
}
2325+
return res;
2326+
};
2327+
2328+
expect(tgpu.resolve([f])).toMatchInlineSnapshot(`
2329+
"fn f() -> i32 {
2330+
var res = -1;
2331+
{
2332+
res = 1i;
2333+
}
2334+
return res;
2335+
}"
2336+
`);
2337+
expect(state.counter).toBe(0);
2338+
});
2339+
2340+
it('skips lhs if known at compile time', () => {
2341+
const f1 = tgpu.fn(
2342+
[d.bool],
2343+
d.i32,
2344+
)((b) => {
2345+
'use gpu';
2346+
let res = -1;
2347+
// oxlint-disable-next-line(no-constant-binary-expression) -- part of the test
2348+
if (false || b) {
2349+
res = 1;
2350+
}
2351+
return res;
2352+
});
2353+
2354+
const f2 = tgpu.fn(
2355+
[d.bool],
2356+
d.i32,
2357+
)((b) => {
2358+
'use gpu';
2359+
let res = -1;
2360+
// oxlint-disable-next-line(no-constant-binary-expression) -- part of the test
2361+
if (true && b) {
2362+
res = 1;
2363+
}
2364+
return res;
2365+
});
2366+
2367+
expect(tgpu.resolve([f1, f2])).toMatchInlineSnapshot(`
2368+
"fn f1(b: bool) -> i32 {
2369+
var res = -1;
2370+
if (b) {
2371+
res = 1i;
2372+
}
2373+
return res;
2374+
}
2375+
2376+
fn f2(b: bool) -> i32 {
2377+
var res = -1;
2378+
if (b) {
2379+
res = 1i;
2380+
}
2381+
return res;
2382+
}"
2383+
`);
2384+
});
2385+
});
22122386
});

0 commit comments

Comments
 (0)