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/data/wgslTypes.ts b/packages/typegpu/src/data/wgslTypes.ts index 820f1911c7..dc85031925 100644 --- a/packages/typegpu/src/data/wgslTypes.ts +++ b/packages/typegpu/src/data/wgslTypes.ts @@ -1713,6 +1713,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 isMarkedInternal(value) && (value as Bool).type === 'bool'; +} + export function isNumericSchema( schema: unknown, ): schema is AbstractInt | AbstractFloat | F32 | F16 | I32 | U32 { diff --git a/packages/typegpu/src/std/boolean.ts b/packages/typegpu/src/std/boolean.ts index 89bb2e8898..bcdef078c6 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 { sub } from './operators.ts'; function correspondingBooleanVectorSchema(dataType: BaseData) { @@ -164,19 +169,82 @@ 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 (typeof value === 'number' && isNaN(value)) { + return false; + } + + if (isVecInstance(value)) { + if (value.length === 2) { + return vec2b(cpuNot(value.x), cpuNot(value.y)); + } + if (value.length === 3) { + 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 !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 + * not(NaN) // returns false **as in WGSL** */ 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]) => { + 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}))`; + } + + return 'false'; + }, }); const cpuOr = (lhs: T, rhs: T) => VectorOps.or[lhs.kind](lhs, rhs); diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index b0b9f1e740..37e503024f 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -157,6 +157,35 @@ function operatorToType< const unaryOpCodeToCodegen = { '-': neg[$gpuCallable].call.bind(neg), void: () => snip(undefined, wgsl.Void, 'constant'), + '!': (ctx: GenerationCtx, [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)) { + 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'); + }, } satisfies Partial unknown>>; const binaryOpCodeToCodegen = { diff --git a/packages/typegpu/tests/std/boolean/not.test.ts b/packages/typegpu/tests/std/boolean/not.test.ts index 9de0abf2e5..1927d2ad7d 100644 --- a/packages/typegpu/tests/std/boolean/not.test.ts +++ b/packages/typegpu/tests/std/boolean/not.test.ts @@ -1,11 +1,158 @@ -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 { not } from '../../../src/std/boolean.ts'; +import tgpu, { d, std } from '../../../src/index.js'; -describe('neg', () => { - it('negates', () => { - 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)); +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(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(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', () => { + expect(not(null)).toBe(true); + expect(not(undefined)).toBe(true); + expect(not({})).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], + 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('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 }>({}); + + 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('mimics JS on non-primitive values', ({ root }) => { + const buffer = root.createUniform(d.mat4x4f); + 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 = !std.atomicLoad(a); + const _b5 = !p; + const _b6 = !p.$; + }); + + 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; + 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 b973015c9e..d179d0ab4b 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'; @@ -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; @@ -2068,4 +2069,144 @@ describe('wgslGenerator', () => { }" `); }); + + 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(` + "fn testFn(b: bool) -> bool { + return !b; + }" + `); + }); + + 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(` + "fn testFn(n: i32) -> bool { + return !bool(n); + }" + `); + }); + + it('handles unary operator `!` on non-primitive values', ({ root }) => { + const buffer = root.createUniform(d.mat4x4f); + 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 = !std.atomicLoad(a); + const _b5 = !p; + const _b6 = !p.$; + }); + + 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; + let _b4 = !bool(atomicLoad(&a)); + const _b5 = false; + let _b6 = !bool((*p)); + }" + `); + }); + + 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; + }; + + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "fn f() -> i32 { + if ((true || false)) { + return 1; + } + return -1; + }" + `); + }); + + it('handles unary operator `!` on 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'; + if (!!slot.$ && !!accessor.$) { + return 1; + } + return -1; + }; + + 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 operand', () => { + const slot = tgpu.slot<{ a?: number }>({}); + + const f = () => { + 'use gpu'; + // oxlint-disable-next-line + if (!!slot.$.a) { + return slot.$.a; + } + return 1929; + }; + + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "fn f() -> i32 { + return 1929; + }" + `); + }); });