Skip to content
Draft
Show file tree
Hide file tree
Changes from 8 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
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 @@ -140,13 +140,6 @@ export const noUnsupportedSyntax = createRule({
report(node, `'new' expression`);
},

PrivateIdentifier(node) {
if (!directives.getEnclosingTypegpuFunction()) {
return;
}
report(node, 'private identifier');
},

Property(node) {
if (!directives.getEnclosingTypegpuFunction()) {
return;
Expand Down
10 changes: 1 addition & 9 deletions packages/eslint-plugin/tests/rules/noUnsupportedSyntax.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ describe('noUnsupportedSyntax', () => {
"const fn = () => { 'use gpu'; const x = 1; }",
"const fn = () => { 'use gpu'; const x = Struct({ prop: 1}); }",
"const fn = () => { 'use gpu'; let x = 1; }",
"const fn = () => { 'use gpu'; obj.#buffer.$ = 1; }",
],
Comment thread
aleksanderkatan marked this conversation as resolved.
invalid: [
{
Expand Down Expand Up @@ -209,15 +210,6 @@ describe('noUnsupportedSyntax', () => {
},
],
},
{
code: "const fn = () => { 'use gpu'; obj.#buffer.$ = 1; }",
errors: [
{
messageId: 'unexpected',
data: { snippet: '#buffer', syntax: 'private identifier' },
},
],
},
{
code: "const fn = () => { 'use gpu'; const obj = { [key]: 1 }; }",
errors: [
Expand Down
7 changes: 6 additions & 1 deletion packages/tinyest-for-wgsl/src/externals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,14 @@ function extractPropAccessChain(ancestorChain: JsNode[]): string[] {
chain.push(current.name);
} else if (current.type === 'ThisExpression') {
chain.push('this');
} else if (current.type === 'MemberExpression' && !current.computed) {
} else if (current.type === 'MemberExpression') {
if (current.computed) {
break;
}
if (current.property.type === 'PrivateName') {
chain.push(`#${current.property.id.name}`);
} else if (current.property.type === 'PrivateIdentifier') {
chain.push(`#${current.property.name}`);
} else if (current.property.type === 'Identifier') {
chain.push(current.property.name);
} else {
Expand Down
8 changes: 8 additions & 0 deletions packages/tinyest-for-wgsl/src/parsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,14 @@ const Transpilers: Partial<{
return [NODE.memberAccess, object, property];
},

PrivateName(ctx, node) {
return `#${node.id.name}`;
},

PrivateIdentifier(ctx, node) {
return `#${node.name}`;
},

UpdateExpression(ctx, node) {
const operator = node.operator;
const argument = transpile(ctx, node.argument) as tinyest.Expression;
Expand Down
33 changes: 32 additions & 1 deletion packages/tinyest-for-wgsl/tests/parsers.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import babel from '@babel/parser';
import type { Node } from '@babel/types';
import type { ClassDeclaration, ClassProperty, Expression, Node } from '@babel/types';
import * as acorn from 'acorn';
import { describe, expect, it } from 'vitest';
import { transpileFn } from '../src/parsers.ts';
Expand Down Expand Up @@ -258,4 +258,35 @@ describe('transpileFn', () => {
`);
}),
);

it(
'handles private property access',
dualTest((p) => {
// `this.#v` is only valid inside a class body, so we parse a class and pluck out the arrow function.
const tree = p(`
class Foo {
#v = 0;
fn = () => {
const k = this.#v;
};
}
`) as ClassDeclaration | acorn.Program;
const cls = (tree.type === 'Program' ? tree.body[0] : tree) as
| ClassDeclaration
| acorn.ClassDeclaration;
const props = cls.body.body;
const lastProp = props.at(-1) as ClassProperty | acorn.PropertyDefinition;
const fn = lastProp.value as Expression | acorn.Expression;

const { externalNames } = transpileFn(fn);

expect(externalNames).toMatchInlineSnapshot(`
{
"this": {
"#v": "this.#v",
},
}
`);
}),
);
});
59 changes: 59 additions & 0 deletions packages/typegpu/tests/externalPropAccess.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,63 @@ describe('external prop access', () => {
}"
`);
});

it('supports private property access', () => {
class Cls {
#const = tgpu.const(d.u32, 1);

fn = () => {
'use gpu';
const a = this.#const.$;
};
Comment thread
aleksanderkatan marked this conversation as resolved.
}

const cls = new Cls();

expect(tgpu.resolve([cls.fn])).toMatchInlineSnapshot(`
"const const_1: u32 = 1u;

fn fn_1() {
const a = const_1;
}"
`);
});

it('supports private property access in anonymous class', () => {
const cls = new (class {
#const = tgpu.const(d.u32, 1);

fn = () => {
'use gpu';
const a = this.#const.$;
};
})();

expect(tgpu.resolve([cls.fn])).toMatchInlineSnapshot(`
"const const_1: u32 = 1u;

fn fn_1() {
const a = const_1;
}"
`);
});

it('supports props containing #', () => {
const cls = new (class {
'#const' = tgpu.const(d.u32, 0);

fn = () => {
'use gpu';
const a = this['#const'].$;
};
})();

expect(tgpu.resolve([cls.fn])).toMatchInlineSnapshot(`
"const item: u32 = 0u;

fn fn_1() {
const a = item;
}"
`);
});
});
7 changes: 6 additions & 1 deletion packages/unplugin-typegpu/src/babel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
getBlockScope,
initPluginState,
makeAstBackwardsCompatible,
requiresQuotation,
} from './core/common.ts';
import { createFilterForId } from './core/filter.ts';

Expand All @@ -31,7 +32,11 @@ function externalsToNode(externals: Externals | string): t.Expression {
}
return t.objectExpression(
Object.entries(externals).map(([name, value]) =>
t.objectProperty(i(name), externalsToNode(value), false),
t.objectProperty(
requiresQuotation(name) ? t.stringLiteral(name) : i(name),
externalsToNode(value),
false,
),
Comment thread
aleksanderkatan marked this conversation as resolved.
),
);
}
Expand Down
24 changes: 23 additions & 1 deletion packages/unplugin-typegpu/src/core/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,15 @@ function extractLabelledExpression(path: NodePath): [string, NodePath<t.Expressi
// key = value;
// }
return [path.node.key.name, path.get('value') as NodePath<t.Expression>];
} else if (
path.node.type === 'ClassPrivateProperty' &&
path.node.value &&
path.node.key.type === 'PrivateName'
) {
// class Class {
// #key = value;
// }
return [path.node.key.id.name, path.get('value') as NodePath<t.Expression>];
}
}

Expand Down Expand Up @@ -379,9 +388,16 @@ function tryFindIdentifier(node: t.Node): string | undefined {
}
}

/**
* Checks if the given prop in externals needs to be wrapped in ""
*/
export function requiresQuotation(prop: string): boolean {
return prop.startsWith('#');
}

/**
* Checks if `node` contains a label and a tgpu expression that could be named.
* If so, it calls the provided callback. Nodes selected for naming include:
* If so, it calls the provided callback. Nodes selected for naming include (but are not limited to):
*
* `let name = tgpu.bindGroupLayout({});` (VariableDeclarator)
*
Expand Down Expand Up @@ -498,6 +514,12 @@ export const functionVisitor: TraverseOptions<PluginState> = {
);
},

ClassPrivateProperty(path, state) {
performExpressionNaming(state, path, (pathToName, name) =>
state.wrapInAutoName(pathToName, name),
);
},

AssignmentExpression: {
exit(path, state) {
const runtimeFn = operators[path.node.operator as keyof typeof operators];
Expand Down
3 changes: 2 additions & 1 deletion packages/unplugin-typegpu/src/core/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
getBlockScope,
METADATA_FORMAT_VERSION,
makeAstBackwardsCompatible,
requiresQuotation,
} from './common.ts';

import type { Options, UnpluginPluginState, MetadatableFunction, NodeLocation } from './common.ts';
Expand All @@ -38,7 +39,7 @@ function externalsToString(externals: Externals | string): string {
return `() => ${externals}`;
}
const entries = Object.entries(externals).map(
([key, value]) => `${key}: ${externalsToString(value)}`,
([key, value]) => `${requiresQuotation(key) ? `"${key}"` : key}: ${externalsToString(value)}`,
);
Comment thread
aleksanderkatan marked this conversation as resolved.
return `{ ${entries.join(', ')} }`;
}
Expand Down
87 changes: 87 additions & 0 deletions packages/unplugin-typegpu/test/auto-naming.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,47 @@ describe('[BABEL] auto naming', () => {
`);
});

it('works with class private properties', () => {
const code = `\
class Foo {
#const = tgpu.const(d.u32, 1);
#buff;

constructor() {
this.#buff = root.createUniform(d.u32);
}
}
`;

expect(babelTransform(code, { autoNamingEnabled: true })).toMatchInlineSnapshot(`
"class Foo {
#const = /*#__PURE__*/(globalThis.__TYPEGPU_AUTONAME__ ?? (a => a))(tgpu.const(d.u32, 1), "const");
#buff;
constructor() {
this.#buff = /*#__PURE__*/(globalThis.__TYPEGPU_AUTONAME__ ?? (a => a))(root.createUniform(d.u32), "buff");
}
}"
`);
});

it('works with anonymous classes', () => {
const code = `\
const cls = new (class {
myConst = tgpu.const(d.u32, 0);
#const = tgpu.const(d.u32, 1);
})();
console.log(cls);
`;

expect(babelTransform(code, { autoNamingEnabled: true })).toMatchInlineSnapshot(`
"const cls = new class {
myConst = /*#__PURE__*/(globalThis.__TYPEGPU_AUTONAME__ ?? (a => a))(tgpu.const(d.u32, 0), "myConst");
#const = /*#__PURE__*/(globalThis.__TYPEGPU_AUTONAME__ ?? (a => a))(tgpu.const(d.u32, 1), "const");
}();
console.log(cls);"
`);
});

it('works with object properties', () => {
const code = `\
import tgpu from 'typegpu';
Expand Down Expand Up @@ -875,6 +916,52 @@ describe('[ROLLUP] auto naming', () => {
`);
});

it('works with class private properties', async () => {
const code = `\
class Foo {
#const = tgpu.const(d.u32, 1);
#buff;

constructor() {
this.#buff = root.createUniform(d.u32);
}
}
console.log(Foo);
`;

expect(await rollupTransform(code, { autoNamingEnabled: true })).toMatchInlineSnapshot(`
"class Foo {
#const = (/*#__PURE__*/(globalThis.__TYPEGPU_AUTONAME__ ?? (a => a))(tgpu.const(d.u32, 1), "const"));
#buff;

constructor() {
this.#buff = (/*#__PURE__*/(globalThis.__TYPEGPU_AUTONAME__ ?? (a => a))(root.createUniform(d.u32), "buff"));
}
}
console.log(Foo);
"
`);
});

it('works with anonymous classes', async () => {
const code = `\
const cls = new (class {
myConst = tgpu.const(d.u32, 0);
#const = tgpu.const(d.u32, 1);
})();
console.log(cls);
`;

expect(await rollupTransform(code, { autoNamingEnabled: true })).toMatchInlineSnapshot(`
"const cls = new (class {
myConst = (/*#__PURE__*/(globalThis.__TYPEGPU_AUTONAME__ ?? (a => a))(tgpu.const(d.u32, 0), "myConst"));
#const = (/*#__PURE__*/(globalThis.__TYPEGPU_AUTONAME__ ?? (a => a))(tgpu.const(d.u32, 1), "const"));
})();
console.log(cls);
"
`);
});

it('works with object properties', async () => {
const code = `\
import tgpu from 'typegpu';
Expand Down
Loading
Loading