typegpu
Version:
A thin layer between JS and WebGPU/WGSL that improves development experience and allows for faster iteration.
176 lines (175 loc) • 6.54 kB
JavaScript
import * as tinyest from 'tinyest';
const { NodeTypeCatalog: NODE } = tinyest;
export function stringifyNode(node) {
if (isExpression(node)) {
return stringifyExpression(node, '');
}
return stringifyStatement(node, '');
}
function stringifyStatement(node, ident) {
if (isExpression(node)) {
return `${ident}${stringifyExpression(node, ident)};`;
}
if (node[0] === NODE.block) {
const statements = node[1].map((n) => stringifyStatement(n, ident + ' '));
return `{\n${statements.join('\n')}\n${ident}}`;
}
if (node[0] === NODE.return) {
const expr = node[1] === undefined ? '' : ` ${stringifyExpression(node[1], '')}`;
return `${ident}return${expr};`;
}
if (node[0] === NODE.if) {
const cond = stringifyExpression(node[1], ident);
const then = stringifyStatement(node[2], ident);
const base = `${ident}if (${cond}) ${then}`;
if (node[3] !== undefined) {
return `${base} else ${stringifyStatement(node[3], ident)}`;
}
return base;
}
if (node[0] === NODE.let) {
if (node[2] !== undefined) {
return `${ident}let ${node[1]} = ${stringifyExpression(node[2], ident)};`;
}
return `${ident}let ${node[1]};`;
}
if (node[0] === NODE.const) {
if (node[2] !== undefined) {
return `${ident}const ${node[1]} = ${stringifyExpression(node[2], ident)};`;
}
return `${ident}const ${node[1]};`;
}
if (node[0] === NODE.for) {
const init = node[1] ? stringifyStatement(node[1], '') : ';';
const cond = node[2] ? stringifyExpression(node[2], ident) : '';
const update = node[3] ? stringifyStatement(node[3], '') : '';
const body = stringifyStatement(node[4], ident);
return `${ident}for (${init} ${cond}; ${update.slice(0, -1) /* trim the ';' */}) ${body}`;
}
if (node[0] === NODE.while) {
const cond = stringifyExpression(node[1], ident);
const body = stringifyStatement(node[2], ident);
return `${ident}while (${cond}) ${body}`;
}
if (node[0] === NODE.continue) {
return `${ident}continue;`;
}
if (node[0] === NODE.break) {
return `${ident}break;`;
}
if (node[0] === NODE.forOf) {
const leftKind = node[1][0] === NODE.const ? 'const' : 'let';
const leftName = node[1][1];
const right = stringifyExpression(node[2], ident);
const body = stringifyStatement(node[3], ident);
return `${ident}for (${leftKind} ${leftName} of ${right}) ${body}`;
}
assertExhaustive(node);
}
function stringifyExpression(node, ident) {
if (typeof node === 'string') {
return node;
}
if (typeof node === 'boolean') {
return `${node}`;
}
if (node[0] === NODE.numericLiteral) {
return node[1];
}
if (node[0] === NODE.stringLiteral) {
return JSON.stringify(node[1]);
}
if (node[0] === NODE.arrayExpr) {
const elements = node[1].map((n) => stringifyExpression(n, ident));
return `[${elements.join(', ')}]`;
}
if (node[0] === NODE.binaryExpr) {
return `${wrapIfComplex(node[1], ident)} ${node[2]} ${wrapIfComplex(node[3], ident)}`;
}
if (node[0] === NODE.assignmentExpr) {
return `${stringifyExpression(node[1], ident)} ${node[2]} ${stringifyExpression(node[3], ident)}`;
}
if (node[0] === NODE.logicalExpr) {
return `${wrapIfComplex(node[1], ident)} ${node[2]} ${wrapIfComplex(node[3], ident)}`;
}
if (node[0] === NODE.unaryExpr) {
// Unary word operators like void, instanceof and delete require a space
const sep = node[1].length > 1 ? ' ' : '';
return `${node[1]}${sep}${wrapIfComplex(node[2], ident)}`;
}
if (node[0] === NODE.call) {
const callee = wrapIfComplex(node[1], ident);
const args = node[2].map((n) => stringifyExpression(n, ident)).join(', ');
return `${callee}(${args})`;
}
if (node[0] === NODE.memberAccess) {
if (Array.isArray(node[1]) && node[1][0] === NODE.numericLiteral) {
return `(${stringifyExpression(node[1], ident)}).${node[2]}`;
}
return `${wrapIfComplex(node[1], ident)}.${node[2]}`;
}
if (node[0] === NODE.indexAccess) {
return `${wrapIfComplex(node[1], ident)}[${stringifyExpression(node[2], ident)}]`;
}
if (node[0] === NODE.preUpdate) {
return `${node[1]}${wrapIfComplex(node[2], ident)}`;
}
if (node[0] === NODE.postUpdate) {
return `${wrapIfComplex(node[2], ident)}${node[1]}`;
}
if (node[0] === NODE.objectExpr) {
const entries = Object.entries(node[1]).map(([key, val]) => `${key}: ${stringifyExpression(val, ident)}`);
return `{ ${entries.join(', ')} }`;
}
if (node[0] === NODE.conditionalExpr) {
return `${wrapIfComplex(node[1], ident)} ? ${wrapIfComplex(node[2], ident)} : ${wrapIfComplex(node[3], ident)}`;
}
assertExhaustive(node);
}
function assertExhaustive(value) {
throw new Error(`'${JSON.stringify(value)}' was not handled by the stringify function.`);
}
function isExpression(node) {
if (typeof node === 'string' ||
typeof node === 'boolean' ||
node[0] === NODE.numericLiteral ||
node[0] === NODE.stringLiteral ||
node[0] === NODE.arrayExpr ||
node[0] === NODE.binaryExpr ||
node[0] === NODE.assignmentExpr ||
node[0] === NODE.logicalExpr ||
node[0] === NODE.unaryExpr ||
node[0] === NODE.call ||
node[0] === NODE.memberAccess ||
node[0] === NODE.indexAccess ||
node[0] === NODE.preUpdate ||
node[0] === NODE.postUpdate ||
node[0] === NODE.objectExpr ||
node[0] === NODE.conditionalExpr) {
node;
return true;
}
node;
return false;
}
const SIMPLE_NODES = [
NODE.memberAccess, // highest precedence
NODE.indexAccess, // highest precedence
NODE.call, // highest precedence
NODE.arrayExpr, // [] make things not ambiguous
NODE.stringLiteral,
NODE.numericLiteral,
];
/**
* Stringifies expression, and wraps it in parentheses if they cannot be trivially omitted
*/
function wrapIfComplex(node, ident) {
const s = stringifyExpression(node, ident);
if (typeof node === 'string' || typeof node === 'boolean') {
return s;
}
if (SIMPLE_NODES.includes(node[0])) {
return s;
}
return `(${s})`;
}