From 7155c057f3272b84621e5ceba1c4139633f8d53c Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Mon, 4 May 2026 10:07:59 +0200 Subject: [PATCH 01/12] Throw a descriptive error on external infix --- packages/typegpu/src/data/index.ts | 2 ++ packages/typegpu/src/shared/symbols.ts | 5 +++++ packages/typegpu/src/tgsl/wgslGenerator.ts | 8 +++++++- .../typegpu/tests/tgsl/wgslGenerator.test.ts | 16 ++++++++++++++++ 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/typegpu/src/data/index.ts b/packages/typegpu/src/data/index.ts index 5eebe77415..803330e0d8 100644 --- a/packages/typegpu/src/data/index.ts +++ b/packages/typegpu/src/data/index.ts @@ -8,6 +8,7 @@ import { Operator } from 'tsover-runtime'; import { type InfixOperator, infixOperators } from '../tgsl/accessProp.ts'; import { MatBase } from './matrix.ts'; import { VecBase } from './vectorImpl.ts'; +import { $infixOperator } from '../shared/symbols.ts'; function assignInfixOperator( object: T, @@ -21,6 +22,7 @@ function assignInfixOperator( proto[operator] = function (this: unknown, other: unknown): unknown { return opImpl(this, other); }; + proto[operator][$infixOperator] = true; proto[operatorSymbol] = (lhs: unknown, rhs: unknown): unknown => { return opImpl(lhs, rhs); diff --git a/packages/typegpu/src/shared/symbols.ts b/packages/typegpu/src/shared/symbols.ts index 7b677cb859..7e7e5a8e94 100644 --- a/packages/typegpu/src/shared/symbols.ts +++ b/packages/typegpu/src/shared/symbols.ts @@ -34,6 +34,11 @@ export const $cast = Symbol(`typegpu:${version}:$cast`); * Can be called on the GPU */ export const $gpuCallable = Symbol(`typegpu:${version}:$gpuCallable`); +/** + * A marker for functions that come into shader while already being an infix operator dispatch. + * This may happen when code like `external.mul(2)` appears. + */ +export const $infixOperator = Symbol(`typegpu:${version}:$infixOperator`); // // Type tokens diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index f76d4baccd..5993ba0a5b 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -23,7 +23,13 @@ import { import * as wgsl from '../data/wgslTypes.ts'; import { invariant, ResolutionError, WgslTypeError } from '../errors.ts'; import { getName } from '../shared/meta.ts'; -import { $gpuCallable, $internal, $providing, isMarkedInternal } from '../shared/symbols.ts'; +import { + $gpuCallable, + $infixOperator, + $internal, + $providing, + isMarkedInternal, +} from '../shared/symbols.ts'; import { safeStringify } from '../shared/stringify.ts'; import { pow } from '../std/numeric.ts'; import { add, div, mul, neg, sub } from '../std/operators.ts'; diff --git a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts index 43f7b6ce77..f90cd2404b 100644 --- a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts +++ b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts @@ -1971,4 +1971,20 @@ describe('wgslGenerator', () => { `); }); }); + + // TODO: handle these in the future + it('handles external infix operators', () => { + const vec = d.vec2u(1); + const fn = () => { + 'use gpu'; + const x = vec.mul(2); + }; + + expect(() => tgpu.resolve([fn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:fn + - fn*:fn(): Infix operators on external values are unsupported, use 'std' functions instead.] + `); + }); }); From fcfe58b8b6f9672f7431eee7b940fe175d277b74 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Mon, 4 May 2026 11:50:37 +0200 Subject: [PATCH 02/12] Update infix operator tests --- .../typegpu/tests/tgsl/infixOperators.test.ts | 156 ++++++++++-------- .../typegpu/tests/tgsl/wgslGenerator.test.ts | 16 -- 2 files changed, 88 insertions(+), 84 deletions(-) diff --git a/packages/typegpu/tests/tgsl/infixOperators.test.ts b/packages/typegpu/tests/tgsl/infixOperators.test.ts index 2e18ed3331..c97df91bab 100644 --- a/packages/typegpu/tests/tgsl/infixOperators.test.ts +++ b/packages/typegpu/tests/tgsl/infixOperators.test.ts @@ -3,7 +3,7 @@ import tgpu, { d } from '../../src/index.js'; import { it } from 'typegpu-testing-utility'; describe('wgslGenerator', () => { - it('resolves add infix operator', () => { + it('resolves add infix operator in comptime', () => { const testFn = tgpu.fn([])(() => { const v1 = d.vec4f().add(1); const v2 = d.vec3f(2).add(d.vec3f(1, 2, 3)); @@ -23,7 +23,7 @@ describe('wgslGenerator', () => { `); }); - it('resolves sub infix operator', () => { + it('resolves sub infix operator in comptime', () => { const testFn = tgpu.fn([])(() => { const v1 = d.vec4f().sub(1); const v2 = d.vec3f().sub(d.vec3f(1, 2, 3)); @@ -43,7 +43,7 @@ describe('wgslGenerator', () => { `); }); - it('resolves mul infix operator', () => { + it('resolves mul infix operator in comptime', () => { const testFn = tgpu.fn([])(() => { const v1 = d.vec2f(2).mul(3); const v2 = d.vec3f(2).mul(d.vec3f(2, 3, 4)); @@ -70,116 +70,136 @@ describe('wgslGenerator', () => { `); }); - it('resolves mul infix operator on a function return value', () => { - const getVec = tgpu.fn( - [], - d.vec3f, - )(() => { - 'use gpu'; - return d.vec3f(1, 2, 3); - }); - + it('resolves div infix operator in comptime', () => { const testFn = tgpu.fn([])(() => { - 'use gpu'; - const v1 = getVec().mul(getVec()); + const v1 = d.vec4f(1).div(2); + const v2 = d.vec3f(6).div(d.vec3f(1, 2, 3)); + const v3 = d.vec2f(1).div(d.vec2f(2)).div(2); }); expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` - "fn getVec() -> vec3f { - return vec3f(1, 2, 3); - } - - fn testFn() { - var v1 = (getVec() * getVec()); + "fn testFn() { + var v1 = vec4f(0.5); + var v2 = vec3f(6, 3, 2); + var v3 = vec2f(0.25); }" `); }); - it('resolves mul infix operator on a struct property', () => { - const Struct = d.struct({ vec: d.vec3f }); - + it('resolves mod infix operator in comptime', () => { const testFn = tgpu.fn([])(() => { - 'use gpu'; - const s = Struct({ vec: d.vec3f() }); - const v1 = s.vec.mul(s.vec); + const v1 = d.vec4f(11).mod(2); + const v2 = d.vec3f(13.5).mod(d.vec3f(1, 2, 10)); }); expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` - "struct Struct { - vec: vec3f, - } + "fn testFn() { + var v1 = vec4f(1); + var v2 = vec3f(0.5, 1.5, 3.5); + }" + `); + }); - fn testFn() { - var s = Struct(vec3f()); - var v1 = (s.vec * s.vec); + it('resolves mul infix operator on a runtime variable', () => { + const testFn = () => { + 'use gpu'; + const v1 = d.vec2f(1, 2); + return v1.mul(2).mul(3); + }; + + expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` + "fn testFn() -> vec2f { + var v1 = vec2f(1, 2); + return ((v1 * 2f) * 3f); }" `); + expect(testFn()).toStrictEqual(d.vec2f(6, 12)); }); - it('resolves div infix operator', () => { - const testFn = tgpu.fn([])(() => { - const v1 = d.vec4f(1).div(2); - const v2 = d.vec3f(6).div(d.vec3f(1, 2, 3)); - const v3 = d.vec2f(1).div(d.vec2f(2)).div(2); - }); + it('resolves mul infix operator on a function return value', () => { + const getVec = () => { + 'use gpu'; + return d.vec3f(1, 2, 3); + }; + + const testFn = () => { + 'use gpu'; + return getVec().mul(getVec()).mul(2); + }; expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` - "fn testFn() { - var v1 = vec4f(0.5); - var v2 = vec3f(6, 3, 2); - var v3 = vec2f(0.25); + "fn getVec() -> vec3f { + return vec3f(1, 2, 3); + } + + fn testFn() -> vec3f { + return ((getVec() * getVec()) * 2f); }" `); + expect(testFn()).toStrictEqual(d.vec3f(2, 8, 18)); }); - it('resolves mod infix operator', () => { - const testFn = tgpu.fn([])(() => { - const v1 = d.vec4f(11).mod(2); - const v2 = d.vec3f(13.5).mod(d.vec3f(1, 2, 10)); - }); + it('resolves mul infix operator on a struct property', () => { + const Struct = d.struct({ vec: d.vec3f }); + + const testFn = () => { + 'use gpu'; + const s = Struct({ vec: d.vec3f(2) }); + return s.vec.mul(s.vec).mul(2); + }; expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` - "fn testFn() { - var v1 = vec4f(1); - var v2 = vec3f(0.5, 1.5, 3.5); + "struct Struct { + vec: vec3f, + } + + fn testFn() -> vec3f { + var s = Struct(vec3f(2)); + return ((s.vec * s.vec) * 2f); }" `); + expect(testFn()).toStrictEqual(d.vec3f(8)); }); - it('resolves add infix operator on uniform vector', ({ root }) => { + it('resolves mul infix operator on uniform vector', ({ root }) => { const fooUniform = root.createUniform(d.vec3f); - const barUniform = root.createUniform(d.vec3f); const testFn = tgpu.fn([])(() => { - const v1 = fooUniform.$.add(2); // lhs - const v2 = d.vec3f(1, 2, 3).add(barUniform.$); // rhs - const v3 = fooUniform.$.add(barUniform.$); + const v1 = fooUniform.$.mul(2); // lhs + const v2 = d.vec3f(1, 2, 3).mul(fooUniform.$); // rhs + const v3 = fooUniform.$.mul(fooUniform.$).mul(2); }); expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` "@group(0) @binding(0) var fooUniform: vec3f; - @group(0) @binding(1) var barUniform: vec3f; - fn testFn() { - var v1 = (fooUniform + 2f); - var v2 = (vec3f(1, 2, 3) + barUniform); - var v3 = (fooUniform + barUniform); + var v1 = (fooUniform * 2f); + var v2 = (vec3f(1, 2, 3) * fooUniform); + var v3 = ((fooUniform * fooUniform) * 2f); }" `); }); - it('precomputes adds on known values', () => { - const testFn = tgpu.fn([])(() => { - const v1 = d.vec3f(1, 2, 3).add(5); - const v2 = d.vec3f(1, 2, 3).add(d.vec3f(3, 2, 1)); - }); + it('resolves mul infix operator on external', () => { + const v = d.vec3f(2); + + const testFn = () => { + 'use gpu'; + const v1 = v.mul(2); // lhs + const v2 = d.vec3f(3).mul(v); // rhs + const v3 = v.mul(v).mul(4); + return v1.add(v2).add(v3); + }; expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` - "fn testFn() { - var v1 = vec3f(6, 7, 8); - var v2 = vec3f(4); + "fn testFn() -> vec3f { + var v1 = vec3f(4); + var v2 = vec3f(6); + var v3 = vec3f(16); + return ((v1 + v2) + v3); }" `); + expect(testFn()).toStrictEqual(d.vec3f(26)); }); }); diff --git a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts index f90cd2404b..43f7b6ce77 100644 --- a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts +++ b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts @@ -1971,20 +1971,4 @@ describe('wgslGenerator', () => { `); }); }); - - // TODO: handle these in the future - it('handles external infix operators', () => { - const vec = d.vec2u(1); - const fn = () => { - 'use gpu'; - const x = vec.mul(2); - }; - - expect(() => tgpu.resolve([fn])).toThrowErrorMatchingInlineSnapshot(` - [Error: Resolution of the following tree failed: - - - - fn*:fn - - fn*:fn(): Infix operators on external values are unsupported, use 'std' functions instead.] - `); - }); }); From 1a486947ba7a6e1557de90749de3b85d2366a148 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Mon, 4 May 2026 16:12:38 +0200 Subject: [PATCH 03/12] In progress --- apps/typegpu-docs/astro.config.mjs | 2 +- .../src/examples/tests/log-test/index.ts | 27 ++-- .../individual-example-tests/log-test.test.ts | 147 ------------------ packages/typegpu/src/data/dataTypes.ts | 18 --- packages/typegpu/src/data/index.ts | 13 +- packages/typegpu/src/shared/symbols.ts | 2 +- packages/typegpu/src/tgsl/accessProp.ts | 34 ++-- packages/typegpu/src/tgsl/infix.ts | 31 ++++ packages/typegpu/src/tgsl/wgslGenerator.ts | 7 +- .../tests/swizzleMixedValidation.test.ts | 2 +- .../typegpu/tests/tgsl/infixOperators.test.ts | 41 ++++- packages/unplugin-typegpu/test/Untitled-2.js | 13 ++ 12 files changed, 120 insertions(+), 217 deletions(-) create mode 100644 packages/typegpu/src/tgsl/infix.ts create mode 100644 packages/unplugin-typegpu/test/Untitled-2.js diff --git a/apps/typegpu-docs/astro.config.mjs b/apps/typegpu-docs/astro.config.mjs index 4e4597e6d7..dac1922ec5 100644 --- a/apps/typegpu-docs/astro.config.mjs +++ b/apps/typegpu-docs/astro.config.mjs @@ -73,7 +73,7 @@ export default defineConfig({ starlightBlog({ navigation: 'none', }), - starlightTypeDoc({ + !DEV && starlightTypeDoc({ entryPoints: [ '../../packages/typegpu/src/index.d.ts', '../../packages/typegpu/src/data/index.ts', diff --git a/apps/typegpu-docs/src/examples/tests/log-test/index.ts b/apps/typegpu-docs/src/examples/tests/log-test/index.ts index 585a9bccbb..9c357dc74c 100644 --- a/apps/typegpu-docs/src/examples/tests/log-test/index.ts +++ b/apps/typegpu-docs/src/examples/tests/log-test/index.ts @@ -208,19 +208,20 @@ export const controls = defineControls({ .dispatchThreads(), }, 'Different log functionalities': { - onButtonClick: async () => - root - .createGuardedComputePipeline(() => { - 'use gpu'; - console.log('This message should be cleared.'); - console.clear(); - console.log('This is a log message.', 'Index:', 1); - console.debug('This is a debug message.', 'Index:', 2); - console.info('This is an info message.', 'Index:', 3); - console.warn('This is a warn message.', 'Index:', 4); - console.error('This is an error message.', 'Index:', 5); - }) - .dispatchThreads(), + onButtonClick: async () => { + const pipeline = root.createGuardedComputePipeline(() => { + 'use gpu'; + console.log('This message should be cleared.'); + console.clear(); + console.log('This is a log message.', 'Index:', 1); + console.debug('This is a debug message.', 'Index:', 2); + console.info('This is an info message.', 'Index:', 3); + console.warn('This is a warn message.', 'Index:', 4); + console.error('This is an error message.', 'Index:', 5); // something's wrong here!! + }); + console.log(tgpu.resolve([pipeline.pipeline])); + pipeline.dispatchThreads(); + }, }, 'Render pipeline': { onButtonClick: () => { diff --git a/apps/typegpu-docs/tests/individual-example-tests/log-test.test.ts b/apps/typegpu-docs/tests/individual-example-tests/log-test.test.ts index 3ee6a9035b..2cc5f86610 100644 --- a/apps/typegpu-docs/tests/individual-example-tests/log-test.test.ts +++ b/apps/typegpu-docs/tests/individual-example-tests/log-test.test.ts @@ -1297,153 +1297,6 @@ describe('console log example', () => { wrappedCallback(id.x, id.y, id.z); } - @group(0) @binding(0) var sizeUniform: vec3u; - - @group(0) @binding(1) var indexBuffer: atomic; - - struct SerializedLogData { - id: u32, - serializedData: array, - } - - @group(0) @binding(2) var dataBuffer: array; - - var dataBlockIndex: u32; - - var dataByteIndex: u32; - - fn log1serializer() { - - } - - fn log1() { - dataBlockIndex = atomicAdd(&indexBuffer, 1); - if (dataBlockIndex >= 40) { - return; - } - dataBuffer[dataBlockIndex].id = 1; - dataByteIndex = 0; - - log1serializer(); - } - - fn log2serializer() { - - } - - fn log2_1() { - dataBlockIndex = atomicAdd(&indexBuffer, 1); - if (dataBlockIndex >= 40) { - return; - } - dataBuffer[dataBlockIndex].id = 2; - dataByteIndex = 0; - - log2serializer(); - } - - fn nextByteIndex() -> u32 { - let i = dataByteIndex; - dataByteIndex = dataByteIndex + 1u; - return i; - } - - fn serializeI32(n: i32) { - dataBuffer[dataBlockIndex].serializedData[nextByteIndex()] = bitcast(n); - } - - fn log3serializer(_arg_0: i32) { - serializeI32(_arg_0); - } - - fn log3(_arg_0: i32) { - dataBlockIndex = atomicAdd(&indexBuffer, 1); - if (dataBlockIndex >= 40) { - return; - } - dataBuffer[dataBlockIndex].id = 3; - dataByteIndex = 0; - - log3serializer(_arg_0); - } - - fn log4serializer(_arg_0: i32) { - serializeI32(_arg_0); - } - - fn log4(_arg_0: i32) { - dataBlockIndex = atomicAdd(&indexBuffer, 1); - if (dataBlockIndex >= 40) { - return; - } - dataBuffer[dataBlockIndex].id = 4; - dataByteIndex = 0; - - log4serializer(_arg_0); - } - - fn log5serializer(_arg_0: i32) { - serializeI32(_arg_0); - } - - fn log5(_arg_0: i32) { - dataBlockIndex = atomicAdd(&indexBuffer, 1); - if (dataBlockIndex >= 40) { - return; - } - dataBuffer[dataBlockIndex].id = 5; - dataByteIndex = 0; - - log5serializer(_arg_0); - } - - fn log6serializer(_arg_0: i32) { - serializeI32(_arg_0); - } - - fn log6(_arg_0: i32) { - dataBlockIndex = atomicAdd(&indexBuffer, 1); - if (dataBlockIndex >= 40) { - return; - } - dataBuffer[dataBlockIndex].id = 6; - dataByteIndex = 0; - - log6serializer(_arg_0); - } - - fn log7serializer(_arg_0: i32) { - serializeI32(_arg_0); - } - - fn log7(_arg_0: i32) { - dataBlockIndex = atomicAdd(&indexBuffer, 1); - if (dataBlockIndex >= 40) { - return; - } - dataBuffer[dataBlockIndex].id = 7; - dataByteIndex = 0; - - log7serializer(_arg_0); - } - - fn wrappedCallback(_arg_0: u32, _arg_1: u32, _arg_2: u32) { - log1(); - log2_1(); - log3(1i); - log4(2i); - log5(3i); - log6(4i); - log7(5i); - } - - @compute @workgroup_size(1, 1, 1) fn mainCompute(@builtin(global_invocation_id) id: vec3u) { - if (any(id >= sizeUniform)) { - return; - } - wrappedCallback(id.x, id.y, id.z); - } - struct mainVertex_Output { @builtin(position) pos: vec4f, } diff --git a/packages/typegpu/src/data/dataTypes.ts b/packages/typegpu/src/data/dataTypes.ts index fa08821955..6e530d6007 100644 --- a/packages/typegpu/src/data/dataTypes.ts +++ b/packages/typegpu/src/data/dataTypes.ts @@ -23,7 +23,6 @@ import type { $reprPatch, $validVertexSchema, } from '../shared/symbols.ts'; -import { $internal } from '../shared/symbols.ts'; import type { Prettify } from '../shared/utilityTypes.ts'; import { vertexFormats } from '../shared/vertexFormat.ts'; import type { WgslExternalTexture, WgslStorageTexture, WgslTexture } from './texture.ts'; @@ -31,7 +30,6 @@ import type { Snippet } from './snippet.ts'; import type { PackedData } from './vertexFormatData.ts'; import * as wgsl from './wgslTypes.ts'; import type { WgslComparisonSampler, WgslSampler } from './sampler.ts'; -import type { ResolutionCtx } from '../types.ts'; import type { BaseData } from './wgslTypes.ts'; /** @@ -238,22 +236,6 @@ export type AnyConcreteData = Exclude< export const UnknownData = Symbol('UNKNOWN'); export type UnknownData = typeof UnknownData; -export class InfixDispatch { - readonly name: string; - readonly lhs: Snippet; - readonly operator: (ctx: ResolutionCtx, args: [lhs: Snippet, rhs: Snippet]) => Snippet; - - constructor( - name: string, - lhs: Snippet, - operator: (ctx: ResolutionCtx, args: [lhs: Snippet, rhs: Snippet]) => Snippet, - ) { - this.name = name; - this.lhs = lhs; - this.operator = operator; - } -} - export class MatrixColumnsAccess { readonly matrix: Snippet; diff --git a/packages/typegpu/src/data/index.ts b/packages/typegpu/src/data/index.ts index 803330e0d8..51df1b4e93 100644 --- a/packages/typegpu/src/data/index.ts +++ b/packages/typegpu/src/data/index.ts @@ -8,7 +8,7 @@ import { Operator } from 'tsover-runtime'; import { type InfixOperator, infixOperators } from '../tgsl/accessProp.ts'; import { MatBase } from './matrix.ts'; import { VecBase } from './vectorImpl.ts'; -import { $infixOperator } from '../shared/symbols.ts'; +import { infixDispatch } from '../tgsl/infix.ts'; function assignInfixOperator( object: T, @@ -17,12 +17,13 @@ function assignInfixOperator( ) { // oxlint-disable-next-line typescript/no-explicit-any -- anything is possible const proto = object.prototype as any; - const opImpl = infixOperators[operator] as (lhs: unknown, rhs: unknown) => unknown; + const opImpl = infixOperators[operator] as (lhs: unknown, rhs: unknown) => unknown; // dualFn operator - proto[operator] = function (this: unknown, other: unknown): unknown { - return opImpl(this, other); - }; - proto[operator][$infixOperator] = true; + Object.defineProperty(proto, operator, { + get(this) { + return infixDispatch(operator, this, opImpl); + }, + }); proto[operatorSymbol] = (lhs: unknown, rhs: unknown): unknown => { return opImpl(lhs, rhs); diff --git a/packages/typegpu/src/shared/symbols.ts b/packages/typegpu/src/shared/symbols.ts index 7e7e5a8e94..838ee4ffe0 100644 --- a/packages/typegpu/src/shared/symbols.ts +++ b/packages/typegpu/src/shared/symbols.ts @@ -38,7 +38,7 @@ export const $gpuCallable = Symbol(`typegpu:${version}:$gpuCallable`); * A marker for functions that come into shader while already being an infix operator dispatch. * This may happen when code like `external.mul(2)` appears. */ -export const $infixOperator = Symbol(`typegpu:${version}:$infixOperator`); +export const $infixDispatch = Symbol(`typegpu:${version}:$infixOperator`); // // Type tokens diff --git a/packages/typegpu/src/tgsl/accessProp.ts b/packages/typegpu/src/tgsl/accessProp.ts index 3410d3e383..f87caa8aeb 100644 --- a/packages/typegpu/src/tgsl/accessProp.ts +++ b/packages/typegpu/src/tgsl/accessProp.ts @@ -1,13 +1,7 @@ import { stitch } from '../core/resolve/stitch.ts'; import { AutoStruct } from '../data/autoStruct.ts'; import { EntryInputRouter } from '../core/function/entryInputRouter.ts'; -import { - InfixDispatch, - isUnstruct, - MatrixColumnsAccess, - undecorate, - UnknownData, -} from '../data/dataTypes.ts'; +import { isUnstruct, MatrixColumnsAccess, undecorate, UnknownData } from '../data/dataTypes.ts'; import { abstractInt, bool, f16, f32, i32, u32 } from '../data/numeric.ts'; import { derefSnippet } from '../data/ref.ts'; import { isEphemeralSnippet, isSnippet, snip, type Snippet } from '../data/snippet.ts'; @@ -41,6 +35,7 @@ import { $gpuCallable } from '../shared/symbols.ts'; import { add, bitShiftLeft, bitShiftRight, div, mod, mul, sub } from '../std/operators.ts'; import { isKnownAtComptime } from '../types.ts'; import { coerceToSnippet } from './generationHelpers.ts'; +import { infixDispatch } from './infix.ts'; const infixKinds = [ 'vec2f', @@ -66,8 +61,8 @@ export const infixOperators = { mul, div, mod, - bitShiftLeft, - bitShiftRight, + bitShiftLeft, // ??? + bitShiftRight, // ??? } as const; export type InfixOperator = keyof typeof infixOperators; @@ -111,11 +106,7 @@ const swizzleLenToType: Record> export function accessProp(target: Snippet, propName: string): Snippet | undefined { if (infixKinds.includes((target.dataType as BaseData).type) && propName in infixOperators) { const operator = infixOperators[propName as InfixOperator]; - return snip( - new InfixDispatch(propName, target, operator[$gpuCallable].call.bind(operator)), - UnknownData, - /* origin */ target.origin, - ); + return snip(infixDispatch(propName, target, operator), UnknownData, /* origin */ target.origin); } if (isWgslArray(target.dataType) && propName === 'length') { @@ -191,15 +182,12 @@ export function accessProp(target: Snippet, propName: string): Snippet | undefin } const propLength = propName.length; - if (isVec(target.dataType) && propLength >= 1 && propLength <= 4) { - const isXYZW = /^[xyzw]+$/.test(propName); - const isRGBA = /^[rgba]+$/.test(propName); - - if (!isXYZW && !isRGBA) { - // Not a valid swizzle - return undefined; - } - + if ( + isVec(target.dataType) && + propLength >= 1 && + propLength <= 4 && + /^[xyzw]+$|^[rgba]+$/.test(propName) + ) { const swizzleTypeChar = target.dataType.type.includes('bool') ? 'b' : (target.dataType.type[4] as SwizzleableType); diff --git a/packages/typegpu/src/tgsl/infix.ts b/packages/typegpu/src/tgsl/infix.ts new file mode 100644 index 0000000000..7ae2e71bcb --- /dev/null +++ b/packages/typegpu/src/tgsl/infix.ts @@ -0,0 +1,31 @@ +import type { Snippet } from '../data/snippet.ts'; +import { $infixDispatch } from '../shared/symbols.ts'; +import type { ResolutionCtx } from '../types.ts'; +import { coerceToSnippet } from './generationHelpers.ts'; + +export interface InfixDispatch { + [$infixDispatch]: true; + readonly opName: string; + readonly lhs: Snippet; + readonly operator: (ctx: ResolutionCtx, args: [lhs: Snippet, rhs: Snippet]) => Snippet; + (other: unknown): unknown; +} + +export function infixDispatch( + opName: string, + lhs: unknown, + operator: (ctx: ResolutionCtx, args: [lhs: Snippet, rhs: Snippet]) => Snippet, // this is a dualFN, fix this type +): InfixDispatch { + const lhsSnippet = coerceToSnippet(lhs); + const callable = (other: unknown) => { + console.log('infix dispatch called'); + return operator(lhs, other); + }; + const infix = Object.assign(callable, { + [$infixDispatch]: true as const, + opName, + lhs: lhsSnippet, + operator, + }); + return infix; +} diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index 5993ba0a5b..6ec30e4a23 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -25,7 +25,7 @@ import { invariant, ResolutionError, WgslTypeError } from '../errors.ts'; import { getName } from '../shared/meta.ts'; import { $gpuCallable, - $infixOperator, + $infixDispatch, $internal, $providing, isMarkedInternal, @@ -663,11 +663,12 @@ ${this.ctx.pre}}`; ); } - if (callee.value instanceof InfixDispatch) { + if ($infixDispatch in callee.value) { + console.log('$infixDispatch found on callee'); // Infix operator dispatch. if (!argNodes[0]) { throw new WgslTypeError( - `An infix operator '${callee.value.name}' was called without any arguments`, + `An infix operator '${callee.value.opName}' was called without any arguments`, ); } const rhs = this._expression(argNodes[0]); diff --git a/packages/typegpu/tests/swizzleMixedValidation.test.ts b/packages/typegpu/tests/swizzleMixedValidation.test.ts index 82e5069db0..599cde085f 100644 --- a/packages/typegpu/tests/swizzleMixedValidation.test.ts +++ b/packages/typegpu/tests/swizzleMixedValidation.test.ts @@ -71,7 +71,7 @@ describe('Mixed swizzle validation', () => { return mixed; }; - // The resolution should fail because accessProp returns undefined for mixed swizzles + // The resolution should fail because accessProp won't match any prop and will return undefined expect(() => tgpu.resolve([main])).toThrowErrorMatchingInlineSnapshot(` [Error: Resolution of the following tree failed: - diff --git a/packages/typegpu/tests/tgsl/infixOperators.test.ts b/packages/typegpu/tests/tgsl/infixOperators.test.ts index c97df91bab..de02bc5325 100644 --- a/packages/typegpu/tests/tgsl/infixOperators.test.ts +++ b/packages/typegpu/tests/tgsl/infixOperators.test.ts @@ -107,13 +107,13 @@ describe('wgslGenerator', () => { return v1.mul(2).mul(3); }; + expect(testFn()).toStrictEqual(d.vec2f(6, 12)); expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` "fn testFn() -> vec2f { var v1 = vec2f(1, 2); return ((v1 * 2f) * 3f); }" `); - expect(testFn()).toStrictEqual(d.vec2f(6, 12)); }); it('resolves mul infix operator on a function return value', () => { @@ -127,6 +127,7 @@ describe('wgslGenerator', () => { return getVec().mul(getVec()).mul(2); }; + expect(testFn()).toStrictEqual(d.vec3f(2, 8, 18)); expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` "fn getVec() -> vec3f { return vec3f(1, 2, 3); @@ -136,7 +137,6 @@ describe('wgslGenerator', () => { return ((getVec() * getVec()) * 2f); }" `); - expect(testFn()).toStrictEqual(d.vec3f(2, 8, 18)); }); it('resolves mul infix operator on a struct property', () => { @@ -148,6 +148,7 @@ describe('wgslGenerator', () => { return s.vec.mul(s.vec).mul(2); }; + expect(testFn()).toStrictEqual(d.vec3f(8)); expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` "struct Struct { vec: vec3f, @@ -158,7 +159,6 @@ describe('wgslGenerator', () => { return ((s.vec * s.vec) * 2f); }" `); - expect(testFn()).toStrictEqual(d.vec3f(8)); }); it('resolves mul infix operator on uniform vector', ({ root }) => { @@ -192,6 +192,7 @@ describe('wgslGenerator', () => { return v1.add(v2).add(v3); }; + expect(testFn()).toStrictEqual(d.vec3f(26)); expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` "fn testFn() -> vec3f { var v1 = vec3f(4); @@ -200,6 +201,38 @@ describe('wgslGenerator', () => { return ((v1 + v2) + v3); }" `); - expect(testFn()).toStrictEqual(d.vec3f(26)); + }); + + it('resolves mul infix operator on accessors', () => { + const vAccess = tgpu.accessor(d.vec2i, d.vec2i(1, 2)); + + const main = () => { + 'use gpu'; + return vAccess.$.mul(2).mul(3); + }; + + // expect(main()).toMatchInlineSnapshot(d.vec2i(6, 12)); + expect(tgpu.resolve([main])).toMatchInlineSnapshot(` + "fn main() -> vec2i { + return vec2i(6, 12); + }" + `); + }); + + it('correctly casts types', () => { + const main = () => { + 'use gpu'; + const a = d.u32(1); + const b = d.vec3f(2); + return b.mul(a); + }; + + expect(tgpu.resolve([main])).toMatchInlineSnapshot(` + "fn main() -> vec3f { + const a = 1u; + var b = vec3f(2); + return (b * f32(a)); + }" + `); }); }); diff --git a/packages/unplugin-typegpu/test/Untitled-2.js b/packages/unplugin-typegpu/test/Untitled-2.js new file mode 100644 index 0000000000..f90fd32f59 --- /dev/null +++ b/packages/unplugin-typegpu/test/Untitled-2.js @@ -0,0 +1,13 @@ +import tgpu, { d } from 'typegpu'; + +const root = await tgpu.init(); +const buffers = { buf: root.createMutable(d.struct({ pos: d.vec2u })) }; + +const fn1 = () => { + 'use gpu'; + const x = buffers.buf.$; +}; +// externals: { 'buffers': { 'buf': { '$': () => buffers.buf.$ } } } +// ?? externals: { 'buffers': { 'buf': () => buffers.buf } } -- czemu cała ściezka? bo czemu nie +// ?? externals: { 'buffers': { 'buf': { '$' : buffers.buf.$ } } } -- czemu '() => '? comptime .$ +// ?? externals: { 'buffers.buf.$': () => buffers.buf.$ } } -- czemu nie flattenujemy? jest na to issue From b68d116fd048e996ac2e7a941127093e1110c353 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Mon, 4 May 2026 16:48:48 +0200 Subject: [PATCH 04/12] Revert accidental changes --- apps/typegpu-docs/astro.config.mjs | 2 +- .../src/examples/tests/log-test/index.ts | 27 ++-- .../individual-example-tests/log-test.test.ts | 147 ++++++++++++++++++ packages/unplugin-typegpu/test/Untitled-2.js | 13 -- 4 files changed, 161 insertions(+), 28 deletions(-) delete mode 100644 packages/unplugin-typegpu/test/Untitled-2.js diff --git a/apps/typegpu-docs/astro.config.mjs b/apps/typegpu-docs/astro.config.mjs index dac1922ec5..4e4597e6d7 100644 --- a/apps/typegpu-docs/astro.config.mjs +++ b/apps/typegpu-docs/astro.config.mjs @@ -73,7 +73,7 @@ export default defineConfig({ starlightBlog({ navigation: 'none', }), - !DEV && starlightTypeDoc({ + starlightTypeDoc({ entryPoints: [ '../../packages/typegpu/src/index.d.ts', '../../packages/typegpu/src/data/index.ts', diff --git a/apps/typegpu-docs/src/examples/tests/log-test/index.ts b/apps/typegpu-docs/src/examples/tests/log-test/index.ts index 9c357dc74c..585a9bccbb 100644 --- a/apps/typegpu-docs/src/examples/tests/log-test/index.ts +++ b/apps/typegpu-docs/src/examples/tests/log-test/index.ts @@ -208,20 +208,19 @@ export const controls = defineControls({ .dispatchThreads(), }, 'Different log functionalities': { - onButtonClick: async () => { - const pipeline = root.createGuardedComputePipeline(() => { - 'use gpu'; - console.log('This message should be cleared.'); - console.clear(); - console.log('This is a log message.', 'Index:', 1); - console.debug('This is a debug message.', 'Index:', 2); - console.info('This is an info message.', 'Index:', 3); - console.warn('This is a warn message.', 'Index:', 4); - console.error('This is an error message.', 'Index:', 5); // something's wrong here!! - }); - console.log(tgpu.resolve([pipeline.pipeline])); - pipeline.dispatchThreads(); - }, + onButtonClick: async () => + root + .createGuardedComputePipeline(() => { + 'use gpu'; + console.log('This message should be cleared.'); + console.clear(); + console.log('This is a log message.', 'Index:', 1); + console.debug('This is a debug message.', 'Index:', 2); + console.info('This is an info message.', 'Index:', 3); + console.warn('This is a warn message.', 'Index:', 4); + console.error('This is an error message.', 'Index:', 5); + }) + .dispatchThreads(), }, 'Render pipeline': { onButtonClick: () => { diff --git a/apps/typegpu-docs/tests/individual-example-tests/log-test.test.ts b/apps/typegpu-docs/tests/individual-example-tests/log-test.test.ts index 2cc5f86610..3ee6a9035b 100644 --- a/apps/typegpu-docs/tests/individual-example-tests/log-test.test.ts +++ b/apps/typegpu-docs/tests/individual-example-tests/log-test.test.ts @@ -1297,6 +1297,153 @@ describe('console log example', () => { wrappedCallback(id.x, id.y, id.z); } + @group(0) @binding(0) var sizeUniform: vec3u; + + @group(0) @binding(1) var indexBuffer: atomic; + + struct SerializedLogData { + id: u32, + serializedData: array, + } + + @group(0) @binding(2) var dataBuffer: array; + + var dataBlockIndex: u32; + + var dataByteIndex: u32; + + fn log1serializer() { + + } + + fn log1() { + dataBlockIndex = atomicAdd(&indexBuffer, 1); + if (dataBlockIndex >= 40) { + return; + } + dataBuffer[dataBlockIndex].id = 1; + dataByteIndex = 0; + + log1serializer(); + } + + fn log2serializer() { + + } + + fn log2_1() { + dataBlockIndex = atomicAdd(&indexBuffer, 1); + if (dataBlockIndex >= 40) { + return; + } + dataBuffer[dataBlockIndex].id = 2; + dataByteIndex = 0; + + log2serializer(); + } + + fn nextByteIndex() -> u32 { + let i = dataByteIndex; + dataByteIndex = dataByteIndex + 1u; + return i; + } + + fn serializeI32(n: i32) { + dataBuffer[dataBlockIndex].serializedData[nextByteIndex()] = bitcast(n); + } + + fn log3serializer(_arg_0: i32) { + serializeI32(_arg_0); + } + + fn log3(_arg_0: i32) { + dataBlockIndex = atomicAdd(&indexBuffer, 1); + if (dataBlockIndex >= 40) { + return; + } + dataBuffer[dataBlockIndex].id = 3; + dataByteIndex = 0; + + log3serializer(_arg_0); + } + + fn log4serializer(_arg_0: i32) { + serializeI32(_arg_0); + } + + fn log4(_arg_0: i32) { + dataBlockIndex = atomicAdd(&indexBuffer, 1); + if (dataBlockIndex >= 40) { + return; + } + dataBuffer[dataBlockIndex].id = 4; + dataByteIndex = 0; + + log4serializer(_arg_0); + } + + fn log5serializer(_arg_0: i32) { + serializeI32(_arg_0); + } + + fn log5(_arg_0: i32) { + dataBlockIndex = atomicAdd(&indexBuffer, 1); + if (dataBlockIndex >= 40) { + return; + } + dataBuffer[dataBlockIndex].id = 5; + dataByteIndex = 0; + + log5serializer(_arg_0); + } + + fn log6serializer(_arg_0: i32) { + serializeI32(_arg_0); + } + + fn log6(_arg_0: i32) { + dataBlockIndex = atomicAdd(&indexBuffer, 1); + if (dataBlockIndex >= 40) { + return; + } + dataBuffer[dataBlockIndex].id = 6; + dataByteIndex = 0; + + log6serializer(_arg_0); + } + + fn log7serializer(_arg_0: i32) { + serializeI32(_arg_0); + } + + fn log7(_arg_0: i32) { + dataBlockIndex = atomicAdd(&indexBuffer, 1); + if (dataBlockIndex >= 40) { + return; + } + dataBuffer[dataBlockIndex].id = 7; + dataByteIndex = 0; + + log7serializer(_arg_0); + } + + fn wrappedCallback(_arg_0: u32, _arg_1: u32, _arg_2: u32) { + log1(); + log2_1(); + log3(1i); + log4(2i); + log5(3i); + log6(4i); + log7(5i); + } + + @compute @workgroup_size(1, 1, 1) fn mainCompute(@builtin(global_invocation_id) id: vec3u) { + if (any(id >= sizeUniform)) { + return; + } + wrappedCallback(id.x, id.y, id.z); + } + struct mainVertex_Output { @builtin(position) pos: vec4f, } diff --git a/packages/unplugin-typegpu/test/Untitled-2.js b/packages/unplugin-typegpu/test/Untitled-2.js deleted file mode 100644 index f90fd32f59..0000000000 --- a/packages/unplugin-typegpu/test/Untitled-2.js +++ /dev/null @@ -1,13 +0,0 @@ -import tgpu, { d } from 'typegpu'; - -const root = await tgpu.init(); -const buffers = { buf: root.createMutable(d.struct({ pos: d.vec2u })) }; - -const fn1 = () => { - 'use gpu'; - const x = buffers.buf.$; -}; -// externals: { 'buffers': { 'buf': { '$': () => buffers.buf.$ } } } -// ?? externals: { 'buffers': { 'buf': () => buffers.buf } } -- czemu cała ściezka? bo czemu nie -// ?? externals: { 'buffers': { 'buf': { '$' : buffers.buf.$ } } } -- czemu '() => '? comptime .$ -// ?? externals: { 'buffers.buf.$': () => buffers.buf.$ } } -- czemu nie flattenujemy? jest na to issue From 79cd30315cf35cde36e8965008585ed4f87a661d Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Mon, 4 May 2026 16:58:29 +0200 Subject: [PATCH 05/12] Add a type guard for InfixDispatch --- packages/typegpu/src/shared/symbols.ts | 5 ----- packages/typegpu/src/tgsl/infix.ts | 12 +++++++++--- packages/typegpu/src/tgsl/wgslGenerator.ts | 22 ++++------------------ 3 files changed, 13 insertions(+), 26 deletions(-) diff --git a/packages/typegpu/src/shared/symbols.ts b/packages/typegpu/src/shared/symbols.ts index 838ee4ffe0..7b677cb859 100644 --- a/packages/typegpu/src/shared/symbols.ts +++ b/packages/typegpu/src/shared/symbols.ts @@ -34,11 +34,6 @@ export const $cast = Symbol(`typegpu:${version}:$cast`); * Can be called on the GPU */ export const $gpuCallable = Symbol(`typegpu:${version}:$gpuCallable`); -/** - * A marker for functions that come into shader while already being an infix operator dispatch. - * This may happen when code like `external.mul(2)` appears. - */ -export const $infixDispatch = Symbol(`typegpu:${version}:$infixOperator`); // // Type tokens diff --git a/packages/typegpu/src/tgsl/infix.ts b/packages/typegpu/src/tgsl/infix.ts index 7ae2e71bcb..799514b8c4 100644 --- a/packages/typegpu/src/tgsl/infix.ts +++ b/packages/typegpu/src/tgsl/infix.ts @@ -1,10 +1,11 @@ import type { Snippet } from '../data/snippet.ts'; -import { $infixDispatch } from '../shared/symbols.ts'; +import { $internal, isMarkedInternal } from '../shared/symbols.ts'; import type { ResolutionCtx } from '../types.ts'; import { coerceToSnippet } from './generationHelpers.ts'; export interface InfixDispatch { - [$infixDispatch]: true; + [$internal]: true; + type: 'infix-disptach'; readonly opName: string; readonly lhs: Snippet; readonly operator: (ctx: ResolutionCtx, args: [lhs: Snippet, rhs: Snippet]) => Snippet; @@ -22,10 +23,15 @@ export function infixDispatch( return operator(lhs, other); }; const infix = Object.assign(callable, { - [$infixDispatch]: true as const, + [$internal]: true, + type: 'infix-disptach', opName, lhs: lhsSnippet, operator, }); return infix; } + +export function isInfixDispatch(o: unknown): o is InfixDispatch { + return isMarkedInternal(o) && (o as InfixDispatch)?.type === 'infix-disptach'; +} diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index 6ec30e4a23..ed16fb8244 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -1,14 +1,7 @@ import * as tinyest from 'tinyest'; import { stitch } from '../core/resolve/stitch.ts'; import { arrayOf } from '../data/array.ts'; -import { - type AnyData, - ConsoleLog, - InfixDispatch, - isLooseData, - UnknownData, - unptr, -} from '../data/dataTypes.ts'; +import { type AnyData, ConsoleLog, isLooseData, UnknownData, unptr } from '../data/dataTypes.ts'; import { bool, i32, u32 } from '../data/numeric.ts'; import { vec2u, vec3u, vec4u } from '../data/vector.ts'; import { @@ -23,13 +16,7 @@ import { import * as wgsl from '../data/wgslTypes.ts'; import { invariant, ResolutionError, WgslTypeError } from '../errors.ts'; import { getName } from '../shared/meta.ts'; -import { - $gpuCallable, - $infixDispatch, - $internal, - $providing, - isMarkedInternal, -} from '../shared/symbols.ts'; +import { $gpuCallable, $internal, $providing, isMarkedInternal } from '../shared/symbols.ts'; import { safeStringify } from '../shared/stringify.ts'; import { pow } from '../std/numeric.ts'; import { add, div, mul, neg, sub } from '../std/operators.ts'; @@ -59,6 +46,7 @@ import * as forOfUtils from './forOfUtils.ts'; import { isTgpuRange } from '../std/range.ts'; import type { FunctionDefinitionOptions } from './shaderGenerator_members.ts'; import { getAttributesString } from '../data/attributes.ts'; +import { isInfixDispatch } from './infix.ts'; const { NodeTypeCatalog: NODE } = tinyest; @@ -663,9 +651,7 @@ ${this.ctx.pre}}`; ); } - if ($infixDispatch in callee.value) { - console.log('$infixDispatch found on callee'); - // Infix operator dispatch. + if (isInfixDispatch(callee.value)) { if (!argNodes[0]) { throw new WgslTypeError( `An infix operator '${callee.value.opName}' was called without any arguments`, From 08880203b40c66dc2eb0e27085c6633d65fc9b09 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Mon, 4 May 2026 17:14:39 +0200 Subject: [PATCH 06/12] Fix type errors --- packages/typegpu/src/data/dataTypes.ts | 2 +- packages/typegpu/src/data/index.ts | 10 ++++------ packages/typegpu/src/tgsl/accessProp.ts | 6 +++--- packages/typegpu/src/tgsl/infix.ts | 10 ++++++---- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/typegpu/src/data/dataTypes.ts b/packages/typegpu/src/data/dataTypes.ts index 6e530d6007..e6c2380f35 100644 --- a/packages/typegpu/src/data/dataTypes.ts +++ b/packages/typegpu/src/data/dataTypes.ts @@ -1,5 +1,5 @@ import { setName, type TgpuNamable } from '../shared/meta.ts'; -import { isMarkedInternal } from '../shared/symbols.ts'; +import { $internal, isMarkedInternal } from '../shared/symbols.ts'; import type { Infer, InferGPURecord, diff --git a/packages/typegpu/src/data/index.ts b/packages/typegpu/src/data/index.ts index 51df1b4e93..5f146b5084 100644 --- a/packages/typegpu/src/data/index.ts +++ b/packages/typegpu/src/data/index.ts @@ -5,19 +5,19 @@ // NOTE: This is a barrel file, internal files should not import things from this file import { Operator } from 'tsover-runtime'; -import { type InfixOperator, infixOperators } from '../tgsl/accessProp.ts'; +import { type InfixOperatorName, infixOperators } from '../tgsl/accessProp.ts'; import { MatBase } from './matrix.ts'; import { VecBase } from './vectorImpl.ts'; import { infixDispatch } from '../tgsl/infix.ts'; function assignInfixOperator( object: T, - operator: InfixOperator, + operator: InfixOperatorName, operatorSymbol: symbol, ) { // oxlint-disable-next-line typescript/no-explicit-any -- anything is possible const proto = object.prototype as any; - const opImpl = infixOperators[operator] as (lhs: unknown, rhs: unknown) => unknown; // dualFn operator + const opImpl = infixOperators[operator]; // dualFn operator Object.defineProperty(proto, operator, { get(this) { @@ -25,9 +25,7 @@ function assignInfixOperator( }, }); - proto[operatorSymbol] = (lhs: unknown, rhs: unknown): unknown => { - return opImpl(lhs, rhs); - }; + proto[operatorSymbol] = opImpl; } assignInfixOperator(VecBase, 'add', Operator.plus); diff --git a/packages/typegpu/src/tgsl/accessProp.ts b/packages/typegpu/src/tgsl/accessProp.ts index f87caa8aeb..a7533c4ae3 100644 --- a/packages/typegpu/src/tgsl/accessProp.ts +++ b/packages/typegpu/src/tgsl/accessProp.ts @@ -31,7 +31,6 @@ import { isWgslArray, isWgslStruct, } from '../data/wgslTypes.ts'; -import { $gpuCallable } from '../shared/symbols.ts'; import { add, bitShiftLeft, bitShiftRight, div, mod, mul, sub } from '../std/operators.ts'; import { isKnownAtComptime } from '../types.ts'; import { coerceToSnippet } from './generationHelpers.ts'; @@ -65,7 +64,8 @@ export const infixOperators = { bitShiftRight, // ??? } as const; -export type InfixOperator = keyof typeof infixOperators; +export type InfixOperatorName = keyof typeof infixOperators; +export type InfixOperator = (typeof infixOperators)[InfixOperatorName]; type SwizzleableType = 'f' | 'h' | 'i' | 'u' | 'b'; type SwizzleLength = 1 | 2 | 3 | 4; @@ -105,7 +105,7 @@ const swizzleLenToType: Record> export function accessProp(target: Snippet, propName: string): Snippet | undefined { if (infixKinds.includes((target.dataType as BaseData).type) && propName in infixOperators) { - const operator = infixOperators[propName as InfixOperator]; + const operator = infixOperators[propName as InfixOperatorName]; return snip(infixDispatch(propName, target, operator), UnknownData, /* origin */ target.origin); } diff --git a/packages/typegpu/src/tgsl/infix.ts b/packages/typegpu/src/tgsl/infix.ts index 799514b8c4..c0607a2352 100644 --- a/packages/typegpu/src/tgsl/infix.ts +++ b/packages/typegpu/src/tgsl/infix.ts @@ -1,6 +1,7 @@ import type { Snippet } from '../data/snippet.ts'; import { $internal, isMarkedInternal } from '../shared/symbols.ts'; import type { ResolutionCtx } from '../types.ts'; +import type { InfixOperator } from './accessProp.ts'; import { coerceToSnippet } from './generationHelpers.ts'; export interface InfixDispatch { @@ -15,21 +16,22 @@ export interface InfixDispatch { export function infixDispatch( opName: string, lhs: unknown, - operator: (ctx: ResolutionCtx, args: [lhs: Snippet, rhs: Snippet]) => Snippet, // this is a dualFN, fix this type + operator: InfixOperator, ): InfixDispatch { const lhsSnippet = coerceToSnippet(lhs); const callable = (other: unknown) => { console.log('infix dispatch called'); - return operator(lhs, other); + // oxlint-disable-next-line typescript/no-explicit-any + return operator(lhs as any, other as any); }; const infix = Object.assign(callable, { [$internal]: true, - type: 'infix-disptach', + type: 'infix-disptach' as const, opName, lhs: lhsSnippet, operator, }); - return infix; + return infix as InfixDispatch; } export function isInfixDispatch(o: unknown): o is InfixDispatch { From 5e0d612532b2f1ed4aa1a4e710defd1774b1e4d9 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Mon, 4 May 2026 17:28:41 +0200 Subject: [PATCH 07/12] Fix infix handling in wgslGenerator --- packages/typegpu/src/tgsl/infix.ts | 6 ++++-- packages/typegpu/src/tgsl/wgslGenerator.ts | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/typegpu/src/tgsl/infix.ts b/packages/typegpu/src/tgsl/infix.ts index c0607a2352..c527fb9b6c 100644 --- a/packages/typegpu/src/tgsl/infix.ts +++ b/packages/typegpu/src/tgsl/infix.ts @@ -1,15 +1,17 @@ import type { Snippet } from '../data/snippet.ts'; import { $internal, isMarkedInternal } from '../shared/symbols.ts'; -import type { ResolutionCtx } from '../types.ts'; import type { InfixOperator } from './accessProp.ts'; import { coerceToSnippet } from './generationHelpers.ts'; +// Two possible infixDispatch variants +// string, number | vector | matrix, InfixOperator => number | vector | matrix => number | vector | matrix +// string, stnippet, InfixOperator albo samo GPUCallable => Snippet => Snippet export interface InfixDispatch { [$internal]: true; type: 'infix-disptach'; readonly opName: string; readonly lhs: Snippet; - readonly operator: (ctx: ResolutionCtx, args: [lhs: Snippet, rhs: Snippet]) => Snippet; + readonly operator: InfixOperator; (other: unknown): unknown; } diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index ed16fb8244..5f3d3b2633 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -658,7 +658,8 @@ ${this.ctx.pre}}`; ); } const rhs = this._expression(argNodes[0]); - return callee.value.operator(this.ctx, [callee.value.lhs, rhs]); + const callable = callee.value.operator[$gpuCallable]; + return callable.call(this.ctx, [callee.value.lhs, rhs]); } if (callee.value instanceof ConsoleLog) { From 940d2cb0967957af8df51b4bc09c4e1d95fa8a23 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Tue, 5 May 2026 11:09:21 +0200 Subject: [PATCH 08/12] Cleanup infix assignment --- packages/typegpu/src/data/index.ts | 36 +++++++++---------------- packages/typegpu/src/tgsl/accessProp.ts | 4 +-- 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/packages/typegpu/src/data/index.ts b/packages/typegpu/src/data/index.ts index 5f146b5084..55c50f9b7d 100644 --- a/packages/typegpu/src/data/index.ts +++ b/packages/typegpu/src/data/index.ts @@ -11,43 +11,33 @@ import { VecBase } from './vectorImpl.ts'; import { infixDispatch } from '../tgsl/infix.ts'; function assignInfixOperator( - object: T, + base: T, operator: InfixOperatorName, operatorSymbol: symbol, ) { - // oxlint-disable-next-line typescript/no-explicit-any -- anything is possible - const proto = object.prototype as any; - const opImpl = infixOperators[operator]; // dualFn operator + const opImpl = infixOperators[operator]; - Object.defineProperty(proto, operator, { - get(this) { + Object.defineProperty(base.prototype, operatorSymbol, { + value: opImpl, + }); + + Object.defineProperty(base.prototype, operator, { + get() { return infixDispatch(operator, this, opImpl); }, }); - - proto[operatorSymbol] = opImpl; } assignInfixOperator(VecBase, 'add', Operator.plus); +assignInfixOperator(MatBase, 'add', Operator.plus); assignInfixOperator(VecBase, 'sub', Operator.minus); +assignInfixOperator(MatBase, 'sub', Operator.minus); assignInfixOperator(VecBase, 'mul', Operator.star); +assignInfixOperator(MatBase, 'mul', Operator.star); assignInfixOperator(VecBase, 'div', Operator.slash); assignInfixOperator(VecBase, 'mod', Operator.percent); -assignInfixOperator(MatBase, 'add', Operator.plus); -assignInfixOperator(MatBase, 'sub', Operator.minus); -assignInfixOperator(MatBase, 'mul', Operator.star); - -// bitShift does not yet have tsover operator symbol -{ - // oxlint-disable-next-line typescript/no-explicit-any -- anything is possible - const proto = VecBase.prototype as any; - proto.bitShiftLeft = function (this: unknown, other: unknown) { - return (infixOperators.bitShiftLeft as (a: unknown, b: unknown) => unknown)(this, other); - }; - proto.bitShiftRight = function (this: unknown, other: unknown) { - return (infixOperators.bitShiftRight as (a: unknown, b: unknown) => unknown)(this, other); - }; -} +assignInfixOperator(VecBase, 'bitShiftLeft', Symbol()); // bitShift does not yet have tsover operator symbol +assignInfixOperator(VecBase, 'bitShiftRight', Symbol()); // bitShift does not yet have tsover operator symbol export { bool, f16, f32, i32, u16, u32 } from './numeric.ts'; export { diff --git a/packages/typegpu/src/tgsl/accessProp.ts b/packages/typegpu/src/tgsl/accessProp.ts index a7533c4ae3..8d8394df17 100644 --- a/packages/typegpu/src/tgsl/accessProp.ts +++ b/packages/typegpu/src/tgsl/accessProp.ts @@ -60,8 +60,8 @@ export const infixOperators = { mul, div, mod, - bitShiftLeft, // ??? - bitShiftRight, // ??? + bitShiftLeft, + bitShiftRight, } as const; export type InfixOperatorName = keyof typeof infixOperators; From ba269a6ecdbd7bff6a093ebb1e433f9e61e70c0b Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Tue, 5 May 2026 11:09:31 +0200 Subject: [PATCH 09/12] Add a new test --- .../typegpu/tests/tgsl/infixOperators.test.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/typegpu/tests/tgsl/infixOperators.test.ts b/packages/typegpu/tests/tgsl/infixOperators.test.ts index de02bc5325..531c16519a 100644 --- a/packages/typegpu/tests/tgsl/infixOperators.test.ts +++ b/packages/typegpu/tests/tgsl/infixOperators.test.ts @@ -235,4 +235,27 @@ describe('wgslGenerator', () => { }" `); }); + + it('works when application is deferred', ({ root }) => { + const vec1 = d.vec2f(1, 2).mul; + const vec2 = tgpu.comptime(() => d.vec2f(3, 4).mul); + const buf = tgpu.comptime(() => root.createUniform(d.vec2f, [5, 6]).$.add); + + const fn = () => { + 'use gpu'; + const a = vec1(7); + const b = vec2()(8); + const c = buf()(9); + }; + + expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` + "@group(0) @binding(0) var item: vec2f; + + fn fn_1() { + var a = vec2f(7, 14); + var b = vec2f(24, 32); + var c = (item + 9f); + }" + `); + }); }); From f49bffe4699f1a22f4502e15b47d343cc6051642 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Tue, 5 May 2026 12:12:05 +0200 Subject: [PATCH 10/12] Better types and docs for InfixDispatch --- packages/typegpu/src/data/index.ts | 2 +- packages/typegpu/src/tgsl/accessProp.ts | 2 +- packages/typegpu/src/tgsl/infix.ts | 55 +++++++++++-------- packages/typegpu/src/tgsl/wgslGenerator.ts | 5 +- .../typegpu/tests/tgsl/infixOperators.test.ts | 8 +-- 5 files changed, 42 insertions(+), 30 deletions(-) diff --git a/packages/typegpu/src/data/index.ts b/packages/typegpu/src/data/index.ts index 55c50f9b7d..5efb4d049c 100644 --- a/packages/typegpu/src/data/index.ts +++ b/packages/typegpu/src/data/index.ts @@ -23,7 +23,7 @@ function assignInfixOperator( Object.defineProperty(base.prototype, operator, { get() { - return infixDispatch(operator, this, opImpl); + return infixDispatch(this, opImpl); }, }); } diff --git a/packages/typegpu/src/tgsl/accessProp.ts b/packages/typegpu/src/tgsl/accessProp.ts index 8d8394df17..92f939576d 100644 --- a/packages/typegpu/src/tgsl/accessProp.ts +++ b/packages/typegpu/src/tgsl/accessProp.ts @@ -106,7 +106,7 @@ const swizzleLenToType: Record> export function accessProp(target: Snippet, propName: string): Snippet | undefined { if (infixKinds.includes((target.dataType as BaseData).type) && propName in infixOperators) { const operator = infixOperators[propName as InfixOperatorName]; - return snip(infixDispatch(propName, target, operator), UnknownData, /* origin */ target.origin); + return snip(infixDispatch(target, operator), UnknownData, /* origin */ target.origin); } if (isWgslArray(target.dataType) && propName === 'length') { diff --git a/packages/typegpu/src/tgsl/infix.ts b/packages/typegpu/src/tgsl/infix.ts index c527fb9b6c..30a39cb90b 100644 --- a/packages/typegpu/src/tgsl/infix.ts +++ b/packages/typegpu/src/tgsl/infix.ts @@ -1,39 +1,50 @@ -import type { Snippet } from '../data/snippet.ts'; +import { isSnippet, type Snippet } from '../data/snippet.ts'; +import type { AnyMatInstance, AnyNumericVecInstance } from '../data/wgslTypes.ts'; import { $internal, isMarkedInternal } from '../shared/symbols.ts'; import type { InfixOperator } from './accessProp.ts'; -import { coerceToSnippet } from './generationHelpers.ts'; -// Two possible infixDispatch variants -// string, number | vector | matrix, InfixOperator => number | vector | matrix => number | vector | matrix -// string, stnippet, InfixOperator albo samo GPUCallable => Snippet => Snippet +type Numeric = number | AnyNumericVecInstance | AnyMatInstance; + +/** + * In wgslGenerator, the lhs may either be Numeric or Snippet, + * and InfixDispatch is recognized by the $internal symbol. + * InfixDispatch is not called in wgslGenerator. + * @example + * const dispatch = d.vec2u(1).mul; + * const fn = () => { + * 'use gpu'; + * dispatch(2); // lhs is Numeric + * d.vec2u(1).mul(2) // lhs is a snippet + * } + * + * In JS, the lhs is always numeric, and InfixDispatch is callable. + * @example + * const dispatch = d.vec2u(1).mul; + * dispatch(2); + */ export interface InfixDispatch { [$internal]: true; type: 'infix-disptach'; - readonly opName: string; - readonly lhs: Snippet; + readonly lhs: Snippet | Numeric; readonly operator: InfixOperator; - (other: unknown): unknown; + (other: Numeric): Numeric; } -export function infixDispatch( - opName: string, - lhs: unknown, - operator: InfixOperator, -): InfixDispatch { - const lhsSnippet = coerceToSnippet(lhs); - const callable = (other: unknown) => { - console.log('infix dispatch called'); - // oxlint-disable-next-line typescript/no-explicit-any - return operator(lhs as any, other as any); +export function infixDispatch(lhs: Snippet | Numeric, operator: InfixOperator): InfixDispatch { + const callable = (other: Numeric | Snippet) => { + if (isSnippet(lhs)) { + throw new Error('Unexpected snippet lhs in JS infix operator.'); + } + // operator will perform all necessary type checks + return operator(lhs as never, other as never); }; const infix = Object.assign(callable, { - [$internal]: true, + [$internal]: true as const, type: 'infix-disptach' as const, - opName, - lhs: lhsSnippet, + lhs, operator, }); - return infix as InfixDispatch; + return infix; } export function isInfixDispatch(o: unknown): o is InfixDispatch { diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index 5f3d3b2633..3018f08e14 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -654,12 +654,13 @@ ${this.ctx.pre}}`; if (isInfixDispatch(callee.value)) { if (!argNodes[0]) { throw new WgslTypeError( - `An infix operator '${callee.value.opName}' was called without any arguments`, + `An infix operator '${getName(callee.value.operator)}' was called without any arguments`, ); } + const lhs = coerceToSnippet(callee.value.lhs); const rhs = this._expression(argNodes[0]); const callable = callee.value.operator[$gpuCallable]; - return callable.call(this.ctx, [callee.value.lhs, rhs]); + return callable.call(this.ctx, [lhs, rhs]); } if (callee.value instanceof ConsoleLog) { diff --git a/packages/typegpu/tests/tgsl/infixOperators.test.ts b/packages/typegpu/tests/tgsl/infixOperators.test.ts index 531c16519a..e337024711 100644 --- a/packages/typegpu/tests/tgsl/infixOperators.test.ts +++ b/packages/typegpu/tests/tgsl/infixOperators.test.ts @@ -237,14 +237,14 @@ describe('wgslGenerator', () => { }); it('works when application is deferred', ({ root }) => { - const vec1 = d.vec2f(1, 2).mul; - const vec2 = tgpu.comptime(() => d.vec2f(3, 4).mul); + const v = d.vec2f(1, 2).mul; + const u = tgpu.comptime(() => d.vec2f(3, 4).mul); const buf = tgpu.comptime(() => root.createUniform(d.vec2f, [5, 6]).$.add); const fn = () => { 'use gpu'; - const a = vec1(7); - const b = vec2()(8); + const a = v(7); + const b = u()(8); const c = buf()(9); }; From 10c1f7d9837ab2f8350cd024fdad8118b28df4f4 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Wed, 6 May 2026 11:27:24 +0200 Subject: [PATCH 11/12] Fix typo --- packages/typegpu/src/tgsl/infix.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/typegpu/src/tgsl/infix.ts b/packages/typegpu/src/tgsl/infix.ts index 30a39cb90b..ee846f23ee 100644 --- a/packages/typegpu/src/tgsl/infix.ts +++ b/packages/typegpu/src/tgsl/infix.ts @@ -24,7 +24,7 @@ type Numeric = number | AnyNumericVecInstance | AnyMatInstance; */ export interface InfixDispatch { [$internal]: true; - type: 'infix-disptach'; + type: 'infix-dispatch'; readonly lhs: Snippet | Numeric; readonly operator: InfixOperator; (other: Numeric): Numeric; @@ -40,7 +40,7 @@ export function infixDispatch(lhs: Snippet | Numeric, operator: InfixOperator): }; const infix = Object.assign(callable, { [$internal]: true as const, - type: 'infix-disptach' as const, + type: 'infix-dispatch' as const, lhs, operator, }); @@ -48,5 +48,5 @@ export function infixDispatch(lhs: Snippet | Numeric, operator: InfixOperator): } export function isInfixDispatch(o: unknown): o is InfixDispatch { - return isMarkedInternal(o) && (o as InfixDispatch)?.type === 'infix-disptach'; + return isMarkedInternal(o) && (o as InfixDispatch)?.type === 'infix-dispatch'; } From cffa3f21fe775b276d690f42db3e27edd036b932 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Wed, 6 May 2026 11:30:59 +0200 Subject: [PATCH 12/12] Rename infix.ts to infixDispatch.ts --- packages/typegpu/src/data/index.ts | 2 +- packages/typegpu/src/tgsl/accessProp.ts | 2 +- packages/typegpu/src/tgsl/{infix.ts => infixDispatch.ts} | 0 packages/typegpu/src/tgsl/wgslGenerator.ts | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename packages/typegpu/src/tgsl/{infix.ts => infixDispatch.ts} (100%) diff --git a/packages/typegpu/src/data/index.ts b/packages/typegpu/src/data/index.ts index 5efb4d049c..13aa6e1f5c 100644 --- a/packages/typegpu/src/data/index.ts +++ b/packages/typegpu/src/data/index.ts @@ -8,7 +8,7 @@ import { Operator } from 'tsover-runtime'; import { type InfixOperatorName, infixOperators } from '../tgsl/accessProp.ts'; import { MatBase } from './matrix.ts'; import { VecBase } from './vectorImpl.ts'; -import { infixDispatch } from '../tgsl/infix.ts'; +import { infixDispatch } from '../tgsl/infixDispatch.ts'; function assignInfixOperator( base: T, diff --git a/packages/typegpu/src/tgsl/accessProp.ts b/packages/typegpu/src/tgsl/accessProp.ts index 92f939576d..8c88550c63 100644 --- a/packages/typegpu/src/tgsl/accessProp.ts +++ b/packages/typegpu/src/tgsl/accessProp.ts @@ -34,7 +34,7 @@ import { import { add, bitShiftLeft, bitShiftRight, div, mod, mul, sub } from '../std/operators.ts'; import { isKnownAtComptime } from '../types.ts'; import { coerceToSnippet } from './generationHelpers.ts'; -import { infixDispatch } from './infix.ts'; +import { infixDispatch } from './infixDispatch.ts'; const infixKinds = [ 'vec2f', diff --git a/packages/typegpu/src/tgsl/infix.ts b/packages/typegpu/src/tgsl/infixDispatch.ts similarity index 100% rename from packages/typegpu/src/tgsl/infix.ts rename to packages/typegpu/src/tgsl/infixDispatch.ts diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index 3018f08e14..537c7853ec 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -46,7 +46,7 @@ import * as forOfUtils from './forOfUtils.ts'; import { isTgpuRange } from '../std/range.ts'; import type { FunctionDefinitionOptions } from './shaderGenerator_members.ts'; import { getAttributesString } from '../data/attributes.ts'; -import { isInfixDispatch } from './infix.ts'; +import { isInfixDispatch } from './infixDispatch.ts'; const { NodeTypeCatalog: NODE } = tinyest;