From b2c137b03007fd4a66d2d12492db39a03a51cbd1 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Fri, 3 Apr 2026 18:56:51 +0200 Subject: [PATCH 1/8] 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 2/8] 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 3/8] 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 4/8] 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 5/8] 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 6/8] 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 7/8] 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 e892f33b80e75de4aa34530d4c113d7a2d4e0f7a Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Fri, 10 Apr 2026 21:24:54 +0200 Subject: [PATCH 8/8] review changes + correct imports --- packages/typegpu/src/std/boolean.ts | 6 +--- .../typegpu/tests/std/boolean/not.test.ts | 30 +++++++++---------- .../typegpu/tests/tgsl/wgslGenerator.test.ts | 10 ++++--- 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/packages/typegpu/src/std/boolean.ts b/packages/typegpu/src/std/boolean.ts index 08048cafd6..bcdef078c6 100644 --- a/packages/typegpu/src/std/boolean.ts +++ b/packages/typegpu/src/std/boolean.ts @@ -23,7 +23,7 @@ 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) { @@ -225,10 +225,6 @@ export const not = dualImpl({ }, normalImpl: cpuNot, codegenImpl: (_ctx, [arg]) => { - if (isKnownAtComptime(arg)) { - return `${cpuNot(arg.value)}`; - } - const { dataType } = arg; if (isBool(dataType)) { diff --git a/packages/typegpu/tests/std/boolean/not.test.ts b/packages/typegpu/tests/std/boolean/not.test.ts index dfb0814765..1927d2ad7d 100644 --- a/packages/typegpu/tests/std/boolean/not.test.ts +++ b/packages/typegpu/tests/std/boolean/not.test.ts @@ -1,8 +1,7 @@ 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'; +import tgpu, { d, std } from '../../../src/index.js'; describe('not', () => { it('negates booleans', () => { @@ -17,23 +16,22 @@ describe('not', () => { }); 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)); + expect(not(d.vec2b(true, false))).toStrictEqual(d.vec2b(false, true)); + expect(not(d.vec3b(false, false, true))).toStrictEqual(d.vec3b(true, true, false)); + expect(not(d.vec4b(true, true, false, false))).toStrictEqual(d.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)); + expect(not(d.vec2f(0.0, 1.0))).toStrictEqual(d.vec2b(true, false)); + expect(not(d.vec3i(0, 5, -1))).toStrictEqual(d.vec3b(true, false, false)); + expect(not(d.vec4u(0, 0, 1, 0))).toStrictEqual(d.vec4b(true, true, false, true)); + expect(not(d.vec4h(0, 3.14, 0, -2.5))).toStrictEqual(d.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); + expect(not({})).toBe(false); }); it('mimics WGSL behavior on NaN', () => { @@ -138,8 +136,9 @@ describe('not', () => { const _b1 = !buffer.$; const _b2 = !v; const _b3 = !a; - const _b4 = !p; - const _b5 = !p.$; + const _b4 = !std.atomicLoad(a); + const _b5 = !p; + const _b6 = !p.$; }); expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` @@ -150,8 +149,9 @@ describe('not', () => { const _b1 = false; const _b2 = false; const _b3 = false; - const _b4 = false; - let _b5 = !bool((*p)); + let _b4 = !bool(atomicLoad(&a)); + const _b5 = false; + let _b6 = !bool((*p)); }" `); }); diff --git a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts index fd8ed51182..d179d0ab4b 100644 --- a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts +++ b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts @@ -2107,8 +2107,9 @@ describe('wgslGenerator', () => { const _b1 = !buffer.$; const _b2 = !v; const _b3 = !a; - const _b4 = !p; - const _b5 = !p.$; + const _b4 = !std.atomicLoad(a); + const _b5 = !p; + const _b6 = !p.$; }); expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` @@ -2119,8 +2120,9 @@ describe('wgslGenerator', () => { const _b1 = false; const _b2 = false; const _b3 = false; - const _b4 = false; - let _b5 = !bool((*p)); + let _b4 = !bool(atomicLoad(&a)); + const _b5 = false; + let _b6 = !bool((*p)); }" `); });