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
2 changes: 1 addition & 1 deletion packages/typegpu/src/core/function/extractArgs.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { blankSpaces, lineBreaks } from '../whitespaces.ts';
import { blankSpaces, lineBreaks } from '../../rawShaderCodeUtils.ts';

interface FunctionArgsInfo {
args: ArgInfo[];
Expand Down
174 changes: 110 additions & 64 deletions packages/typegpu/src/core/function/fnCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { type ResolvedSnippet, snip } from '../../data/snippet.ts';
import { type BaseData, isWgslData, isWgslStruct, Void } from '../../data/wgslTypes.ts';
import { validateIdentifier } from '../../nameUtils.ts';
import { getFunctionMetadata, getName } from '../../shared/meta.ts';
import { $getNameForward } from '../../shared/symbols.ts';
import { extractIdentifierLikeTokens, renameIdentifiers } from '../../rawShaderCodeUtils.ts';
import { $getNameForward, $internal } from '../../shared/symbols.ts';
import type { ResolutionCtx, ShaderStage } from '../../types.ts';
import {
type ExternalMap,
Expand All @@ -14,6 +15,7 @@ import {
import { ResolvableString } from '../resolve/resolvableString.ts';
import { extractArgs } from './extractArgs.ts';
import type { Implementation, SeparatedEntryArgs } from './fnTypes.ts';
import { parentFunctionNameSlot } from '../slot/internalSlots.ts';

export type FnExternals = {
/**
Expand Down Expand Up @@ -119,7 +121,7 @@ export function createFnCore(
`Invalid argument name "${arg.schemaKey}"${result.error ? `: ${result.error}` : ''}`,
);
}
if (ctx.isIdentifierBanned(arg.schemaKey)) {
if (ctx.gen.isBannedToken(arg.schemaKey)) {
throw new Error(
`Invalid argument name "${arg.schemaKey}", the identifier is a reserved keyword.`,
);
Expand All @@ -136,75 +138,119 @@ export function createFnCore(
});
}

const replacedImpl = replaceExternalsInWgsl(
ctx,
mergeFunctionExternals(externals),
implementation,
);
const externalMap = mergeFunctionExternals(externals);
try {
const scope = ctx[$internal].itemStateStack.pushFunctionScope(
functionType,
{},
returnType,
externalMap,
);
// Pushing a block scope as well, so that any identifiers declared at this point will be scoped to the function body.
ctx.pushBlockScope();

const externalKeys = Object.keys(externalMap);
const identifiers = [
...new Set(
extractIdentifierLikeTokens(implementation).filter((ident) => {
return (
!ctx.gen.isBannedToken(ident) &&
!ctx.gen.isBuiltinGlobal(ident) &&
!externalKeys.some((key) => key === ident || key.startsWith(`${ident}.`))
);
}),
),
];

let header = '';
let body = '';
const clashingIdentifiers = identifiers.filter((ident) =>
ctx.isIdentifierTaken(ident, 'block'),
);

if (functionType !== 'normal' && entryInput) {
const { dataSchema, positionalArgs } = entryInput;
const parts: string[] = [];
if (dataSchema && isArgUsedInBody('in', replacedImpl)) {
parts.push(`in: ${ctx.resolve(dataSchema).value}`);
}
for (const a of positionalArgs) {
const argName = a.schemaKey;
if (isArgUsedInBody(argName, replacedImpl)) {
parts.push(`${getAttributesString(a.type)}${argName}: ${ctx.resolve(a.type).value}`);
}
const uniqueIdentifiers = identifiers.filter(
(ident) => !ctx.isIdentifierTaken(ident, 'block'),
);

for (const ident of clashingIdentifiers) {
const renamed = ctx.makeUniqueIdentifier(ident, 'block');
scope.localRenames.set(ident, renamed);
}
const input = `(${parts.join(', ')})`;

const attributes = isWgslData(returnType) ? getAttributesString(returnType) : '';
const output =
returnType !== Void
? isWgslStruct(returnType)
? ` -> ${ctx.resolve(returnType).value} `
: ` -> ${attributes !== '' ? attributes : '@location(0)'} ${
ctx.resolve(returnType).value
} `
: ' ';

header = `${input}${output}`;
body = replacedImpl;
} else {
const providedArgs = extractArgs(replacedImpl);

if (providedArgs.args.length !== argTypes.length) {
throw new Error(
`WGSL implementation has ${providedArgs.args.length} arguments, while the shell has ${argTypes.length} arguments.`,
);
const renamedImpl = renameIdentifiers(implementation, scope.localRenames);
for (const ident of uniqueIdentifiers) {
ctx.reserveIdentifier(ident, 'block');
}
const replacedImpl = ctx.withSlots([[parentFunctionNameSlot, id]], () =>
replaceExternalsInWgsl(ctx, externalMap, renamedImpl),
);

let header = '';
let body = '';

if (functionType !== 'normal' && entryInput) {
const { dataSchema, positionalArgs } = entryInput;
const parts: string[] = [];
if (dataSchema && isArgUsedInBody('in', replacedImpl)) {
parts.push(`in: ${ctx.resolve(dataSchema).value}`);
}
for (const a of positionalArgs) {
const argName = a.schemaKey;
if (isArgUsedInBody(argName, replacedImpl)) {
parts.push(
`${getAttributesString(a.type)}${argName}: ${ctx.resolve(a.type).value}`,
);
}
}
const input = `(${parts.join(', ')})`;

const attributes = isWgslData(returnType) ? getAttributesString(returnType) : '';
const output =
returnType !== Void
? isWgslStruct(returnType)
? ` -> ${ctx.resolve(returnType).value} `
: ` -> ${attributes !== '' ? attributes : '@location(0)'} ${
ctx.resolve(returnType).value
} `
: ' ';

header = `${input}${output}`;
body = replacedImpl;
} else {
const providedArgs = extractArgs(replacedImpl);

if (providedArgs.args.length !== argTypes.length) {
throw new Error(
`WGSL implementation has ${providedArgs.args.length} arguments, while the shell has ${argTypes.length} arguments.`,
);
}

const input = providedArgs.args
.map(
(argInfo, i) =>
`${argInfo.identifier}: ${checkAndReturnType(
ctx,
`parameter ${argInfo.identifier}`,
argInfo.type,
argTypes[i],
)}`,
)
.join(', ');

const output =
returnType === Void
? ' '
: ` -> ${checkAndReturnType(ctx, 'return type', providedArgs.ret?.type, returnType)} `;

header = `(${input})${output}`;

body = replacedImpl.slice(providedArgs.range.end);
}
const input = providedArgs.args
.map(
(argInfo, i) =>
`${argInfo.identifier}: ${checkAndReturnType(
ctx,
`parameter ${argInfo.identifier}`,
argInfo.type,
argTypes[i],
)}`,
)
.join(', ');

const output =
returnType === Void
? ' '
: ` -> ${checkAndReturnType(ctx, 'return type', providedArgs.ret?.type, returnType)} `;

header = `(${input})${output}`;

body = replacedImpl.slice(providedArgs.range.end);
}

ctx.addDeclaration(`${attributes}fn ${id}${header}${body}`, id);
ctx.addDeclaration(`${attributes}fn ${id}${header}${body}`, id);

return snip(id, returnType, /* origin */ 'runtime');
return snip(id, returnType, /* origin */ 'runtime');
} finally {
ctx[$internal].itemStateStack.pop('blockScope');
ctx[$internal].itemStateStack.pop('functionScope');
}
}

// get data generated by the plugin
Expand Down
18 changes: 13 additions & 5 deletions packages/typegpu/src/core/rawCodeSnippet/tgpuRawCodeSnippet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { makeResolvable } from '../../tgsl/makeResolvable.ts';
import type { InferGPU } from '../../shared/repr.ts';
import { $gpuValueOf, $internal } from '../../shared/symbols.ts';
import { type ExternalMap, replaceExternalsInWgsl } from '../resolve/externals.ts';
import { renameIdentifiers } from '../../rawShaderCodeUtils.ts';
import { parentFunctionNameSlot } from '../slot/internalSlots.ts';

// ----------
// Public API
Expand Down Expand Up @@ -106,11 +108,17 @@ class TgpuRawCodeSnippetImpl<TDataType extends BaseData> implements TgpuRawCodeS
return `raw(${String(this.dataType)}): "${this.#expression}"`;
},
resolve(ctx) {
const replacedExpression = replaceExternalsInWgsl(
ctx,
this.#externals ?? {},
this.#expression,
);
// The code of the snippet can actually change depending on which function it's referenced in.
// For example an identifier can refer to a locally defined variable or argument, or a global,
// and therefore can be renamed or left as is.
void parentFunctionNameSlot.$;

let expression = this.#expression;
if (ctx.topFunctionScope) {
expression = renameIdentifiers(expression, ctx.topFunctionScope.localRenames);
}
Comment on lines +116 to +119

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.

"should rename references to local variables" only holds when another raw-WGSL function in the same resolution already produced the exact {ARG: newName} entry. On its own, a JS-implemented function whose argument clashes with a global and that uses a snippet referencing that argument emits the wrong references.

localRenames is populated only in fnCore.ts's raw-string branch (line ~170). The tinyest AST path (resolveFunction in resolutionCtx.ts) renames function arguments via makeUniqueIdentifier but never records them into scope.localRenames, so a snippet resolved inside that JS function reads an empty rename map.

Reproduced (this is exactly the intended bullet-4 use case minus the sibling raw fn): with const constant = tgpu.const(d.f32,123).$name('a') and jsFn = tgpu.fn([d.f32], d.f32)((a) => { 'use gpu'; return raw('a * 2 + constant').$uses({constant}) }) in a tree with only jsFn, resolution yields:

fn jsFn(a_1: f32) -> f32 {
  return a * 2 + a;   // first `a` silently binds to the global constant, not the arg (renamed to `a_1`)
}

Adding a sibling raw-WGSL fn that consumes the same snippet concurrently flips jsFn to the intended return a_1 * 2 + a; — so the emitted shader is order-dependent and, in the standalone case, wrong by silently reading a same-named global instead of the function's argument. The PR's added test only exercises the favorable ordering.


const replacedExpression = replaceExternalsInWgsl(ctx, this.#externals ?? {}, expression);
Comment on lines 110 to +121

return snip(replacedExpression, this.dataType, this.origin, this.possibleSideEffects);
},
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
@@ -1,6 +1,7 @@
import { isLooseData } from '../../data/dataTypes.ts';
import { isWgslStruct } from '../../data/wgslTypes.ts';
import { getName, hasTinyestMetadata, isNamable, setName } from '../../shared/meta.ts';
import { anyIdent } from '../../rawShaderCodeUtils.ts';
import { logger } from '../../tgpuLogger.ts';
import { isWgsl, type ResolutionCtx } from '../../types.ts';
import type { FnExternals } from '../function/fnCore.ts';
Expand Down Expand Up @@ -67,7 +68,6 @@ export function addReturnTypeToExternals(
}
}

export const anyIdent = /([$_\p{XID_Start}][$\p{XID_Continue}]*)/u; // WGSL ident, modified to include $
const anyPropChain = new RegExp(`(${anyIdent.source})(\\.${anyIdent.source})*`, 'ug');
export const boundedPropChain = new RegExp(
`(?<![\\p{XID_Continue}\\$.])${anyPropChain.source}(?![\\p{XID_Continue}\\$])`,
Expand Down
3 changes: 1 addition & 2 deletions packages/typegpu/src/core/resolve/namespace.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { ResolvedSnippet } from '../../data/snippet.ts';
import { bannedTokens, builtins } from '../../nameUtils.ts';
import { $internal } from '../../shared/symbols.ts';
import { ShelllessRepository } from '../../tgsl/shellless.ts';
import type { TgpuLazy, TgpuSlot } from '../slot/slotTypes.ts';
Expand Down Expand Up @@ -36,7 +35,7 @@ class NamespaceImpl implements Namespace {
constructor(strategy: 'random' | 'strict') {
this[$internal] = {
strategy,
takenGlobalIdentifiers: new Set([...bannedTokens, ...builtins]),
takenGlobalIdentifiers: new Set(),
shelllessRepo: new ShelllessRepository(),
memoizedResolves: new WeakMap(),
memoizedLazy: new WeakMap(),
Expand Down
8 changes: 8 additions & 0 deletions packages/typegpu/src/core/resolve/resolveData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import type {
WgslArray,
WgslStruct,
} from '../../data/wgslTypes.ts';
import { validateProp } from '../../nameUtils.ts';
import { getName } from '../../shared/meta.ts';
import { $internal } from '../../shared/symbols.ts';
import { assertExhaustive } from '../../shared/utilityTypes.ts';
Expand Down Expand Up @@ -125,6 +126,13 @@ function resolveStructProperty(ctx: ResolutionCtx, [key, property]: [string, Bas
* @returns The resolved struct name.
*/
function resolveStruct(ctx: ResolutionCtx, struct: WgslStruct) {
Object.keys(struct.propTypes).forEach((key) => {
const result = validateProp(ctx, key);
if (!result.success) {
throw new Error(`Invalid property key '${key}'${result.error ? `: ${result.error}` : ''}`);
}
});

if (struct[$internal].isAbstruct) {
throw new Error('Cannot resolve abstract struct types to WGSL.');
}
Expand Down
55 changes: 50 additions & 5 deletions packages/typegpu/src/core/resolve/tgpuResolve.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { type ResolvedSnippet, snip } from '../../data/snippet.ts';
import { Void } from '../../data/wgslTypes.ts';
import { getName } from '../../internal.ts';
import { extractIdentifierLikeTokens, renameIdentifiers } from '../../rawShaderCodeUtils.ts';
import { type ResolutionResult, resolve as resolveImpl } from '../../resolutionCtx.ts';
import { $internal, $resolve, $soul } from '../../shared/symbols.ts';
import { isBindGroupLayout } from '../../tgpuBindGroupLayout.ts';
Expand Down Expand Up @@ -211,11 +212,55 @@ function resolveFromTemplate(options: TgpuExtendedResolveOptions): ResolutionRes
const resolutionObj: SelfResolvable = {
[$internal]: true,
[$resolve](ctx): ResolvedSnippet {
return snip(
replaceExternalsInWgsl(ctx, externals, template ?? ''),
Void,
/* origin */ 'runtime',
);
try {
// It's technically not a function we're resolving, but we need a place to store local renames, so we treat the whole
const scope = ctx[$internal].itemStateStack.pushFunctionScope(
'normal',
{},
Void,
externals,
);
// Pushing a block scope as well, so that any identifiers declared at this point will be scoped to the function body.
ctx.pushBlockScope();

const identifiers = [
...new Set(
extractIdentifierLikeTokens(template ?? '').filter((ident) => {
return (
!ctx.gen.isBannedToken(ident) &&
!ctx.gen.isBuiltinGlobal(ident) &&
externals[ident] === undefined
);
}),
),
];

const clashingIdentifiers = identifiers.filter((ident) =>
ctx.isIdentifierTaken(ident, 'global'),
);

const uniqueIdentifiers = identifiers.filter(
(ident) => !ctx.isIdentifierTaken(ident, 'global'),
);

for (const ident of clashingIdentifiers) {
const renamed = ctx.makeUniqueIdentifier(ident, 'global');
scope.localRenames.set(ident, renamed);
}
const renamedImpl = renameIdentifiers(template ?? '', scope.localRenames);
for (const ident of uniqueIdentifiers) {
ctx.reserveIdentifier(ident, 'global');
}
Comment on lines +247 to +253

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.

Renaming every clashing identifier at permanent global scope mangles WGSL grammar tokens inside templates on re-resolution — the committed namespace.test.ts snapshot (var<private> foo: Boid resolved twice) currently fails because the address-space token itself is rewritten to var<private_1> foo_1: Boid, which is invalid WGSL. private, function, workgroup, read/write/read_write, @location/@builtin(position)/@interpolate(flat) args, and vec3u/ptr/texture_2d/sampler_comparison are in neither bannedTokens nor builtinGlobals, so on any second resolve of the same template in a shared namespace they all get rewritten.


return snip(
replaceExternalsInWgsl(ctx, externals, renamedImpl),
Void,
/* origin */ 'runtime',
);
} finally {
ctx[$internal].itemStateStack.pop('blockScope');
ctx[$internal].itemStateStack.pop('functionScope');
}
},
toString: () => '<root>',
};
Expand Down
1 change: 1 addition & 0 deletions packages/typegpu/src/core/slot/internalSlots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ import type { ShaderStage } from '../../types.ts';
import { slot } from './slot.ts';

export const shaderStageSlot = slot<ShaderStage | null>(null);
export const parentFunctionNameSlot = slot<string | null>(null);
17 changes: 0 additions & 17 deletions packages/typegpu/src/core/whitespaces.ts

This file was deleted.

Loading
Loading