unplugin-typegpu
Version:
Build plugins for TypeGPU, enabling seamless JavaScript -> WGSL transpilation and improved debugging.
346 lines (343 loc) • 13.4 kB
JavaScript
//#region rolldown:runtime
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
key = keys[i];
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
get: ((k) => from[k]).bind(null, key),
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
});
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
value: mod,
enumerable: true
}) : target, mod));
//#endregion
let tinyest_for_wgsl = require("tinyest-for-wgsl");
tinyest_for_wgsl = __toESM(tinyest_for_wgsl);
let __babel_types = require("@babel/types");
__babel_types = __toESM(__babel_types);
let magic_string = require("magic-string");
magic_string = __toESM(magic_string);
//#region src/core/common.ts
/**
* Each breaking change to the metadata format requires a bump to this number.
* It's used at runtime by `typegpu` to determine how to interpret a function's metadata.
*/
const METADATA_FORMAT_VERSION = 1;
function initPluginState(state, methods) {
var _state$opts$autoNamin;
state.tgpuAliases = new Set(state.opts.forceTgpuAlias ? [state.opts.forceTgpuAlias] : []);
state.autoNamingEnabled = (_state$opts$autoNamin = state.opts.autoNamingEnabled) !== null && _state$opts$autoNamin !== void 0 ? _state$opts$autoNamin : true;
state.inUseGpuScope = false;
state.alreadyTransformed = /* @__PURE__ */ new WeakSet();
Object.assign(state, methods);
}
/** Regular expressions used for early pruning (to avoid unnecessary parsing, which is expensive) */
const earlyPruneRegex = [/["']use gpu["']/, /t(ype)?gpu/];
const defaultOptions = {
include: /\.m?[jt]sx?(?:\?.*)?$/,
autoNamingEnabled: true,
earlyPruning: true
};
/**
* Returns the block scope of a function declaration, if one exists.
* Used to hoist a function declaration to the top of the scope it's visible in.
*/
function getBlockScope(path) {
if (!path.node.id) return;
const binding = path.scope.getBinding(path.node.id.name);
if (!binding) return;
let scopePath = binding.scope.path;
if (__babel_types.isNode(scopePath.node.body)) scopePath = scopePath.get("body");
if (__babel_types.isBlockStatement(scopePath.node) || __babel_types.isProgram(scopePath.node)) return scopePath;
}
/**
* Checks if `node` is an alias for the 'tgpu' object, traditionally
* available via `import tgpu from 'typegpu'`.
*/
function isTgpu(state, node) {
let path = "";
let tail = node;
while (true) if (tail.type === "MemberExpression") {
if (tail.property.type === "StringLiteral" && tail.property.value === "~unstable") {
tail = tail.object;
continue;
}
if (tail.property.type !== "Identifier") break;
path = path ? `${tail.property.name}.${path}` : tail.property.name;
tail = tail.object;
} else if (tail.type === "Identifier") {
path = path ? `${tail.name}.${path}` : tail.name;
break;
} else break;
return state.tgpuAliases.has(path);
}
function gatherTgpuAliases(state, node) {
if (node.source.value !== "typegpu") return;
for (const spec of node.specifiers) if (spec.type === "ImportDefaultSpecifier" || spec.type === "ImportSpecifier" && spec.imported.type === "Identifier" && spec.imported.name === "tgpu") state.tgpuAliases.add(spec.local.name);
else if (spec.type === "ImportNamespaceSpecifier") state.tgpuAliases.add(`${spec.local.name}.tgpu`);
}
const fnShellFunctionNames = [
"fn",
"vertexFn",
"fragmentFn",
"computeFn"
];
function isShellImplementationCall(node, state) {
return node.callee.type === "CallExpression" && node.callee.callee.type === "MemberExpression" && node.callee.callee.property.type === "Identifier" && fnShellFunctionNames.includes(node.callee.callee.property.name) && node.arguments.length === 1 && isTgpu(state, node.callee.callee.object);
}
/**
* Extracts a name and expression from nodes that contain a label and an expression,
* such as VariableDeclarator or PropertyDefinition.
* Returns a tuple of [name, expression] if found, otherwise undefined.
*
* @example
* extractLabelledExpression(node`let name = tgpu.bindGroupLayout({});`)
* // ["name", node`tgpu.bindGroupLayout({})`]
*/
function extractLabelledExpression(path) {
if (path.node.type === "VariableDeclarator" && path.node.id.type === "Identifier" && path.node.init) return [path.node.id.name, path.get("init")];
else if (path.node.type === "AssignmentExpression") {
const maybeName = tryFindIdentifier(path.node.left);
if (maybeName) return [maybeName, path.get("right")];
} else if (path.node.type === "ObjectProperty" && path.node.key.type === "Identifier") return [path.node.key.name, path.get("value")];
else if (path.node.type === "ClassProperty" && path.node.value && path.node.key.type === "Identifier") return [path.node.key.name, path.get("value")];
}
function getFunctionName(path) {
var _extractLabelledExpre, _path$node$id;
const maybeName = path.parentPath ? (_extractLabelledExpre = extractLabelledExpression(path.parentPath)) === null || _extractLabelledExpre === void 0 ? void 0 : _extractLabelledExpre[0] : void 0;
return maybeName !== null && maybeName !== void 0 ? maybeName : path.node.type === "FunctionDeclaration" || path.node.type === "FunctionExpression" ? (_path$node$id = path.node.id) === null || _path$node$id === void 0 ? void 0 : _path$node$id.name : void 0;
}
const resourceConstructors = [
"bindGroupLayout",
"vertexLayout",
"privateVar",
"workgroupVar",
"const",
"slot",
"accessor",
"mutableAccessor",
"comptime",
...fnShellFunctionNames,
"createBuffer",
"createMutable",
"createReadonly",
"createUniform",
"createQuerySet",
"createPipeline",
"createComputePipeline",
"createGuardedComputePipeline",
"createRenderPipeline",
"createTexture",
"createSampler",
"createComparisonSampler",
"struct",
"unstruct",
"createView"
];
/**
* Checks if `node` should be wrapped in an autoname function.
* Since it is mostly for debugging and clean WGSL generation,
* some false positives and false negatives are admissible.
*/
function containsResourceConstructorCall(node, state) {
if (node.type === "CallExpression") {
if (isShellImplementationCall(node, state)) return true;
if (node.callee.type === "Identifier" && resourceConstructors.includes(node.callee.name)) return true;
if (node.callee.type === "MemberExpression") {
if (node.callee.property.type === "Identifier") {
if (resourceConstructors.includes(node.callee.property.name)) return true;
if (node.callee.property.name === "$name") return false;
}
return containsResourceConstructorCall(node.callee.object, state);
}
}
if (node.type === "TaggedTemplateExpression") return containsResourceConstructorCall(node.tag, state);
return false;
}
/**
* Tries to find an identifier in a node.
*
* @example
* // syntax is simplified, imagine the arguments are appropriate nodes instead
* tryFindIdentifier('myBuffer'); // 'myBuffer'
* tryFindIdentifier('buffers.myBuffer'); // 'myBuffer'
* tryFindIdentifier('this.myBuffer'); // 'myBuffer'
* tryFindIdentifier('[a, b]'); // undefined
*/
function tryFindIdentifier(node) {
if (node.type === "Identifier") return node.name;
if (node.type === "PrivateName") return tryFindIdentifier(node.id);
if (node.type === "MemberExpression") return tryFindIdentifier(node.property);
}
/**
* 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:
*
* `let name = tgpu.bindGroupLayout({});` (VariableDeclarator)
*
* `name = tgpu.bindGroupLayout({});` (AssignmentExpression)
*
* `property: tgpu.bindGroupLayout({})` (Property/ObjectProperty)
*
* Since it is mostly for debugging and clean WGSL generation,
* some false positives and false negatives are admissible.
*
* This function is NOT used for auto-naming shell-less functions.
* Those are handled separately.
*
* @privateRemarks
* When adding new checks, you need to call this method in the corresponding node in Babel.
*/
function performExpressionNaming(state, path, namingCallback) {
if (!state.autoNamingEnabled) return;
const labelledExpression = extractLabelledExpression(path);
if (labelledExpression) {
const [label, expressionPath] = labelledExpression;
if (containsResourceConstructorCall(expressionPath.node, state)) namingCallback(expressionPath, label);
}
}
const operators = {
"+": "__tsover_add",
"-": "__tsover_sub",
"*": "__tsover_mul",
"/": "__tsover_div",
"%": "__tsover_mod",
"+=": "__tsover_add",
"-=": "__tsover_sub",
"*=": "__tsover_mul",
"/=": "__tsover_div",
"%=": "__tsover_mod"
};
function containsUseGpuDirective(node) {
var _node$body$directives, _node$body;
return ("directives" in node.body ? (_node$body$directives = (_node$body = node.body) === null || _node$body === void 0 ? void 0 : _node$body.directives) !== null && _node$body$directives !== void 0 ? _node$body$directives : [] : []).map((directive) => directive.value.value).includes("use gpu");
}
const fnNodeToTranspiledMap = /* @__PURE__ */ new WeakMap();
function functionOnExit(path, state) {
const node = path.node;
if (!containsUseGpuDirective(node)) return;
state.inUseGpuScope = false;
if (state.alreadyTransformed.has(node)) return;
const ast = fnNodeToTranspiledMap.get(path.node);
const maybeName = getFunctionName(path);
if (!ast) throw new Error(`No metadata found for function ${maybeName !== null && maybeName !== void 0 ? maybeName : "<unnamed>"}`);
state.assignMetadata(path, maybeName, ast);
state.alreadyTransformed.add(node);
path.skip();
}
const functionVisitor = {
ImportDeclaration(path, state) {
gatherTgpuAliases(state, path.node);
},
VariableDeclarator(path, state) {
performExpressionNaming(state, path, (pathToName, name) => {
state.wrapInAutoName(pathToName, name);
});
},
ObjectProperty(path, state) {
performExpressionNaming(state, path, (pathToName, name) => state.wrapInAutoName(pathToName, name));
},
ClassProperty(path, state) {
performExpressionNaming(state, path, (pathToName, name) => state.wrapInAutoName(pathToName, name));
},
AssignmentExpression: { exit(path, state) {
const runtimeFn = operators[path.node.operator];
if (state.inUseGpuScope && runtimeFn) state.replaceWithAssignmentOverload(path, runtimeFn);
performExpressionNaming(state, path, (pathToName, name) => state.wrapInAutoName(pathToName, name));
path.skip();
} },
BinaryExpression: { exit(path, state) {
const runtimeFn = operators[path.node.operator];
if (state.inUseGpuScope && runtimeFn) state.replaceWithBinaryOverload(path, runtimeFn);
path.skip();
} },
ArrowFunctionExpression: {
enter(path, state) {
if (containsUseGpuDirective(path.node)) {
fnNodeToTranspiledMap.set(path.node, (0, tinyest_for_wgsl.transpileFn)(path.node));
if (state.inUseGpuScope) throw new Error(`Nesting 'use gpu' functions is not allowed`);
state.inUseGpuScope = true;
}
},
exit: functionOnExit
},
FunctionExpression: {
enter(path, state) {
if (containsUseGpuDirective(path.node)) {
fnNodeToTranspiledMap.set(path.node, (0, tinyest_for_wgsl.transpileFn)(path.node));
if (state.inUseGpuScope) throw new Error(`Nesting 'use gpu' functions is not allowed`);
state.inUseGpuScope = true;
}
},
exit: functionOnExit
},
FunctionDeclaration: {
enter(path, state) {
if (containsUseGpuDirective(path.node)) {
fnNodeToTranspiledMap.set(path.node, (0, tinyest_for_wgsl.transpileFn)(path.node));
if (state.inUseGpuScope) throw new Error(`Nesting 'use gpu' functions is not allowed`);
state.inUseGpuScope = true;
}
},
exit: functionOnExit
},
CallExpression: { exit(path, state) {
const node = path.node;
if (isShellImplementationCall(node, state)) {
const implementation = node.arguments[0];
if (implementation && (implementation.type === "FunctionExpression" || implementation.type === "ArrowFunctionExpression") && !containsUseGpuDirective(implementation)) state.assignMetadata(path.get("arguments.0"), getFunctionName(path.get("arguments.0")), (0, tinyest_for_wgsl.transpileFn)(implementation));
}
} }
};
//#endregion
Object.defineProperty(exports, 'METADATA_FORMAT_VERSION', {
enumerable: true,
get: function () {
return METADATA_FORMAT_VERSION;
}
});
Object.defineProperty(exports, '__toESM', {
enumerable: true,
get: function () {
return __toESM;
}
});
Object.defineProperty(exports, 'defaultOptions', {
enumerable: true,
get: function () {
return defaultOptions;
}
});
Object.defineProperty(exports, 'earlyPruneRegex', {
enumerable: true,
get: function () {
return earlyPruneRegex;
}
});
Object.defineProperty(exports, 'functionVisitor', {
enumerable: true,
get: function () {
return functionVisitor;
}
});
Object.defineProperty(exports, 'getBlockScope', {
enumerable: true,
get: function () {
return getBlockScope;
}
});
Object.defineProperty(exports, 'initPluginState', {
enumerable: true,
get: function () {
return initPluginState;
}
});