Skip to content
Open
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
35 changes: 35 additions & 0 deletions apps/typegpu-docs/src/examples/tests/log-test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,41 @@ export const controls = defineControls({
})
.dispatchThreads(16),
},
'Template literals': {
onButtonClick: () =>
root
.createGuardedComputePipeline(() => {
'use gpu';
console.log(`Log with template literal: ${4 + 4}`);
})
.dispatchThreads(),
},
'Template literals with helper function': {
onButtonClick: () => {
const helper = (n: number) => {
'use gpu';
return n;
};

return root
.createGuardedComputePipeline(() => {
'use gpu';
console.log(`Log with template literal: ${helper(4 + 16)}`);
})
.dispatchThreads();
},
},
'Template literals with complex content': {
onButtonClick: () =>
root
.createGuardedComputePipeline(() => {
'use gpu';
console.log(
`Log with template literal: \`first\` \n new line \n another line with slash: \\`,
);
})
.dispatchThreads(),
},
});

export function onCleanup() {
Expand Down
7 changes: 0 additions & 7 deletions packages/eslint-plugin/src/rules/noUnsupportedSyntax.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,13 +154,6 @@ export const noUnsupportedSyntax = createRule({
report(node, 'spread element');
},

TemplateLiteral(node) {
if (!directives.getEnclosingTypegpuFunction()) {
return;
}
report(node, 'template literal');
},

ThrowStatement(node) {
if (!directives.getEnclosingTypegpuFunction()) {
return;
Expand Down
18 changes: 0 additions & 18 deletions packages/eslint-plugin/tests/rules/noUnsupportedSyntax.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,24 +219,6 @@ describe('noUnsupportedSyntax', () => {
{ messageId: 'unexpected', data: { snippet: '...arr', syntax: 'spread element' } },
],
},
{
code: "const func = function() { 'use gpu'; const x = `hello`; }",
errors: [
{
messageId: 'unexpected',
data: { snippet: '`hello`', syntax: 'template literal' },
},
],
},
{
code: "const fn = () => { 'use gpu'; const x = tag`hello`; }",
errors: [
{
messageId: 'unexpected',
data: { snippet: '`hello`', syntax: 'template literal' },
},
],
},
{
code: "const fn = () => { 'use gpu'; throw new Error('x'); }",
errors: [
Expand Down
19 changes: 19 additions & 0 deletions packages/tinyest-for-wgsl/src/transpilers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ export const baseTranspilers = {
return [NODE.binaryExpr, left, node.operator as tinyest.BinaryOperator, right];
},

TemplateLiteral(ctx, node, transpile) {
const expressions = node.expressions.map((expr) => transpile(ctx, expr) as tinyest.Expression);
const quasis = node.quasis.map((quasi) => quasi.value.cooked ?? cookQuasis(quasi.value.raw));
return [NODE.templateLiteral, quasis, expressions];
},

LogicalExpression(ctx, node, transpile) {
const left = transpile(ctx, node.left) as tinyest.Expression;
const right = transpile(ctx, node.right) as tinyest.Expression;
Expand Down Expand Up @@ -429,3 +435,16 @@ function extractId(ident: tinyest.Identifier): string {
}
return ident[1];
}

function cookQuasis(raw: string): string {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fallback is unreachable for the supported syntax: cooked is only null for tagged templates with invalid escapes, and TaggedTemplateExpression is not handled by the transpiler. If it is ever reached, it is also wrong: \ is cooked before \\\\, so \ becomes \ + newline, and the final .replaceAll(/\\/g, '\\') is a no-op. Consider removing the fallback or implementing a proper single-pass cook.

return raw
.replaceAll(/\\n/g, '\n')
.replaceAll(/\\r/g, '\r')
.replaceAll(/\\t/g, '\t')
.replaceAll(/\\b/g, '\b')
.replaceAll(/\\f/g, '\f')
.replaceAll(/\\v/g, '\v')
.replaceAll(/\\\\/g, '\\')
.replaceAll(/\\`/g, '`')
.replaceAll(/\\/g, '\\');
}
8 changes: 8 additions & 0 deletions packages/tinyest/src/nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const NodeTypeCatalog = {
conditionalExpr: 105,
nullLiteral: 106,
booleanLiteral: 107,
templateLiteral: 108,
} as const;

export type NodeTypeCatalog = typeof NodeTypeCatalog;
Expand Down Expand Up @@ -261,6 +262,12 @@ export type Null = readonly [type: NodeTypeCatalog['nullLiteral']];
/** boolean can either be encoded as a node, or as a plain literal */
export type Bool = boolean | readonly [type: NodeTypeCatalog['booleanLiteral'], boolean];

export type TemplateLiteral = readonly [
type: NodeTypeCatalog['templateLiteral'],
quasis: string[],
expressions: Expression[],
];

export type Literal = Num | Str | Bool | Null;

export type Expression =
Expand All @@ -277,6 +284,7 @@ export type Expression =
| PreUpdate
| PostUpdate
| Call
| TemplateLiteral
| Literal;

export type AnyNode = Statement | Expression;
Expand Down
19 changes: 18 additions & 1 deletion packages/typegpu/src/shared/tseynit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,22 @@ function stringifyExpression(node: tinyest.Expression): string {
return 'null';
}

if (node[0] === NODE.templateLiteral) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The quasis are already cooked text, so a backtick or ${ in a quasi produces invalid JS in the stringified expression. I verified stringifyNode([108, ['a`b', 'c'], [[5, '4']]]) yields `a`b${4}c`. Use JSON.stringify or re-escape the quasis when building the output.

const quasis = node[1];
const expressions = node[2];

let result = '`';
quasis.forEach((quasi, i) => {
result += quasi;
if (i < expressions.length && expressions[i]) {
const expression = expressions[i];
result += '${' + stringifyExpression(expression) + '}';
}
});

return result + '`';
}

assertExhaustive(node);
}

Expand Down Expand Up @@ -228,7 +244,8 @@ function isExpression(node: tinyest.AnyNode): node is tinyest.Expression {
node[0] === NODE.postUpdate ||
node[0] === NODE.objectExpr ||
node[0] === NODE.conditionalExpr ||
node[0] === NODE.nullLiteral
node[0] === NODE.nullLiteral ||
node[0] === NODE.templateLiteral
) {
node satisfies tinyest.Expression;
return true;
Expand Down
25 changes: 25 additions & 0 deletions packages/typegpu/src/tgsl/generationHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,28 @@ export class ArrayExpression implements SelfResolvable, WithComptimeValue {
return ctx.gen.typeInstantiation(this.type, this.elements);
}
}

export class TemplateLiteralExpression implements WithComptimeValue {
readonly [$internal] = true;
readonly elements: Snippet[];

constructor(elements: Snippet[]) {
this.elements = elements;
}

toString(): string {
return 'TemplateLiteralExpression';
}

[$comptimeValueOf](): ComptimeValue {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$comptimeValueOf returns the array of elements, not the template's string value. For a comptime value, the template literal should evaluate to its concatenated string (e.g. a${1}b -> a1b), so asComptime on this object returns a misleading array. This is currently only used in the console.log path, but the wrong comptime value is a latent trap if the expression is reused elsewhere.

const values: unknown[] = [];
for (const elem of this.elements) {
const known = asComptime(elem);
if (!known) {
return { comptime: false };
}
values.push(known.value);
}
return { comptime: true, value: values };
}
}
33 changes: 32 additions & 1 deletion packages/typegpu/src/tgsl/wgslGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
coerceToSnippet,
concretize,
numericLiteralToSnippet,
TemplateLiteralExpression,
} from './generationHelpers.ts';
import { Swizzle } from './swizzle.ts';
import { accessIndex } from './accessIndex.ts';
Expand Down Expand Up @@ -523,7 +524,12 @@ export class WgslGenerator implements ShaderGenerator {
const callee = mathToStd.get(_callee) ?? _callee;

if (supportedLogOps().includes(callee)) {
return this.ctx.generateLog(callee, args);
return this.ctx.generateLog(
callee,
args.flatMap((arg) =>
arg.value instanceof TemplateLiteralExpression ? arg.value.elements : [arg],
),
Comment on lines +529 to +531

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only flattens one level. A nested template literal such as console.log(`outer ${`inner ${4}`} end`) leaves the inner TemplateLiteralExpression in the log args. I verified that logIdToMeta then contains a TemplateLiteralExpression object instead of the inner text, and the runtime would print that object. Consider making the flattening recursive.

);
}

if (callee === constant) {
Expand Down Expand Up @@ -1180,6 +1186,31 @@ The generated shader will evaluate them in declaration order.`,
return snip(expression[1], UnknownData, /* origin */ 'constant', false);
}

if (expression[0] === NODE.templateLiteral) {
const [_, quasis, expressions] = expression;
const expressionsSnippets = expressions.map((expr) => this._expression(expr));
const allConstant = expressionsSnippets.every((value) => value.origin === 'constant');

const quasisSnippets = quasis.map((quasi) =>
snip(quasi, UnknownData, /* origin */ 'constant', false),
);

const values = quasisSnippets.flatMap((quasi, i) => {
const expr = expressionsSnippets[i];
if (expr) {
return [quasi, expr];
}
return [quasi];
});

return snip(
new TemplateLiteralExpression(values),
UnknownData,
/* origin */ allConstant ? 'constant' : 'runtime',
values.some((v) => v.possibleSideEffects),
);
}

if (expression[0] === NODE.preUpdate) {
throw new Error('Cannot use pre-updates in TypeGPU functions.');
}
Expand Down
Loading
Loading