diff --git a/apps/typegpu-docs/src/examples/algorithms/bitonic-sort/index.ts b/apps/typegpu-docs/src/examples/algorithms/bitonic-sort/index.ts index 459d3d296f..adb688bafc 100644 --- a/apps/typegpu-docs/src/examples/algorithms/bitonic-sort/index.ts +++ b/apps/typegpu-docs/src/examples/algorithms/bitonic-sort/index.ts @@ -129,7 +129,7 @@ const initKernel = tgpu.computeFn({ return; } - randf.seed3(d.vec3f(d.f32(idx & 0xffff), d.f32(idx >> 16), initSeed.$)); + randf.seed3(d.vec3f(d.f32(idx & 0xffff), d.f32(idx >>> 16), initSeed.$)); const n = randf.sample(); initLayout.$.data[idx] = d.u32(std.floor(n * 256.0)); }); diff --git a/apps/typegpu-docs/src/examples/algorithms/genetic-racing/index.ts b/apps/typegpu-docs/src/examples/algorithms/genetic-racing/index.ts index 1c3052a73c..c46d985586 100644 --- a/apps/typegpu-docs/src/examples/algorithms/genetic-racing/index.ts +++ b/apps/typegpu-docs/src/examples/algorithms/genetic-racing/index.ts @@ -291,7 +291,7 @@ const finalizeReductionPipeline = root.createGuardedComputePipeline(() => { 'use gpu'; const packed = std.atomicLoad(reductionLayout.$.packed); reductionLayout.$.bestIdx = packed & 0xffff; - reductionLayout.$.bestFitness = (d.f32(packed >> 16) / 65535) * 64; + reductionLayout.$.bestFitness = (d.f32(packed >>> 16) / 65535) * 64; }); const colors = { diff --git a/apps/typegpu-docs/src/examples/geometry/lines-combinations/index.ts b/apps/typegpu-docs/src/examples/geometry/lines-combinations/index.ts index 944dab8986..dcf98fdfca 100644 --- a/apps/typegpu-docs/src/examples/geometry/lines-combinations/index.ts +++ b/apps/typegpu-docs/src/examples/geometry/lines-combinations/index.ts @@ -203,7 +203,7 @@ const mainFragment = tgpu.fragmentFn({ } return vec4f( color, - select(f32(0), f32(1), (u32(screenPosition.x) >> 3) % 2 !== (u32(screenPosition.y) >> 3) % 2), + select(f32(0), f32(1), (u32(screenPosition.x) >>> 3) % 2 !== (u32(screenPosition.y) >>> 3) % 2), ); }); diff --git a/apps/typegpu-docs/src/examples/image-processing/ascii-filter/index.ts b/apps/typegpu-docs/src/examples/image-processing/ascii-filter/index.ts index b47993c246..0b4b6db242 100644 --- a/apps/typegpu-docs/src/examples/image-processing/ascii-filter/index.ts +++ b/apps/typegpu-docs/src/examples/image-processing/ascii-filter/index.ts @@ -44,7 +44,7 @@ const characterFn = tgpu.fn( // Convert 2D bitmap position to 1D bit index (row-major order) const a = d.u32(pos.x + 5 * pos.y); // Extract the bit at position 'a' from the character bitmap 'n' - return d.f32((n >> a) & 1); + return d.f32((n >>> a) & 1); }); const video = document.querySelector('video') as HTMLVideoElement; diff --git a/apps/typegpu-docs/src/examples/image-processing/selfie-segmentation/inference/video-preprocess.ts b/apps/typegpu-docs/src/examples/image-processing/selfie-segmentation/inference/video-preprocess.ts index 036671a302..b7531e50df 100644 --- a/apps/typegpu-docs/src/examples/image-processing/selfie-segmentation/inference/video-preprocess.ts +++ b/apps/typegpu-docs/src/examples/image-processing/selfie-segmentation/inference/video-preprocess.ts @@ -43,7 +43,7 @@ export const videoPreprocessKernel = tgpu.computeFn({ return; } - const coord = d.vec2u(i & MODEL_COORD_MASK, std.bitShiftRight(i, MODEL_COORD_SHIFT)); + const coord = d.vec2u(i & MODEL_COORD_MASK, i >>> MODEL_COORD_SHIFT); const pixel = d.vec2f(coord) + 0.5; const cropUv = d.vec2f(MODEL_SIZE.x - pixel.x, pixel.y) / MODEL_SIZE; const sourceUv = diff --git a/apps/typegpu-docs/src/examples/image-processing/selfie-segmentation/post-processing/kernels.ts b/apps/typegpu-docs/src/examples/image-processing/selfie-segmentation/post-processing/kernels.ts index 5d57dc8de7..93658f1040 100644 --- a/apps/typegpu-docs/src/examples/image-processing/selfie-segmentation/post-processing/kernels.ts +++ b/apps/typegpu-docs/src/examples/image-processing/selfie-segmentation/post-processing/kernels.ts @@ -86,7 +86,7 @@ export const upsampleMaskLayout = tgpu.bindGroupLayout({ const maskCoord = (i: number) => { 'use gpu'; - return d.vec2u(i & MODEL_COORD_MASK, std.bitShiftRight(i, MODEL_COORD_SHIFT)); + return d.vec2u(i & MODEL_COORD_MASK, i >>> MODEL_COORD_SHIFT); }; const maskIndex = (coord: d.v2i) => { diff --git a/apps/typegpu-docs/src/examples/rendering/radiance-cascades/index.ts b/apps/typegpu-docs/src/examples/rendering/radiance-cascades/index.ts index 15b96c6e1a..5400247b37 100644 --- a/apps/typegpu-docs/src/examples/rendering/radiance-cascades/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/radiance-cascades/index.ts @@ -158,7 +158,7 @@ const cascadePassPipeline = root } if (layer + 1 < cascadeAmountUniform.$ && T > 0.01) { - const probesU = d.vec2u(std.max(probes.x >> 1, 1), std.max(probes.y >> 1, 1)); + const probesU = d.vec2u(std.max(probes.x >>> 1, 1), std.max(probes.y >>> 1, 1)); const tileOrigin = d.vec2f(dirActual) * d.vec2f(probesU); const probePixel = std.clamp( probePos * d.vec2f(probesU), @@ -254,7 +254,7 @@ const overlayFrag = tgpu.fragmentFn({ const debugLayer = overlayDebugCascadeUniform.$; const cascadeProbes = cascadeProbesUniform.$; const probes = std.max( - d.vec2u(cascadeProbes.x >> debugLayer, cascadeProbes.y >> debugLayer), + d.vec2u(cascadeProbes.x >>> debugLayer, cascadeProbes.y >>> debugLayer), d.vec2u(1), ); const raysDimStored = d.u32(2) << debugLayer; @@ -304,7 +304,7 @@ const overlayFrag = tgpu.fragmentFn({ const rayDist = sdf.sdLine(uv, probePos, probePos + rayDir * std.max(rayEndDistance, 0.01)); if (rayDist < minRayDist) { - const dirStored = d.vec2u((ri % raysDimActual) >> 1, d.u32(ri / raysDimActual) >> 1); + const dirStored = d.vec2u((ri % raysDimActual) >>> 1, d.u32(ri / raysDimActual) >>> 1); const sample = std.textureLoad( overlayDebugBGL.$.cascadeTex, d.vec2i(dirStored * probes + probe), diff --git a/apps/typegpu-docs/src/examples/simulation/game-of-life/index.ts b/apps/typegpu-docs/src/examples/simulation/game-of-life/index.ts index d7f57cad9e..096fd5a21c 100644 --- a/apps/typegpu-docs/src/examples/simulation/game-of-life/index.ts +++ b/apps/typegpu-docs/src/examples/simulation/game-of-life/index.ts @@ -180,7 +180,7 @@ const sampleBitpacked = (sampleUv: d.v2f, gs: number): number => { const packedX = cellX / 32; const bitIndex = cellX % 32; const packed = std.textureLoad(displayLayout.$.source, d.vec2u(packedX, cellY)).x; - return (packed >> bitIndex) & d.u32(1); + return (packed >>> bitIndex) & d.u32(1); }; const cellSamplerSlot = tgpu.slot<(uv: d.v2f, gs: number) => number>(sampleRegular); diff --git a/apps/typegpu-docs/src/examples/simulation/game-of-life/shaders/bitpacked-compute.ts b/apps/typegpu-docs/src/examples/simulation/game-of-life/shaders/bitpacked-compute.ts index 6b6276884a..98ed9b6f84 100644 --- a/apps/typegpu-docs/src/examples/simulation/game-of-life/shaders/bitpacked-compute.ts +++ b/apps/typegpu-docs/src/examples/simulation/game-of-life/shaders/bitpacked-compute.ts @@ -43,12 +43,12 @@ const gatherNeighborhood = ( const shiftLeft = (center: number, left: number): number => { 'use gpu'; - return (center << 1) | (left >> 31); + return (center << 1) | (left >>> 31); }; const shiftRight = (center: number, right: number): number => { 'use gpu'; - return (center >> 1) | (right << 31); + return (center >>> 1) | (right << 31); }; const bitpackedNeighbors = (n: d.Infer): d.Infer => { diff --git a/packages/eslint-plugin/src/rules/noUnsupportedSyntax.ts b/packages/eslint-plugin/src/rules/noUnsupportedSyntax.ts index 00773ef0d4..6bcd642c44 100644 --- a/packages/eslint-plugin/src/rules/noUnsupportedSyntax.ts +++ b/packages/eslint-plugin/src/rules/noUnsupportedSyntax.ts @@ -240,6 +240,6 @@ export const noUnsupportedSyntax = createRule({ }), }); -const unsupportedAssignmentOps = ['&&=', '**=', '||=', '>>>=', '??=']; -const unsupportedBinaryOps = ['==', '!=', '>>>', 'in', 'instanceof', '|>']; +const unsupportedAssignmentOps = ['&&=', '**=', '||=', '??=']; +const unsupportedBinaryOps = ['==', '!=', 'in', 'instanceof', '|>']; const unsupportedUnaryOps = ['+', 'typeof', 'void', 'delete']; diff --git a/packages/eslint-plugin/tests/rules/noUnsupportedSyntax.test.ts b/packages/eslint-plugin/tests/rules/noUnsupportedSyntax.test.ts index 1e04b7d909..7ee0b6798a 100644 --- a/packages/eslint-plugin/tests/rules/noUnsupportedSyntax.test.ts +++ b/packages/eslint-plugin/tests/rules/noUnsupportedSyntax.test.ts @@ -21,7 +21,7 @@ describe('noUnsupportedSyntax', () => { ], }, { - code: "const fn = () => { 'use gpu'; let a = 0; a **= 1; a ??= 1; a &&= 1; a ||= 1; a >>>= 1; }", + code: "const fn = () => { 'use gpu'; let a = 0; a **= 1; a ??= 1; a &&= 1; a ||= 1; }", errors: [ { messageId: 'unexpected', @@ -39,10 +39,6 @@ describe('noUnsupportedSyntax', () => { messageId: 'unexpected', data: { snippet: 'a ||= 1', syntax: "assignment expression '||='" }, }, - { - messageId: 'unexpected', - data: { snippet: 'a >>>= 1', syntax: "assignment expression '>>>='" }, - }, ], }, { @@ -91,12 +87,8 @@ describe('noUnsupportedSyntax', () => { ], }, { - code: "const fn = () => { 'use gpu'; a >>> b; c in d; e instanceof Foo; return g != 0; }", + code: "const fn = () => { 'use gpu'; c in d; e instanceof Foo; return g != 0; }", errors: [ - { - messageId: 'unexpected', - data: { snippet: 'a >>> b', syntax: "binary operator '>>>'" }, - }, { messageId: 'unexpected', data: { snippet: 'c in d', syntax: "binary operator 'in'" }, diff --git a/packages/typegpu-geometry/src/lines/lineVariableWidth.ts b/packages/typegpu-geometry/src/lines/lineVariableWidth.ts index 07c5d8cada..b6870a71ab 100644 --- a/packages/typegpu-geometry/src/lines/lineVariableWidth.ts +++ b/packages/typegpu-geometry/src/lines/lineVariableWidth.ts @@ -41,7 +41,7 @@ export const lineVariableWidth = tgpu.fn( } const coreVertexIndex = (vertexIndex - 2) & 0b11; - const joinVertexIndex = (vertexIndex - 2) >> 2; + const joinVertexIndex = (vertexIndex - 2) >>> 2; let join = JoinInput(); const normAB = normalize(AB); diff --git a/packages/typegpu-geometry/src/lines/polylineVariableWidth.ts b/packages/typegpu-geometry/src/lines/polylineVariableWidth.ts index 9c884f7a0b..e56f198061 100644 --- a/packages/typegpu-geometry/src/lines/polylineVariableWidth.ts +++ b/packages/typegpu-geometry/src/lines/polylineVariableWidth.ts @@ -64,7 +64,7 @@ export const polylineVariableWidth = tgpu.fn( } const coreVertexIndex = (vertexIndex - 2) & 0b11; - const joinVertexIndex = (vertexIndex - 2) >> 2; + const joinVertexIndex = (vertexIndex - 2) >>> 2; let join = JoinInput(); let isCap = false; let shouldJoin = false; diff --git a/packages/typegpu-noise/src/utils.ts b/packages/typegpu-noise/src/utils.ts index f2533c4399..4392e66433 100644 --- a/packages/typegpu-noise/src/utils.ts +++ b/packages/typegpu-noise/src/utils.ts @@ -36,8 +36,7 @@ export const rotl = tgpu.fn( [d.u32, d.u32], d.u32, )((x, k) => { - // TODO(#2768) - remove the conditional expression below - return std.isBeingTranspiled() ? (x << k) | (x >> (32 - k)) : (x << k) | (x >>> (32 - k)); + return (x << k) | (x >>> (32 - k)); }); /** @@ -65,13 +64,13 @@ export const hash = tgpu.fn( d.u32, )((value) => { if (std.isBeingTranspiled()) { - let x = value ^ (value >> 17); + let x = value ^ (value >>> 17); x *= d.u32(0xed5ad4bb); - x ^= x >> 11; + x ^= x >>> 11; x *= d.u32(0xac4c1b51); - x ^= x >> 15; + x ^= x >>> 15; x *= d.u32(0x31848bab); - x ^= x >> 14; + x ^= x >>> 14; return x; } else { let x = value ^ (value >>> 17); diff --git a/packages/typegpu-radiance-cascades/src/cascades.ts b/packages/typegpu-radiance-cascades/src/cascades.ts index 5a32202d24..590aefda73 100644 --- a/packages/typegpu-radiance-cascades/src/cascades.ts +++ b/packages/typegpu-radiance-cascades/src/cascades.ts @@ -111,7 +111,7 @@ export const cascadePassCompute = tgpu.computeFn({ const params = cascadePassBGL.$.staticParams; const layer = cascadePassBGL.$.layer; const probes = std.max( - d.vec2u(params.baseProbes.x >> layer, params.baseProbes.y >> layer), + d.vec2u(params.baseProbes.x >>> layer, params.baseProbes.y >>> layer), d.vec2u(1, 1), ); @@ -144,8 +144,8 @@ export const cascadePassCompute = tgpu.computeFn({ let accum = d.vec4f(); - for (let i = 0; i < 4; i++) { - const dirActual = dirStored * 2 + d.vec2u(i & 1, i >> 1); + for (let i = d.u32(0); i < 4; i++) { + const dirActual = dirStored * 2 + d.vec2u(i & 1, i >>> 1); const rayIndex = d.f32(dirActual.y * raysDimActual + dirActual.x) + 0.5; const angle = (rayIndex / rayCountActual) * (Math.PI * 2) - Math.PI; const cosA = std.cos(angle); @@ -162,7 +162,7 @@ export const cascadePassCompute = tgpu.computeFn({ let T = d.f32(marchResult.transmittance); if (layer < params.cascadeCount - 1 && T > 0.01) { - const probesU = std.max(d.vec2u(probes.x >> 1, probes.y >> 1), d.vec2u(1)); + const probesU = std.max(d.vec2u(probes.x >>> 1, probes.y >>> 1), d.vec2u(1)); const tileOrigin = d.vec2f(dirActual) * d.vec2f(probesU); const probePixel = std.clamp( probePos * d.vec2f(probesU), @@ -226,7 +226,7 @@ export const buildRadianceFieldCompute = tgpu.computeFn({ let sum = d.vec3f(); for (let i = d.u32(0); i < 4; i++) { - const offset = d.vec2f(i & 1, i >> 1) * uvStride; + const offset = d.vec2f(i & 1, i >>> 1) * uvStride; const sample = std.textureSampleLevel( buildRadianceFieldBGL.$.src, buildRadianceFieldBGL.$.srcSampler, diff --git a/packages/typegpu-sort/src/bitonic/bitonicSort.ts b/packages/typegpu-sort/src/bitonic/bitonicSort.ts index a4bba2eade..5cb888379c 100644 --- a/packages/typegpu-sort/src/bitonic/bitonicSort.ts +++ b/packages/typegpu-sort/src/bitonic/bitonicSort.ts @@ -111,7 +111,7 @@ const bitonicStepKernel = tgpu.computeFn({ const maskBelow = stride - 1; const below = tid & maskBelow; - const above = tid >> shift; + const above = tid >>> shift; const i = below + above * (stride << 1); const ixj = i + stride; diff --git a/packages/typegpu/src/data/vectorOps.ts b/packages/typegpu/src/data/vectorOps.ts index 3bd64a6aa0..8a6e1ca636 100644 --- a/packages/typegpu/src/data/vectorOps.ts +++ b/packages/typegpu/src/data/vectorOps.ts @@ -1116,13 +1116,13 @@ export const VectorOps = { bitShiftRight: { vec2i: binaryComponentWise2i2u((a, b) => a >> b), - vec2u: binaryComponentWise2u((a, b) => a >> b), + vec2u: binaryComponentWise2u((a, b) => a >>> b), vec3i: binaryComponentWise3i3u((a, b) => a >> b), - vec3u: binaryComponentWise3u((a, b) => a >> b), + vec3u: binaryComponentWise3u((a, b) => a >>> b), vec4i: binaryComponentWise4i4u((a, b) => a >> b), - vec4u: binaryComponentWise4u((a, b) => a >> b), + vec4u: binaryComponentWise4u((a, b) => a >>> b), } as Record< VecKind, (a: T, b: U) => T diff --git a/packages/typegpu/src/data/wgslTypes.ts b/packages/typegpu/src/data/wgslTypes.ts index 7495706c62..47558a746c 100644 --- a/packages/typegpu/src/data/wgslTypes.ts +++ b/packages/typegpu/src/data/wgslTypes.ts @@ -1598,6 +1598,18 @@ export function isMat(value: unknown): value is Mat2x2f | Mat3x3f | Mat4x4f { return isMat2x2f(value) || isMat3x3f(value) || isMat4x4f(value); } +export function isInteger(value: unknown): value is AbstractInt | I32 | U32 { + return ( + isMarkedInternal(value) && ['abstractInt', 'i32', 'u32'].includes((value as AnyWgslData)?.type) + ); +} + +export function isIntegerVec( + value: unknown, +): value is Vec2i | Vec3i | Vec4i | Vec2u | Vec3u | Vec4u { + return isVec(value) && isInteger(value.primitive); +} + export function isFloat32VecInstance(element: unknown): element is AnyFloat32VecInstance { return isVecInstance(element) && ['vec2f', 'vec3f', 'vec4f'].includes(element.kind); } diff --git a/packages/typegpu/src/std/operators.ts b/packages/typegpu/src/std/operators.ts index 4a95e690af..9d472e40f4 100644 --- a/packages/typegpu/src/std/operators.ts +++ b/packages/typegpu/src/std/operators.ts @@ -1,6 +1,6 @@ import { dualImpl } from '../core/function/dualImpl.ts'; import { stitch } from '../core/resolve/stitch.ts'; -import { abstractFloat, f16, f32, i32, u32 } from '../data/numeric.ts'; +import { abstractFloat, f16, f32, u32 } from '../data/numeric.ts'; import { vec2i, vec2u, vec3i, vec3u, vec4i, vec4u, vecTypeToConstructor } from '../data/vector.ts'; import { VectorOps } from '../data/vectorOps.ts'; import { @@ -290,8 +290,6 @@ export const neg = dualImpl({ sideEffects: false, }); -const anyConcreteInteger = [i32, u32, vec2i, vec3i, vec4i, vec2u, vec3u, vec4u] as BaseData[]; - const intVecToUnsignedVec = { vec2i: vec2u, vec2u: vec2u, @@ -302,49 +300,36 @@ const intVecToUnsignedVec = { } as const; const bitShiftSignature = (lhs: BaseData, rhs: BaseData) => { - const lhsUnified = unify([lhs], anyConcreteInteger)?.[0]; - if (!lhsUnified) { - throw new SignatureNotSupportedError([lhs], anyConcreteInteger); + const lhsUnified = unify([lhs], [vec2i, vec3i, vec4i, vec2u, vec3u, vec4u])?.[0]; + if (!lhsUnified || !isVec(lhsUnified)) { + throw new SignatureNotSupportedError([lhs], [vec2i, vec3i, vec4i, vec2u, vec3u, vec4u]); } - let rhsType: BaseData; - if (isVec(lhsUnified)) { - const cc = lhsUnified.componentCount; - const vecU = cc === 2 ? vec2u : cc === 3 ? vec3u : vec4u; - const rhsUnified = unify([rhs], [u32, vecU])?.[0]; - if (!rhsUnified) { - throw new SignatureNotSupportedError([rhs], [u32, vecU]); - } - rhsType = rhsUnified; - } else { - rhsType = u32; + const cc = lhsUnified.componentCount; + const vecU = cc === 2 ? vec2u : cc === 3 ? vec3u : vec4u; + const rhsUnified = unify([rhs], [u32, vecU])?.[0]; + if (!rhsUnified) { + throw new SignatureNotSupportedError([rhs], [u32, vecU]); } return { - argTypes: [lhsUnified, rhsType], + argTypes: [lhsUnified, rhsUnified], returnType: lhsUnified, }; }; -function cpuBitShiftLeft(lhs: number, rhs: number): number; -function cpuBitShiftLeft(lhs: T, rhs: number): T; -function cpuBitShiftLeft(lhs: T, rhs: vecIToVecU): T; -function cpuBitShiftLeft( - lhs: number | AnyIntegerVecInstance, - rhs: number | vecIToVecU, -) { - if (typeof lhs === 'number' && typeof rhs === 'number') { - return lhs << rhs; - } +function cpuBitShiftLeft(lhs: T, rhs: number | vecIToVecU): T { if (isInteger32VecInstance(lhs) && isUint32VecInstance(rhs) && lhs.length == rhs.length) { return VectorOps.bitShiftLeft[lhs.kind](lhs, rhs); } + if (isInteger32VecInstance(lhs) && typeof rhs === 'number') { const rhsVec = intVecToUnsignedVec[lhs.kind](rhs); return VectorOps.bitShiftLeft[lhs.kind](lhs, rhsVec); } + throw new Error( - 'bitShiftLeft called with invalid arguments, expected types: number or integer vector (rhs must be the same arity as lhs).', + "'bitShiftLeft' called with invalid arguments, expected: left-hand side to be an integer vector, right-hand side to be a number or unsigned integer vector of the same arity as the left-hand side.", ); } @@ -358,30 +343,24 @@ export const bitShiftLeft = dualImpl({ const schema = cc === 2 ? 'vec2u' : cc === 3 ? 'vec3u' : 'vec4u'; return stitch`(${lhs} << ${schema}(${rhs}))`; } + return stitch`(${lhs} << ${rhs})`; }, sideEffects: false, }); -function cpuBitShiftRight(lhs: number, rhs: number): number; -function cpuBitShiftRight(lhs: T, rhs: number): T; -function cpuBitShiftRight(lhs: T, rhs: vecIToVecU): T; -function cpuBitShiftRight( - lhs: number | AnyIntegerVecInstance, - rhs: number | vecIToVecU, -) { - if (typeof lhs === 'number' && typeof rhs === 'number') { - return lhs >> rhs; - } +function cpuBitShiftRight(lhs: T, rhs: number | vecIToVecU): T { if (isInteger32VecInstance(lhs) && isUint32VecInstance(rhs) && lhs.length == rhs.length) { return VectorOps.bitShiftRight[lhs.kind](lhs, rhs); } + if (isInteger32VecInstance(lhs) && typeof rhs === 'number') { const rhsVec = intVecToUnsignedVec[lhs.kind](rhs); return VectorOps.bitShiftRight[lhs.kind](lhs, rhsVec); } + throw new Error( - 'bitShiftRight called with invalid arguments, expected types: number or integer vector (rhs must be the same arity as lhs).', + "'bitShiftRight' called with invalid arguments, expected: left-hand side to be an integer vector, right-hand side to be a number or unsigned integer vector of the same arity as the left-hand side.", ); } @@ -395,6 +374,7 @@ export const bitShiftRight = dualImpl({ const schema = cc === 2 ? 'vec2u' : cc === 3 ? 'vec3u' : 'vec4u'; return stitch`(${lhs} >> ${schema}(${rhs}))`; } + return stitch`(${lhs} >> ${rhs})`; }, sideEffects: false, diff --git a/packages/typegpu/src/tgsl/infixDispatch.ts b/packages/typegpu/src/tgsl/infixDispatch.ts index eae455c01a..10faddf6c2 100644 --- a/packages/typegpu/src/tgsl/infixDispatch.ts +++ b/packages/typegpu/src/tgsl/infixDispatch.ts @@ -61,7 +61,7 @@ export function assignInfixOperator( // Returning this from a getter will work as if this was a vector/matrix's method. function jsInfixDispatchFor(this: unknown, arg: unknown) { // operator will perform all necessary type checks - return opImpl(this as never, arg as never); + return (opImpl as (lhs: unknown, rhs: unknown) => unknown)(this, arg); } Object.defineProperty(base.prototype, operator, { diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index c56ca5f135..f904d7766c 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -58,6 +58,7 @@ import { getAttributesString } from '../data/attributes.ts'; import { validSelectBranchTypes } from '../std/boolean.ts'; import { isInfixDispatch } from './infixDispatch.ts'; import type { VariableScope } from '../core/variable/tgpuVariable.ts'; +import { logger } from '../tgpuLogger.ts'; const { NodeTypeCatalog: NODE } = tinyest; @@ -72,6 +73,7 @@ const parenthesizedOps = [ '>=', '<<', '>>', + '>>>', '+', '-', '*', @@ -86,7 +88,7 @@ const parenthesizedOps = [ const binaryLogicalOps = ['&&', '||', '==', '!=', '===', '!==', '<', '<=', '>', '>=']; -const bitShiftOps: string[] = ['<<', '>>', '<<=', '>>=']; +const bitShiftOps: string[] = ['<<', '>>', '<<=', '>>=', '>>>', '>>>=']; const OP_MAP = { // @@ -94,9 +96,7 @@ const OP_MAP = { // '===': '==', '!==': '!=', - get '>>>'(): never { - throw new Error('The `>>>` operator is unsupported in TypeGPU functions.'); - }, + '>>>': '>>', get in(): never { throw new Error('The `in` operator is unsupported in TypeGPU functions.'); }, @@ -115,9 +115,7 @@ const OP_MAP = { // // assignment // - get '>>>='(): never { - throw new Error('The `>>>=` operator is unsupported in TypeGPU functions.'); - }, + '>>>=': '>>=', get '**='(): never { throw new Error('The `**=` operator is unsupported in TypeGPU functions.'); }, @@ -431,16 +429,37 @@ ${this.ctx.pre}}`; let convRhs: Snippet; if (bitShiftOps.includes(op)) { - // rhs must be u32 (or vecN for vector lhs) + const lhsDataType = lhsExpr.dataType; + if (!wgsl.isInteger(lhsDataType) && !wgsl.isIntegerVec(lhsDataType)) { + throw new WgslTypeError( + `Expression: ${stringifyNode(expression)}\nLeft-hand side of '${op}' must be an integer or vector of integers.\nGot ${this.ctx.resolve(lhsDataType).value}.`, + ); + } + + const lhsPrimitive = wgsl.isVec(lhsDataType) ? lhsDataType.primitive : lhsDataType; + + if (['>>>', '>>>='].includes(op) && lhsPrimitive.type !== 'u32') { + throw new WgslTypeError( + `Expression: ${stringifyNode(expression)}\nLeft-hand side of '${op}' must be an unsigned integer or vector of unsigned integers.\nGot ${this.ctx.resolve(lhsDataType).value}.\nUse ${op.slice(1)} instead.`, + ); + } + + if (['>>', '>>='].includes(op) && lhsPrimitive.type === 'u32') { + logger.warn( + 'deprecated', + `\nExpression: ${stringifyNode(expression)}\nUsing u32 or vecN as left-hand side of ${op} is deprecated.\nUse >${op} instead.`, + ); + } + + // rhs must be u32 (or vecN for vector lhs) according to the WGSL spec let rhsTarget: wgsl.BaseData; - if (wgsl.isVec(lhsExpr.dataType)) { - const cc = lhsExpr.dataType.componentCount; + if (wgsl.isVec(lhsDataType)) { + const cc = lhsDataType.componentCount; rhsTarget = cc === 2 ? vec2u : cc === 3 ? vec3u : vec4u; } else { rhsTarget = u32; } convRhs = tryConvertSnippet(this.ctx, rhsExpr, rhsTarget, false); - // if lhs is not an integer type, the browser will return a descriptive wgsl error convLhs = lhsExpr; } else { const forcedType = exprType === NODE.assignmentExpr ? [lhsExpr.dataType] : undefined; diff --git a/packages/typegpu/tests/std/numeric/bitShift.test.ts b/packages/typegpu/tests/std/numeric/bitShift.test.ts index 728b41a060..3797e6f88a 100644 --- a/packages/typegpu/tests/std/numeric/bitShift.test.ts +++ b/packages/typegpu/tests/std/numeric/bitShift.test.ts @@ -1,246 +1,518 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { u32, i32, vec3f, vec3i, vec3u, f32, vec2u } from 'typegpu/data'; import { bitShiftLeft, bitShiftRight } from 'typegpu/std'; import { tgpu } from 'typegpu'; describe('bit shift', () => { - it('casts rhs to u32', () => { + it('casts abstract type rhs to u32', () => { const f = () => { 'use gpu'; const x = i32(256); - return x << 4; + return (x << 4) | (x >> 4); }; expect(tgpu.resolve([f])).toMatchInlineSnapshot(` "fn f() -> i32 { const x = 256i; - return (x << 4u); + return ((x << 4u) | (x >> 4u)); }" `); }); - it('does not cast rhs to i32 (no call to convertToCommonType)', () => { + it('casts f32 rhs to u32', () => { const f = () => { 'use gpu'; - const shift = u32(4); + const shift = f32(4); const x = i32(256); - return x << shift; + return (x << shift) | (x >> shift); }; expect(tgpu.resolve([f])).toMatchInlineSnapshot(` "fn f() -> i32 { - const shift = 4u; + const shift = 4f; const x = 256i; - return (x << shift); + return ((x << u32(shift)) | (x >> u32(shift))); }" `); }); - it('casts float rhs to u32', () => { + it('throws when lhs is not an integer', () => { const f = () => { 'use gpu'; - const shift = f32(4); - const x = i32(256); + const x = f32(256); + return x << 4; + }; + + expect(() => tgpu.resolve([f])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:f + - fn*:f(): Expression: x << 4 + Left-hand side of '<<' must be an integer or vector of integers. + Got f32.] + `); + }); + + it('throws when lhs is not an integer vector', () => { + const f = () => { + 'use gpu'; + const shift = vec3u(7); + const x = vec3f(256); + // @ts-ignore + return x >> shift; + }; + + expect(() => tgpu.resolve([f])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:f + - fn*:f(): Expression: x >> shift + Left-hand side of '>>' must be an integer or vector of integers. + Got vec3f.] + `); + }); + + it('throws when using vectors of different arity', () => { + const f = () => { + 'use gpu'; + const shift = vec2u(4); + const x = vec3i(256); + // @ts-expect-error: part of the test return x << shift; }; + expect(() => tgpu.resolve([f])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:f + - fn*:f(): Cannot convert value of type 'vec2u' to any of the target types: [vec3u]] + `); + }); +}); + +describe('bit shifts << and <<=', () => { + it('works with i32 and u32 lhs', () => { + const f = () => { + 'use gpu'; + const x = i32(256); + const y = u32(256); + return (x << 4) | (y << 4); + }; + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` "fn f() -> i32 { - const shift = 4f; const x = 256i; - return (x << u32(shift)); + const y = 256u; + return ((x << 4u) | i32((y << 4u))); }" `); }); - it('works with vectors via std functions', () => { + it('works with integer vectors', () => { const f = () => { 'use gpu'; const shift = vec3u(4); const x = vec3i(256); - const y = bitShiftLeft(x, shift); - const z = bitShiftRight(x, shift); + const y = vec3u(256); + // @ts-ignore + const _z = x << shift; + // @ts-ignore + const _w = y << shift; }; expect(tgpu.resolve([f])).toMatchInlineSnapshot(` "fn f() { let shift = vec3u(4); let x = vec3i(256); - let y = (x << shift); - let z = (x >> shift); + let y = vec3u(256); + let _z = (x << shift); + let _w = (y << shift); + }" + `); + }); + + it('generates correct wgsl for <<=', () => { + const f = () => { + 'use gpu'; + let x = u32(8); + x <<= 4; + }; + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "fn f() { + var x = 8u; + x <<= 4u; }" `); }); +}); - it('works with vectors via infix methods', () => { +describe('bit shift >> and >>=', () => { + it('works with i32 lhs', () => { + const f = () => { + 'use gpu'; + const x = i32(256); + return x >> 4; + }; + + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "fn f() -> i32 { + const x = 256i; + return (x >> 4u); + }" + `); + }); + + it('warns when lhs is u32', () => { + using consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const f = () => { + 'use gpu'; + const x = u32(256); + return x >> 4; + }; + + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "fn f() -> u32 { + const x = 256u; + return (x >> 4u); + }" + `); + + expect(consoleWarnSpy).toHaveBeenCalledOnce(); + expect(consoleWarnSpy.mock.calls[0]).toMatchInlineSnapshot(` + [ + "⚠️ [deprecated] ", + " + Expression: x >> 4 + Using u32 or vecN as left-hand side of >> is deprecated. + Use >>> instead.", + ] + `); + }); + + it('works with i32 vector', () => { const f = () => { 'use gpu'; const shift = vec3u(4); const x = vec3i(256); - const y = x.bitShiftLeft(shift); - const z = x.bitShiftRight(shift); + // @ts-ignore + const _z = x >> shift; }; expect(tgpu.resolve([f])).toMatchInlineSnapshot(` "fn f() { let shift = vec3u(4); let x = vec3i(256); - let y = (x << shift); - let z = (x >> shift); + let _z = (x >> shift); }" `); }); - it('works with vector lhs and numeric rhs via infix/std methods', () => { + it('warns when lhs is u32 vector', () => { + using consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const f = () => { 'use gpu'; - const shift = u32(4); - const x = vec3i(256); - const y = x.bitShiftLeft(shift); - const z = x.bitShiftRight(shift); + const shift = vec3u(4); + const x = vec3u(256); + // @ts-ignore + const _z = x >> shift; }; expect(tgpu.resolve([f])).toMatchInlineSnapshot(` "fn f() { - const shift = 4u; - let x = vec3i(256); - let y = (x << vec3u(shift)); - let z = (x >> vec3u(shift)); + let shift = vec3u(4); + let x = vec3u(256); + let _z = (x >> shift); + }" + `); + + expect(consoleWarnSpy).toHaveBeenCalledOnce(); + expect(consoleWarnSpy.mock.calls[0]).toMatchInlineSnapshot(` + [ + "⚠️ [deprecated] ", + " + Expression: x >> shift + Using u32 or vecN as left-hand side of >> is deprecated. + Use >>> instead.", + ] + `); + }); + + it('generates correct wgsl for >>=', () => { + const f = () => { + 'use gpu'; + let x = i32(8); + x >>= 4; + }; + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "fn f() { + var x = 8i; + x >>= 4u; }" `); }); +}); - it('>>= works with numerics', () => { +describe('bit shift >>> and >>>=', () => { + it('works with u32 lhs', () => { const f = () => { 'use gpu'; - const shift = u32(4); - let x = i32(256); - x >>= shift; + const x = u32(256); + return x >>> 4; }; expect(tgpu.resolve([f])).toMatchInlineSnapshot(` - "fn f() { - const shift = 4u; - var x = 256i; - x >>= shift; + "fn f() -> u32 { + const x = 256u; + return (x >> 4u); }" `); }); - it('<< works with vectors', () => { + it('throws when lhs is i32', () => { + const f = () => { + 'use gpu'; + const x = i32(256); + return x >>> 4; + }; + + expect(() => tgpu.resolve([f])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:f + - fn*:f(): Expression: x >>> 4 + Left-hand side of '>>>' must be an unsigned integer or vector of unsigned integers. + Got i32. + Use >> instead.] + `); + }); + + it('works with u32 vectors', () => { const f = () => { 'use gpu'; const shift = vec3u(4); - const x = vec3i(256); - // @ts-expect-error: part of the test - return x << shift; + const x = vec3u(256); + // @ts-ignore + const _z = x >>> shift; }; expect(tgpu.resolve([f])).toMatchInlineSnapshot(` - "fn f() -> vec3i { + "fn f() { let shift = vec3u(4); - let x = vec3i(256); - return (x << shift); + let x = vec3u(256); + let _z = (x >> shift); }" `); }); - it('>>= works with vectors', () => { + it('throws when lhs is i32 vector', () => { const f = () => { 'use gpu'; const shift = vec3u(4); - let x = vec3i(256); - // @ts-expect-error: part of the test - x >>= shift; + const x = vec3i(256); + // @ts-ignore + const _z = x >>> shift; }; + expect(() => tgpu.resolve([f])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:f + - fn*:f(): Expression: x >>> shift + Left-hand side of '>>>' must be an unsigned integer or vector of unsigned integers. + Got vec3i. + Use >> instead.] + `); + }); + + it('generates correct wgsl for >>>=', () => { + const f = () => { + 'use gpu'; + let x = u32(8); + x >>>= 4; + }; expect(tgpu.resolve([f])).toMatchInlineSnapshot(` "fn f() { - let shift = vec3u(4); - var x = vec3i(256); - x >>= shift; + var x = 8u; + x >>= 4u; }" `); }); +}); + +describe('std.bitShift', () => { + it('throws in JS when lhs is a number', () => { + // @ts-expect-error + expect(() => bitShiftLeft(2, 1)).toThrowErrorMatchingInlineSnapshot( + `[Error: 'bitShiftLeft' called with invalid arguments, expected: left-hand side to be an integer vector, right-hand side to be a number or unsigned integer vector of the same arity as the left-hand side.]`, + ); - it('computes correct values for number << number', () => { - expect(bitShiftLeft(1, 4)).toBe(16); - expect(bitShiftRight(256, 4)).toBe(16); + // @ts-expect-error + expect(() => bitShiftRight(2, 1)).toThrowErrorMatchingInlineSnapshot( + `[Error: 'bitShiftRight' called with invalid arguments, expected: left-hand side to be an integer vector, right-hand side to be a number or unsigned integer vector of the same arity as the left-hand side.]`, + ); }); - it('computes correct values for vector << vector', () => { + it('computes values that match WGSL behavior', () => { const result1 = bitShiftLeft(vec3i(1, 2, 3), vec3u(1, 2, 3)); expect(Array.from(result1)).toStrictEqual([2, 8, 24]); - const result2 = bitShiftRight(vec3i(16, 32, 64), vec3u(1, 2, 3)); - expect(Array.from(result2)).toStrictEqual([8, 8, 8]); + const result2 = bitShiftRight(vec3u(0x80000001), vec3u(1, 2, 3)); + expect(Array.from(result2)).toStrictEqual([1073741824, 536870912, 268435456]); + + const result3 = bitShiftRight(vec3i(0x80000001), vec3u(1, 2, 3)); + expect(Array.from(result3)).toStrictEqual([-1073741824, -536870912, -268435456]); + }); + + it('throws during WGSL generation when lhs is a number', () => { + const f1 = () => { + 'use gpu'; + const x = 256; + // @ts-expect-error + bitShiftLeft(x, 1); + }; + expect(() => tgpu.resolve([f1])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:f1 + - fn*:f1() + - fn:bitShiftLeft: Unsupported data types: i32. Supported types are: vec2i, vec3i, vec4i, vec2u, vec3u, vec4u.] + `); + + const f2 = () => { + 'use gpu'; + const x = 256; + // @ts-expect-error + bitShiftRight(x, 1); + }; + expect(() => tgpu.resolve([f2])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:f2 + - fn*:f2() + - fn:bitShiftRight: Unsupported data types: i32. Supported types are: vec2i, vec3i, vec4i, vec2u, vec3u, vec4u.] + `); + }); + + it('generates correct wgsl for vector operands', () => { + const f = () => { + 'use gpu'; + const shift = vec3u(4); + const x = vec3i(256); + const y = vec3u(256); + const _z = bitShiftLeft(x, shift); + const _w = bitShiftRight(y, shift); + }; + + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "fn f() { + let shift = vec3u(4); + let x = vec3i(256); + let y = vec3u(256); + let _z = (x << shift); + let _w = (y >> shift); + }" + `); }); - it('computes correct values for vector << number', () => { - const result1 = bitShiftLeft(vec3i(1, 2, 3), 2); - expect(Array.from(result1)).toStrictEqual([4, 8, 12]); + it('can be invoked as infix method', () => { + const f = () => { + 'use gpu'; + const shift = vec3u(4); + const x = vec3i(256); + const y = vec3u(256); + const _z = x.bitShiftLeft(shift); + const _w = y.bitShiftRight(shift); + }; - const result2 = bitShiftRight(vec3i(16, 32, 64), 2); - expect(Array.from(result2)).toStrictEqual([4, 8, 16]); + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "fn f() { + let shift = vec3u(4); + let x = vec3i(256); + let y = vec3u(256); + let _z = (x << shift); + let _w = (y >> shift); + }" + `); }); - it('throws when calling bitShiftLeft/Right on float vectors', () => { + it('rhs can be a number', () => { + const f = () => { + 'use gpu'; + const x = vec3i(256); + const _z = x.bitShiftLeft(4); + const _w = x.bitShiftRight(4); + }; + + expect(tgpu.resolve([f])).toMatchInlineSnapshot(` + "fn f() { + let x = vec3i(256); + let _z = (x << vec3u(4u)); + let _w = (x >> vec3u(4u)); + }" + `); + }); + + it('throws when lhs is a float vector', () => { const x = vec3f(1, 2, 3); - // @ts-expect-error: part of the test + // @ts-expect-error expect(() => bitShiftLeft(x, vec3u(1, 2, 3))).toThrowErrorMatchingInlineSnapshot( - `[Error: bitShiftLeft called with invalid arguments, expected types: number or integer vector (rhs must be the same arity as lhs).]`, + `[Error: 'bitShiftLeft' called with invalid arguments, expected: left-hand side to be an integer vector, right-hand side to be a number or unsigned integer vector of the same arity as the left-hand side.]`, ); - // @ts-expect-error: part of the test + + // @ts-expect-error expect(() => bitShiftRight(x, vec3u(1, 2, 3))).toThrowErrorMatchingInlineSnapshot( - `[Error: bitShiftRight called with invalid arguments, expected types: number or integer vector (rhs must be the same arity as lhs).]`, + `[Error: 'bitShiftRight' called with invalid arguments, expected: left-hand side to be an integer vector, right-hand side to be a number or unsigned integer vector of the same arity as the left-hand side.]`, ); }); - it('throws when calling bitShiftLeft/Right with vectors of different arity', () => { + it('throws when operands are different arity vectors', () => { const x = vec3i(1, 2, 3); - //@ts-expect-error: part of the test + // @ts-expect-error expect(() => bitShiftLeft(x, vec2u(1))).toThrowErrorMatchingInlineSnapshot( - `[Error: bitShiftLeft called with invalid arguments, expected types: number or integer vector (rhs must be the same arity as lhs).]`, + `[Error: 'bitShiftLeft' called with invalid arguments, expected: left-hand side to be an integer vector, right-hand side to be a number or unsigned integer vector of the same arity as the left-hand side.]`, + ); + // @ts-expect-error + expect(() => bitShiftRight(x, vec2u(1))).toThrowErrorMatchingInlineSnapshot( + `[Error: 'bitShiftRight' called with invalid arguments, expected: left-hand side to be an integer vector, right-hand side to be a number or unsigned integer vector of the same arity as the left-hand side.]`, ); - const f = () => { + const f1 = () => { 'use gpu'; const shift = vec2u(4); let x = vec3i(256); - // @ts-expect-error: part of the test + // @ts-expect-error x.bitShiftLeft(shift); }; - expect(() => tgpu.resolve([f])).toThrowErrorMatchingInlineSnapshot(` + expect(() => tgpu.resolve([f1])).toThrowErrorMatchingInlineSnapshot(` [Error: Resolution of the following tree failed: - - - fn*:f - - fn*:f(): Unsupported data types: vec2u. Supported types are: u32, vec3u.] + - fn*:f1 + - fn*:f1(): Unsupported data types: vec2u. Supported types are: u32, vec3u.] `); - }); - it('throws when using raw << with vectors of different arity', () => { - const f = () => { + const f2 = () => { 'use gpu'; const shift = vec2u(4); let x = vec3i(256); - // @ts-expect-error: part of the test - x << shift; + // @ts-expect-error + x.bitShiftRight(shift); }; - expect(() => tgpu.resolve([f])).toThrowErrorMatchingInlineSnapshot(` + expect(() => tgpu.resolve([f2])).toThrowErrorMatchingInlineSnapshot(` [Error: Resolution of the following tree failed: - - - fn*:f - - fn*:f(): Cannot convert value of type 'vec2u' to any of the target types: [vec3u]] + - fn*:f2 + - fn*:f2(): Unsupported data types: vec2u. Supported types are: u32, vec3u.] `); }); - it('bitShiftLeft/Right is available only on integer vectors (at type level)', () => { + it('is available only on integer vectors (at type level)', () => { const x = vec3f(1, 2, 3); - // @ts-expect-error: part of the test + // @ts-expect-error x.bitShiftLeft; - // @ts-expect-error: part of the test + // @ts-expect-error x.bitShiftRight; const y = vec3i(1, 2, 3);