Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
669e47f
Remove setName from mergeExternals
aleksanderkatan Jun 16, 2026
692ac90
Name unnamed externals in propAccess and getById
aleksanderkatan Jun 16, 2026
a944c60
Limit tgpuDeclare to one external
aleksanderkatan Jun 16, 2026
754eb92
Remove unnecessary merge from tgpuResolve
aleksanderkatan Jun 16, 2026
a217940
Limit rawCodeSnippet to one $uses
aleksanderkatan Jun 16, 2026
1f8bccb
Add FnExternals, and throw if $uses is called multiple times
aleksanderkatan Jun 16, 2026
b0ec447
Prepare string-implemented functions for external swap
aleksanderkatan Jun 16, 2026
236075c
Finish replacing old externals with new externals && cleanup
aleksanderkatan Jun 19, 2026
7c7faad
More cleanup
aleksanderkatan Jun 19, 2026
a03b83f
Detect $uses on function with plugin metadata earlier
aleksanderkatan Jun 19, 2026
1a16a8b
Add FnExternals docs
aleksanderkatan Jun 19, 2026
9f33691
Add external name collision tests
aleksanderkatan Jun 19, 2026
052f36b
Add more tests & fix fragment out external
aleksanderkatan Jun 19, 2026
5ec7052
More tests && allow redundant external
aleksanderkatan Jun 19, 2026
54b8b27
Update docs
aleksanderkatan Jun 19, 2026
d7d48ae
Add compute tests
aleksanderkatan Jun 22, 2026
fc8b147
Move external tests to external.test.ts
aleksanderkatan Jun 22, 2026
cfb074a
Fix old external tests by tweaking the api
aleksanderkatan Jun 22, 2026
5b26ee6
nr fix
aleksanderkatan Jun 22, 2026
312f78d
Merge branch 'main' into fix/forbid-multiple-uses
aleksanderkatan Jun 22, 2026
29b0354
self-review fixes
aleksanderkatan Jun 22, 2026
f9a4385
Fix multiple .$uses error
aleksanderkatan Jun 22, 2026
741af47
Merge branch 'main' into fix/forbid-multiple-uses
aleksanderkatan Jun 23, 2026
a7abca9
Initial rewrite
aleksanderkatan Jun 24, 2026
b0b5472
Add an end of chain test
aleksanderkatan Jun 24, 2026
ca3e9e1
Simplify end of chain tracking
aleksanderkatan Jun 24, 2026
48727bb
Update snapshots
aleksanderkatan Jun 24, 2026
e346c8e
Update declare tests
aleksanderkatan Jun 24, 2026
4974305
Remove warn test
aleksanderkatan Jun 24, 2026
65fcedf
Simplify regex
aleksanderkatan Jun 24, 2026
38a0643
Update packages/typegpu/src/core/resolve/externals.ts
aleksanderkatan Jun 24, 2026
960994b
Merge remote-tracking branch 'origin/main' into fix/external-traversa…
aleksanderkatan Jun 26, 2026
f969d37
Simplify naming
aleksanderkatan Jun 26, 2026
73dc847
Remove cache
aleksanderkatan Jul 1, 2026
4573395
Merge remote-tracking branch 'origin/main' into fix/external-traversa…
aleksanderkatan Jul 1, 2026
a5d2b0a
Copilot review fixes
aleksanderkatan Jul 2, 2026
a9da9a7
Merge remote-tracking branch 'origin/main' into fix/external-traversa…
aleksanderkatan Jul 2, 2026
1acd191
Merge branch 'main' into fix/external-traversal-for-raw-wgsl-implemen…
aleksanderkatan Jul 2, 2026
cb6c68f
Merge remote-tracking branch 'origin/main' into fix/external-traversa…
aleksanderkatan Jul 6, 2026
10e93da
Add some regex tests
aleksanderkatan Jul 6, 2026
c6bc427
Add tests
aleksanderkatan Jul 9, 2026
7ee8f2d
Throw in raw-wgsl case
aleksanderkatan Jul 9, 2026
f09f85c
fix: Don't treat strings as resolvable
iwoplaza Jul 9, 2026
049e477
Fix the resolvable check
aleksanderkatan Jul 9, 2026
737c910
Update tgsl tests
aleksanderkatan Jul 9, 2026
44bab90
Move rawfn tests
aleksanderkatan Jul 9, 2026
a63b41f
Add ResolvableString and forbid strings in raw fn
aleksanderkatan Jul 9, 2026
e4c4757
lint & types
aleksanderkatan Jul 9, 2026
c0d3132
lint
aleksanderkatan Jul 9, 2026
34ae441
Fix import
aleksanderkatan Jul 9, 2026
3d934c9
Review fixes
aleksanderkatan Jul 10, 2026
83271c4
Merge remote-tracking branch 'origin/main' into fix/restrict-using-sl…
aleksanderkatan Jul 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/typegpu/src/core/function/fnCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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),
]),
),
});
}
Expand Down
2 changes: 1 addition & 1 deletion packages/typegpu/src/core/resolve/externals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`)
Expand Down
16 changes: 16 additions & 0 deletions packages/typegpu/src/core/resolve/resolvableString.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
4 changes: 1 addition & 3 deletions packages/typegpu/src/core/resolve/stitch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '';
Expand Down
7 changes: 1 addition & 6 deletions packages/typegpu/src/core/slot/accessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,12 +151,7 @@ abstract class AccessorBase<
}

[$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());
}
}

Expand Down
4 changes: 2 additions & 2 deletions packages/typegpu/src/core/valueProxyUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const valueProxyHandler: ProxyHandler<SelfResolvable & WithOwnSnippet> =
return new Proxy(
{
[$internal]: true,
[$resolve]: (ctx) => ctx.resolve(accessed.value, accessed.dataType),
[$resolve]: (ctx) => ctx.resolveSnippet(accessed),
[$ownSnippet]: accessed,
toString: () => `${String(target)}[${prop}]`,
},
Expand All @@ -49,7 +49,7 @@ export const valueProxyHandler: ProxyHandler<SelfResolvable & WithOwnSnippet> =
return new Proxy(
{
[$internal]: true,
[$resolve]: (ctx) => ctx.resolve(accessed.value, accessed.dataType),
[$resolve]: (ctx) => ctx.resolveSnippet(accessed),
[$ownSnippet]: accessed,
toString: () => `${String(target)}.${prop}`,
},
Expand Down
19 changes: 13 additions & 6 deletions packages/typegpu/src/resolutionCtx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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}'`);
Expand Down Expand Up @@ -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)}` : ''
}`,
);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/typegpu/src/tgsl/generationHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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-----`,
Expand Down
39 changes: 19 additions & 20 deletions packages/typegpu/src/tgsl/wgslGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<u32>(${argStr}) & 0x7fffffff) > 0x7f800000) || ${resultStr})`
: dataType.type === 'f16'
: argExpr.dataType.type === 'f16'
? `(((bitcast<u32>(${argStr}) & 0x7fff) > 0x7c00) || ${resultStr})`
: resultStr;

Expand Down Expand Up @@ -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');
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 === '<deferred>') {
Expand All @@ -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};` : '';
}

Expand Down Expand Up @@ -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}`;
Expand Down Expand Up @@ -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);
Expand All @@ -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};` : '';
}

Expand Down
3 changes: 1 addition & 2 deletions packages/typegpu/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export type ResolvableObject =
| AnyData
| ((...args: never[]) => unknown);

export type Wgsl = Eventual<string | number | boolean | ResolvableObject>;
export type Wgsl = Eventual<number | boolean | ResolvableObject>;

export type TgpuShaderStage = 'compute' | 'vertex' | 'fragment';

Expand Down Expand Up @@ -426,7 +426,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) ||
Expand Down
41 changes: 41 additions & 0 deletions packages/typegpu/tests/rawFn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
- <root>
- 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:
- <root>
- 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:
- <root>
- 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.]
`);
});
});
Loading
Loading