Skip to content

Commit 8a8e7ac

Browse files
authored
feat: unary operator ! support and std.not (#2346)
1 parent 18c2634 commit 8a8e7ac

6 files changed

Lines changed: 404 additions & 18 deletions

File tree

apps/typegpu-docs/tests/individual-example-tests/tgsl-parsing-test.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,8 @@ describe('tgsl parsing test example', () => {
4444
s = (s && true);
4545
s = (s && true);
4646
s = (s && true);
47-
s = (s && !false);
4847
s = (s && true);
49-
s = (s && !false);
5048
s = (s && true);
51-
s = (s && !false);
5249
s = (s && true);
5350
s = (s && true);
5451
s = (s && true);
@@ -58,9 +55,12 @@ describe('tgsl parsing test example', () => {
5855
s = (s && true);
5956
s = (s && true);
6057
s = (s && true);
61-
s = (s && !false);
6258
s = (s && true);
63-
s = (s && !false);
59+
s = (s && true);
60+
s = (s && true);
61+
s = (s && true);
62+
s = (s && true);
63+
s = (s && true);
6464
s = (s && true);
6565
s = (s && true);
6666
var vec = vec3<bool>(true, false, true);

packages/typegpu/src/data/wgslTypes.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1713,6 +1713,10 @@ export function isVoid(value: unknown): value is Void {
17131713
return isMarkedInternal(value) && (value as Void).type === 'void';
17141714
}
17151715

1716+
export function isBool(value: unknown): value is Bool {
1717+
return isMarkedInternal(value) && (value as Bool).type === 'bool';
1718+
}
1719+
17161720
export function isNumericSchema(
17171721
schema: unknown,
17181722
): schema is AbstractInt | AbstractFloat | F32 | F16 | I32 | U32 {

packages/typegpu/src/std/boolean.ts

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ import {
1313
type AnyVecInstance,
1414
type AnyWgslData,
1515
type BaseData,
16+
isBool,
17+
isNumericSchema,
18+
isVec,
19+
isVecBool,
1620
isVecInstance,
1721
type v2b,
1822
type v3b,
@@ -164,19 +168,82 @@ export const ge = dualImpl({
164168

165169
// logical ops
166170

167-
const cpuNot = <T extends AnyBooleanVecInstance>(value: T): T => VectorOps.neg[value.kind](value);
171+
type VecInstanceToBooleanVecInstance<T extends AnyVecInstance> = T extends AnyVec2Instance
172+
? v2b
173+
: T extends AnyVec3Instance
174+
? v3b
175+
: v4b;
176+
177+
function cpuNot(value: boolean): boolean;
178+
function cpuNot(value: number): boolean;
179+
function cpuNot<T extends AnyVecInstance>(value: T): VecInstanceToBooleanVecInstance<T>;
180+
function cpuNot(value: unknown): boolean;
181+
function cpuNot(value: unknown): boolean | AnyBooleanVecInstance {
182+
if (typeof value === 'number' && isNaN(value)) {
183+
return false;
184+
}
185+
186+
if (isVecInstance(value)) {
187+
if (value.length === 2) {
188+
return vec2b(cpuNot(value.x), cpuNot(value.y));
189+
}
190+
if (value.length === 3) {
191+
return vec3b(cpuNot(value.x), cpuNot(value.y), cpuNot(value.z));
192+
}
193+
if (value.length === 4) {
194+
return vec4b(cpuNot(value.x), cpuNot(value.y), cpuNot(value.z), cpuNot(value.w));
195+
}
196+
}
197+
198+
return !value;
199+
}
168200

169201
/**
170-
* Returns **component-wise** `!value`.
202+
* Returns the logical negation of the given value.
203+
* For scalars (bool, number), returns `!value`.
204+
* For boolean vectors, returns **component-wise** `!value`.
205+
* For numeric vectors, returns a boolean vector with component-wise truthiness negation.
206+
* For all other types, returns the truthiness negation (in WGSL, this applies only if the value is known at compile-time).
171207
* @example
172-
* not(vec2b(false, true)) // returns vec2b(true, false)
208+
* not(true) // returns false
209+
* not(-1) // returns false
210+
* not(0) // returns true
173211
* not(vec3b(true, true, false)) // returns vec3b(false, false, true)
212+
* not(vec3f(1.0, 0.0, -1.0)) // returns vec3b(false, true, false)
213+
* not({a: 1882}) // returns false
214+
* not(NaN) // returns false **as in WGSL**
174215
*/
175216
export const not = dualImpl({
176217
name: 'not',
177-
signature: (...argTypes) => ({ argTypes, returnType: argTypes[0] }),
218+
signature: (arg) => {
219+
const returnType = isVec(arg) ? correspondingBooleanVectorSchema(arg) : bool;
220+
return {
221+
argTypes: [arg],
222+
returnType,
223+
};
224+
},
178225
normalImpl: cpuNot,
179-
codegenImpl: (_ctx, [arg]) => stitch`!(${arg})`,
226+
codegenImpl: (_ctx, [arg]) => {
227+
const { dataType } = arg;
228+
229+
if (isBool(dataType)) {
230+
return stitch`!${arg}`;
231+
}
232+
if (isNumericSchema(dataType)) {
233+
return stitch`!bool(${arg})`;
234+
}
235+
236+
if (isVecBool(dataType)) {
237+
return stitch`!(${arg})`;
238+
}
239+
240+
if (isVec(dataType)) {
241+
const vecConstructorStr = `vec${dataType.componentCount}<bool>`;
242+
return stitch`!(${vecConstructorStr}(${arg}))`;
243+
}
244+
245+
return 'false';
246+
},
180247
});
181248

182249
const cpuOr = <T extends AnyBooleanVecInstance>(lhs: T, rhs: T) => VectorOps.or[lhs.kind](lhs, rhs);

packages/typegpu/src/tgsl/wgslGenerator.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,35 @@ function operatorToType<
157157
const unaryOpCodeToCodegen = {
158158
'-': neg[$gpuCallable].call.bind(neg),
159159
void: () => snip(undefined, wgsl.Void, 'constant'),
160+
'!': (ctx: GenerationCtx, [argExpr]: Snippet[]) => {
161+
if (argExpr === undefined) {
162+
throw new Error('The unary operator `!` expects 1 argument, but 0 were provided.');
163+
}
164+
165+
if (isKnownAtComptime(argExpr)) {
166+
return snip(!argExpr.value, bool, 'constant');
167+
}
168+
169+
const { value, dataType } = argExpr;
170+
const argStr = ctx.resolve(value, dataType).value;
171+
172+
if (wgsl.isBool(dataType)) {
173+
return snip(`!${argStr}`, bool, 'runtime');
174+
}
175+
if (wgsl.isNumericSchema(dataType)) {
176+
const resultStr = `!bool(${argStr})`;
177+
const nanGuardedStr = // abstractFloat will be resolved as comptime known value
178+
dataType.type === 'f32'
179+
? `(((bitcast<u32>(${argStr}) & 0x7fffffff) > 0x7f800000) || ${resultStr})`
180+
: dataType.type === 'f16'
181+
? `(((bitcast<u32>(${argStr}) & 0x7fff) > 0x7c00) || ${resultStr})`
182+
: resultStr;
183+
184+
return snip(nanGuardedStr, bool, 'runtime');
185+
}
186+
187+
return snip(false, bool, 'constant');
188+
},
160189
} satisfies Partial<Record<tinyest.UnaryOperator, (...args: never[]) => unknown>>;
161190

162191
const binaryOpCodeToCodegen = {
Lines changed: 152 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,156 @@
1-
import { describe, expect, it } from 'vitest';
2-
import { vec2b, vec3b, vec4b } from '../../../src/data/index.ts';
1+
import { describe, expect } from 'vitest';
2+
import { it } from 'typegpu-testing-utility';
33
import { not } from '../../../src/std/boolean.ts';
4+
import tgpu, { d, std } from '../../../src/index.js';
45

5-
describe('neg', () => {
6-
it('negates', () => {
7-
expect(not(vec2b(true, false))).toStrictEqual(vec2b(false, true));
8-
expect(not(vec3b(false, false, true))).toStrictEqual(vec3b(true, true, false));
9-
expect(not(vec4b(true, true, false, false))).toStrictEqual(vec4b(false, false, true, true));
6+
describe('not', () => {
7+
it('negates booleans', () => {
8+
expect(not(true)).toBe(false);
9+
expect(not(false)).toBe(true);
10+
});
11+
12+
it('converts numbers to booleans and negates', () => {
13+
expect(not(0)).toBe(true);
14+
expect(not(-1)).toBe(false);
15+
expect(not(42)).toBe(false);
16+
});
17+
18+
it('negates boolean vectors', () => {
19+
expect(not(d.vec2b(true, false))).toStrictEqual(d.vec2b(false, true));
20+
expect(not(d.vec3b(false, false, true))).toStrictEqual(d.vec3b(true, true, false));
21+
expect(not(d.vec4b(true, true, false, false))).toStrictEqual(d.vec4b(false, false, true, true));
22+
});
23+
24+
it('converts numeric vectors to booleans vectors and negates component-wise', () => {
25+
expect(not(d.vec2f(0.0, 1.0))).toStrictEqual(d.vec2b(true, false));
26+
expect(not(d.vec3i(0, 5, -1))).toStrictEqual(d.vec3b(true, false, false));
27+
expect(not(d.vec4u(0, 0, 1, 0))).toStrictEqual(d.vec4b(true, true, false, true));
28+
expect(not(d.vec4h(0, 3.14, 0, -2.5))).toStrictEqual(d.vec4b(true, false, true, false));
29+
});
30+
31+
it('negates truthiness check', () => {
32+
expect(not(null)).toBe(true);
33+
expect(not(undefined)).toBe(true);
34+
expect(not({})).toBe(false);
35+
});
36+
37+
it('mimics WGSL behavior on NaN', () => {
38+
expect(not(NaN)).toBe(false);
39+
});
40+
41+
it('generates correct WGSL on a boolean runtime-known argument', () => {
42+
const testFn = tgpu.fn(
43+
[d.bool],
44+
d.bool,
45+
)((v) => {
46+
return not(v);
47+
});
48+
expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(`
49+
"fn testFn(v: bool) -> bool {
50+
return !v;
51+
}"
52+
`);
53+
});
54+
55+
it('generates correct WGSL on a numeric runtime-known argument', () => {
56+
const testFn = tgpu.fn(
57+
[d.i32],
58+
d.bool,
59+
)((v) => {
60+
return not(v);
61+
});
62+
expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(`
63+
"fn testFn(v: i32) -> bool {
64+
return !bool(v);
65+
}"
66+
`);
67+
});
68+
69+
it('generates correct WGSL on a boolean vector runtime-known argument', () => {
70+
const testFn = tgpu.fn(
71+
[d.vec3b],
72+
d.vec3b,
73+
)((v) => {
74+
return not(v);
75+
});
76+
expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(`
77+
"fn testFn(v: vec3<bool>) -> vec3<bool> {
78+
return !(v);
79+
}"
80+
`);
81+
});
82+
83+
it('generates correct WGSL on a numeric vector runtime-known argument', () => {
84+
const testFn = tgpu.fn(
85+
[d.vec3f],
86+
d.vec3b,
87+
)((v) => {
88+
return not(v);
89+
});
90+
expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(`
91+
"fn testFn(v: vec3f) -> vec3<bool> {
92+
return !(vec3<bool>(v));
93+
}"
94+
`);
95+
});
96+
97+
it('generates correct WGSL on a numeric vector comptime-known argument', () => {
98+
const f = () => {
99+
'use gpu';
100+
const v = not(d.vec4f(Infinity, -Infinity, 0, NaN));
101+
};
102+
103+
expect(tgpu.resolve([f])).toMatchInlineSnapshot(`
104+
"fn f() {
105+
var v = vec4<bool>(false, false, true, false);
106+
}"
107+
`);
108+
});
109+
110+
it('evaluates at compile time for comptime-known arguments', () => {
111+
const getN = tgpu.comptime(() => 42);
112+
const slot = tgpu.slot<{ a?: number }>({});
113+
114+
const f = () => {
115+
'use gpu';
116+
if (not(getN()) && not(slot.$.a) && not(d.vec4f(1, 8, 8, 2)).x) {
117+
return 1;
118+
}
119+
return -1;
120+
};
121+
122+
expect(tgpu.resolve([f])).toMatchInlineSnapshot(`
123+
"fn f() -> i32 {
124+
if (((false && true) && false)) {
125+
return 1;
126+
}
127+
return -1;
128+
}"
129+
`);
130+
});
131+
132+
it('mimics JS on non-primitive values', ({ root }) => {
133+
const buffer = root.createUniform(d.mat4x4f);
134+
const testFn = tgpu.fn([d.vec3f, d.atomic(d.u32), d.ptrPrivate(d.u32)])((v, a, p) => {
135+
const _b0 = not(buffer);
136+
const _b1 = not(buffer.$);
137+
const _b2 = not(v);
138+
const _b3 = not(a);
139+
const _b4 = not(std.atomicLoad(a));
140+
const _b5 = not(p);
141+
const _b6 = not(p.$);
142+
});
143+
144+
expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(`
145+
"fn testFn(v: vec3f, a: atomic<u32>, p: ptr<private, u32>) {
146+
const _b0 = false;
147+
let _b1 = false;
148+
var _b2 = !(vec3<bool>(v));
149+
let _b3 = false;
150+
let _b4 = !bool(atomicLoad(&a));
151+
let _b5 = false;
152+
let _b6 = !bool((*p));
153+
}"
154+
`);
10155
});
11156
});

0 commit comments

Comments
 (0)