diff --git a/packages/typegpu/src/core/function/fnCore.ts b/packages/typegpu/src/core/function/fnCore.ts index 7dd753a5af..dbc9e1cffd 100644 --- a/packages/typegpu/src/core/function/fnCore.ts +++ b/packages/typegpu/src/core/function/fnCore.ts @@ -11,6 +11,7 @@ import { replaceExternalsInWgsl, mergeFunctionExternals, } from '../resolve/externals.ts'; +import { ResolvableString } from '../resolve/resolvableString.ts'; import { extractArgs } from './extractArgs.ts'; import type { Implementation, SeparatedEntryArgs } from './fnTypes.ts'; @@ -127,7 +128,10 @@ export function createFnCore( this.setExternals('args', { in: Object.fromEntries( - entryInput.positionalArgs.map((a) => [a.schemaKey, a.schemaKey]), + entryInput.positionalArgs.map((a) => [ + a.schemaKey, + new ResolvableString(a.schemaKey), + ]), ), }); } diff --git a/packages/typegpu/src/core/resolve/externals.ts b/packages/typegpu/src/core/resolve/externals.ts index 3120dd2274..856f3cbb8b 100644 --- a/packages/typegpu/src/core/resolve/externals.ts +++ b/packages/typegpu/src/core/resolve/externals.ts @@ -110,7 +110,7 @@ export function replaceExternalsInWgsl( for (const [i, elem] of chain.entries()) { currentItem = (currentItem as ExternalMap)[elem]; - if (isResolvable(currentItem)) { + if (isResolvable(currentItem) || typeof currentItem === 'string') { suffix = chain .slice(i + 1) .map((s) => `.${s}`) diff --git a/packages/typegpu/src/core/resolve/resolvableString.ts b/packages/typegpu/src/core/resolve/resolvableString.ts new file mode 100644 index 0000000000..38f33ddab2 --- /dev/null +++ b/packages/typegpu/src/core/resolve/resolvableString.ts @@ -0,0 +1,16 @@ +import { UnknownData } from '../../data/dataTypes.ts'; +import { snip } from '../../data/snippet.ts'; +import { $internal, $resolve } from '../../shared/symbols.ts'; + +export class ResolvableString { + [$internal] = true; + #value; + + constructor(value: string) { + this.#value = value; + } + + [$resolve]() { + return snip(this.#value, UnknownData, 'runtime', true); + } +} diff --git a/packages/typegpu/src/core/resolve/stitch.ts b/packages/typegpu/src/core/resolve/stitch.ts index 912c81d93a..b9664d8c3b 100644 --- a/packages/typegpu/src/core/resolve/stitch.ts +++ b/packages/typegpu/src/core/resolve/stitch.ts @@ -15,9 +15,7 @@ export function stitch( const ctx = getResolutionCtx() as ResolutionCtx; function resolveSnippet(maybeSnippet: Snippet | string | number) { - return isSnippet(maybeSnippet) - ? ctx.resolve(maybeSnippet.value, maybeSnippet.dataType).value - : maybeSnippet; + return isSnippet(maybeSnippet) ? ctx.resolveSnippet(maybeSnippet).value : maybeSnippet; } let result = ''; diff --git a/packages/typegpu/src/core/slot/accessor.ts b/packages/typegpu/src/core/slot/accessor.ts index ecf12c16f7..7ef128ad26 100644 --- a/packages/typegpu/src/core/slot/accessor.ts +++ b/packages/typegpu/src/core/slot/accessor.ts @@ -163,12 +163,7 @@ abstract class AccessorBase< abstract readonly $: InferGPU; [$resolve](ctx: ResolutionCtx): ResolvedSnippet { - const snippet = this.#createSnippet(); - return snip( - ctx.resolve(snippet.value, snippet.dataType).value, - snippet.dataType as T, - snippet.origin, - ); + return ctx.resolveSnippet(this.#createSnippet()); } } diff --git a/packages/typegpu/src/core/valueProxyUtils.ts b/packages/typegpu/src/core/valueProxyUtils.ts index ad401ea46e..819367f2ea 100644 --- a/packages/typegpu/src/core/valueProxyUtils.ts +++ b/packages/typegpu/src/core/valueProxyUtils.ts @@ -32,7 +32,7 @@ export const valueProxyHandler: ProxyHandler = return new Proxy( { [$internal]: true, - [$resolve]: (ctx) => ctx.resolve(accessed.value, accessed.dataType), + [$resolve]: (ctx) => ctx.resolveSnippet(accessed), [$ownSnippet]: accessed, toString: () => `${String(target)}[${prop}]`, }, @@ -49,7 +49,7 @@ export const valueProxyHandler: ProxyHandler = return new Proxy( { [$internal]: true, - [$resolve]: (ctx) => ctx.resolve(accessed.value, accessed.dataType), + [$resolve]: (ctx) => ctx.resolveSnippet(accessed), [$ownSnippet]: accessed, toString: () => `${String(target)}.${prop}`, }, diff --git a/packages/typegpu/src/resolutionCtx.ts b/packages/typegpu/src/resolutionCtx.ts index 8686b05e8b..ac65354505 100644 --- a/packages/typegpu/src/resolutionCtx.ts +++ b/packages/typegpu/src/resolutionCtx.ts @@ -958,6 +958,18 @@ export class ResolutionCtxImpl implements ResolutionCtx { } resolve(item: unknown, schema?: BaseData | UnknownData): ResolvedSnippet { + if (typeof item === 'string') { + if (!schema || schema === UnknownData) { + throw new Error( + `Strings cannot be injected into WGSL directly (tried to inject '${item}'). Look for TypeGPU APIs that cover your use-case, or resort to using tgpu['~unstable'].rawCodeSnippet for raw code injection.`, + ); + } + // For example: + // () => { 'use gpu'; const color = d.vec3f(); return color; } + // snip('color', d.vec3f) ^^^^^ + return snip(item, schema, /* origin */ 'runtime'); + } + if ((isTgpuFn(item) || isShelllessImpl(item)) && !isProviding(item)) { // We skip providing functions to only perform the checks on slot-less functions. if ( @@ -1009,11 +1021,6 @@ export class ResolutionCtxImpl implements ResolutionCtx { return snip(item ? 'true' : 'false', bool, /* origin */ 'constant'); } - if (typeof item === 'string') { - // Already resolved - return snip(item, Void, /* origin */ 'runtime'); - } - if (schema && isWgslArray(schema)) { if (!Array.isArray(item)) { throw new WgslTypeError(`Cannot coerce ${item} into value of type '${schema}'`); @@ -1042,7 +1049,7 @@ export class ResolutionCtxImpl implements ResolutionCtx { throw new WgslTypeError( `Value ${safeStringify(item)} is not resolvable${ - schema ? ` to type ${safeStringify(schema)}` : '' + schema && schema !== UnknownData ? ` to type ${safeStringify(schema)}` : '' }`, ); } diff --git a/packages/typegpu/src/tgsl/generationHelpers.ts b/packages/typegpu/src/tgsl/generationHelpers.ts index cc645e63dd..de29bf0da7 100644 --- a/packages/typegpu/src/tgsl/generationHelpers.ts +++ b/packages/typegpu/src/tgsl/generationHelpers.ts @@ -167,7 +167,7 @@ export class ArrayExpression implements SelfResolvable { for (const elem of this.elements) { // We check if there are no references among the elements if (isAlias(elem) && !isNaturallyEphemeral(elem.dataType)) { - const snippetStr = ctx.resolve(elem.value, elem.dataType).value; + const snippetStr = ctx.resolveSnippet(elem).value; const snippetType = ctx.resolve(concretize(elem.dataType as BaseData)).value; throw new WgslTypeError( `'${snippetStr}' reference cannot be used in an array constructor.\n-----\nTry '${snippetType}(${snippetStr})' or 'arrayOf(${snippetType}, count)([...])' to copy the value instead.\n-----`, diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index 55d8ceb50e..3762e95da1 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -173,18 +173,17 @@ const unaryOpCodeToCodegen = { return snip(!argExpr.value, bool, 'constant'); } - const { value, dataType } = argExpr; - const argStr = ctx.resolve(value, dataType).value; + const argStr = ctx.resolveSnippet(argExpr).value; - if (wgsl.isBool(dataType)) { + if (wgsl.isBool(argExpr.dataType)) { return snip(`!${argStr}`, bool, 'runtime'); } - if (wgsl.isNumericSchema(dataType)) { + if (wgsl.isNumericSchema(argExpr.dataType)) { const resultStr = `!bool(${argStr})`; const nanGuardedStr = // abstractFloat will be resolved as comptime known value - dataType.type === 'f32' + argExpr.dataType.type === 'f32' ? `(((bitcast(${argStr}) & 0x7fffffff) > 0x7f800000) || ${resultStr})` - : dataType.type === 'f16' + : argExpr.dataType.type === 'f16' ? `(((bitcast(${argStr}) & 0x7fff) > 0x7c00) || ${resultStr})` : resultStr; @@ -363,7 +362,7 @@ ${this.ctx.pre}}`; // we can skip lhs const convRhs = tryConvertSnippet(this.ctx, rhsExpr, bool, false); - const rhsStr = this.ctx.resolve(convRhs.value, convRhs.dataType).value; + const rhsStr = this.ctx.resolveSnippet(convRhs).value; return snip(rhsStr, bool, 'runtime'); } @@ -452,8 +451,8 @@ ${this.ctx.pre}}`; ]; } - const lhsStr = this.ctx.resolve(convLhs.value, convLhs.dataType).value; - const rhsStr = this.ctx.resolve(convRhs.value, convRhs.dataType).value; + const lhsStr = this.ctx.resolveSnippet(convLhs).value; + const rhsStr = this.ctx.resolveSnippet(convRhs).value; const type = operatorToType(convLhs.dataType, op, convRhs.dataType); if (exprType === NODE.assignmentExpr) { @@ -498,7 +497,7 @@ ${this.ctx.pre}}`; return codegen(this.ctx, [argExpr]); } - const argStr = this.ctx.resolve(argExpr.value, argExpr.dataType).value; + const argStr = this.ctx.resolveSnippet(argExpr).value; const type = operatorToType(argExpr.dataType, op); // Result of an operation, so not a reference to anything @@ -583,7 +582,7 @@ ${this.ctx.pre}}`; // Either `Struct({ x: 1, y: 2 })`, or `Struct(otherStruct)`. // In both cases, we just let the argument resolve everything. return snip( - this.ctx.resolve(arg.value, callee.value).value, + this.ctx.resolveSnippet(arg).value, callee.value, // A new struct, so not a reference. /* origin */ 'runtime', @@ -623,7 +622,7 @@ ${this.ctx.pre}}`; // `d.arrayOf(...)(otherArr)`. // We just let the argument resolve everything. return snip( - this.ctx.resolve(arg.value, callee.value).value, + this.ctx.resolveSnippet(arg).value, callee.value, // A new array, so not a reference. /* origin */ 'runtime', @@ -1160,7 +1159,7 @@ Try 'return ${typeStr}(${str});' instead. this.ctx.defineVariable(rawId, snippet); const rhsSnippet = tryConvertSnippet(this.ctx, eq, definitionDataType, false); - const rhsStr = this.ctx.resolve(rhsSnippet.value, rhsSnippet.dataType).value; + const rhsStr = this.ctx.resolveSnippet(rhsSnippet).value; // Even though the user defined a 'let' (expecting it to be reassigned), the // reassignment might happen in a pruned branch, in which case we can generate @@ -1276,7 +1275,7 @@ Try 'return ${typeStr}(${str});' instead. this.ctx.defineVariable(rawId, snippet); const rhsSnippet = tryConvertSnippet(this.ctx, eq, definitionDataType, false); - const rhsStr = this.ctx.resolve(rhsSnippet.value, rhsSnippet.dataType).value; + const rhsStr = this.ctx.resolveSnippet(rhsSnippet).value; let emittedVarType: 'var' | 'let' | 'const' | `#VAR_${number}#`; if (varType === '') { @@ -1294,8 +1293,8 @@ Try 'return ${typeStr}(${str});' instead. protected _statement(statement: tinyest.Statement): string { if (typeof statement === 'string') { const id = this._identifier(statement); - const resolved = id.value && this.ctx.resolve(id.value).value; - // oxlint-disable-next-line typescript/no-base-to-string + const resolved = + id.value !== undefined && id.value !== null ? this.ctx.resolveSnippet(id).value : ''; return resolved ? `${this.ctx.pre}${resolved};` : ''; } @@ -1388,7 +1387,7 @@ ${this.ctx.pre}else ${alternate}`; try { const [_, condition, body] = statement; const condSnippet = this._typedExpression(condition, bool); - const conditionStr = this.ctx.resolve(condSnippet.value).value; + const conditionStr = this.ctx.resolveSnippet(condSnippet).value; const bodyStr = this._block(blockifySingleStatement(body)); return `${this.ctx.pre}while (${conditionStr}) ${bodyStr}`; @@ -1508,7 +1507,7 @@ ${this.ctx.pre}else ${alternate}`; // Post-update statement const [_, op, arg] = statement; const argExpr = this._expression(arg); - const argStr = this.ctx.resolve(argExpr.value, argExpr.dataType).value; + const argStr = this.ctx.resolveSnippet(argExpr).value; validateSnippetMutation(argExpr, statement); this.tryMarkModified(arg); @@ -1531,8 +1530,8 @@ ${this.ctx.pre}else ${alternate}`; } const expr = this._expression(statement); - const resolved = expr.value && this.ctx.resolve(expr.value).value; - // oxlint-disable-next-line typescript/no-base-to-string + const resolved = + expr.value !== undefined && expr.value !== null ? this.ctx.resolveSnippet(expr).value : ''; return resolved ? `${this.ctx.pre}${resolved};` : ''; } diff --git a/packages/typegpu/src/types.ts b/packages/typegpu/src/types.ts index be6c98c0f8..8a29443cdb 100644 --- a/packages/typegpu/src/types.ts +++ b/packages/typegpu/src/types.ts @@ -68,7 +68,7 @@ export type ResolvableObject = | AnyData | ((...args: never[]) => unknown); -export type Wgsl = Eventual; +export type Wgsl = Eventual; export type TgpuShaderStage = 'compute' | 'vertex' | 'fragment'; @@ -422,7 +422,6 @@ export function isWgsl(value: unknown): value is Wgsl { return ( typeof value === 'number' || typeof value === 'boolean' || - typeof value === 'string' || isSelfResolvable(value) || isWgslData(value) || isSlot(value) || diff --git a/packages/typegpu/tests/rawFn.test.ts b/packages/typegpu/tests/rawFn.test.ts index 13cf4bfcbd..ce773fbc8a 100644 --- a/packages/typegpu/tests/rawFn.test.ts +++ b/packages/typegpu/tests/rawFn.test.ts @@ -594,3 +594,44 @@ describe('tgpu.computeFn with raw string WGSL implementation', () => { `); }); }); + +describe('string injection', () => { + it('is forbidden via direct externals', () => { + const fn = tgpu.fn([])`() { + ext; + };`.$uses({ ext: 'call()' }); + + expect(() => tgpu.resolve([fn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn:fn: Strings cannot be injected into WGSL directly (tried to inject 'call()'). Look for TypeGPU APIs that cover your use-case, or resort to using tgpu['~unstable'].rawCodeSnippet for raw code injection.] + `); + }); + + it('is forbidden via indirect externals', () => { + const fn = tgpu.fn([])`() { + ext.p; + };`.$uses({ ext: { p: 'call()' } }); + + expect(() => tgpu.resolve([fn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn:fn: Strings cannot be injected into WGSL directly (tried to inject 'call()'). Look for TypeGPU APIs that cover your use-case, or resort to using tgpu['~unstable'].rawCodeSnippet for raw code injection.] + `); + }); + + it('is forbidden via slots', () => { + const slot = tgpu.slot('call()'); + + const fn = tgpu.fn([])`() { + slot; + };`.$uses({ slot }); + + expect(() => tgpu.resolve([fn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn:fn + - slot:slot: Strings cannot be injected into WGSL directly (tried to inject 'call()'). Look for TypeGPU APIs that cover your use-case, or resort to using tgpu['~unstable'].rawCodeSnippet for raw code injection.] + `); + }); +}); diff --git a/packages/typegpu/tests/slot.test.ts b/packages/typegpu/tests/slot.test.ts index 19ba0eb15f..b8dc9283ec 100644 --- a/packages/typegpu/tests/slot.test.ts +++ b/packages/typegpu/tests/slot.test.ts @@ -2,8 +2,8 @@ import { describe, expect } from 'vitest'; import { tgpu, d, std } from 'typegpu'; import { it } from 'typegpu-testing-utility'; -const RED = 'vec3f(1., 0., 0.)'; -const GREEN = 'vec3f(0., 1., 0.)'; +const RED = d.vec3f(1, 0, 0); +const GREEN = d.vec3f(0, 1, 0); describe('tgpu.slot', () => { it('resolves to default value if no value provided', () => { @@ -15,7 +15,7 @@ describe('tgpu.slot', () => { expect(tgpu.resolve([getColor])).toMatchInlineSnapshot(` "fn getColor() -> vec3f { - return vec3f(1., 0., 0.); + return vec3f(1, 0, 0); }" `); }); @@ -38,7 +38,7 @@ describe('tgpu.slot', () => { expect(tgpu.resolve([main])).toMatchInlineSnapshot(` "fn getColor() -> vec3f { - return vec3f(0., 1., 0.); + return vec3f(0, 1, 0); } fn main() { @@ -48,14 +48,14 @@ describe('tgpu.slot', () => { }); it('resolves to provided value', () => { - const colorSlot = tgpu.slot(); // no default + const colorSlot = tgpu.slot(); // no default const getColor = tgpu.fn([], d.vec3f)`() { return colorSlot; }`.$uses({ colorSlot }); // overriding to green - const getColorWithGreen = getColor.with(colorSlot, 'vec3f(0., 1., 0.)'); + const getColorWithGreen = getColor.with(colorSlot, d.vec3f(0, 1, 0)); const main = tgpu.fn([])`() { getColorWithGreen(); @@ -64,7 +64,7 @@ describe('tgpu.slot', () => { // should be green expect(tgpu.resolve([main])).toMatchInlineSnapshot(` "fn getColor() -> vec3f { - return vec3f(0., 1., 0.); + return vec3f(0, 1, 0); } fn main() { @@ -81,15 +81,15 @@ describe('tgpu.slot', () => { }`.$uses({ colorSlot }); expect(() => tgpu.resolve([getColor])).toThrowErrorMatchingInlineSnapshot(` - [Error: Resolution of the following tree failed: - - - - fn:getColor - - slot:color: Missing value for 'slot:color'] - `); + [Error: Resolution of the following tree failed: + - + - fn:getColor + - slot:color: Missing value for 'slot:color'] + `); }); it('prefers closer scope', () => { - const colorSlot = tgpu.slot(); // no default + const colorSlot = tgpu.slot(); // no default const getColor = tgpu.fn([], d.vec3f)`() -> vec3f { return colorSlot; @@ -111,11 +111,11 @@ describe('tgpu.slot', () => { expect(tgpu.resolve([main])).toMatchInlineSnapshot(` "fn getColor() -> vec3f { - return vec3f(1., 0., 0.); + return vec3f(1, 0, 0); } fn getColor_1() -> vec3f { - return vec3f(0., 1., 0.); + return vec3f(0, 1, 0); } fn wrapper() { @@ -131,7 +131,7 @@ describe('tgpu.slot', () => { it('reuses common nested functions', () => { const sizeSlot = tgpu.slot<1 | 100>(); - const colorSlot = tgpu.slot(); + const colorSlot = tgpu.slot(); const getSize = tgpu.fn([], d.f32)`() { return sizeSlot; }`.$uses({ sizeSlot }); @@ -168,7 +168,7 @@ describe('tgpu.slot', () => { expect(tgpu.resolve([main])).toMatchInlineSnapshot(` "fn getSize() -> f32 { return 1; } - fn getColor() -> vec3f { return vec3f(1., 0., 0.); } + fn getColor() -> vec3f { return vec3f(1, 0, 0); } fn sizeAndColor() { getSize(); @@ -190,7 +190,7 @@ describe('tgpu.slot', () => { sizeAndColor_1(); } - fn getColor_1() -> vec3f { return vec3f(0., 1., 0.); } + fn getColor_1() -> vec3f { return vec3f(0, 1, 0); } fn sizeAndColor_2() { getSize(); @@ -423,4 +423,25 @@ describe('tgpu.slot', () => { }" `); }); + + it('allows comptime strings', () => { + const precisionSlot = tgpu.slot('f16'); + + const getFloat = () => { + 'use gpu'; + if (precisionSlot.$ === 'f32') { + return d.f32(); + } else { + return d.f16(); + } + }; + + expect(tgpu.resolve([getFloat])).toMatchInlineSnapshot(` + "fn getFloat() -> f16 { + { + return 0h; + } + }" + `); + }); }); diff --git a/packages/typegpu/tests/tgslFn.test.ts b/packages/typegpu/tests/tgslFn.test.ts index 6f4d102ae8..2f71e3de9f 100644 --- a/packages/typegpu/tests/tgslFn.test.ts +++ b/packages/typegpu/tests/tgslFn.test.ts @@ -1229,3 +1229,71 @@ describe('tgsl fn when using plugin', () => { `); }); }); + +describe('string injection', () => { + it('is forbidden directly', () => { + const fn = () => { + 'use gpu'; + const x = 1; + ('call()'); + }; + + expect(() => tgpu.resolve([fn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:fn + - fn*:fn(): Strings cannot be injected into WGSL directly (tried to inject 'call()'). Look for TypeGPU APIs that cover your use-case, or resort to using tgpu['~unstable'].rawCodeSnippet for raw code injection.] + `); + }); + + it('is forbidden via direct externals', () => { + const call = 'call()'; + + const fn = () => { + 'use gpu'; + const x = 1; + call; + }; + + expect(() => tgpu.resolve([fn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:fn + - fn*:fn(): Strings cannot be injected into WGSL directly (tried to inject 'call()'). Look for TypeGPU APIs that cover your use-case, or resort to using tgpu['~unstable'].rawCodeSnippet for raw code injection.] + `); + }); + + it('is forbidden via indirect externals', () => { + const call = ['call()']; + + const fn = () => { + 'use gpu'; + const x = 1; + call[0]; + }; + + expect(() => tgpu.resolve([fn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:fn + - fn*:fn(): Strings cannot be injected into WGSL directly (tried to inject 'call()'). Look for TypeGPU APIs that cover your use-case, or resort to using tgpu['~unstable'].rawCodeSnippet for raw code injection.] + `); + }); + + it('is forbidden via slots', () => { + const slot = tgpu.slot('call()'); + + const fn = () => { + 'use gpu'; + const x = 1; + slot.$; + }; + + expect(() => tgpu.resolve([fn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:fn + - fn*:fn(): Strings cannot be injected into WGSL directly (tried to inject 'call()'). Look for TypeGPU APIs that cover your use-case, or resort to using tgpu['~unstable'].rawCodeSnippet for raw code injection.] + `); + }); +});