Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions packages/typegpu/src/core/resolve/externals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ export function replaceExternalsInWgsl(
// continue anyway, we still might need to resolve the external
}

if (typeof external === 'string') {
return acc.replaceAll(externalRegex, external);
}

if (isResolvable(external)) {
if (isNamable(external) && getName(external) === undefined) {
setName(external, externalName.split('.').at(-1) as string);
Expand Down
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
17 changes: 11 additions & 6 deletions packages/typegpu/src/resolutionCtx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -958,6 +958,16 @@ 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.`,
);
}
// Already resolved
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 +1019,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 +1047,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
1 change: 0 additions & 1 deletion packages/typegpu/src/types.ts
Original file line number Diff line number Diff line change
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
43 changes: 30 additions & 13 deletions packages/typegpu/tests/slot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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);
}"
`);
});
Expand All @@ -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() {
Expand All @@ -48,14 +48,14 @@ describe('tgpu.slot', () => {
});

it('resolves to provided value', () => {
const colorSlot = tgpu.slot<string>(); // no default
const colorSlot = tgpu.slot<d.v3f>(); // 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();
Expand All @@ -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() {
Expand All @@ -89,7 +89,7 @@ describe('tgpu.slot', () => {
});

it('prefers closer scope', () => {
const colorSlot = tgpu.slot<string>(); // no default
const colorSlot = tgpu.slot<d.v3f>(); // no default

const getColor = tgpu.fn([], d.vec3f)`() -> vec3f {
return colorSlot;
Expand All @@ -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() {
Expand All @@ -131,7 +131,7 @@ describe('tgpu.slot', () => {

it('reuses common nested functions', () => {
const sizeSlot = tgpu.slot<1 | 100>();
const colorSlot = tgpu.slot<typeof RED | typeof GREEN>();
const colorSlot = tgpu.slot<d.v3f>();

const getSize = tgpu.fn([], d.f32)`() { return sizeSlot; }`.$uses({ sizeSlot });

Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand Down Expand Up @@ -423,4 +423,21 @@ describe('tgpu.slot', () => {
}"
`);
});

it(`doesn't allow arbitrary shader code to be injected`, () => {
const bar = tgpu.slot('// a comment');

function foo() {
'use gpu';
bar.$;
return 2;
}

expect(() => tgpu.resolve([foo])).toThrowErrorMatchingInlineSnapshot(`
[Error: Resolution of the following tree failed:
- <root>
- fn*:foo
- fn*:foo(): Strings cannot be injected into WGSL directly (tried to inject '// a comment'). Look for TypeGPU APIs that cover your use-case, or resort to using tgpu['~unstable'].rawCodeSnippet for raw code injection.]
`);
});
});
Loading
Loading