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
11 changes: 10 additions & 1 deletion packages/eslint-plugin/src/rules/noUnsupportedSyntax.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,16 @@ export const noUnsupportedSyntax = createRule({
if (!directives.getEnclosingTypegpuFunction()) {
return;
}
report(node, 'assignment pattern (default parameter)');
// Default values for plain identifier parameters are supported;
// defaults inside destructuring patterns are not.
const isDefaultedIdentifierParam =
(node.parent.type === 'ArrowFunctionExpression' ||
node.parent.type === 'FunctionExpression' ||
node.parent.type === 'FunctionDeclaration') &&
node.left.type === 'Identifier';
if (!isDefaultedIdentifierParam) {
report(node, 'assignment pattern (destructuring default)');
}
},

AwaitExpression(node) {
Expand Down
21 changes: 11 additions & 10 deletions packages/eslint-plugin/tests/rules/noUnsupportedSyntax.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ describe('noUnsupportedSyntax', () => {
"const cls = new (class { #priv = 1; fn = () => { 'use gpu'; const a = this.#priv; } } )()",
"const fn = () => { 'use gpu'; switch (x) { case 1: break; } }",
"const fn = () => { 'use gpu'; const obj = { [key]: 1 }; }",
"const fn = (arg = 1) => { 'use gpu'; }",
"function fn(arg = 1) { 'use gpu'; }",
"const fn = function(arg = 1) { 'use gpu'; }",
],
invalid: [
{
Expand Down Expand Up @@ -44,29 +47,27 @@ describe('noUnsupportedSyntax', () => {
],
},
{
code: "const fn = (arg = 1) => { 'use gpu'; }",
code: "const fn = ({ arg = 1 }) => { 'use gpu'; }",
errors: [
{
messageId: 'unexpected',
data: { snippet: 'arg = 1', syntax: 'assignment pattern (default parameter)' },
data: { snippet: 'arg = 1', syntax: 'assignment pattern (destructuring default)' },
},
],
},
{
code: "function fn(arg = 1) { 'use gpu'; }",
code: "const fn = () => { 'use gpu'; const { a = 1 } = foo; }",
errors: [
{
messageId: 'unexpected',
data: { snippet: 'arg = 1', syntax: 'assignment pattern (default parameter)' },
data: {
snippet: '{ a = 1 } = foo',
syntax: 'variable declaration using destructuring',
},
},
],
},
{
code: "const fn = function(arg = 1) { 'use gpu'; }",
errors: [
{
messageId: 'unexpected',
data: { snippet: 'arg = 1', syntax: 'assignment pattern (default parameter)' },
data: { snippet: 'a = 1', syntax: 'assignment pattern (destructuring default)' },
},
],
},
Expand Down
43 changes: 33 additions & 10 deletions packages/tinyest-for-wgsl/src/functionParts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,23 +72,28 @@ function validateFunction(functionNode: FunctionNode): void {

const unsupportedTypes = new Set(
functionNode.params.flatMap((param) =>
param.type === 'ObjectPattern' || param.type === 'Identifier' ? [] : [param.type],
param.type === 'ObjectPattern' ||
param.type === 'Identifier' ||
(param.type === 'AssignmentPattern' && param.left.type === 'Identifier')
? []
: [param.type],
),
);
if (unsupportedTypes.size > 0) {
throw new Error(`Unsupported function parameter type(s): ${[...unsupportedTypes].join(', ')}`);
}
}

type SupportedParam =
| babel.Identifier
| acorn.Identifier
| babel.ObjectPattern
| acorn.ObjectPattern
| babel.AssignmentPattern
| acorn.AssignmentPattern;

function parseParams(functionNode: FunctionNode): tinyest.FuncParameter[] {
return (
functionNode.params as (
| babel.Identifier
| acorn.Identifier
| babel.ObjectPattern
| acorn.ObjectPattern
)[]
).map((param) =>
return (functionNode.params as SupportedParam[]).map((param) =>
param.type === 'ObjectPattern'
? {
type: tinyest.FuncParameterType.destructuredObject,
Expand All @@ -102,13 +107,30 @@ function parseParams(functionNode: FunctionNode): tinyest.FuncParameter[] {
}
: {
type: tinyest.FuncParameterType.identifier,
name: param.name,
name:
param.type === 'AssignmentPattern'
? (param.left as babel.Identifier | acorn.Identifier).name
: param.name,
},
);
}

function parseParamDefaults(
functionNode: FunctionNode,
): (babel.Expression | acorn.Expression | undefined)[] {
return (functionNode.params as SupportedParam[]).map((param) =>
param.type === 'AssignmentPattern' ? param.right : undefined,
);
}

export function extractFunctionParts(rootNode: JsNode): {
params: tinyest.FuncParameter[];
/**
* Raw default value expressions (`(a, b = 2) => ...`), aligned by index
* with `params`. Transpiled and attached to the params by `transpileFn`,
* which owns the transpilation context.
*/
paramDefaults: (babel.Expression | acorn.Expression | undefined)[];
body: acorn.BlockStatement | acorn.Expression | babel.BlockStatement | babel.Expression;
} {
const functionNode = unwrapToFunction(rootNode);
Expand All @@ -117,6 +139,7 @@ export function extractFunctionParts(rootNode: JsNode): {

return {
params: parseParams(functionNode),
paramDefaults: parseParamDefaults(functionNode),
body: functionNode.body,
};
}
13 changes: 11 additions & 2 deletions packages/tinyest-for-wgsl/src/parsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,17 @@ function createParser(kind: 'acorn' | 'babel' | 'legacy') {

return {
transpileFn(rootNode: JsNode, options: TranspilationOptions): TranspilationResult {
const { params, body } = extractFunctionParts(rootNode);
const ctx = createContext(params, options);
const { params: rawParams, paramDefaults, body } = extractFunctionParts(rootNode);
const ctx = createContext(rawParams, options);

// Transpiled with the param names already declared, so identifiers used in
// a default value only count as externals when they're not other params.
const params = rawParams.map((param, i): tinyest.FuncParameter => {
const defaultNode = paramDefaults[i];
return defaultNode && param.type === tinyest.FuncParameterType.identifier
? { ...param, default: transpile(ctx, defaultNode) as tinyest.Expression }
: param;
});

const tinyestBody = transpile(ctx, body);

Expand Down
35 changes: 35 additions & 0 deletions packages/tinyest-for-wgsl/tests/parsers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,41 @@ describe('transpileBabelFn and transpileAcornFn', () => {
}),
);

it(
'supports default parameter values',
dualTest((p, transpileFn) => {
const { params, body, externalNames } = transpileFn(p('(a, b = 2, c = e) => a + b + c'));

expect(params).toStrictEqual([
{ type: 'i', name: 'a' },
{ type: 'i', name: 'b', default: [5, '2'] },
{ type: 'i', name: 'c', default: 'e' },
]);
expect(JSON.stringify(body)).toMatchInlineSnapshot(
`"[0,[[10,[1,[1,"a","+","b"],"+","c"]]]]"`,
);
// 'e' is external, even though it only appears in a default value.
expect(externalNames).toMatchInlineSnapshot(`
Map {
"e" => "e",
}
`);
}),
);

it(
'does not treat params referenced in default values as external names',
dualTest((p, transpileFn) => {
const { params, externalNames } = transpileFn(p('(a, b = a) => a + b'));

expect(params).toStrictEqual([
{ type: 'i', name: 'a' },
{ type: 'i', name: 'b', default: 'a' },
]);
expect(externalNames).toMatchInlineSnapshot(`Map {}`);
}),
);

it(
'respects local declarations when gathering external names',
dualTest((p, transpileFn) => {
Expand Down
5 changes: 5 additions & 0 deletions packages/tinyest/src/nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,11 @@ export type FuncParameter =
| {
type: typeof FuncParameterType.identifier;
name: string;
/**
* The parameter's default value expression (`(a, b = 2) => ...`),
* used when a call site omits the argument.
*/
default?: Expression | undefined;
}
| {
type: typeof FuncParameterType.destructuredObject;
Expand Down
7 changes: 6 additions & 1 deletion packages/typegpu-gl/src/glslGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1284,7 +1284,12 @@ The generated shader will omit it, so its runtime side effects will not occur.`)
}

try {
const body = this._block(options.body, /* allowInlining */ false).code;
const body = this._block(
options.body,
/* allowInlining */ false,
/* externalMap */ undefined,
options.paramDefaults,
).code;
const returnType = options.determineReturnType();

if (options.functionType !== 'normal') {
Expand Down
23 changes: 23 additions & 0 deletions packages/typegpu-gl/tests/glslGenerator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,29 @@ describe('GlslGenerator - variable declarations', () => {
expect(result.code).not.toContain('var ');
});

it('supports default parameter values', () => {
const shift = (v: d.v2f, amount: number = 2, by: d.v2f = v) => {
'use gpu';
return std.add(v, std.mul(by, d.f32(amount)));
};

const foo = () => {
'use gpu';
return shift(d.vec2f(1, 2));
};

expect(tgpu.resolve([foo], glOptions())).toMatchInlineSnapshot(`
"vec2 shift(vec2 v) {
vec2 by = v;
return (v + (by * 2.0));
}

vec2 foo() {
return shift(vec2(1, 2));
}"
`);
});

it('translates f32 variable declaration to float', () => {
const fragFn = tgpu.fragmentFn({
out: d.vec4f,
Expand Down
25 changes: 23 additions & 2 deletions packages/typegpu/src/resolutionCtx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
type Snippet,
withValue,
} from './data/snippet.ts';
import { type BaseData, isPtr, isWgslArray, isWgslStruct, Void } from './data/wgslTypes.ts';
import { type BaseData, isPtr, isVoid, isWgslArray, isWgslStruct, Void } from './data/wgslTypes.ts';
import { invariant, MissingSlotValueError, ResolutionError, WgslTypeError } from './errors.ts';
import { provideCtx, topLevelState } from './execMode.ts';
import { naturalsExcept } from './shared/generators.ts';
Expand All @@ -39,7 +39,7 @@ import { LogGeneratorImpl, LogGeneratorNullImpl } from './tgsl/consoleLog/logGen
import type { LogGenerator, LogResources, SupportedLogOp } from './tgsl/consoleLog/types.ts';
import { getBestConversion } from './tgsl/conversion.ts';
import { coerceToSnippet, concretize, numericLiteralToSnippet } from './tgsl/generationHelpers.ts';
import type { ShaderGenerator } from './tgsl/shaderGenerator.ts';
import type { ParamDefault, ShaderGenerator } from './tgsl/shaderGenerator.ts';
import { WgslGenerator } from './tgsl/wgslGenerator.ts';
import type {
BlockScopeLayer,
Expand Down Expand Up @@ -674,6 +674,10 @@ export class ResolutionCtxImpl implements ResolutionCtx {
}
} else {
for (const [i, argType] of options.argTypes.entries()) {
if (isVoid(argType)) {
// Omitted at the call site, the parameter's default value is used instead
continue;
}
const astParam = options.params[i];
// We know if arguments are passed by reference or by value, because we
// enforce that based on the whether the argument is a pointer or not.
Expand Down Expand Up @@ -734,6 +738,22 @@ export class ResolutionCtxImpl implements ResolutionCtx {
}
}

// Parameters omitted at the call site fall back to their default values (`(a, b = 2) => ...`)
const paramDefaults: ParamDefault[] = [];
if (!options.entryInput) {
for (const [i, param] of options.params.entries()) {
const argType = options.argTypes[i];
const omitted = argType === undefined || isVoid(argType);
if (
omitted &&
param.type === FuncParameterType.identifier &&
param.default !== undefined
) {
paramDefaults.push({ name: param.name, expression: param.default });
}
}
}

let returnType: BaseData | undefined;

const code = this.gen.functionDefinition({
Expand All @@ -742,6 +762,7 @@ export class ResolutionCtxImpl implements ResolutionCtx {
workgroupSize: options.workgroupSize,
args,
body: options.body,
paramDefaults,
determineReturnType: () => {
if (returnType) {
// Already determined
Expand Down
12 changes: 11 additions & 1 deletion packages/typegpu/src/tgsl/shaderGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Block } from 'tinyest';
import type { Block, Expression } from 'tinyest';
import type { BaseData } from '../data/wgslTypes.ts';
import type { ResolvedSnippet, Snippet } from '../data/snippet.ts';
import type { VariableScope } from '../core/variable/tgpuVariable.ts';
Expand All @@ -15,10 +15,20 @@ export interface FunctionDefinitionOptions {
readonly workgroupSize?: readonly number[] | undefined;
readonly args: readonly FunctionArgument[];
readonly body: Block;
/**
* Default values of parameters that were omitted at the call site
* (`(a, b = 2) => ...` called as `fn(a)` or `fn(a, undefined)`), in parameter order.
*/
readonly paramDefaults?: readonly ParamDefault[] | undefined;

determineReturnType(): BaseData;
}

export interface ParamDefault {
readonly name: string;
readonly expression: Expression;
}

export interface ConstantDefinitionOptions {
readonly id: string;
readonly dataType: BaseData;
Expand Down
Loading
Loading