diff --git a/apps/typegpu-docs/src/content/docs/apis/data-schemas.mdx b/apps/typegpu-docs/src/content/docs/apis/data-schemas.mdx index ef950cd8b1..56ad66c9c7 100644 --- a/apps/typegpu-docs/src/content/docs/apis/data-schemas.mdx +++ b/apps/typegpu-docs/src/content/docs/apis/data-schemas.mdx @@ -17,6 +17,31 @@ Schemas can also be called to create instances of the types they represent. TypeGPU provides data schemas for scalar, vector and matrix types, as well as constructors for struct and array schemas. +:::danger +WGSL has no `undefined` or `null`. + +If you are tempted to write the following (or any code that relies on `undefined` in JS): +```ts twoslash +// @noUncheckedIndexedAccess: true +import { tgpu, d } from 'typegpu'; + +const arr = tgpu.const(d.arrayOf(d.u32, 7), [0, 1, 2, 3, 4, 5, 6]); +// ---cut--- +const length = () => { + 'use gpu'; + let i = 0; + let e = arr.$[i]; + // ^? + while (!!e) { + i++; + e = arr.$[i]; + } + return i; +} +``` +Be aware that this is undefined behavior in WGSL. +::: + ## Scalars, vectors and matrices ### Constructors 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 996fd3e99e..f8146e0c32 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 @@ -33,7 +33,7 @@ describe('tgsl parsing test example', () => { } fn negateStruct(input: Schema) -> Schema { - let result = Schema(!(input.vec2b), !(input.vec4b), !(input.vec3b), !input.bool); + let result = Schema(!input.vec2b, !input.vec4b, !input.vec3b, !input.bool); return result; } @@ -64,12 +64,12 @@ describe('tgsl parsing test example', () => { s = (s && true); s = (s && true); let vec = vec3(true, false, true); - s = (s && all(!(vec) == negate(vec))); + s = (s && all(!vec == negate(vec))); let inputStruct = Schema(vec2(false, true), vec4(false, true, false, true), vec3(true, true, false), true); let resultStruct = negateStruct(inputStruct); - s = (s && all(!(inputStruct.vec2b) == resultStruct.vec2b)); - s = (s && all(!(inputStruct.vec4b) == resultStruct.vec4b)); - s = (s && all(!(inputStruct.vec3b) == resultStruct.vec3b)); + s = (s && all(!inputStruct.vec2b == resultStruct.vec2b)); + s = (s && all(!inputStruct.vec4b == resultStruct.vec4b)); + s = (s && all(!inputStruct.vec3b == resultStruct.vec3b)); s = (s && (!inputStruct.bool == resultStruct.bool)); return s; } diff --git a/packages/typegpu/src/data/numeric.ts b/packages/typegpu/src/data/numeric.ts index 075f8d5014..1b9384fb86 100644 --- a/packages/typegpu/src/data/numeric.ts +++ b/packages/typegpu/src/data/numeric.ts @@ -1,22 +1,56 @@ import { $internal } from '../shared/symbols.ts'; -import type { AbstractFloat, AbstractInt, Bool, F16, F32, I32, U16, U32 } from './wgslTypes.ts'; +import { + isBool, + type AbstractFloat, + type AbstractInt, + type Bool, + type F16, + type F32, + type I32, + type U16, + type U32, +} from './wgslTypes.ts'; import { callableSchema } from '../core/function/createCallableSchema.ts'; -import { FiniteMathAssumptionError } from '../errors.ts'; +import { FiniteMathAssumptionError, SignatureNotSupportedError, WgslTypeError } from '../errors.ts'; +import { isSnippetNumeric } from './snippet.ts'; +import { UnknownData } from './dataTypes.ts'; const boolCast = callableSchema({ name: 'bool', schema: () => bool, argTypes: (arg) => (arg ? [arg] : []), - normalImpl(v?: number | boolean) { - if (v === undefined) { - return false; - } + normalImpl(v: number | boolean = false) { if (typeof v === 'boolean') { return v; } - return !!v; + + if (typeof v === 'number') { + if (!Number.isFinite(v)) { + throw new FiniteMathAssumptionError(v, bool); + } + return Boolean(v); + } + + throw new Error( + `Invalid argument type for 'd.bool'. Got '${typeof v}', expected number or boolean`, + ); + }, + codegenImpl: (ctx, [v]) => { + // check if zero arguments were passed + if (v === undefined) { + return ctx.gen.typeInstantiation(bool, []); + } + + if (isBool(v.dataType) || isSnippetNumeric(v)) { + return ctx.gen.typeInstantiation(bool, [v]); + } + + if (v.dataType === UnknownData) { + throw new WgslTypeError("Unknown argument type for 'd.bool'."); + } + + throw new SignatureNotSupportedError([v.dataType], [bool, u32, i32, f32, f16]); }, - codegenImpl: (ctx, [v]) => ctx.gen.typeInstantiation(bool, v ? [v] : []), }); /** diff --git a/packages/typegpu/src/data/wgslTypes.ts b/packages/typegpu/src/data/wgslTypes.ts index f9c5f10cfd..cc0cfa2fb6 100644 --- a/packages/typegpu/src/data/wgslTypes.ts +++ b/packages/typegpu/src/data/wgslTypes.ts @@ -604,7 +604,8 @@ 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: number | boolean) => boolean) & (() => boolean)> { readonly type: 'bool'; // Type-tokens, not available at runtime @@ -1532,6 +1533,10 @@ export function isVecInstance(value: unknown): value is AnyVecInstance { return isMarkedInternal(v) && typeof v.kind === 'string' && v.kind.startsWith('vec'); } +export function isVecBoolInstance(value: unknown): value is v2b | v3b | v4b { + return isVecInstance(value) && value.kind.includes('b'); +} + export function isVec2(value: unknown): value is Vec2f | Vec2h | Vec2i | Vec2u { const v = value as AnyWgslData | undefined; return isMarkedInternal(v) && typeof v.type === 'string' && v.type.startsWith('vec2'); diff --git a/packages/typegpu/src/std/boolean.ts b/packages/typegpu/src/std/boolean.ts index 8d6d673f93..09ce22c109 100644 --- a/packages/typegpu/src/std/boolean.ts +++ b/packages/typegpu/src/std/boolean.ts @@ -30,14 +30,14 @@ import { type AnyWgslData, type BaseData, isBool, - isNumericSchema, - isVec, isVecBool, + isVecBoolInstance, isVecInstance, type v2b, type v3b, type v4b, } from '../data/wgslTypes.ts'; +import { SignatureNotSupportedError } from '../errors.ts'; import { unify } from '../tgsl/conversion.ts'; import { sub } from './operators.ts'; @@ -191,82 +191,49 @@ export const ge = dualImpl({ // logical ops -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; +function cpuNot(value: T): T; +function cpuNot(value: T): T { + if (typeof value === 'boolean') { + return !value as T; } - 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)); - } + if (!isVecBoolInstance(value)) { + throw new Error(`'std.not' requires a boolean or boolean vector.`); } - return !value; + switch (value.length) { + case 2: + return vec2b(cpuNot(value.x), cpuNot(value.y)) as T; + case 3: + return vec3b(cpuNot(value.x), cpuNot(value.y), cpuNot(value.z)) as T; + case 4: + return vec4b(cpuNot(value.x), cpuNot(value.y), cpuNot(value.z), cpuNot(value.w)) as T; + } } /** * Returns the logical negation of the given value. - * For scalars (bool, number), returns `!value`. + * For booleans 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(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: (arg) => { - const returnType = isVec(arg) ? correspondingBooleanVectorSchema(arg) : bool; + if (!isBool(arg) && !isVecBool(arg)) { + throw new SignatureNotSupportedError([arg], [bool, vec2b, vec3b, vec4b]); + } + return { argTypes: [arg], - returnType, + returnType: arg, }; }, normalImpl: cpuNot, - 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'; - }, + codegenImpl: (_ctx, [arg]) => stitch`!${arg}`, sideEffects: false, }); diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index 854911eaca..bc29d8ba0b 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -19,7 +19,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 { eq, ne, lt, le, gt, ge } from '../std/boolean.ts'; +import { eq, ne, lt, le, gt, ge, not } from '../std/boolean.ts'; import { isGPUCallable, @@ -183,20 +183,17 @@ const unaryOpCodeToCodegen = { const argStr = ctx.resolveSnippet(argExpr).value; - if (wgsl.isBool(argExpr.dataType)) { - return snip(`!${argStr}`, bool, 'runtime', argExpr.possibleSideEffects); - } - if (wgsl.isNumericSchema(argExpr.dataType)) { - const resultStr = `!bool(${argStr})`; - const nanGuardedStr = // abstractFloat will be resolved as comptime known value - argExpr.dataType.type === 'f32' || argExpr.dataType.type === 'f16' - ? `(((bitcast(${argExpr.dataType.type === 'f16' ? `f32(${argStr})` : argStr}) << 1u) - 1u) >= 0xff000000)` - : resultStr; - - return snip(nanGuardedStr, bool, 'runtime', argExpr.possibleSideEffects); + if (!wgsl.isBool(argExpr.dataType)) { + throw new WgslTypeError( + `Unary operator ! requires boolean operand. Got ${String(argExpr.dataType)}.${ + wgsl.isVecBool(argExpr.dataType) + ? ` For component-wise negation, use 'std.${not.toString()}'.` + : '' + }`, + ); } - return snip(false, bool, 'constant', false); + return snip(`!${argStr}`, bool, 'runtime', argExpr.possibleSideEffects); }, } satisfies Partial unknown>>; diff --git a/packages/typegpu/tests/numeric.test.ts b/packages/typegpu/tests/numeric.test.ts index 5d5aa3598f..dd9843c3af 100644 --- a/packages/typegpu/tests/numeric.test.ts +++ b/packages/typegpu/tests/numeric.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest'; import { tgpu, d } from 'typegpu'; +import { UnknownData } from 'typegpu/~internal'; describe('f32', () => { it('differs in type from other numeric schemas', () => { @@ -49,6 +50,34 @@ describe('f16', () => { }); }); +describe('bool', () => { + it('correctly casts values to booleans', () => { + expect(d.bool(0)).toBe(false); + expect(d.bool(1)).toBe(true); + expect(d.bool(false)).toBe(false); + expect(d.bool(true)).toBe(true); + }); + + it('throws if argument is not a number or boolean', () => { + // @ts-expect-error + expect(() => d.bool({})).toThrowErrorMatchingInlineSnapshot( + `[Error: Invalid argument type for 'd.bool'. Got 'object', expected number or boolean]`, + ); + }); + + it('throws if passed number is not finite', () => { + expect(() => d.bool(NaN)).toThrowErrorMatchingInlineSnapshot( + `[Error: Cannot convert value 'NaN' to type bool because of the Finite Math Assumption (see: https://www.w3.org/TR/WGSL/#finite-math-assumption)]`, + ); + }); + + it('does not accept a possibly undefined argument', () => { + const possiblyUndefined = 1 as number | undefined; + // @ts-expect-error + () => d.bool(possiblyUndefined); + }); +}); + it('has correct default values', () => { expect(d.f32()).toBe(0); expect(d.f16()).toBe(0); @@ -77,6 +106,68 @@ describe('TGSL', () => { }" `); }); + + describe('bool', () => { + it('works with scalars', () => { + const f = tgpu.fn([d.bool, d.u32, d.i32, d.f32, d.f16])((b, u, i, f, h) => { + const _bb = d.bool(b); + const _ub = d.bool(u); + const _ib = d.bool(i); + const _fb = d.bool(f); + const _hb = d.bool(h); + }); + + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "fn f(b: bool, u: u32, i: i32, f_1: f32, h: f16) { + let _bb = b; + let _ub = bool(u); + let _ib = bool(i); + let _fb = bool(f_1); + let _hb = bool(h); + }" + `); + }); + + it('throws when argument datatype is unknown', () => { + const ud = tgpu['~unstable'].rawCodeSnippet( + '', + UnknownData as unknown as d.AnyData, + 'runtime', + false, + ); + + const f = () => { + 'use gpu'; + // @ts-expect-error + const _bud = d.bool(ud.$); + }; + + expect(() => tgpu.resolve([f])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:f + - fn*:f() + - fn:bool: Unknown argument type for 'd.bool'.] + `); + }); + + it('throws when argument datatype is not numeric and boolean', () => { + const f = () => { + 'use gpu'; + const v = d.vec3f(); + // @ts-expect-error + const _bv = d.bool(v); + }; + + expect(() => tgpu.resolve([f])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:f + - fn*:f() + - fn:bool: Unsupported data types: vec3f. Supported types are: bool, u32, i32, f32, f16.] + `); + }); + }); }); describe('Edge cases', () => { diff --git a/packages/typegpu/tests/std/boolean/not.test.ts b/packages/typegpu/tests/std/boolean/not.test.ts index 5a7fb2e57a..6ee6ba550f 100644 --- a/packages/typegpu/tests/std/boolean/not.test.ts +++ b/packages/typegpu/tests/std/boolean/not.test.ts @@ -1,7 +1,7 @@ import { describe, expect } from 'vitest'; import { it } from 'typegpu-testing-utility'; import { not } from 'typegpu/std'; -import { tgpu, d, std } from 'typegpu'; +import { tgpu, d } from 'typegpu'; describe('not', () => { it('negates booleans', () => { @@ -9,224 +9,100 @@ describe('not', () => { 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('throws on non-boolean operand', () => { + // @ts-expect-error + expect(() => not(0)).toThrowErrorMatchingInlineSnapshot( + `[Error: 'std.not' requires a boolean or boolean vector.]`, + ); + // @ts-expect-error + expect(() => not(d.vec3f())).toThrowErrorMatchingInlineSnapshot( + `[Error: 'std.not' requires a boolean or boolean vector.]`, + ); + // @ts-expect-error + expect(() => not({})).toThrowErrorMatchingInlineSnapshot( + `[Error: 'std.not' requires a boolean or boolean vector.]`, + ); }); it('generates correct WGSL on a boolean runtime-known argument', () => { const testFn = tgpu.fn( [d.bool], d.bool, - )((v) => { - return not(v); + )((b) => { + return not(b); }); expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` - "fn testFn(v: bool) -> bool { - return !v; + "fn testFn(b: bool) -> bool { + return !b; }" `); }); - 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); + )((vb) => { + return not(vb); }); 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 { - return -1; + "fn testFn(vb: vec3) -> vec3 { + return !vb; }" `); }); - 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 = not(buffer); - const _b1 = not(buffer.$); - const _b2 = not(v); - const _b3 = not(a); - const _b4 = not(std.atomicLoad(a)); - const _b5 = not(p); - const _b6 = not(p.$); - }); - - expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` - "fn testFn(v: vec3f, a: atomic, p: ptr) { - const _b0 = false; - let _b1 = false; - let _b2 = !(vec3(v)); - let _b3 = false; - let _b4 = !bool(atomicLoad(&a)); - let _b5 = false; - let _b6 = !bool((*p)); - }" - `); - }); - - it('converts numeric vectors to boolean 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', () => { - const s = {}; - expect(not(null)).toBe(true); - expect(not(undefined)).toBe(true); - 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], - 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( + it('throws on non-boolean runtime-known operands', () => { + const testFn1 = tgpu.fn( [d.i32], d.bool, - )((v) => { - return not(v); + )((x) => { + // @ts-expect-error + return not(x); }); - 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); - }" + expect(() => tgpu.resolve([testFn1])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn:testFn1 + - fn:not: Unsupported data types: i32. Supported types are: bool, vec2, vec3, vec4.] `); - }); - it('generates correct WGSL on a numeric vector runtime-known argument', () => { - const testFn = tgpu.fn( - [d.vec3f], - d.vec3b, - )((v) => { - return not(v); + const Boid = d.struct({ pos: d.vec3f }); + const testFn2 = tgpu.fn( + [Boid], + d.bool, + )((s) => { + // @ts-expect-error + return not(s); }); - expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` - "fn testFn(v: vec3f) -> vec3 { - return !(vec3(v)); - }" + expect(() => tgpu.resolve([testFn2])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn:testFn2 + - fn:not: Unsupported data types: struct. Supported types are: bool, vec2, vec3, vec4.] `); }); - it('evaluates at compile time for comptime-known arguments', () => { - const getN = tgpu.comptime(() => 42); - const slot = tgpu.slot<{ a?: number }>({}); + it('injects the result into WGSL if operand is comptime-known', () => { + const b = true; + const vb = d.vec3b(true, false, true); const f = () => { 'use gpu'; - if (not(getN()) && not(slot.$.a) && not(d.vec4f(1, 8, 8, 2)).x) { - return 1; - } - return -1; + const _notB = not(b); + const _notVb = not(vb); }; - expect(tgpu.resolve([f])).toMatchInlineSnapshot(` - "fn f() -> i32 { - return -1; + "fn f() { + const _notB = false; + let _notVb = vec3(false, true, false); }" `); }); diff --git a/packages/typegpu/tests/tgsl/sideEffects.test.ts b/packages/typegpu/tests/tgsl/sideEffects.test.ts index eed9425a3b..6d9c076f91 100644 --- a/packages/typegpu/tests/tgsl/sideEffects.test.ts +++ b/packages/typegpu/tests/tgsl/sideEffects.test.ts @@ -480,13 +480,6 @@ describe('code without side-effects', () => { }).toEqual(false); }); - test('logical not of impure value of complex datatype', () => { - expectSideEffects(() => { - 'use gpu'; - return !impureStruct(); - }).toEqual(false); - }); - test('operators && and || with pure runtime operands', () => { expectSideEffects(() => { 'use gpu'; @@ -566,7 +559,7 @@ describe('code with side-effects', () => { test('logical not of impure value', () => { expectSideEffects(() => { 'use gpu'; - return !impureInt(); + return !impureBool(); }).toEqual(true); }); diff --git a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts index 48af1fa33e..2efa2d1359 100644 --- a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts +++ b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts @@ -1762,125 +1762,107 @@ 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(` + describe('handles unary operator !', () => { + it('works with 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('throws on non-boolean runtime-known operand', () => { + const testFn = tgpu.fn( + [d.vec3f], + d.bool, + )((n) => { + return !n; + }); + + expect(() => tgpu.resolve([testFn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn:testFn: Unary operator ! requires boolean operand. Got vec3f.] `); - }); - - 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 { - { - return 1; - } - return -1; - }" - `); - }); - - it('handles unary operator `!` on operands from slots and accessors', () => { - const Boid = d.struct({ - pos: d.vec2f, - vel: d.vec2f, + it('throws on vector runtime-known operand and provides info about std.not', () => { + const testFn = tgpu.fn( + [d.vec3b], + d.bool, + )((n) => { + return !n; + }); + + expect(() => tgpu.resolve([testFn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn:testFn: Unary operator ! requires boolean operand. Got vec3. For component-wise negation, use 'std.not'.] + `); }); - 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; - }; + it('mimics js on comptime-known operands', () => { + const Boid = d.struct({ + pos: d.vec2f, + vel: d.vec2f, + }); - expect(tgpu.resolve([f])).toMatchInlineSnapshot(` - "fn f() -> i32 { - { - return 1; - } - return -1; - }" - `); - }); + const b = false; + const falsyNumber = 0; + const truthyNumber = 1; + const slot = tgpu.slot>({ pos: d.vec2f(), vel: d.vec2f() }); + const accessor = tgpu.accessor(d.vec4u, d.vec4u(1, 8, 8, 2)); + const falsy = tgpu.comptime(() => undefined); - it('handles chained unary operators `!`', () => { - const testFn = tgpu.fn( - [d.i32], - d.bool, - )((n) => { - // oxlint-disable-next-line - return !!!!!false || !!!n; + const f = () => { + 'use gpu'; + let r = false; + r = !b; + r = !falsyNumber; + r = !truthyNumber; + r = !slot.$; + r = !accessor.$; + r = !falsy(); + return r; + }; + + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "fn f() -> bool { + var r = false; + r = true; + r = true; + r = false; + r = false; + r = false; + r = true; + return r; + }" + `); }); - expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` - "fn testFn(n: i32) -> bool { - return true; - }" - `); + it('chain', () => { + const x = 0; + const testFn = tgpu.fn( + [d.bool], + d.bool, + )((b) => { + // oxlint-disable-next-line + return !!!b || !!!!x; + }); + + expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` + "fn testFn(b: bool) -> bool { + return (!!!b || false); + }" + `); + }); }); it('throws a readable error when assigning an argument reference', () => {