From b2c137b03007fd4a66d2d12492db39a03a51cbd1 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Fri, 3 Apr 2026 18:56:51 +0200 Subject: [PATCH 01/15] operator ! --- packages/typegpu/src/data/wgslTypes.ts | 4 + packages/typegpu/src/tgsl/wgslGenerator.ts | 29 +++- .../typegpu/tests/tgsl/wgslGenerator.test.ts | 124 +++++++++++++++++- 3 files changed, 155 insertions(+), 2 deletions(-) diff --git a/packages/typegpu/src/data/wgslTypes.ts b/packages/typegpu/src/data/wgslTypes.ts index 9a71fc252b..fdb69c0fc9 100644 --- a/packages/typegpu/src/data/wgslTypes.ts +++ b/packages/typegpu/src/data/wgslTypes.ts @@ -1700,6 +1700,10 @@ export function isVoid(value: unknown): value is Void { return isMarkedInternal(value) && (value as Void).type === 'void'; } +export function isBool(value: unknown): value is Bool { + return (value as Bool).type === 'bool'; +} + export function isNumericSchema( schema: unknown, ): schema is AbstractInt | AbstractFloat | F32 | F16 | I32 | U32 { diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index 3e48be5795..a58d4f6c47 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -27,7 +27,7 @@ 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 { isGPUCallable, isKnownAtComptime } from '../types.ts'; +import { isGPUCallable, isKnownAtComptime, ResolutionCtx } from '../types.ts'; import { convertStructValues, convertToCommonType, tryConvertSnippet } from './conversion.ts'; import { ArrayExpression, @@ -157,6 +157,33 @@ function operatorToType< const unaryOpCodeToCodegen = { '-': neg[$gpuCallable].call.bind(neg), void: () => snip(undefined, wgsl.Void, 'constant'), + '!': (ctx: ResolutionCtx, [argExpr]: Snippet[]) => { + if (argExpr === undefined) { + throw new Error('The unary operator `!` expects 1 argument, but 0 were provided.'); + } + + if (isKnownAtComptime(argExpr)) { + return snip(!argExpr.value, bool, 'constant'); + } + + const { value, dataType } = argExpr; + const argStr = ctx.resolve(value, dataType).value; + + if (wgsl.isBool(dataType)) { + return snip(`!${argStr}`, bool, 'runtime'); + } + if (wgsl.isNumericSchema(dataType)) { + return snip(`!bool(${argStr})`, bool, 'runtime'); + } + + if (wgsl.isVec(dataType)) { + console.warn('Use `std.not` for the WGSL `!` unary operator `!` on vector types.'); + } + + throw new Error( + `The unary operator \`!\` cannot determine truthiness for runtime value of type: ${ctx.resolve(dataType).value}.`, + ); + }, } satisfies Partial unknown>>; const binaryOpCodeToCodegen = { diff --git a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts index b973015c9e..ae4ac971a5 100644 --- a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts +++ b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts @@ -1,5 +1,5 @@ import * as tinyest from 'tinyest'; -import { beforeEach, describe, expect } from 'vitest'; +import { beforeEach, describe, expect, vi } from 'vitest'; import { namespace } from '../../src/core/resolve/namespace.ts'; import * as d from '../../src/data/index.ts'; import { abstractFloat, abstractInt } from '../../src/data/numeric.ts'; @@ -2068,4 +2068,126 @@ describe('wgslGenerator', () => { }" `); }); + + describe('unary', () => { + it('handles unary operator `!` on boolean runtime-known argument', () => { + const testFn = tgpu.fn( + [d.bool], + d.bool, + )((b) => { + return !b; + }); + + expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` + "fn testFn(b: bool) -> bool { + return !b; + }" + `); + }); + + it('handles unary operator `!` on numeric runtime-known argument', () => { + const testFn = tgpu.fn( + [d.i32], + d.bool, + )((n) => { + return !n; + }); + + expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` + "fn testFn(n: i32) -> bool { + return !bool(n); + }" + `); + }); + + it('warns and throws when cannot determine truthiness in unary operator `!`', ({ root }) => { + using warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + const buffer = root.createUniform(d.mat4x4f); + const testFn1 = tgpu.fn( + [], + d.bool, + )(() => { + return !buffer.$; + }); + expect(() => tgpu.resolve([testFn1])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn:testFn1: The unary operator \`!\` cannot determine truthiness for runtime value of type: mat4x4f.] + `); + expect(warnSpy).not.toHaveBeenCalled(); + + const testFn2 = tgpu.fn( + [d.vec3f], + d.bool, + )((v) => { + return !v; + }); + + expect(() => tgpu.resolve([testFn2])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn:testFn2: The unary operator \`!\` cannot determine truthiness for runtime value of type: vec3f.] + `); + expect(warnSpy.mock.calls[0]![0]).toMatchInlineSnapshot( + `"Use \`std.not\` for the WGSL \`!\` unary operator \`!\` on vector types."`, + ); + }); + + it('handles unary operator `!` on numeric and boolean comptime-known arguments', () => { + const getN = tgpu.comptime(() => 1882); + + const f = () => { + 'use gpu'; + if (!(getN() === 7) || !getN()) { + return 1; + } + return -1; + }; + + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "fn f() -> i32 { + if ((true || false)) { + return 1; + } + return -1; + }" + `); + }); + + it('handles multiple unary operators `!`', () => { + const testFn = tgpu.fn( + [d.i32], + d.bool, + )((n) => { + // oxlint-disable-next-line + return !!!!!false || !!!n; + }); + + expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` + "fn testFn(n: i32) -> bool { + return (true || !!!bool(n)); + }" + `); + }); + + it('handles unary operator `!` on complex comptime-known argument', () => { + const fnSlot = tgpu.slot<{ a?: number }>({}); + + const f = () => { + 'use gpu'; + // oxlint-disable-next-line + if (!!fnSlot.$.a) { + return fnSlot.$.a; + } + return 1929; + }; + + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "fn f() -> i32 { + return 1929; + }" + `); + }); + }); }); From 4f793c1e6d7d1db6abd52fe62f379d107daa1e14 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Tue, 7 Apr 2026 11:51:29 +0200 Subject: [PATCH 02/15] operator ! cleanup --- packages/typegpu/src/data/wgslTypes.ts | 2 +- packages/typegpu/src/tgsl/wgslGenerator.ts | 6 +- .../typegpu/tests/tgsl/wgslGenerator.test.ts | 173 ++++++++++-------- 3 files changed, 104 insertions(+), 77 deletions(-) diff --git a/packages/typegpu/src/data/wgslTypes.ts b/packages/typegpu/src/data/wgslTypes.ts index fdb69c0fc9..935594dc00 100644 --- a/packages/typegpu/src/data/wgslTypes.ts +++ b/packages/typegpu/src/data/wgslTypes.ts @@ -1701,7 +1701,7 @@ export function isVoid(value: unknown): value is Void { } export function isBool(value: unknown): value is Bool { - return (value as Bool).type === 'bool'; + return isMarkedInternal(value) && (value as Bool).type === 'bool'; } export function isNumericSchema( diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index a58d4f6c47..de8518e5ed 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -27,7 +27,7 @@ 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 { isGPUCallable, isKnownAtComptime, ResolutionCtx } from '../types.ts'; +import { isGPUCallable, isKnownAtComptime } from '../types.ts'; import { convertStructValues, convertToCommonType, tryConvertSnippet } from './conversion.ts'; import { ArrayExpression, @@ -157,7 +157,7 @@ function operatorToType< const unaryOpCodeToCodegen = { '-': neg[$gpuCallable].call.bind(neg), void: () => snip(undefined, wgsl.Void, 'constant'), - '!': (ctx: ResolutionCtx, [argExpr]: Snippet[]) => { + '!': (ctx: GenerationCtx, [argExpr]: Snippet[]) => { if (argExpr === undefined) { throw new Error('The unary operator `!` expects 1 argument, but 0 were provided.'); } @@ -177,7 +177,7 @@ const unaryOpCodeToCodegen = { } if (wgsl.isVec(dataType)) { - console.warn('Use `std.not` for the WGSL `!` unary operator `!` on vector types.'); + console.warn('Use `std.not` for the WGSL unary operator `!` on vector types.'); } throw new Error( diff --git a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts index ae4ac971a5..9c0b046e50 100644 --- a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts +++ b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts @@ -2069,83 +2069,84 @@ describe('wgslGenerator', () => { `); }); - describe('unary', () => { - it('handles unary operator `!` on boolean runtime-known argument', () => { - const testFn = tgpu.fn( - [d.bool], - d.bool, - )((b) => { - return !b; - }); + it('handles unary operator `!` on boolean runtime-known operand', () => { + const testFn = tgpu.fn( + [d.bool], + d.bool, + )((b) => { + return !b; + }); - expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` + expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` "fn testFn(b: bool) -> bool { return !b; }" `); - }); + }); - it('handles unary operator `!` on numeric runtime-known argument', () => { - const testFn = tgpu.fn( - [d.i32], - d.bool, - )((n) => { - return !n; - }); + it('handles unary operator `!` on numeric runtime-known operand', () => { + const testFn = tgpu.fn( + [d.i32], + d.bool, + )((n) => { + return !n; + }); - expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` + expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` "fn testFn(n: i32) -> bool { return !bool(n); }" `); - }); + }); - it('warns and throws when cannot determine truthiness in unary operator `!`', ({ root }) => { - using warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + it('warns and throws when cannot determine truthiness of a unary operator `!` operand', ({ + root, + }) => { + using warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); - const buffer = root.createUniform(d.mat4x4f); - const testFn1 = tgpu.fn( - [], - d.bool, - )(() => { - return !buffer.$; - }); - expect(() => tgpu.resolve([testFn1])).toThrowErrorMatchingInlineSnapshot(` + const buffer = root.createUniform(d.mat4x4f); + const testFn1 = tgpu.fn( + [], + d.bool, + )(() => { + return !buffer.$; + }); + expect(() => tgpu.resolve([testFn1])).toThrowErrorMatchingInlineSnapshot(` [Error: Resolution of the following tree failed: - - fn:testFn1: The unary operator \`!\` cannot determine truthiness for runtime value of type: mat4x4f.] `); - expect(warnSpy).not.toHaveBeenCalled(); + expect(warnSpy).not.toHaveBeenCalled(); - const testFn2 = tgpu.fn( - [d.vec3f], - d.bool, - )((v) => { - return !v; - }); + const testFn2 = tgpu.fn( + [d.vec3f], + d.bool, + )((v) => { + return !v; + }); - expect(() => tgpu.resolve([testFn2])).toThrowErrorMatchingInlineSnapshot(` + expect(() => tgpu.resolve([testFn2])).toThrowErrorMatchingInlineSnapshot(` [Error: Resolution of the following tree failed: - - fn:testFn2: The unary operator \`!\` cannot determine truthiness for runtime value of type: vec3f.] `); - expect(warnSpy.mock.calls[0]![0]).toMatchInlineSnapshot( - `"Use \`std.not\` for the WGSL \`!\` unary operator \`!\` on vector types."`, - ); - }); + expect(warnSpy.mock.calls[0]![0]).toMatchInlineSnapshot( + `"Use \`std.not\` for the WGSL unary operator \`!\` on vector types."`, + ); + }); - it('handles unary operator `!` on numeric and boolean comptime-known arguments', () => { - const getN = tgpu.comptime(() => 1882); + it('handles unary operator `!` on numeric and boolean comptime-known operands', () => { + const getN = tgpu.comptime(() => 1882); - const f = () => { - 'use gpu'; - if (!(getN() === 7) || !getN()) { - return 1; - } - return -1; - }; + const f = () => { + 'use gpu'; + if (!(getN() === 7) || !getN()) { + return 1; + } + return -1; + }; - expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` "fn f() -> i32 { if ((true || false)) { return 1; @@ -2153,41 +2154,67 @@ describe('wgslGenerator', () => { return -1; }" `); + }); + + it('handles unary operator `!` on operands from slots and accessors', () => { + const Boid = d.struct({ + pos: d.vec2f, + vel: d.vec2f, }); - it('handles multiple unary operators `!`', () => { - const testFn = tgpu.fn( - [d.i32], - d.bool, - )((n) => { - // oxlint-disable-next-line - return !!!!!false || !!!n; - }); + const slot = tgpu.slot>({ pos: d.vec2f(), vel: d.vec2f() }); + const accessor = tgpu.accessor(d.vec4u, d.vec4u(1, 8, 8, 2)); + + const f = () => { + 'use gpu'; + if (!!slot.$ && !!accessor.$) { + return 1; + } + return -1; + }; - expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "fn f() -> i32 { + if ((true && true)) { + return 1; + } + return -1; + }" + `); + }); + + it('handles chained unary operators `!`', () => { + const testFn = tgpu.fn( + [d.i32], + d.bool, + )((n) => { + // oxlint-disable-next-line + return !!!!!false || !!!n; + }); + + expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` "fn testFn(n: i32) -> bool { return (true || !!!bool(n)); }" `); - }); + }); - it('handles unary operator `!` on complex comptime-known argument', () => { - const fnSlot = tgpu.slot<{ a?: number }>({}); + it('handles unary operator `!` on complex comptime-known operand', () => { + const slot = tgpu.slot<{ a?: number }>({}); - const f = () => { - 'use gpu'; - // oxlint-disable-next-line - if (!!fnSlot.$.a) { - return fnSlot.$.a; - } - return 1929; - }; + const f = () => { + 'use gpu'; + // oxlint-disable-next-line + if (!!slot.$.a) { + return slot.$.a; + } + return 1929; + }; - expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` "fn f() -> i32 { return 1929; }" `); - }); }); }); From 21125b7fc02e42ebac42a0f6b4a165888b1837ef Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Tue, 7 Apr 2026 18:05:20 +0200 Subject: [PATCH 03/15] std.not --- .../tgsl-parsing-test.test.ts | 10 +- packages/typegpu/src/std/boolean.ts | 77 ++++++++++- .../typegpu/tests/std/boolean/not.test.ts | 130 +++++++++++++++++- .../typegpu/tests/tgsl/wgslGenerator.test.ts | 1 + 4 files changed, 204 insertions(+), 14 deletions(-) diff --git a/apps/typegpu-docs/tests/individual-example-tests/tgsl-parsing-test.test.ts b/apps/typegpu-docs/tests/individual-example-tests/tgsl-parsing-test.test.ts index 007436f39e..00d7e99d0e 100644 --- a/apps/typegpu-docs/tests/individual-example-tests/tgsl-parsing-test.test.ts +++ b/apps/typegpu-docs/tests/individual-example-tests/tgsl-parsing-test.test.ts @@ -44,11 +44,8 @@ describe('tgsl parsing test example', () => { s = (s && true); s = (s && true); s = (s && true); - s = (s && !false); s = (s && true); - s = (s && !false); s = (s && true); - s = (s && !false); s = (s && true); s = (s && true); s = (s && true); @@ -58,9 +55,12 @@ describe('tgsl parsing test example', () => { s = (s && true); s = (s && true); s = (s && true); - s = (s && !false); s = (s && true); - s = (s && !false); + s = (s && true); + s = (s && true); + s = (s && true); + s = (s && true); + s = (s && true); s = (s && true); s = (s && true); var vec = vec3(true, false, true); diff --git a/packages/typegpu/src/std/boolean.ts b/packages/typegpu/src/std/boolean.ts index 89bb2e8898..4b565e4fd7 100644 --- a/packages/typegpu/src/std/boolean.ts +++ b/packages/typegpu/src/std/boolean.ts @@ -13,12 +13,17 @@ import { type AnyVecInstance, type AnyWgslData, type BaseData, + isBool, + isNumericSchema, + isVec, + isVecBool, isVecInstance, type v2b, type v3b, type v4b, } from '../data/wgslTypes.ts'; import { unify } from '../tgsl/conversion.ts'; +import { isKnownAtComptime } from '../types.ts'; import { sub } from './operators.ts'; function correspondingBooleanVectorSchema(dataType: BaseData) { @@ -164,19 +169,81 @@ export const ge = dualImpl({ // logical ops -const cpuNot = (value: T): T => VectorOps.neg[value.kind](value); +type VecInstanceToBooleanVecInstance = T extends AnyVec2Instance + ? v2b + : T extends AnyVec3Instance + ? v3b + : v4b; + +function cpuNot(value: boolean): boolean; +function cpuNot(value: number): boolean; +function cpuNot(value: T): VecInstanceToBooleanVecInstance; +function cpuNot(value: unknown): boolean; +function cpuNot(value: unknown): boolean | AnyBooleanVecInstance { + if (isVecInstance(value)) { + if (value.length === 2) { + return vec2b(!value.x, !value.y); + } + if (value.length === 3) { + return vec3b(!value.x, !value.y, !value.z); + } + return vec4b(!value.x, !value.y, !value.z, !value.w); + } + + return !value; +} /** - * Returns **component-wise** `!value`. + * Returns the logical negation of the given value. + * For scalars (bool, number), returns `!value`. + * For boolean vectors, returns **component-wise** `!value`. + * For numeric vectors, returns a boolean vector with component-wise truthiness negation. + * For all other types, returns the truthiness negation (in WGSL, this applies only if the value is known at compile-time). * @example - * not(vec2b(false, true)) // returns vec2b(true, false) + * not(true) // returns false + * not(-1) // returns false + * not(0) // returns true * not(vec3b(true, true, false)) // returns vec3b(false, false, true) + * not(vec3f(1.0, 0.0, -1.0)) // returns vec3b(false, true, false) + * not({a: 1882}) // returns false */ export const not = dualImpl({ name: 'not', - signature: (...argTypes) => ({ argTypes, returnType: argTypes[0] }), + signature: (arg) => { + const returnType = isVec(arg) ? correspondingBooleanVectorSchema(arg) : bool; + return { + argTypes: [arg], + returnType, + }; + }, normalImpl: cpuNot, - codegenImpl: (_ctx, [arg]) => stitch`!(${arg})`, + codegenImpl: (ctx, [arg]) => { + if (isKnownAtComptime(arg)) { + return `${!arg.value}`; + } + + const { dataType } = arg; + + if (isBool(dataType)) { + return stitch`!${arg}`; + } + if (isNumericSchema(dataType)) { + return stitch`!bool(${arg})`; + } + + if (isVecBool(dataType)) { + return stitch`!(${arg})`; + } + + if (isVec(dataType)) { + const vecConstructorStr = `vec${dataType.componentCount}`; + return stitch`!${vecConstructorStr}(${arg})`; + } + + throw new Error( + `\`std.not\` cannot determine truthiness for runtime value of type: ${ctx.resolve(dataType).value}.`, + ); + }, }); const cpuOr = (lhs: T, rhs: T) => VectorOps.or[lhs.kind](lhs, rhs); diff --git a/packages/typegpu/tests/std/boolean/not.test.ts b/packages/typegpu/tests/std/boolean/not.test.ts index 9de0abf2e5..422ac37237 100644 --- a/packages/typegpu/tests/std/boolean/not.test.ts +++ b/packages/typegpu/tests/std/boolean/not.test.ts @@ -1,11 +1,133 @@ -import { describe, expect, it } from 'vitest'; -import { vec2b, vec3b, vec4b } from '../../../src/data/index.ts'; +import { describe, expect } from 'vitest'; +import { it } from 'typegpu-testing-utility'; +import { vec2b, vec2f, vec3b, vec3i, vec4b, vec4h, vec4u } from '../../../src/data/index.ts'; import { not } from '../../../src/std/boolean.ts'; +import tgpu, { d } from '../../../src/index.js'; -describe('neg', () => { - it('negates', () => { +describe('not', () => { + it('negates booleans', () => { + expect(not(true)).toBe(false); + expect(not(false)).toBe(true); + }); + + it('converts numbers to booleans and negates', () => { + expect(not(0)).toBe(true); + expect(not(-1)).toBe(false); + expect(not(42)).toBe(false); + }); + + it('negates boolean vectors', () => { expect(not(vec2b(true, false))).toStrictEqual(vec2b(false, true)); expect(not(vec3b(false, false, true))).toStrictEqual(vec3b(true, true, false)); expect(not(vec4b(true, true, false, false))).toStrictEqual(vec4b(false, false, true, true)); }); + + it('converts numeric vectors to booleans vectors and negates component-wise', () => { + expect(not(vec2f(0.0, 1.0))).toStrictEqual(vec2b(true, false)); + expect(not(vec3i(0, 5, -1))).toStrictEqual(vec3b(true, false, false)); + expect(not(vec4u(0, 0, 1, 0))).toStrictEqual(vec4b(true, true, false, true)); + expect(not(vec4h(0, 3.14, 0, -2.5))).toStrictEqual(vec4b(true, false, true, false)); + }); + + it('negates truthiness check', () => { + const s = {}; + expect(not(null)).toBe(true); + expect(not(undefined)).toBe(true); + expect(not(s)).toBe(false); + }); + + it('generates correct WGSL on a boolean runtime-known argument', () => { + const testFn = tgpu.fn( + [d.bool], + d.bool, + )((v) => { + return not(v); + }); + expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` + "fn testFn(v: bool) -> bool { + return !v; + }" + `); + }); + + it('generates correct WGSL on a numeric runtime-known argument', () => { + const testFn = tgpu.fn( + [d.i32], + d.bool, + )((v) => { + return not(v); + }); + expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` + "fn testFn(v: i32) -> bool { + return !bool(v); + }" + `); + }); + + it('generates correct WGSL on a boolean vector runtime-known argument', () => { + const testFn = tgpu.fn( + [d.vec3b], + d.vec3b, + )((v) => { + return not(v); + }); + expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` + "fn testFn(v: vec3) -> vec3 { + return !(v); + }" + `); + }); + + it('generates correct WGSL on a numeric vector runtime-known argument', () => { + const testFn = tgpu.fn( + [d.vec3f], + d.vec3b, + )((v) => { + return not(v); + }); + expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` + "fn testFn(v: vec3f) -> vec3 { + return !vec3(v); + }" + `); + }); + + it('evaluates at compile time for comptime-known arguments', () => { + const getN = tgpu.comptime(() => 42); + const slot = tgpu.slot<{ a?: number }>({}); + + const f = () => { + 'use gpu'; + if (not(getN()) && not(slot.$.a) && not(d.vec4f(1, 8, 8, 2)).x) { + return 1; + } + return -1; + }; + + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "fn f() -> i32 { + if (((false && true) && false)) { + return 1; + } + return -1; + }" + `); + }); + + it('throws when cannot determine truthiness of argument at compile time', ({ root }) => { + const buffer = root.createUniform(d.mat4x4f); + const testFn = tgpu.fn( + [], + d.bool, + )(() => { + return not(buffer.$); + }); + + expect(() => tgpu.resolve([testFn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn:testFn + - fn:not: \`std.not\` cannot determine truthiness for runtime value of type: mat4x4f.] + `); + }); }); diff --git a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts index 9c0b046e50..ecb8934258 100644 --- a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts +++ b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts @@ -2111,6 +2111,7 @@ describe('wgslGenerator', () => { )(() => { return !buffer.$; }); + expect(() => tgpu.resolve([testFn1])).toThrowErrorMatchingInlineSnapshot(` [Error: Resolution of the following tree failed: - From 8308ba7444a2f59160055ed4aa1a1dde35295e2c Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Tue, 7 Apr 2026 18:11:46 +0200 Subject: [PATCH 04/15] missing braces --- packages/typegpu/src/std/boolean.ts | 2 +- packages/typegpu/tests/std/boolean/not.test.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/typegpu/src/std/boolean.ts b/packages/typegpu/src/std/boolean.ts index 4b565e4fd7..83d9880781 100644 --- a/packages/typegpu/src/std/boolean.ts +++ b/packages/typegpu/src/std/boolean.ts @@ -237,7 +237,7 @@ export const not = dualImpl({ if (isVec(dataType)) { const vecConstructorStr = `vec${dataType.componentCount}`; - return stitch`!${vecConstructorStr}(${arg})`; + return stitch`!(${vecConstructorStr}(${arg}))`; } throw new Error( diff --git a/packages/typegpu/tests/std/boolean/not.test.ts b/packages/typegpu/tests/std/boolean/not.test.ts index 422ac37237..d130c0c7b3 100644 --- a/packages/typegpu/tests/std/boolean/not.test.ts +++ b/packages/typegpu/tests/std/boolean/not.test.ts @@ -86,10 +86,10 @@ describe('not', () => { return not(v); }); expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` - "fn testFn(v: vec3f) -> vec3 { - return !vec3(v); - }" - `); + "fn testFn(v: vec3f) -> vec3 { + return !(vec3(v)); + }" + `); }); it('evaluates at compile time for comptime-known arguments', () => { From 3d768c7254e69338140e401b15f8172dce60cf64 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Fri, 10 Apr 2026 15:31:27 +0200 Subject: [PATCH 05/15] more accurate behavior comparing to JS --- packages/typegpu/src/std/boolean.ts | 4 +- packages/typegpu/src/tgsl/wgslGenerator.ts | 8 +-- .../typegpu/tests/std/boolean/not.test.ts | 30 +++++++---- .../typegpu/tests/tgsl/wgslGenerator.test.ts | 53 ++++++++----------- 4 files changed, 42 insertions(+), 53 deletions(-) diff --git a/packages/typegpu/src/std/boolean.ts b/packages/typegpu/src/std/boolean.ts index 83d9880781..3cba7dd6d4 100644 --- a/packages/typegpu/src/std/boolean.ts +++ b/packages/typegpu/src/std/boolean.ts @@ -240,9 +240,7 @@ export const not = dualImpl({ return stitch`!(${vecConstructorStr}(${arg}))`; } - throw new Error( - `\`std.not\` cannot determine truthiness for runtime value of type: ${ctx.resolve(dataType).value}.`, - ); + return 'false'; }, }); diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index 4db0509a58..e35324580b 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -176,13 +176,7 @@ const unaryOpCodeToCodegen = { return snip(`!bool(${argStr})`, bool, 'runtime'); } - if (wgsl.isVec(dataType)) { - console.warn('Use `std.not` for the WGSL unary operator `!` on vector types.'); - } - - throw new Error( - `The unary operator \`!\` cannot determine truthiness for runtime value of type: ${ctx.resolve(dataType).value}.`, - ); + return snip(false, bool, 'constant'); }, } satisfies Partial unknown>>; diff --git a/packages/typegpu/tests/std/boolean/not.test.ts b/packages/typegpu/tests/std/boolean/not.test.ts index d130c0c7b3..56e94b8c9d 100644 --- a/packages/typegpu/tests/std/boolean/not.test.ts +++ b/packages/typegpu/tests/std/boolean/not.test.ts @@ -114,20 +114,28 @@ describe('not', () => { `); }); - it('throws when cannot determine truthiness of argument at compile time', ({ root }) => { + it('mimics JS on non-primitive values', ({ root }) => { const buffer = root.createUniform(d.mat4x4f); - const testFn = tgpu.fn( - [], - d.bool, - )(() => { - return not(buffer.$); + const testFn = tgpu.fn([d.vec3f, d.atomic(d.u32), d.ptrPrivate(d.u32)])((v, a, p) => { + const _b0 = !buffer; + const _b1 = !buffer.$; + const _b2 = !v; + const _b3 = !a; + const _b4 = !p; + const _b5 = !p.$; }); - expect(() => tgpu.resolve([testFn])).toThrowErrorMatchingInlineSnapshot(` - [Error: Resolution of the following tree failed: - - - - fn:testFn - - fn:not: \`std.not\` cannot determine truthiness for runtime value of type: mat4x4f.] + expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` + "@group(0) @binding(0) var buffer: mat4x4f; + + fn testFn(v: vec3f, a: atomic, p: ptr) { + const _b0 = false; + const _b1 = false; + const _b2 = false; + const _b3 = false; + const _b4 = false; + let _b5 = !bool((*p)); + }" `); }); }); diff --git a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts index ecb8934258..fd8ed51182 100644 --- a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts +++ b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts @@ -16,6 +16,7 @@ import { CodegenState } from '../../src/types.ts'; import { it } from 'typegpu-testing-utility'; import { ArrayExpression } from '../../src/tgsl/generationHelpers.ts'; import { extractSnippetFromFn } from '../utils/parseResolved.ts'; +import { UnknownData } from '../../src/tgsl/shaderGenerator_members.ts'; const { NodeTypeCatalog: NODE } = tinyest; @@ -2099,41 +2100,29 @@ describe('wgslGenerator', () => { `); }); - it('warns and throws when cannot determine truthiness of a unary operator `!` operand', ({ - root, - }) => { - using warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); - + it('handles unary operator `!` on non-primitive values', ({ root }) => { const buffer = root.createUniform(d.mat4x4f); - const testFn1 = tgpu.fn( - [], - d.bool, - )(() => { - return !buffer.$; - }); - - expect(() => tgpu.resolve([testFn1])).toThrowErrorMatchingInlineSnapshot(` - [Error: Resolution of the following tree failed: - - - - fn:testFn1: The unary operator \`!\` cannot determine truthiness for runtime value of type: mat4x4f.] - `); - expect(warnSpy).not.toHaveBeenCalled(); - - const testFn2 = tgpu.fn( - [d.vec3f], - d.bool, - )((v) => { - return !v; + const testFn = tgpu.fn([d.vec3f, d.atomic(d.u32), d.ptrPrivate(d.u32)])((v, a, p) => { + const _b0 = !buffer; + const _b1 = !buffer.$; + const _b2 = !v; + const _b3 = !a; + const _b4 = !p; + const _b5 = !p.$; }); - expect(() => tgpu.resolve([testFn2])).toThrowErrorMatchingInlineSnapshot(` - [Error: Resolution of the following tree failed: - - - - fn:testFn2: The unary operator \`!\` cannot determine truthiness for runtime value of type: vec3f.] - `); - expect(warnSpy.mock.calls[0]![0]).toMatchInlineSnapshot( - `"Use \`std.not\` for the WGSL unary operator \`!\` on vector types."`, - ); + expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` + "@group(0) @binding(0) var buffer: mat4x4f; + + fn testFn(v: vec3f, a: atomic, p: ptr) { + const _b0 = false; + const _b1 = false; + const _b2 = false; + const _b3 = false; + const _b4 = false; + let _b5 = !bool((*p)); + }" + `); }); it('handles unary operator `!` on numeric and boolean comptime-known operands', () => { From c959ad21bb2eeeb4c5a51d15fa939fac85d08df8 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Fri, 10 Apr 2026 16:31:21 +0200 Subject: [PATCH 06/15] std.not mimics WGSL --- packages/typegpu/src/std/boolean.ts | 10 +++++++++- packages/typegpu/src/tgsl/wgslGenerator.ts | 3 ++- packages/typegpu/tests/std/boolean/not.test.ts | 4 ++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/typegpu/src/std/boolean.ts b/packages/typegpu/src/std/boolean.ts index 3cba7dd6d4..041cbcf183 100644 --- a/packages/typegpu/src/std/boolean.ts +++ b/packages/typegpu/src/std/boolean.ts @@ -180,6 +180,10 @@ function cpuNot(value: number): boolean; function cpuNot(value: T): VecInstanceToBooleanVecInstance; function cpuNot(value: unknown): boolean; function cpuNot(value: unknown): boolean | AnyBooleanVecInstance { + if (typeof value === 'number' && isNaN(value)) { + return false; + } + if (isVecInstance(value)) { if (value.length === 2) { return vec2b(!value.x, !value.y); @@ -206,6 +210,7 @@ function cpuNot(value: unknown): boolean | AnyBooleanVecInstance { * not(vec3b(true, true, false)) // returns vec3b(false, false, true) * not(vec3f(1.0, 0.0, -1.0)) // returns vec3b(false, true, false) * not({a: 1882}) // returns false + * not(NaN) // returns false **as in WGSL** */ export const not = dualImpl({ name: 'not', @@ -217,8 +222,11 @@ export const not = dualImpl({ }; }, normalImpl: cpuNot, - codegenImpl: (ctx, [arg]) => { + codegenImpl: (_ctx, [arg]) => { if (isKnownAtComptime(arg)) { + if (typeof arg.value === 'number' && isNaN(arg.value)) { + return 'false'; + } return `${!arg.value}`; } diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index e35324580b..5affbd3ba2 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -173,7 +173,8 @@ const unaryOpCodeToCodegen = { return snip(`!${argStr}`, bool, 'runtime'); } if (wgsl.isNumericSchema(dataType)) { - return snip(`!bool(${argStr})`, bool, 'runtime'); + // no bool cast, because bool(NaN) is true in WGSL but in JS it's false + return snip(`!(${argStr} != 0)`, bool, 'runtime'); } return snip(false, bool, 'constant'); diff --git a/packages/typegpu/tests/std/boolean/not.test.ts b/packages/typegpu/tests/std/boolean/not.test.ts index 56e94b8c9d..c650d1304f 100644 --- a/packages/typegpu/tests/std/boolean/not.test.ts +++ b/packages/typegpu/tests/std/boolean/not.test.ts @@ -36,6 +36,10 @@ describe('not', () => { expect(not(s)).toBe(false); }); + it('mimics WGSL behavior on NaN', () => { + expect(not(NaN)).toBe(false); + }); + it('generates correct WGSL on a boolean runtime-known argument', () => { const testFn = tgpu.fn( [d.bool], From b7cb36f7eaa89bcca04fa1823d5a393dd1373d68 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Fri, 10 Apr 2026 18:49:28 +0200 Subject: [PATCH 07/15] correct NaN handling --- packages/typegpu/src/std/boolean.ts | 13 ++++++------- packages/typegpu/src/tgsl/wgslGenerator.ts | 11 +++++++++-- packages/typegpu/tests/std/boolean/not.test.ts | 13 +++++++++++++ 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/packages/typegpu/src/std/boolean.ts b/packages/typegpu/src/std/boolean.ts index 041cbcf183..08048cafd6 100644 --- a/packages/typegpu/src/std/boolean.ts +++ b/packages/typegpu/src/std/boolean.ts @@ -186,12 +186,14 @@ function cpuNot(value: unknown): boolean | AnyBooleanVecInstance { if (isVecInstance(value)) { if (value.length === 2) { - return vec2b(!value.x, !value.y); + return vec2b(cpuNot(value.x), cpuNot(value.y)); } if (value.length === 3) { - return vec3b(!value.x, !value.y, !value.z); + return vec3b(cpuNot(value.x), cpuNot(value.y), cpuNot(value.z)); + } + if (value.length === 4) { + return vec4b(cpuNot(value.x), cpuNot(value.y), cpuNot(value.z), cpuNot(value.w)); } - return vec4b(!value.x, !value.y, !value.z, !value.w); } return !value; @@ -224,10 +226,7 @@ export const not = dualImpl({ normalImpl: cpuNot, codegenImpl: (_ctx, [arg]) => { if (isKnownAtComptime(arg)) { - if (typeof arg.value === 'number' && isNaN(arg.value)) { - return 'false'; - } - return `${!arg.value}`; + return `${cpuNot(arg.value)}`; } const { dataType } = arg; diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index 5affbd3ba2..37e503024f 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -173,8 +173,15 @@ const unaryOpCodeToCodegen = { return snip(`!${argStr}`, bool, 'runtime'); } if (wgsl.isNumericSchema(dataType)) { - // no bool cast, because bool(NaN) is true in WGSL but in JS it's false - return snip(`!(${argStr} != 0)`, bool, 'runtime'); + const resultStr = `!bool(${argStr})`; + const nanGuardedStr = // abstractFloat will be resolved as comptime + dataType.type === 'f32' + ? `(((bitcast(${argStr}) & 0x7fffffff) > 0x7f800000) || ${resultStr})` + : dataType.type === 'f16' + ? `(((bitcast(${argStr}) & 0x7fff) > 0x7c00) || ${resultStr})` + : resultStr; + + return snip(nanGuardedStr, bool, 'runtime'); } return snip(false, bool, 'constant'); diff --git a/packages/typegpu/tests/std/boolean/not.test.ts b/packages/typegpu/tests/std/boolean/not.test.ts index c650d1304f..dfb0814765 100644 --- a/packages/typegpu/tests/std/boolean/not.test.ts +++ b/packages/typegpu/tests/std/boolean/not.test.ts @@ -96,6 +96,19 @@ describe('not', () => { `); }); + it('generates correct WGSL on a numeric vector comptime-known argument', () => { + const f = () => { + 'use gpu'; + const v = not(d.vec4f(Infinity, -Infinity, 0, NaN)); + }; + + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "fn f() { + var v = vec4(false, false, true, false); + }" + `); + }); + it('evaluates at compile time for comptime-known arguments', () => { const getN = tgpu.comptime(() => 42); const slot = tgpu.slot<{ a?: number }>({}); From 412b47353f1a3e95de553119609b94a721b45b01 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Wed, 8 Apr 2026 14:24:03 +0200 Subject: [PATCH 08/15] initial short circuit eval --- packages/typegpu/src/tgsl/wgslGenerator.ts | 27 +++ .../typegpu/tests/std/boolean/not.test.ts | 3 - .../typegpu/tests/tgsl/wgslGenerator.test.ts | 159 ++++++++++++++++-- 3 files changed, 168 insertions(+), 21 deletions(-) diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index 37e503024f..bee9ecf3d8 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -352,6 +352,33 @@ ${this.ctx.pre}}`; // Logical/Binary/Assignment Expression const [exprType, lhs, op, rhs] = expression; const lhsExpr = this._expression(lhs); + + // Short Circuit Evaluation + if ((op === '||' || op === '&&') && isKnownAtComptime(lhsExpr)) { + let evalRhs = !lhsExpr.value; + if (op === '&&') { + evalRhs = !evalRhs; + } + + if (!evalRhs) { + return op === '||' ? snip(true, bool, 'constant') : snip(false, bool, 'constant'); + } + + const rhsExpr = this._expression(rhs); + + if (isKnownAtComptime(rhsExpr)) { + return snip(rhsExpr.value, bool, 'constant'); + } + + if (rhsExpr.dataType === UnknownData) { + throw new WgslTypeError(`Right-hand side of '${op}' is of unknown type`); + } + + const convRhs = tryConvertSnippet(this.ctx, rhsExpr, bool, false); + const rhsStr = this.ctx.resolve(convRhs.value, convRhs.dataType).value; + return snip(rhsStr, bool, 'runtime'); + } + const rhsExpr = this._expression(rhs); if (rhsExpr.value instanceof RefOperator) { diff --git a/packages/typegpu/tests/std/boolean/not.test.ts b/packages/typegpu/tests/std/boolean/not.test.ts index dfb0814765..59ab4feef6 100644 --- a/packages/typegpu/tests/std/boolean/not.test.ts +++ b/packages/typegpu/tests/std/boolean/not.test.ts @@ -123,9 +123,6 @@ describe('not', () => { expect(tgpu.resolve([f])).toMatchInlineSnapshot(` "fn f() -> i32 { - if (((false && true) && false)) { - return 1; - } return -1; }" `); diff --git a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts index fd8ed51182..e2108c5590 100644 --- a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts +++ b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts @@ -2137,13 +2137,13 @@ describe('wgslGenerator', () => { }; expect(tgpu.resolve([f])).toMatchInlineSnapshot(` - "fn f() -> i32 { - if ((true || false)) { - return 1; - } - return -1; - }" - `); + "fn f() -> i32 { + { + return 1; + } + return -1; + }" + `); }); it('handles unary operator `!` on operands from slots and accessors', () => { @@ -2164,13 +2164,13 @@ describe('wgslGenerator', () => { }; expect(tgpu.resolve([f])).toMatchInlineSnapshot(` - "fn f() -> i32 { - if ((true && true)) { - return 1; - } - return -1; - }" - `); + "fn f() -> i32 { + { + return 1; + } + return -1; + }" + `); }); it('handles chained unary operators `!`', () => { @@ -2183,10 +2183,10 @@ describe('wgslGenerator', () => { }); expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` - "fn testFn(n: i32) -> bool { - return (true || !!!bool(n)); - }" - `); + "fn testFn(n: i32) -> bool { + return true; + }" + `); }); it('handles unary operator `!` on complex comptime-known operand', () => { @@ -2207,4 +2207,127 @@ describe('wgslGenerator', () => { }" `); }); + + 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 `||` short-circuit evaluation', () => { + const f = () => { + 'use gpu'; + let res = -1; + 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 `&&` short-circuit evaluation', () => { + const f = () => { + 'use gpu'; + let res = -1; + if (false && getTrackedBool()) { + res = 1; + } + return res; + }; + + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "fn f() -> i32 { + var res = -1; + return res; + }" + `); + expect(state.counter).toBe(0); + }); + + it('handles chained `||` short-circuit evaluation', () => { + state.result = false; + + const f = () => { + 'use gpu'; + let res = -1; + 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 `&&` short-circuit evaluation', () => { + const f = () => { + 'use gpu'; + let res = -1; + if (getTrackedBool() && false && getTrackedBool() && getTrackedBool() && getTrackedBool()) { + res = 1; + } + return res; + }; + + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "fn f() -> i32 { + var res = -1; + return res; + }" + `); + expect(state.counter).toBe(1); + }); + + it('handles mixed logical operators short-circuit evaluation', () => { + const f = () => { + 'use gpu'; + let res = -1; + 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); + }); + }); }); From ca7ed2283e030ddcdb08c5380858299a716eb9e3 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Fri, 10 Apr 2026 12:27:10 +0200 Subject: [PATCH 09/15] tests --- packages/typegpu/src/tgsl/wgslGenerator.ts | 16 +++-- .../typegpu/tests/tgsl/wgslGenerator.test.ts | 61 +++++++++++++++++-- 2 files changed, 63 insertions(+), 14 deletions(-) diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index bee9ecf3d8..6f62c546d1 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -355,25 +355,23 @@ ${this.ctx.pre}}`; // Short Circuit Evaluation if ((op === '||' || op === '&&') && isKnownAtComptime(lhsExpr)) { - let evalRhs = !lhsExpr.value; - if (op === '&&') { - evalRhs = !evalRhs; - } + const evalRhs = op === '&&' ? !!lhsExpr.value : !lhsExpr.value; if (!evalRhs) { - return op === '||' ? snip(true, bool, 'constant') : snip(false, bool, 'constant'); + return snip(op === '||', bool, 'constant'); } const rhsExpr = this._expression(rhs); - if (isKnownAtComptime(rhsExpr)) { - return snip(rhsExpr.value, bool, 'constant'); - } - if (rhsExpr.dataType === UnknownData) { throw new WgslTypeError(`Right-hand side of '${op}' is of unknown type`); } + if (isKnownAtComptime(rhsExpr)) { + return snip(rhsExpr.value, bool, 'constant'); + } + + // we can skip lhs const convRhs = tryConvertSnippet(this.ctx, rhsExpr, bool, false); const rhsStr = this.ctx.resolve(convRhs.value, convRhs.dataType).value; return snip(rhsStr, bool, 'runtime'); diff --git a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts index e2108c5590..47daed4491 100644 --- a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts +++ b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts @@ -2224,10 +2224,11 @@ describe('wgslGenerator', () => { state.result = true; }); - it('handles `||` short-circuit evaluation', () => { + 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; } @@ -2246,10 +2247,11 @@ describe('wgslGenerator', () => { expect(state.counter).toBe(0); }); - it('handles `&&` short-circuit evaluation', () => { + 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; } @@ -2265,12 +2267,13 @@ describe('wgslGenerator', () => { expect(state.counter).toBe(0); }); - it('handles chained `||` short-circuit evaluation', () => { + 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; } @@ -2289,10 +2292,11 @@ describe('wgslGenerator', () => { expect(state.counter).toEqual(1); }); - it('handles chained `&&` short-circuit evaluation', () => { + 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; } @@ -2308,10 +2312,11 @@ describe('wgslGenerator', () => { expect(state.counter).toBe(1); }); - it('handles mixed logical operators short-circuit evaluation', () => { + 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; } @@ -2329,5 +2334,51 @@ describe('wgslGenerator', () => { `); 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; + }" + `); + }); }); }); From 1cfc9e479193e816b470598c57d7ef871b33e844 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Tue, 14 Apr 2026 16:41:12 +0200 Subject: [PATCH 10/15] initial commit --- .../src/core/function/createCallableSchema.ts | 7 +- packages/typegpu/src/data/numeric.ts | 36 ++- packages/typegpu/src/data/wgslTypes.ts | 2 +- packages/typegpu/src/tgsl/conversion.ts | 6 + packages/typegpu/src/tgsl/wgslGenerator.ts | 24 +- .../typegpu/tests/tgsl/wgslGenerator.test.ts | 237 +++++++++++++++++- 6 files changed, 276 insertions(+), 36 deletions(-) diff --git a/packages/typegpu/src/core/function/createCallableSchema.ts b/packages/typegpu/src/core/function/createCallableSchema.ts index 970c950fa8..369bca3f02 100644 --- a/packages/typegpu/src/core/function/createCallableSchema.ts +++ b/packages/typegpu/src/core/function/createCallableSchema.ts @@ -1,4 +1,4 @@ -import { type MapValueToSnippet, type ResolvedSnippet, snip } from '../../data/snippet.ts'; +import { type MapValueToSnippet, snip, type Snippet } from '../../data/snippet.ts'; import { type BaseData, isPtr } from '../../data/wgslTypes.ts'; import { setName } from '../../shared/meta.ts'; import { $gpuCallable } from '../../shared/symbols.ts'; @@ -12,10 +12,7 @@ interface CallableSchemaOptions { readonly name: string; readonly schema: () => BaseData; readonly normalImpl: T; - readonly codegenImpl: ( - ctx: ResolutionCtx, - args: MapValueToSnippet>, - ) => ResolvedSnippet; + readonly codegenImpl: (ctx: ResolutionCtx, args: MapValueToSnippet>) => Snippet; readonly argTypes: ( ...inArgTypes: MapValueToDataType> ) => (BaseData | BaseData[])[]; diff --git a/packages/typegpu/src/data/numeric.ts b/packages/typegpu/src/data/numeric.ts index d49368843b..fceb36a4ca 100644 --- a/packages/typegpu/src/data/numeric.ts +++ b/packages/typegpu/src/data/numeric.ts @@ -1,6 +1,8 @@ import { $internal } from '../shared/symbols.ts'; +import { isBool, isNumericSchema } from './wgslTypes.ts'; import type { AbstractFloat, AbstractInt, Bool, F16, F32, I32, U16, U32 } from './wgslTypes.ts'; import { callableSchema } from '../core/function/createCallableSchema.ts'; +import { snip } from './snippet.ts'; export const abstractInt = { [$internal]: {}, @@ -22,16 +24,36 @@ const boolCast = callableSchema({ name: 'bool', schema: () => bool, argTypes: (arg) => (arg ? [arg] : []), - normalImpl(v?: number | boolean) { - if (v === undefined) { - return false; + normalImpl(v?: unknown) { + return !!v; + }, + codegenImpl: (ctx, [arg]) => { + if (arg === undefined) { + return snip(false, bool, 'constant'); } - if (typeof v === 'boolean') { - return v; + + const { dataType } = arg; + + if (isBool(dataType)) { + return ctx.gen.typeInstantiation(bool, [arg]); } - return !!v; + + if (isNumericSchema(dataType)) { + const argStr = ctx.resolveSnippet(arg).value; + const resultStr = `bool(${argStr})`; + + const nanGuardedStr = + dataType.type === 'f32' + ? `(((bitcast(${argStr}) & 0x7fffffff) <= 0x7f800000) && ${resultStr})` + : dataType.type === 'f16' + ? `(((bitcast(${argStr}) & 0x7fff) <= 0x7c00) && ${resultStr})` + : resultStr; + + return snip(nanGuardedStr, bool, 'runtime'); + } + + return snip(true, bool, 'constant'); }, - codegenImpl: (ctx, args) => ctx.gen.typeInstantiation(bool, args), }); /** diff --git a/packages/typegpu/src/data/wgslTypes.ts b/packages/typegpu/src/data/wgslTypes.ts index 935594dc00..ca4a7c2c81 100644 --- a/packages/typegpu/src/data/wgslTypes.ts +++ b/packages/typegpu/src/data/wgslTypes.ts @@ -597,7 +597,7 @@ export type mBaseForVec = T extends v2f * Boolean schema representing a single WGSL bool value. * Cannot be used inside buffers as it is not host-shareable. */ -export interface Bool extends BaseData, DualFn<(v?: number | boolean) => boolean> { +export interface Bool extends BaseData, DualFn<(v?: unknown) => boolean> { readonly type: 'bool'; // Type-tokens, not available at runtime diff --git a/packages/typegpu/src/tgsl/conversion.ts b/packages/typegpu/src/tgsl/conversion.ts index 0d8ae68163..272ae746ec 100644 --- a/packages/typegpu/src/tgsl/conversion.ts +++ b/packages/typegpu/src/tgsl/conversion.ts @@ -1,6 +1,7 @@ import { stitch } from '../core/resolve/stitch.ts'; import { UnknownData } from '../data/dataTypes.ts'; import { undecorate } from '../data/dataTypes.ts'; +import { bool } from '../data/numeric.ts'; import { derefSnippet, RefOperator } from '../data/ref.ts'; import { schemaCallWrapperGPU } from '../data/schemaCallWrapper.ts'; import { snip, type Snippet } from '../data/snippet.ts'; @@ -20,6 +21,7 @@ import { import { invariant, WgslTypeError } from '../errors.ts'; import { DEV, TEST } from '../shared/env.ts'; import { safeStringify } from '../shared/stringify.ts'; +import { $gpuCallable } from '../shared/symbols.ts'; import { assertExhaustive } from '../shared/utilityTypes.ts'; import type { ResolutionCtx } from '../types.ts'; @@ -324,6 +326,10 @@ export function tryConvertSnippet( return snip(value, target, origin); } + if (target.type === 'bool') { + return bool[$gpuCallable].call(ctx, [snippet]); + } + if (dataType === UnknownData) { // Commit unknown to the expected type. return snip(stitch`${snip(value, target, origin)}`, target, origin); diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index 6f62c546d1..e473e25202 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -162,29 +162,13 @@ const unaryOpCodeToCodegen = { throw new Error('The unary operator `!` expects 1 argument, but 0 were provided.'); } - if (isKnownAtComptime(argExpr)) { - return snip(!argExpr.value, bool, 'constant'); - } - - const { value, dataType } = argExpr; - const argStr = ctx.resolve(value, dataType).value; + const convArg = bool[$gpuCallable].call(ctx, [argExpr]); - if (wgsl.isBool(dataType)) { - return snip(`!${argStr}`, bool, 'runtime'); - } - if (wgsl.isNumericSchema(dataType)) { - const resultStr = `!bool(${argStr})`; - const nanGuardedStr = // abstractFloat will be resolved as comptime - dataType.type === 'f32' - ? `(((bitcast(${argStr}) & 0x7fffffff) > 0x7f800000) || ${resultStr})` - : dataType.type === 'f16' - ? `(((bitcast(${argStr}) & 0x7fff) > 0x7c00) || ${resultStr})` - : resultStr; - - return snip(nanGuardedStr, bool, 'runtime'); + if (isKnownAtComptime(convArg)) { + return snip(!convArg.value, bool, 'constant'); } - return snip(false, bool, 'constant'); + return snip(`!${convArg.value}`, bool, 'runtime'); }, } satisfies Partial unknown>>; diff --git a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts index 47daed4491..624ca0458c 100644 --- a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts +++ b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts @@ -2112,9 +2112,7 @@ describe('wgslGenerator', () => { }); expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` - "@group(0) @binding(0) var buffer: mat4x4f; - - fn testFn(v: vec3f, a: atomic, p: ptr) { + "fn testFn(v: vec3f, a: atomic, p: ptr) { const _b0 = false; const _b1 = false; const _b2 = false; @@ -2208,6 +2206,239 @@ describe('wgslGenerator', () => { `); }); + describe('handles truthiness check', () => { + it('boolean runtime-known operand', () => { + const testFn = tgpu.fn( + [d.bool], + d.i32, + )((b) => { + let res = -1; + if (b) { + res = 1; + } + return res; + }); + + expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` + "fn testFn(b: bool) -> i32 { + var res = -1; + if (b) { + res = 1i; + } + return res; + }" + `); + }); + + it('numeric runtime-known operand', () => { + const testFn = tgpu.fn( + [d.i32, d.f32], + d.i32, + )((n, f) => { + let res = -1; + if (n) { + res = 1; + } + if (f) { + res = 2; + } + return res; + }); + + expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` + "fn testFn(n: i32, f: f32) -> i32 { + var res = -1; + if (bool(n)) { + res = 1i; + } + if ((((bitcast(f) & 0x7fffffff) <= 0x7f800000) && bool(f))) { + res = 2i; + } + return res; + }" + `); + }); + + it('non-primitive values', ({ root }) => { + const buffer = root.createUniform(d.mat4x4f); + const testFn = tgpu.fn( + [d.vec3f, d.atomic(d.u32), d.ptrPrivate(d.u32)], + d.i32, + )((v, a, p) => { + let res = -1; + if (buffer) { + res = 0; + } + if (buffer.$) { + res = 1; + } + if (v) { + res = 2; + } + if (a) { + res = 3; + } + if (std.atomicLoad(a)) { + res = 4; + } + if (p) { + res = 5; + } + if (p.$) { + res = 6; + } + return res; + }); + + expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` + "fn testFn(v: vec3f, a: atomic, p: ptr) -> i32 { + var res = -1; + { + res = 0i; + } + { + res = 1i; + } + { + res = 2i; + } + { + res = 3i; + } + if (bool(atomicLoad(&a))) { + res = 4i; + } + { + res = 5i; + } + if (bool((*p))) { + res = 6i; + } + return res; + }" + `); + }); + + it('primitive comptime-known operands', () => { + const getTruthy = tgpu.comptime(() => 1882); + const getFalsy = tgpu.comptime(() => 0); + + const f = () => { + 'use gpu'; + let res = -1; + if (getTruthy()) { + res = 1; + } + if (getFalsy()) { + res = 2; + } + return res; + }; + + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "fn f() -> i32 { + var res = -1; + { + res = 1i; + } + return res; + }" + `); + }); + + it('operands from slots and accessors', () => { + const Boid = d.struct({ + pos: d.vec2f, + vel: d.vec2f, + }); + + const slot = tgpu.slot>({ pos: d.vec2f(), vel: d.vec2f() }); + const accessor = tgpu.accessor(d.vec4u, d.vec4u(1, 8, 8, 2)); + + const f = () => { + 'use gpu'; + let res = -1; + if (slot.$) { + res = 1; + } + if (accessor.$) { + res = 2; + } + return res; + }; + + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "fn f() -> i32 { + var res = -1; + { + res = 1i; + } + { + res = 2i; + } + return res; + }" + `); + }); + + it('complex comptime-known operand', () => { + const slotEmpty = tgpu.slot<{ a?: number }>({}); + const slotFull = tgpu.slot<{ a?: number }>({ a: 42 }); + + const f = () => { + 'use gpu'; + let res = -1; + if (slotEmpty.$.a) { + res = 1; + } + if (slotFull.$.a) { + res = 2; + } + return res; + }; + + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "fn f() -> i32 { + var res = -1; + { + res = 2i; + } + return res; + }" + `); + }); + + it('operand of && and ||', () => { + const testFn = tgpu.fn( + [d.i32, d.bool], + d.i32, + )((n, b) => { + let res = 0; + if (b && n) { + res = 1; + } + if (n || b) { + res = 2; + } + + return res; + }); + + expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` + "fn testFn(n: i32, b: bool) -> i32 { + var res = 0; + if ((i32(b) && n)) { + res = 1i; + } + if ((n || i32(b))) { + res = 2i; + } + return res; + }" + `); + }); + }); + describe('short-circuit evaluation', () => { const state = { counter: 0, From 5f26d96c37120a9ae9a94da8c0e1c24769ad733d Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Tue, 14 Apr 2026 16:49:28 +0200 Subject: [PATCH 11/15] test align --- packages/typegpu/tests/tgsl/wgslGenerator.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts index 096a7620ca..78f0061d70 100644 --- a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts +++ b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts @@ -2113,9 +2113,7 @@ describe('wgslGenerator', () => { }); expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` - "@group(0) @binding(0) var buffer: mat4x4f; - - fn testFn(v: vec3f, a: atomic, p: ptr) { + "fn testFn(v: vec3f, a: atomic, p: ptr) { const _b0 = false; const _b1 = false; const _b2 = false; From 427197472faf7823bfb94d0da5a8166578a7a1ee Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Tue, 14 Apr 2026 17:50:45 +0200 Subject: [PATCH 12/15] truthiness check --- packages/typegpu/src/std/boolean.ts | 1 - packages/typegpu/src/tgsl/wgslGenerator.ts | 45 ++++++++++++++----- .../typegpu/tests/tgsl/typeInference.test.ts | 39 +++++++++------- .../typegpu/tests/tgsl/wgslGenerator.test.ts | 4 +- 4 files changed, 59 insertions(+), 30 deletions(-) diff --git a/packages/typegpu/src/std/boolean.ts b/packages/typegpu/src/std/boolean.ts index b16aeba5d3..bcb74ff623 100644 --- a/packages/typegpu/src/std/boolean.ts +++ b/packages/typegpu/src/std/boolean.ts @@ -23,7 +23,6 @@ import { type v4b, } from '../data/wgslTypes.ts'; import { unify } from '../tgsl/conversion.ts'; -import { isKnownAtComptime } from '../types.ts'; import { sub } from './operators.ts'; function correspondingBooleanVectorSchema(dataType: BaseData) { diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index 061e478671..cfd82d4f17 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -328,17 +328,12 @@ ${this.ctx.pre}}`; return snip(expression, bool, /* origin */ 'constant'); } - 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 evalRhs = op === '&&' ? lhsExpr.value : !lhsExpr.value; if (!evalRhs) { @@ -347,14 +342,14 @@ ${this.ctx.pre}}`; 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 snip(!!rhsExpr.value, bool, 'constant'); } + 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.resolve(convRhs.value, convRhs.dataType).value; @@ -363,6 +358,32 @@ ${this.ctx.pre}}`; const rhsExpr = this._expression(rhs); + // they are not know 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, + ]; + + const lhsStr = this.ctx.resolve(convLhs.value, convLhs.dataType).value; + const rhsStr = this.ctx.resolve(convRhs.value, convRhs.dataType).value; + + return snip(`(${lhsStr} ${op} ${rhsStr})`, bool, /* origin */ 'runtime'); + } + + 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) { throw new WgslTypeError( stitch`Cannot assign a ref to an existing variable '${lhsExpr}', define a new variable instead.`, diff --git a/packages/typegpu/tests/tgsl/typeInference.test.ts b/packages/typegpu/tests/tgsl/typeInference.test.ts index 10c6c81726..55d6df4a0c 100644 --- a/packages/typegpu/tests/tgsl/typeInference.test.ts +++ b/packages/typegpu/tests/tgsl/typeInference.test.ts @@ -271,7 +271,7 @@ describe('wgsl generator type inference', () => { `); }); - it('throws when if condition is not boolean', () => { + it('converts if condition to boolean', () => { const myFn = tgpu.fn( [], d.bool, @@ -282,14 +282,17 @@ describe('wgsl generator type inference', () => { return false; }); - expect(() => tgpu.resolve([myFn])).toThrowErrorMatchingInlineSnapshot(` - [Error: Resolution of the following tree failed: - - - - fn:myFn: Cannot convert value of type 'vec2' to any of the target types: [bool]] + expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(` + "fn myFn() -> bool { + { + return true; + } + return false; + }" `); }); - it('throws when while condition is not boolean', () => { + it('converts while condition to boolean', () => { const myFn = tgpu.fn( [], d.bool, @@ -300,14 +303,17 @@ describe('wgsl generator type inference', () => { return false; }); - expect(() => tgpu.resolve([myFn])).toThrowErrorMatchingInlineSnapshot(` - [Error: Resolution of the following tree failed: - - - - fn:myFn: Cannot convert value of type 'mat2x2f' to any of the target types: [bool]] + expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(` + "fn myFn() -> bool { + while (true) { + return true; + } + return false; + }" `); }); - it('throws when for condition is not boolean', () => { + it('converts for condition to boolean', () => { const myFn = tgpu.fn( [], d.bool, @@ -318,10 +324,13 @@ describe('wgsl generator type inference', () => { return false; }); - expect(() => tgpu.resolve([myFn])).toThrowErrorMatchingInlineSnapshot(` - [Error: Resolution of the following tree failed: - - - - fn:myFn: Cannot convert value of type 'abstractInt' to any of the target types: [bool]] + expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(` + "fn myFn() -> bool { + for (var i = 0; true; (i < 10i)) { + return true; + } + return false; + }" `); }); diff --git a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts index 78f0061d70..22f5f953dd 100644 --- a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts +++ b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts @@ -2429,10 +2429,10 @@ describe('wgslGenerator', () => { expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` "fn testFn(n: i32, b: bool) -> i32 { var res = 0; - if ((i32(b) && n)) { + if ((b && bool(n))) { res = 1i; } - if ((n || i32(b))) { + if ((bool(n) || b)) { res = 2i; } return res; From 5f4942103faf214a14c21ad8d44a5514786724dc Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Tue, 14 Apr 2026 18:18:27 +0200 Subject: [PATCH 13/15] circular deps --- packages/typegpu/src/tgsl/conversion.ts | 6 ------ packages/typegpu/src/tgsl/wgslGenerator.ts | 5 +++++ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/typegpu/src/tgsl/conversion.ts b/packages/typegpu/src/tgsl/conversion.ts index 272ae746ec..0d8ae68163 100644 --- a/packages/typegpu/src/tgsl/conversion.ts +++ b/packages/typegpu/src/tgsl/conversion.ts @@ -1,7 +1,6 @@ import { stitch } from '../core/resolve/stitch.ts'; import { UnknownData } from '../data/dataTypes.ts'; import { undecorate } from '../data/dataTypes.ts'; -import { bool } from '../data/numeric.ts'; import { derefSnippet, RefOperator } from '../data/ref.ts'; import { schemaCallWrapperGPU } from '../data/schemaCallWrapper.ts'; import { snip, type Snippet } from '../data/snippet.ts'; @@ -21,7 +20,6 @@ import { import { invariant, WgslTypeError } from '../errors.ts'; import { DEV, TEST } from '../shared/env.ts'; import { safeStringify } from '../shared/stringify.ts'; -import { $gpuCallable } from '../shared/symbols.ts'; import { assertExhaustive } from '../shared/utilityTypes.ts'; import type { ResolutionCtx } from '../types.ts'; @@ -326,10 +324,6 @@ export function tryConvertSnippet( return snip(value, target, origin); } - if (target.type === 'bool') { - return bool[$gpuCallable].call(ctx, [snippet]); - } - if (dataType === UnknownData) { // Commit unknown to the expected type. return snip(stitch`${snip(value, target, origin)}`, target, origin); diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index cfd82d4f17..5453c3d217 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -313,6 +313,11 @@ ${this.ctx.pre}}`; // convert the result. return result; } + + if (wgsl.isBool(expectedType)) { + return bool[$gpuCallable].call(this.ctx, [result]); + } + return tryConvertSnippet(this.ctx, result, expectedType); } finally { this.ctx.expectedType = prevExpectedType; From a3c136fbd8132b456923e7ed2fc1da7b7514d94f Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Tue, 14 Apr 2026 18:33:11 +0200 Subject: [PATCH 14/15] correct f16 nan handling --- packages/typegpu/src/data/numeric.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/typegpu/src/data/numeric.ts b/packages/typegpu/src/data/numeric.ts index fceb36a4ca..9fce9e52f7 100644 --- a/packages/typegpu/src/data/numeric.ts +++ b/packages/typegpu/src/data/numeric.ts @@ -43,11 +43,9 @@ const boolCast = callableSchema({ const resultStr = `bool(${argStr})`; const nanGuardedStr = - dataType.type === 'f32' - ? `(((bitcast(${argStr}) & 0x7fffffff) <= 0x7f800000) && ${resultStr})` - : dataType.type === 'f16' - ? `(((bitcast(${argStr}) & 0x7fff) <= 0x7c00) && ${resultStr})` - : resultStr; + dataType.type === 'f32' || dataType.type === 'f16' + ? `(((bitcast(${dataType.type === 'f16' ? `f32(${argStr})` : argStr}) & 0x7fffffff) <= 0x7f800000) && ${resultStr})` + : resultStr; return snip(nanGuardedStr, bool, 'runtime'); } From 6372c0a2197ff48d023b8de9a0fc22f79e405422 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Wed, 29 Apr 2026 12:33:44 +0200 Subject: [PATCH 15/15] review changes --- packages/typegpu/src/data/numeric.ts | 2 +- packages/typegpu/src/tgsl/wgslGenerator.ts | 2 +- .../typegpu/tests/tgsl/wgslGenerator.test.ts | 57 +++++++++++++------ 3 files changed, 42 insertions(+), 19 deletions(-) diff --git a/packages/typegpu/src/data/numeric.ts b/packages/typegpu/src/data/numeric.ts index 9fce9e52f7..b898346306 100644 --- a/packages/typegpu/src/data/numeric.ts +++ b/packages/typegpu/src/data/numeric.ts @@ -44,7 +44,7 @@ const boolCast = callableSchema({ const nanGuardedStr = dataType.type === 'f32' || dataType.type === 'f16' - ? `(((bitcast(${dataType.type === 'f16' ? `f32(${argStr})` : argStr}) & 0x7fffffff) <= 0x7f800000) && ${resultStr})` + ? `(((bitcast(${dataType.type === 'f16' ? `f32(${argStr})` : argStr}) << 1u) - 1u) < 0xff000000)` : resultStr; return snip(nanGuardedStr, bool, 'runtime'); diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index 6104bd4f53..c8cf4e251b 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -363,7 +363,7 @@ ${this.ctx.pre}}`; const rhsExpr = this._expression(rhs); - // they are not know at comptime + // they are not known at comptime if (lhsExpr.dataType === UnknownData) { throw new WgslTypeError(`Left-hand side of '${op}' is of unknown type`); } diff --git a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts index b8d0c1cde7..25a1c0f7c9 100644 --- a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts +++ b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts @@ -1,5 +1,5 @@ import * as tinyest from 'tinyest'; -import { beforeEach, describe, expect, vi } from 'vitest'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; import { namespace } from '../../src/core/resolve/namespace.ts'; import * as d from '../../src/data/index.ts'; import { abstractFloat, abstractInt } from '../../src/data/numeric.ts'; @@ -2258,9 +2258,9 @@ describe('wgslGenerator', () => { it('numeric runtime-known operand', () => { const testFn = tgpu.fn( - [d.i32, d.f32], + [d.i32, d.f32, d.f16], d.i32, - )((n, f) => { + )((n, f, h) => { let res = -1; if (n) { res = 1; @@ -2268,18 +2268,24 @@ describe('wgslGenerator', () => { if (f) { res = 2; } + if (h) { + res = 3; + } return res; }); expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` - "fn testFn(n: i32, f: f32) -> i32 { + "fn testFn(n: i32, f: f32, h: f16) -> i32 { var res = -1; if (bool(n)) { res = 1i; } - if ((((bitcast(f) & 0x7fffffff) <= 0x7f800000) && bool(f))) { + if ((((bitcast(f) << 1u) - 1u) < 0xff000000)) { res = 2i; } + if ((((bitcast(f32(h)) << 1u) - 1u) < 0xff000000)) { + res = 3i; + } return res; }" `); @@ -2301,12 +2307,6 @@ describe('wgslGenerator', () => { if (v) { res = 2; } - if (a) { - res = 3; - } - if (std.atomicLoad(a)) { - res = 4; - } if (p) { res = 5; } @@ -2328,12 +2328,6 @@ describe('wgslGenerator', () => { { res = 2i; } - { - res = 3i; - } - if (bool(atomicLoad(&a))) { - res = 4i; - } { res = 5i; } @@ -2345,6 +2339,35 @@ describe('wgslGenerator', () => { `); }); + it('atomic', () => { + const testFn = tgpu.fn( + [d.atomic(d.u32)], + d.i32, + )((a) => { + let res = -1; + if (a) { + res = 3; + } + if (std.atomicLoad(a)) { + res = 4; + } + return res; + }); + + expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` + "fn testFn(a: atomic) -> i32 { + var res = -1; + { + res = 3i; + } + if (bool(atomicLoad(&a))) { + res = 4i; + } + return res; + }" + `); + }); + it('primitive comptime-known operands', () => { const getTruthy = tgpu.comptime(() => 1882); const getFalsy = tgpu.comptime(() => 0);