diff --git a/packages/typegpu/src/shared/tseynit.ts b/packages/typegpu/src/shared/tseynit.ts new file mode 100644 index 0000000000..c272e988b5 --- /dev/null +++ b/packages/typegpu/src/shared/tseynit.ts @@ -0,0 +1,213 @@ +import * as tinyest from 'tinyest'; + +const { NodeTypeCatalog: NODE } = tinyest; + +export function stringifyNode(node: tinyest.AnyNode): string { + if (isExpression(node)) { + return stringifyExpression(node, ''); + } + return stringifyStatement(node, ''); +} + +function stringifyStatement(node: tinyest.Statement, ident: string): string { + if (isExpression(node)) { + return `${ident}${stringifyExpression(node, ident)};`; + } + + if (node[0] === NODE.block) { + const statements = node[1].map((n) => stringifyStatement(n, ident + ' ')); + return `{\n${statements.join('\n')}\n${ident}}`; + } + + if (node[0] === NODE.return) { + const expr = node[1] === undefined ? '' : ` ${stringifyExpression(node[1], '')}`; + return `${ident}return${expr};`; + } + + if (node[0] === NODE.if) { + const cond = stringifyExpression(node[1], ident); + const then = stringifyStatement(node[2], ident); + const base = `${ident}if (${cond}) ${then}`; + if (node[3] !== undefined) { + return `${base} else ${stringifyStatement(node[3], ident)}`; + } + return base; + } + + if (node[0] === NODE.let) { + if (node[2] !== undefined) { + return `${ident}let ${node[1]} = ${stringifyExpression(node[2], ident)};`; + } + return `${ident}let ${node[1]};`; + } + + if (node[0] === NODE.const) { + if (node[2] !== undefined) { + return `${ident}const ${node[1]} = ${stringifyExpression(node[2], ident)};`; + } + return `${ident}const ${node[1]};`; + } + + if (node[0] === NODE.for) { + const init = node[1] ? stringifyStatement(node[1], '') : ';'; + const cond = node[2] ? stringifyExpression(node[2], ident) : ''; + const update = node[3] ? stringifyStatement(node[3], '') : ''; + const body = stringifyStatement(node[4], ident); + return `${ident}for (${init} ${cond}; ${update.slice(0, -1) /* trim the ';' */}) ${body}`; + } + + if (node[0] === NODE.while) { + const cond = stringifyExpression(node[1], ident); + const body = stringifyStatement(node[2], ident); + return `${ident}while (${cond}) ${body}`; + } + + if (node[0] === NODE.continue) { + return `${ident}continue;`; + } + + if (node[0] === NODE.break) { + return `${ident}break;`; + } + + if (node[0] === NODE.forOf) { + const leftKind = node[1][0] === NODE.const ? 'const' : 'let'; + const leftName = node[1][1]; + const right = stringifyExpression(node[2], ident); + const body = stringifyStatement(node[3], ident); + return `${ident}for (${leftKind} ${leftName} of ${right}) ${body}`; + } + + assertExhaustive(node); +} + +function stringifyExpression(node: tinyest.Expression, ident: string): string { + if (typeof node === 'string') { + return node; + } + + if (typeof node === 'boolean') { + return `${node}`; + } + + if (node[0] === NODE.numericLiteral) { + return node[1]; + } + + if (node[0] === NODE.stringLiteral) { + return JSON.stringify(node[1]); + } + + if (node[0] === NODE.arrayExpr) { + const elements = node[1].map((n) => stringifyExpression(n, ident)); + return `[${elements.join(', ')}]`; + } + + if (node[0] === NODE.binaryExpr) { + return `${wrapIfComplex(node[1], ident)} ${node[2]} ${wrapIfComplex(node[3], ident)}`; + } + + if (node[0] === NODE.assignmentExpr) { + return `${stringifyExpression(node[1], ident)} ${node[2]} ${stringifyExpression(node[3], ident)}`; + } + + if (node[0] === NODE.logicalExpr) { + return `${wrapIfComplex(node[1], ident)} ${node[2]} ${wrapIfComplex(node[3], ident)}`; + } + + if (node[0] === NODE.unaryExpr) { + // Unary word operators like void, instanceof and delete require a space + const sep = node[1].length > 1 ? ' ' : ''; + return `${node[1]}${sep}${wrapIfComplex(node[2], ident)}`; + } + + if (node[0] === NODE.call) { + const callee = wrapIfComplex(node[1], ident); + const args = node[2].map((n) => stringifyExpression(n, ident)).join(', '); + return `${callee}(${args})`; + } + + if (node[0] === NODE.memberAccess) { + if (Array.isArray(node[1]) && node[1][0] === NODE.numericLiteral) { + return `(${stringifyExpression(node[1], ident)}).${node[2]}`; + } + return `${wrapIfComplex(node[1], ident)}.${node[2]}`; + } + + if (node[0] === NODE.indexAccess) { + return `${wrapIfComplex(node[1], ident)}[${stringifyExpression(node[2], ident)}]`; + } + + if (node[0] === NODE.preUpdate) { + return `${node[1]}${wrapIfComplex(node[2], ident)}`; + } + + if (node[0] === NODE.postUpdate) { + return `${wrapIfComplex(node[2], ident)}${node[1]}`; + } + + if (node[0] === NODE.objectExpr) { + const entries = Object.entries(node[1]).map( + ([key, val]) => `${key}: ${stringifyExpression(val, ident)}`, + ); + return `{ ${entries.join(', ')} }`; + } + + if (node[0] === NODE.conditionalExpr) { + return `${wrapIfComplex(node[1], ident)} ? ${wrapIfComplex(node[2], ident)} : ${wrapIfComplex(node[3], ident)}`; + } + + assertExhaustive(node); +} + +function assertExhaustive(value: never): never { + throw new Error(`'${JSON.stringify(value)}' was not handled by the stringify function.`); +} + +function isExpression(node: tinyest.AnyNode): node is tinyest.Expression { + if ( + typeof node === 'string' || + typeof node === 'boolean' || + node[0] === NODE.numericLiteral || + node[0] === NODE.stringLiteral || + node[0] === NODE.arrayExpr || + node[0] === NODE.binaryExpr || + node[0] === NODE.assignmentExpr || + node[0] === NODE.logicalExpr || + node[0] === NODE.unaryExpr || + node[0] === NODE.call || + node[0] === NODE.memberAccess || + node[0] === NODE.indexAccess || + node[0] === NODE.preUpdate || + node[0] === NODE.postUpdate || + node[0] === NODE.objectExpr || + node[0] === NODE.conditionalExpr + ) { + node satisfies tinyest.Expression; + return true; + } + node satisfies Exclude; + return false; +} + +const SIMPLE_NODES: number[] = [ + NODE.memberAccess, // highest precedence + NODE.indexAccess, // highest precedence + NODE.call, // highest precedence + NODE.arrayExpr, // [] make things not ambiguous + NODE.stringLiteral, + NODE.numericLiteral, +]; +/** + * Stringifies expression, and wraps it in parentheses if they cannot be trivially omitted + */ +function wrapIfComplex(node: tinyest.Expression, ident: string): string { + const s = stringifyExpression(node, ident); + if (typeof node === 'string' || typeof node === 'boolean') { + return s; + } + if (SIMPLE_NODES.includes(node[0])) { + return s; + } + return `(${s})`; +} diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index 83cc68d0fe..b370cdc1ca 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -1,7 +1,7 @@ import * as tinyest from 'tinyest'; import { stitch } from '../core/resolve/stitch.ts'; import { arrayOf } from '../data/array.ts'; -import { type AnyData, InfixDispatch, isLooseData, UnknownData, unptr } from '../data/dataTypes.ts'; +import { type AnyData, InfixDispatch, UnknownData, unptr } from '../data/dataTypes.ts'; import { bool, i32, u32 } from '../data/numeric.ts'; import { vec2u, vec3u, vec4u } from '../data/vector.ts'; import { @@ -44,6 +44,7 @@ import { mathToStd, supportedLogOps } from './jsPolyfills.ts'; import type { ExternalMap } from '../core/resolve/externals.ts'; import * as forOfUtils from './forOfUtils.ts'; import { isTgpuRange } from '../std/range.ts'; +import { stringifyNode } from '../shared/tseynit.ts'; import type { FunctionDefinitionOptions } from './shaderGenerator_members.ts'; import { getAttributesString } from '../data/attributes.ts'; @@ -376,7 +377,7 @@ ${this.ctx.pre}}`; if (rhsExpr.value instanceof RefOperator) { throw new WgslTypeError( - stitch`Cannot assign a ref to an existing variable '${lhsExpr}', define a new variable instead.`, + stitch`Cannot assign a ref to an existing variable '${stringifyNode(lhs)}', define a new variable instead.`, ); } @@ -384,6 +385,10 @@ ${this.ctx.pre}}`; throw new Error('Please use the === operator instead of =='); } + if (op === '!=') { + throw new Error('Please use the !== operator instead of !='); + } + if (op === '===' && isKnownAtComptime(lhsExpr) && isKnownAtComptime(rhsExpr)) { return snip(lhsExpr.value === rhsExpr.value, bool, 'constant'); } @@ -434,14 +439,19 @@ ${this.ctx.pre}}`; convLhs.origin === 'constant-tgpu-const-ref' || convLhs.origin === 'runtime-tgpu-const-ref' ) { + if (isKnownAtComptime(convLhs)) { + throw new WgslTypeError( + `'${stringifyNode(expression)}' is invalid, because the left side is defined outside of the shader, and therefore is immutable during its execution. Try using tgpu.privateVar or buffers.`, + ); + } throw new WgslTypeError( - `'${lhsStr} = ${rhsStr}' is invalid, because ${lhsStr} is a constant. This error may also occur when assigning to a value defined outside of a TypeGPU function's scope.`, + `'${stringifyNode(expression)}' is invalid, because the left side is a constant.`, ); } if (lhsExpr.origin === 'argument') { throw new WgslTypeError( - `'${lhsStr} ${op} ${rhsStr}' is invalid, because non-pointer arguments cannot be mutated.`, + `'${stringifyNode(expression)}' is invalid, because non-pointer arguments cannot be mutated.`, ); } @@ -452,18 +462,18 @@ ${this.ctx.pre}}`; !wgsl.isNaturallyEphemeral(rhsExpr.dataType) ) { throw new WgslTypeError( - `'${lhsStr} = ${rhsStr}' is invalid, because argument references cannot be assigned.\n-----\nTry '${lhsStr} = ${ + `'${stringifyNode(expression)}' is invalid, because argument references cannot be assigned.\n-----\nTry '${stringifyNode(lhs)} = ${ this.ctx.resolve(rhsExpr.dataType).value - }(${rhsStr})' to copy the value instead.\n-----`, + }(${stringifyNode(rhs)})' to copy the value instead.\n-----`, ); } // Compound assignment operators are okay, e.g. +=, -=, *=, /=, ... if (op === '=' && !isEphemeralSnippet(rhsExpr)) { throw new WgslTypeError( - `'${lhsStr} = ${rhsStr}' is invalid, because references cannot be assigned.\n-----\nTry '${lhsStr} = ${ + `'${stringifyNode(expression)}' is invalid, because references cannot be assigned.\n-----\nTry '${stringifyNode(lhs)} = ${ this.ctx.resolve(rhsExpr.dataType).value - }(${rhsStr})' to copy the value instead.\n-----`, + }(${stringifyNode(rhs)})' to copy the value instead.\n-----`, ); } } @@ -512,11 +522,7 @@ ${this.ctx.pre}}`; const accessed = accessProp(target, property); if (!accessed) { - throw new Error( - stitch`Property '${property}' not found on value '${target}' of type ${this.ctx.resolve( - target.dataType, - )}`, - ); + throw new Error(`Property '${property}' not found on '${stringifyNode(targetNode)}'`); } return accessed; } @@ -532,11 +538,8 @@ ${this.ctx.pre}}`; const accessed = accessIndex(target, property); if (!accessed) { - const targetStr = this.ctx.resolve(target.value, target.dataType).value; - const propertyStr = this.ctx.resolve(property.value, property.dataType).value; - throw new Error( - `Index access '${targetStr}[${propertyStr}]' is invalid. If the value is an array, to address this, consider one of the following approaches: (1) declare the array using 'tgpu.const', (2) store the array in a buffer, or (3) define the array within the GPU function scope.`, + `Index access '${stringifyNode(expression)}' is invalid. If the value is an array, to address this, consider one of the following approaches: (1) declare the array using 'tgpu.const', (2) store the array in a buffer, or (3) define the array within the GPU function scope.`, ); } @@ -549,9 +552,7 @@ ${this.ctx.pre}}`; typeof expression[1] === 'string' ? numericLiteralToSnippet(parseNumericString(expression[1])) : numericLiteralToSnippet(expression[1]); - if (!type) { - throw new Error(`Invalid numeric literal ${expression[1]}`); - } + invariant(type, `Expected ${stringifyNode(expression)} to be valid numeric literal`); return type; } @@ -668,7 +669,7 @@ ${this.ctx.pre}}`; const argType = strictSignature.argTypes[i]; if (!argType) { throw new WgslTypeError( - `Function '${getName(callee.value)}' was called with too many arguments`, + `Call '${stringifyNode(expression)}' is invalid since the function expected fewer arguments`, ); } return this._typedExpression(arg, argType); @@ -813,9 +814,7 @@ ${this.ctx.pre}}`; } throw new WgslTypeError( - `No target type could be inferred for object with keys [${Object.keys(obj).join( - ', ', - )}], please wrap the object in the corresponding schema.`, + `No target type could be inferred for object '${stringifyNode(expression)}', please wrap the object in the corresponding schema.`, ); } @@ -847,7 +846,7 @@ ${this.ctx.pre}}`; const converted = convertToCommonType(this.ctx, valuesSnippets); if (!converted) { throw new WgslTypeError( - 'The given values cannot be automatically converted to a common type. Consider wrapping the array in an appropriate schema', + `Values '${stringifyNode(expression)}' cannot be automatically converted to a common type. Consider wrapping the array in an appropriate schema`, ); } @@ -868,7 +867,7 @@ ${this.ctx.pre}}`; return testExpression.value ? this._expression(consequent) : this._expression(alternative); } else { throw new Error( - `Ternary operator is only supported for comptime-known checks (used with '${testExpression.value}'). For runtime checks, please use 'std.select' or if/else statements.`, + `Ternary operator '${stringifyNode(expression)}' is invalid, because only comptime-known checks are supported. For runtime checks, please use 'std.select' or if/else statements.`, ); } } @@ -937,7 +936,7 @@ ${this.ctx.pre}}`; if (returnSnippet.value instanceof RefOperator) { throw new WgslTypeError( - stitch`Cannot return references, returning '${returnSnippet.value.snippet}'`, + `Cannot return '${stringifyNode(returnNode)}' because it is a d.ref`, ); } @@ -960,7 +959,7 @@ ${this.ctx.pre}}`; this.ctx.topFunctionScope?.functionType === 'normal' ) { throw new WgslTypeError( - stitch`Cannot return references to arguments, returning '${returnSnippet}'. Copy the argument before returning it.`, + `'${stringifyNode(statement)}' is invalid, cannot return references to arguments. Copy the argument before returning it.`, ); } @@ -969,7 +968,7 @@ ${this.ctx.pre}}`; !isEphemeralSnippet(returnSnippet) && returnSnippet.origin !== 'this-function' ) { - const str = this.ctx.resolve(returnSnippet.value, returnSnippet.dataType).value; + const str = stringifyNode(returnNode); const typeStr = this.ctx.resolve(unptr(returnSnippet.dataType)).value; throw new WgslTypeError( `'return ${str};' is invalid, cannot return references. @@ -1054,17 +1053,15 @@ ${this.ctx.pre}else ${alternate}`; const eq = rawValue !== undefined ? this._expression(rawValue) : undefined; if (!eq) { - throw new Error(`Cannot create variable '${rawId}' without an initial value.`); + throw new Error( + `'${stringifyNode(statement)}' is invalid because all variables need initializers.`, + ); } const ephemeral = isEphemeralSnippet(eq); let dataType = eq.dataType as wgsl.BaseData; const naturallyEphemeral = wgsl.isNaturallyEphemeral(dataType); - if (isLooseData(eq.dataType)) { - throw new Error(`Cannot create variable '${rawId}' with loose data type.`); - } - if (eq.value instanceof RefOperator) { // We're assigning a newly created `d.ref()` if (eq.dataType !== UnknownData) { @@ -1090,7 +1087,7 @@ ${this.ctx.pre}else ${alternate}`; if (!ephemeral) { // Referential if (stmtType === NODE.let) { - const rhsStr = this.ctx.resolve(eq.value).value; + const rhsStr = stringifyNode(rawValue ?? ''); const rhsTypeStr = this.ctx.resolve(unptr(eq.dataType)).value; throw new WgslTypeError( @@ -1138,7 +1135,7 @@ ${this.ctx.pre}else ${alternate}`; if (eq.origin === 'argument') { if (!naturallyEphemeral) { - const rhsStr = this.ctx.resolve(eq.value).value; + const rhsStr = stringifyNode(rawValue ?? ''); const rhsTypeStr = this.ctx.resolve(unptr(eq.dataType)).value; throw new WgslTypeError( @@ -1256,7 +1253,7 @@ ${this.ctx.pre}else ${alternate}`; !wgsl.isNaturallyEphemeral(elements[0]?.dataType) ) { throw new WgslTypeError( - 'Cannot unroll loop. The elements of iterable are emphemeral but not naturally ephemeral.', + `Cannot unroll '${stringifyNode(iterable)}'. The elements of iterable are constructed in place but are not value types.`, ); } diff --git a/packages/typegpu/tests/constant.test.ts b/packages/typegpu/tests/constant.test.ts index 707aa9ef2e..a2b98ae541 100644 --- a/packages/typegpu/tests/constant.test.ts +++ b/packages/typegpu/tests/constant.test.ts @@ -87,7 +87,7 @@ describe('tgpu.const', () => { [Error: Resolution of the following tree failed: - - fn*:fn - - fn*:fn(): 'boid.pos = vec3f()' is invalid, because boid.pos is a constant. This error may also occur when assigning to a value defined outside of a TypeGPU function's scope.] + - fn*:fn(): 'boid.$.pos = d.vec3f(0, 0, 0)' is invalid, because the left side is a constant.] `); // Since we freeze the object, we cannot mutate when running the function in JS either diff --git a/packages/typegpu/tests/ref.test.ts b/packages/typegpu/tests/ref.test.ts index 4eb9f5b1b4..02578bf38e 100644 --- a/packages/typegpu/tests/ref.test.ts +++ b/packages/typegpu/tests/ref.test.ts @@ -51,7 +51,7 @@ describe('d.ref', () => { [Error: Resolution of the following tree failed: - - fn*:hello - - fn*:hello(): Cannot assign a ref to an existing variable '(&foo)', define a new variable instead.] + - fn*:hello(): Cannot assign a ref to an existing variable 'foo', define a new variable instead.] `); }); @@ -242,14 +242,14 @@ describe('d.ref', () => { [Error: Resolution of the following tree failed: - - fn*:foo - - fn*:foo(): Cannot return references, returning 'value'] + - fn*:foo(): Cannot return 'value' because it is a d.ref] `); expect(() => tgpu.resolve([bar])).toThrowErrorMatchingInlineSnapshot(` [Error: Resolution of the following tree failed: - - fn*:bar - - fn*:bar(): Cannot return references, returning '0'] + - fn*:bar(): Cannot return 'd.ref(0)' because it is a d.ref] `); }); diff --git a/packages/typegpu/tests/swizzleMixedValidation.test.ts b/packages/typegpu/tests/swizzleMixedValidation.test.ts index 82e5069db0..baf3360b0c 100644 --- a/packages/typegpu/tests/swizzleMixedValidation.test.ts +++ b/packages/typegpu/tests/swizzleMixedValidation.test.ts @@ -76,7 +76,7 @@ describe('Mixed swizzle validation', () => { [Error: Resolution of the following tree failed: - - fn*:main - - fn*:main(): Property 'xrgy' not found on value 'vec' of type vec4f] + - fn*:main(): Property 'xrgy' not found on 'vec'] `); }); @@ -93,7 +93,7 @@ describe('Mixed swizzle validation', () => { [Error: Resolution of the following tree failed: - - fn*:main - - fn*:main(): Property 'rgxw' not found on value 'vec' of type vec4f] + - fn*:main(): Property 'rgxw' not found on 'vec'] `); }); }); diff --git a/packages/typegpu/tests/tgsl/argumentOrigin.test.ts b/packages/typegpu/tests/tgsl/argumentOrigin.test.ts index 3fd4f3ce40..1a16cd05c3 100644 --- a/packages/typegpu/tests/tgsl/argumentOrigin.test.ts +++ b/packages/typegpu/tests/tgsl/argumentOrigin.test.ts @@ -20,7 +20,7 @@ describe('function argument origin tracking', () => { - - fn*:main - fn*:main() - - fn*:foo(i32): 'a += 1i' is invalid, because non-pointer arguments cannot be mutated.] + - fn*:foo(i32): 'a += 1' is invalid, because non-pointer arguments cannot be mutated.] `); }); @@ -43,7 +43,7 @@ describe('function argument origin tracking', () => { - - fn*:main - fn*:main() - - fn*:foo(struct:Foo): '_arg_0.a += 1f' is invalid, because non-pointer arguments cannot be mutated.] + - fn*:foo(struct:Foo): 'a += 1' is invalid, because non-pointer arguments cannot be mutated.] `); }); @@ -64,7 +64,7 @@ describe('function argument origin tracking', () => { - - fn*:main - fn*:main() - - fn*:foo(vec3f): 'a.x += 1f' is invalid, because non-pointer arguments cannot be mutated.] + - fn*:foo(vec3f): 'a.x += 1' is invalid, because non-pointer arguments cannot be mutated.] `); }); @@ -85,7 +85,7 @@ describe('function argument origin tracking', () => { - - fn*:main - fn*:main() - - fn*:foo(vec3f): 'b.x += 1f' is invalid, because non-pointer arguments cannot be mutated.] + - fn*:foo(vec3f): 'b.x += 1' is invalid, because non-pointer arguments cannot be mutated.] `); }); @@ -156,7 +156,26 @@ describe('function argument origin tracking', () => { - - fn*:main - fn*:main() - - fn*:foo(vec3f): Cannot return references to arguments, returning 'a'. Copy the argument before returning it.] + - fn*:foo(vec3f): 'return a;' is invalid, cannot return references to arguments. Copy the argument before returning it.] `); }); + + it('throws a descriptive error when assigning a const to let', () => { + const testFn = () => { + 'use gpu'; + const a = d.vec3f(); + let b = a; + }; + + expect(() => tgpu.resolve([testFn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:testFn + - fn*:testFn(): 'let b = a' is invalid, because references cannot be assigned to 'let' variable declarations. + ----- + - Try 'let b = vec3f(a)' if you need to reassign 'b' later + - Try 'const b = a' if you won't reassign 'b' later. + -----] + `); + }); }); diff --git a/packages/typegpu/tests/tgsl/ternaryOperator.test.ts b/packages/typegpu/tests/tgsl/ternaryOperator.test.ts index c41cc6b27c..8cfb3d5cec 100644 --- a/packages/typegpu/tests/tgsl/ternaryOperator.test.ts +++ b/packages/typegpu/tests/tgsl/ternaryOperator.test.ts @@ -143,7 +143,7 @@ describe('ternary operator', () => { expect(() => tgpu.resolve([myFn])).toThrowErrorMatchingInlineSnapshot(` [Error: Resolution of the following tree failed: - - - fn:myFn: Ternary operator is only supported for comptime-known checks (used with '(n > 0u)'). For runtime checks, please use 'std.select' or if/else statements.] + - fn:myFn: Ternary operator '(n > 0) ? n : (-n)' is invalid, because only comptime-known checks are supported. For runtime checks, please use 'std.select' or if/else statements.] `); }); }); diff --git a/packages/typegpu/tests/tgsl/tseynit.test.ts b/packages/typegpu/tests/tgsl/tseynit.test.ts new file mode 100644 index 0000000000..7add17a97f --- /dev/null +++ b/packages/typegpu/tests/tgsl/tseynit.test.ts @@ -0,0 +1,343 @@ +import { describe, expect, it } from 'vitest'; +import * as tinyest from 'tinyest'; +import { getMetaData } from '../../src/shared/meta.ts'; +import { stringifyNode } from '../../src/shared/tseynit.ts'; +import tgpu, { d } from 'typegpu'; + +function getBodyAst(fn: () => void) { + const ast = getMetaData(fn)?.ast?.body; + if (!ast) { + throw new Error('Expected ast to be defined'); + } + return ast; +} + +describe('ast to JS transformation', () => { + const N = tinyest.NodeTypeCatalog; + + describe('stringify expression', () => { + it('handles identifiers', () => { + expect(stringifyNode('myVar')).toBe('myVar'); + }); + + it('handles boolean literals', () => { + expect(stringifyNode(true)).toBe('true'); + expect(stringifyNode(false)).toBe('false'); + }); + + it('handles numeric literals', () => { + expect(stringifyNode([N.numericLiteral, '42'] satisfies tinyest.Num)).toBe('42'); + expect(stringifyNode([N.numericLiteral, '6.7'] satisfies tinyest.Num)).toBe('6.7'); + expect(stringifyNode([N.numericLiteral, '-0.0'] satisfies tinyest.Num)).toBe('-0.0'); + }); + + it('handles string literals', () => { + expect(stringifyNode([N.stringLiteral, 'hello'] satisfies tinyest.Str)).toMatchInlineSnapshot( + `""hello""`, + ); + expect( + stringifyNode([N.stringLiteral, "'hello'"] satisfies tinyest.Str), + ).toMatchInlineSnapshot(`""'hello'""`); + expect( + stringifyNode([N.stringLiteral, '"hello"'] satisfies tinyest.Str), + ).toMatchInlineSnapshot(`""\\"hello\\"""`); + expect( + stringifyNode([N.stringLiteral, '`hello`'] satisfies tinyest.Str), + ).toMatchInlineSnapshot(`""\`hello\`""`); + expect( + stringifyNode([N.stringLiteral, `hello\``] satisfies tinyest.Str), + ).toMatchInlineSnapshot(`""hello\`""`); + }); + + it('handles array expressions', () => { + const node: tinyest.ArrayExpression = [ + N.arrayExpr, + [ + [N.numericLiteral, '1'], + [N.numericLiteral, '2'], + [N.stringLiteral, 'three'], + ], + ]; + expect(stringifyNode(node)).toBe(`[1, 2, "three"]`); + }); + + it('handles binary expressions', () => { + const node: tinyest.BinaryExpression = [ + N.binaryExpr, + [N.numericLiteral, '1'], + '+', + [N.numericLiteral, '2'], + ]; + expect(stringifyNode(node)).toBe('1 + 2'); + }); + + it('wraps nested binary sub-expression in parens', () => { + const node: tinyest.BinaryExpression = [ + N.binaryExpr, + [N.binaryExpr, [N.numericLiteral, '1'], '+', [N.numericLiteral, '2']], + '*', + [N.numericLiteral, '3'], + ]; + expect(stringifyNode(node)).toBe('(1 + 2) * 3'); + }); + + it('handles unary symbol operators', () => { + const nodeA: tinyest.UnaryExpression = [N.unaryExpr, '-', 'x']; + expect(stringifyNode(nodeA)).toBe('-x'); + const nodeB: tinyest.UnaryExpression = [N.unaryExpr, '-', [N.binaryExpr, 'x', '+', 'y']]; + expect(stringifyNode(nodeB)).toBe('-(x + y)'); + }); + + it('handles unary word operators', () => { + const node: tinyest.UnaryExpression = [N.unaryExpr, 'typeof', 'x']; + expect(stringifyNode(node)).toBe('typeof x'); + }); + + it('handles logical expressions', () => { + const node: tinyest.LogicalExpression = [N.logicalExpr, 'a', '&&', 'b']; + expect(stringifyNode(node)).toBe('a && b'); + }); + + it('handles assignment expressions', () => { + const node: tinyest.AssignmentExpression = [ + N.assignmentExpr, + 'x', + '=', + [N.numericLiteral, '1'], + ]; + expect(stringifyNode(node)).toBe('x = 1'); + }); + + it('handles function calls', () => { + const node: tinyest.Call = [ + N.call, + 'foo', + [ + [N.numericLiteral, '1'], + [N.numericLiteral, '2'], + ], + ]; + expect(stringifyNode(node)).toBe('foo(1, 2)'); + }); + + it('handles member access', () => { + const nodeA: tinyest.MemberAccess = [N.memberAccess, [N.memberAccess, 'a', 'b'], 'c']; + expect(stringifyNode(nodeA)).toBe('a.b.c'); + const nodeB: tinyest.MemberAccess = [N.memberAccess, [N.numericLiteral, '1'], 'toString']; + expect(stringifyNode(nodeB)).toBe('(1).toString'); + }); + + it('handles index access', () => { + const nodeA: tinyest.IndexAccess = [N.indexAccess, 'arr', [N.numericLiteral, '0']]; + expect(stringifyNode(nodeA)).toBe('arr[0]'); + const nodeB: tinyest.IndexAccess = [ + N.indexAccess, + [N.arrayExpr, ['a', 'b', 'c']], + [N.numericLiteral, '0'], + ]; + expect(stringifyNode(nodeB)).toBe('[a, b, c][0]'); + }); + + it('handles post-update', () => { + const node: tinyest.PostUpdate = [N.postUpdate, '++', 'i']; + expect(stringifyNode(node)).toBe('i++'); + }); + + it('handles pre-update', () => { + const node: tinyest.PreUpdate = [N.preUpdate, '--', 'i']; + expect(stringifyNode(node)).toBe('--i'); + }); + + it('handles object expressions', () => { + const node: tinyest.ObjectExpression = [N.objectExpr, { a: [N.numericLiteral, '1'], b: 'x' }]; + expect(stringifyNode(node)).toBe('{ a: 1, b: x }'); + }); + + it('handles conditional expressions', () => { + const node: tinyest.ConditionalExpression = [ + N.conditionalExpr, + 'x', + [N.numericLiteral, '1'], + [N.numericLiteral, '2'], + ]; + expect(stringifyNode(node)).toBe('x ? 1 : 2'); + }); + }); + + // 'use gpu' is used here so that we don't have to write the AST manually + describe('stringify statement', () => { + it('handles blocks', () => { + const fn = () => { + 'use gpu'; + [1, 2, 'three']; + [4, 5]; + }; + expect(stringifyNode(getBodyAst(fn))).toMatchInlineSnapshot(` + "{ + [1, 2, "three"]; + [4, 5]; + }" + `); + }); + + it('handles declarations', () => { + const fn = () => { + 'use gpu'; + const val = 42; + let i = 0; + }; + expect(stringifyNode(getBodyAst(fn))).toMatchInlineSnapshot(` + "{ + const val = 42; + let i = 0; + }" + `); + }); + + it('handles return statement', () => { + const fnA = () => { + 'use gpu'; + return 0; + }; + const fnB = () => { + 'use gpu'; + return; + }; + expect(stringifyNode(getBodyAst(fnA))).toMatchInlineSnapshot(` + "{ + return 0; + }" + `); + expect(stringifyNode(getBodyAst(fnB))).toMatchInlineSnapshot(` + "{ + return; + }" + `); + }); + + it('handles if/else statement', () => { + const fn = () => { + 'use gpu'; + let x = 0; + const cond = false; + if (cond) { + x = 1; + } else { + x = 2; + } + }; + expect(stringifyNode(getBodyAst(fn))).toMatchInlineSnapshot(` + "{ + let x = 0; + const cond = false; + if (cond) { + x = 1; + } else { + x = 2; + } + }" + `); + }); + + it('handles for loop', () => { + const fn = () => { + 'use gpu'; + for (let i = 0; i < 10; i++) { + const j = i; + } + }; + expect(stringifyNode(getBodyAst(fn))).toMatchInlineSnapshot(` + "{ + for (let i = 0; i < 10; i++) { + const j = i; + } + }" + `); + }); + + it('handles while loop', () => { + const fn = () => { + 'use gpu'; + let x = 10; + while (x) { + x /= 2; + } + }; + expect(stringifyNode(getBodyAst(fn))).toMatchInlineSnapshot(` + "{ + let x = 10; + while (x) { + x /= 2; + } + }" + `); + }); + + it('handles continue and break', () => { + const fn = () => { + 'use gpu'; + while (true) { + if (true) { + continue; + } + break; + } + }; + expect(stringifyNode(getBodyAst(fn))).toMatchInlineSnapshot(` + "{ + while (true) { + if (true) { + continue; + } + break; + } + }" + `); + }); + + it('handles for-of loop', () => { + const fn = () => { + 'use gpu'; + for (const item of [1, 2, 3]) { + } + }; + expect(stringifyNode(getBodyAst(fn))).toMatchInlineSnapshot(` + "{ + for (const item of [1, 2, 3]) { + + } + }" + `); + }); + + it('does not retain TS types', () => { + const fn = () => { + 'use gpu'; + let a: number = 0; + const b = 1 satisfies unknown; + }; + expect(stringifyNode(getBodyAst(fn))).toMatchInlineSnapshot(` + "{ + let a = 0; + const b = 1; + }" + `); + }); + + it('handles undefined', () => { + const slot = tgpu.slot(d.u32); + const fn = () => { + 'use gpu'; + if (slot.$ !== undefined) { + } + }; + expect(stringifyNode(getBodyAst(fn))).toMatchInlineSnapshot(` + "{ + if (slot.$ !== undefined) { + + } + }" + `); + }); + }); +}); diff --git a/packages/typegpu/tests/tgsl/typeInference.test.ts b/packages/typegpu/tests/tgsl/typeInference.test.ts index 10c6c81726..c2ac6bdc6c 100644 --- a/packages/typegpu/tests/tgsl/typeInference.test.ts +++ b/packages/typegpu/tests/tgsl/typeInference.test.ts @@ -267,7 +267,7 @@ describe('wgsl generator type inference', () => { expect(() => tgpu.resolve([myFn])).toThrowErrorMatchingInlineSnapshot(` [Error: Resolution of the following tree failed: - - - fn:myFn: No target type could be inferred for object with keys [pos, vel], please wrap the object in the corresponding schema.] + - fn:myFn: No target type could be inferred for object '{ pos: d.vec2f(), vel: d.vec2f() }', please wrap the object in the corresponding schema.] `); }); diff --git a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts index 43f7b6ce77..0c4604df36 100644 --- a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts +++ b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts @@ -1321,7 +1321,7 @@ describe('wgslGenerator', () => { expect(() => tgpu.resolve([testFn])).toThrowErrorMatchingInlineSnapshot(` [Error: Resolution of the following tree failed: - - - fn:testFn: Index access 'array(9, 8, 7, 6)[i]' is invalid. If the value is an array, to address this, consider one of the following approaches: (1) declare the array using 'tgpu.const', (2) store the array in a buffer, or (3) define the array within the GPU function scope.] + - fn:testFn: Index access 'myArray[i]' is invalid. If the value is an array, to address this, consider one of the following approaches: (1) declare the array using 'tgpu.const', (2) store the array in a buffer, or (3) define the array within the GPU function scope.] `); }); @@ -1340,6 +1340,85 @@ describe('wgslGenerator', () => { `); }); + it('throws a descriptive error when calling a function with too many arguments', () => { + const testFn = tgpu.fn([])(() => {}); + const main = () => { + 'use gpu'; + // @ts-ignore + testFn(1, 2); + }; + + expect(() => tgpu.resolve([main])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:main + - fn*:main(): Call 'testFn(1, 2)' is invalid since the function expected fewer arguments] + `); + }); + + it('throws a descriptive error when creating a non-uniform array', () => { + const testFn = () => { + 'use gpu'; + const t = [1, 2, d.vec2u()]; + }; + + expect(() => tgpu.resolve([testFn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:testFn + - fn*:testFn(): Values '[1, 2, d.vec2u()]' cannot be automatically converted to a common type. Consider wrapping the array in an appropriate schema] + `); + }); + + it('throws a descriptive error when returning a reference', ({ root }) => { + const myUniform = root.createUniform(d.vec3u); + const testFn = () => { + 'use gpu'; + const v = myUniform.$; + return v; + }; + + expect(() => tgpu.resolve([testFn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:testFn + - fn*:testFn(): 'return v;' is invalid, cannot return references. + ----- + Try 'return vec3u(v);' instead. + -----] + `); + }); + + it('throws a descriptive error when declaring a variable without initializer', () => { + const testFn = () => { + 'use gpu'; + // oxlint-disable-next-line typegpu/no-uninitialized-variables + let a; + }; + + expect(() => tgpu.resolve([testFn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:testFn + - fn*:testFn(): 'let a;' is invalid because all variables need initializers.] + `); + }); + + it('throws a descriptive error when declaring a loose variable', () => { + const Unstruct = d.unstruct({ prop: d.vec4f }); + const testFn = () => { + 'use gpu'; + let a = Unstruct(); + }; + + expect(() => tgpu.resolve([testFn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:testFn + - fn*:testFn(): Function 'Unstruct' is not marked with the 'use gpu' directive and cannot be used in a shader] + `); + }); + it('throws a descriptive error when declaring a const inside TGSL', () => { const testFn = tgpu.fn( [d.u32], @@ -1779,6 +1858,41 @@ describe('wgslGenerator', () => { `); }); + it('throws a readable error when assigning an argument reference', () => { + const testFn = tgpu.fn([d.vec3u])((v) => { + let u = d.vec3u(); + u = v; + }); + + expect(() => tgpu.resolve([testFn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn:testFn: 'u = v' is invalid, because argument references cannot be assigned. + ----- + Try 'u = vec3u(v)' to copy the value instead. + -----] + `); + }); + + it('throws a readable error when assigning a reference', () => { + const testFn = () => { + 'use gpu'; + let u = d.vec3u(); + const v = d.vec3u(); + u = v; + }; + + expect(() => tgpu.resolve([testFn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:testFn + - fn*:testFn(): 'u = v' is invalid, because references cannot be assigned. + ----- + Try 'u = vec3u(v)' to copy the value instead. + -----] + `); + }); + it('handles unary operator `!` on complex comptime-known operand', () => { const slot = tgpu.slot<{ a?: number }>({}); diff --git a/packages/typegpu/tests/tgslFn.test.ts b/packages/typegpu/tests/tgslFn.test.ts index 9278a82a00..0cdcecc7ea 100644 --- a/packages/typegpu/tests/tgslFn.test.ts +++ b/packages/typegpu/tests/tgslFn.test.ts @@ -1016,7 +1016,7 @@ describe('tgsl fn when using plugin', () => { expect(() => tgpu.resolve([f])).toThrowErrorMatchingInlineSnapshot(` [Error: Resolution of the following tree failed: - - - fn:f: '0 = 2' is invalid, because 0 is a constant. This error may also occur when assigning to a value defined outside of a TypeGPU function's scope.] + - fn:f: 'a = 2' is invalid, because the left side is defined outside of the shader, and therefore is immutable during its execution. Try using tgpu.privateVar or buffers.] `); }); }); diff --git a/packages/typegpu/tests/unroll.test.ts b/packages/typegpu/tests/unroll.test.ts index c35689684e..1a1c7ead83 100644 --- a/packages/typegpu/tests/unroll.test.ts +++ b/packages/typegpu/tests/unroll.test.ts @@ -199,7 +199,7 @@ describe('tgpu.unroll', () => { [Error: Resolution of the following tree failed: - - fn*:f - - fn*:f(): Cannot unroll loop. The elements of iterable are emphemeral but not naturally ephemeral.] + - fn*:f(): Cannot unroll 'tgpu.unroll([Boid()])'. The elements of iterable are constructed in place but are not value types.] `); });