@nodescript/core
Version:
Visual programming language for Browser and Node
449 lines • 16.9 kB
JavaScript
import { convertAuto, isSchemaCompatible } from '../util/index.js';
import { CompilerError } from './CompilerError.js';
export class CompilerScope {
constructor(job, graph) {
this.job = job;
this.graph = graph;
this.emittedNodes = [];
this.lineExprMap = new Map();
this.emittedNodes = this.computeEmittedNodes();
this.linkMap = graph.computeLinkMap();
this.async = this.emittedNodes.some(_ => _.getModuleSpec().result.async);
}
get scopeId() {
return this.graph.scopeId;
}
get code() {
return this.job.code;
}
get symbols() {
return this.job.symbols;
}
get options() {
return this.job.options;
}
getEmittedNodes() {
return this.emittedNodes;
}
isAsync() {
return this.async;
}
emitNodeFunctions() {
for (const node of this.emittedNodes) {
this.emitNode(node);
}
}
computeEmittedNodes() {
if (this.options.emitAll) {
return this.graph.getNodes();
}
return this.graph.getEmittedNodes();
}
emitNode(node) {
const nodeUid = node.nodeUid;
this.emitComment(`${node.ref} ${nodeUid}`);
const sym = this.symbols.getNodeSym(nodeUid);
this.code.block(`${this.asyncSym(node)}function ${sym}(params, ctx) {`, `}`, () => {
if (this.isNodeCached(node)) {
this.code.line(`let $c = ctx.cache.get("${node.nodeUid}");`);
this.code.line(`if ($c) { return $c.res; }`);
this.code.block(`$c = (${this.asyncSym(node)}() => {`, `})();`, () => {
this.emitNodeBodyIntrospect(node);
});
this.code.line(`ctx.cache.set("${node.nodeUid}", { res: $c });`);
this.code.line(`return $c;`);
}
else {
this.emitNodeBodyIntrospect(node);
}
});
if (this.options.graphId) {
// Emit friendlier function names for stack trace introspection
const fnName = ['ns', this.options.graphId, nodeUid].join(':');
this.code.line(`Object.defineProperty(${sym}, 'name', { value: ${JSON.stringify(fnName)} });`);
}
}
emitNodeBodyIntrospect(node) {
const resSym = '$r';
const nodeUid = node.nodeUid;
this.code.line(`let ${resSym};`);
if (this.options.introspect) {
this.code.block('try {', '}', () => {
if (!node.supportsSubgraph()) {
// For subgraphs, pending check is done prior to calling the subgraph the first time
// (see getSubgraphExpr which calls checkPendingNode instead of doing it here)
this.code.line(`ctx.checkPendingNode(${JSON.stringify(nodeUid)});`);
}
this.code.line(`ctx.nodeEvaluated.emit({` +
`nodeUid: ${JSON.stringify(nodeUid)},` +
`progress: 0` +
`});`);
this.emitNodeBodyRaw(node, resSym);
if (this.options.introspect) {
this.code.line(`ctx.nodeEvaluated.emit({` +
`nodeUid: ${JSON.stringify(nodeUid)},` +
`result: ${resSym},` +
`});`);
}
this.code.line(`return ${resSym};`);
});
this.code.block('catch (error) {', '}', () => {
this.code.line(`ctx.nodeEvaluated.emit({` +
`nodeUid: ${JSON.stringify(nodeUid)},` +
`error,` +
`});`);
this.code.line('throw error;');
});
}
else {
this.emitNodeBodyRaw(node, resSym);
this.code.line(`return ${resSym};`);
}
}
emitNodeBodyRaw(node, resSym) {
this.emitNodePreamble(node);
if (node.isExpanded()) {
this.emitExpandedNode(node, resSym);
}
else {
this.emitRegularNode(node, resSym);
}
}
emitRegularNode(node, resSym) {
this.emitNodeCompute(node, resSym);
}
emitExpandedNode(node, resSym) {
// Expanded nodes always produce an array by
// repeating the computation per each value of expanded property
this.emitExpandedPreamble(node);
this.code.line(`${resSym} = []`);
this.code.block(`for (let $i = 0; $i < $l; $i++) {`, `}`, () => {
if (this.options.introspect) {
this.code.line(`ctx.nodeEvaluated.emit({` +
`nodeUid: ${JSON.stringify(node.nodeUid)},` +
`progress: $i / $l,` +
`});`);
}
const tempSym = `$t`;
this.code.line(`let ${tempSym};`);
this.emitNodeCompute(node, tempSym);
this.code.line(`${resSym}.push(${tempSym});`);
});
}
emitNodePreamble(node) {
const syms = [];
for (const line of node.effectiveLines()) {
const lineUid = line.lineUid;
const sym = this.symbols.createLineSym(lineUid);
const { decl, expr } = this.createLineDecl(line, sym);
if (decl) {
this.code.line(`const ${sym} = ${decl}`);
syms.push(sym);
}
this.lineExprMap.set(lineUid, expr);
}
if (node.isAsync()) {
// Note: leaving dangled promises makes JS report "Uncaught (in promise)",
// despite the fact they are `await`ed further down the line.
this.code.line(`await Promise.all([${syms.join(',')}]);`);
}
}
emitExpandedPreamble(node) {
const args = [];
for (const line of node.expandedLines()) {
const lineUid = line.lineUid;
const sym = this.symbols.getLineSym(lineUid);
args.push(`${sym}.length`);
}
this.code.line(`const $l = Math.min(${args.join(',')});`);
}
createLineDecl(line, sym) {
const targetSchema = line.getSchema();
const linkNode = line.getLinkNode();
const linkKey = line.linkKey;
// Linked
if (linkNode) {
if (this.options.comments) {
this.code.line(`// Line: ${line.lineUid}`);
this.code.line(`// Schema: ${JSON.stringify(targetSchema)}`);
}
const async = linkNode.isAsync();
// 1. figure if type conversion is necessary
let sourceSchema = linkNode.getModuleSpec().result.schema;
if (linkKey) {
sourceSchema = { type: 'any' };
}
// 2. create a base expression for calling the linked function, i.e. r1(params, ctx)
const linkSym = this.symbols.getNodeSym(linkNode.nodeUid);
let callExpr = `${linkSym}(params, ctx)`;
// 3. compose in linkKey operation
if (linkKey) {
callExpr = this.code.compose(async, callExpr, _ => {
return `ctx.lib.get(${_}, ${JSON.stringify(linkKey)})`;
});
}
if (line.isDeferred()) {
// Deferred:
return {
decl: `ctx.deferred(() => ${this.convertTypeExpr(async, callExpr, sourceSchema, targetSchema)})`,
expr: sym,
};
}
else if (line.isExpanded()) {
// Expanded:
// the linked call is awaited, then wrapped into ctx.toArray;
// type conversion happens inside the loop, synchronously
const expr = `${sym}[$i]`;
return {
decl: `ctx.toArray(${this.awaitSym(linkNode)}${callExpr})`,
expr: this.convertTypeExpr(false, expr, sourceSchema, targetSchema),
};
}
// Regular linked
return {
decl: this.convertTypeExpr(async, callExpr, sourceSchema, targetSchema),
expr: `${this.awaitSym(linkNode)}${sym}`,
};
}
// Static value
const value = convertAuto(line.getStaticValue(), targetSchema);
return {
expr: this.escapeValue(value),
};
}
emitNodeCompute(node, resSym) {
switch (node.ref) {
case '@system/Param':
return this.emitParamNode(node, resSym);
case '@system/Input':
return this.emitInputNode(node, resSym);
case '@system/Scope':
return this.emitScopeNode(node, resSym);
case '@system/Result':
case '@system/Output':
return this.emitOutputNode(node, resSym);
case '@system/Comment':
case '@system/Frame':
return;
case '@system/AI':
return this.emitAI(node, resSym);
case '@system/EvalSync':
return this.emitEvalSync(node, resSym);
case '@system/EvalAsync':
return this.emitEvalAsync(node, resSym);
case '@system/EvalJson':
return this.emitEvalJson(node, resSym);
case '@system/EvalTemplate':
return this.emitEvalTemplate(node, resSym);
default:
return this.emitGenericCompute(node, resSym);
}
}
emitParamNode(node, resSym) {
const prop = node.getProp('key');
const key = prop?.value;
if (key) {
this.code.line(`${resSym} = params[${JSON.stringify(key)}]`);
}
else {
this.code.line(`${resSym} = undefined;`);
}
}
emitInputNode(node, resSym) {
this.code.line(`${resSym} = { ...params, ...ctx.getScopeData() };`);
// TODO phase out the workaround after subgraphs are migrated to Scope + Param nodes
// this.code.line(`${resSym} = params;`);
}
emitScopeNode(node, resSym) {
this.code.line(`${resSym} = ctx.getScopeData();`);
}
emitOutputNode(node, resSym) {
this.code.block(`const $p = {`, `}`, () => {
this.emitNodeProps(node);
});
this.code.line(`${resSym} = $p.value;`);
}
emitEvalSync(node, resSym) {
this.emitEvalLike(node, resSym, false, 'code', 'args');
}
emitEvalAsync(node, resSym) {
this.emitEvalLike(node, resSym, true, 'code', 'args');
}
emitAI(node, resSym) {
const async = node.isAsync();
this.emitEvalLike(node, resSym, async, 'code', 'inputs');
}
emitEvalLike(node, resSym, async, codeKey, argsKey) {
const code = node.getProp(codeKey)?.value ?? '';
this.code.block(`const $p = {`, `}`, () => {
const prop = node.getProp(argsKey);
if (prop) {
this.emitProp(prop);
}
});
const args = node.getProp(argsKey)?.getEntries().filter(_ => _.key.trim() !== '') ?? [];
const argList = args.map(_ => _.key).join(',');
const argVals = args.map(_ => `$p.${argsKey}[${JSON.stringify(_.key)}]`).join(',');
if (async) {
this.code.block(`${resSym} = await (async (${argList}) => {`, `})(${argVals})`, () => {
this.code.line(code);
});
}
else {
this.code.block(`${resSym} = ((${argList}) => {`, `})(${argVals})`, () => {
this.code.line(code);
});
}
}
emitEvalJson(node, resSym) {
const code = node.getProp('code')?.value ?? '';
try {
// Make sure it's actually a JSON
JSON.parse(code);
this.code.line(`${resSym} = ${code};`);
}
catch (error) {
this.code.line(`throw new Error(${JSON.stringify(error.message)})`);
}
}
emitEvalTemplate(node, resSym) {
const template = (node.getProp('template')?.value ?? '')
.replace(/`/g, '\\`')
.replace(/\\\\`/g, '\\\\\\`');
this.code.block(`const $p = {`, `}`, () => {
const prop = node.getProp('args');
if (prop) {
this.emitProp(prop);
}
});
const args = node.getProp('args')?.getEntries() ?? [];
const argList = args.map(_ => _.key).join(',');
const argVals = args.map(_ => `$p.args[${JSON.stringify(_.key)}]`).join(',');
this.code.block(`${resSym} = ((${argList}) => {`, `})(${argVals})`, () => {
this.code.line(`return \`${template}\``);
});
}
emitGenericCompute(node, resSym) {
const computeSym = this.symbols.getComputeSym(node.ref);
const scopeSym = node.getModuleSpec().newScope ? `ctx.newScope()` : `ctx`;
const subgraphSym = this.getSubgraphExpr(node);
const argsExpr = [scopeSym, subgraphSym].filter(Boolean).join(',');
this.code.block(`${resSym} = ${this.awaitSym(node)}${computeSym}({`, `}, ${argsExpr});`, () => {
this.emitNodeProps(node);
});
}
getSubgraphExpr(node) {
const subgraph = node.getSubgraph();
if (!subgraph) {
return '';
}
const rootNode = subgraph.getRootNode();
if (!rootNode) {
return `() => undefined`;
}
const sym = this.symbols.getNodeSym(rootNode.nodeUid);
if (this.options.introspect) {
return [
`(scopeData, ctx) => {`,
`ctx.scopeCaptured.emit({ nodeUid: ${JSON.stringify(node.nodeUid)}, params: scopeData });`,
`ctx.checkPendingNode(${JSON.stringify(node.nodeUid)});`,
`return ${sym}(params, ctx.setScopeData(scopeData));`,
`}`,
].join('');
}
return `(scopeData, ctx) => ${sym}(params, ctx.setScopeData(scopeData))`;
}
emitNodeProps(node) {
for (const prop of node.getProps()) {
this.emitProp(prop);
}
}
emitProp(prop) {
if (prop.isUsesEntries()) {
this.emitEntries(prop);
}
else {
this.emitSingleProp(prop);
}
}
emitEntries(prop) {
const { schema } = prop.getParamSpec();
switch (schema.type) {
case 'array':
return this.emitArrayEntries(prop);
case 'object':
return this.emitObjectEntries(prop);
}
}
emitArrayEntries(prop) {
this.code.block(`${JSON.stringify(prop.propKey)}: [`, '],', () => {
for (const p of prop.getEntries()) {
const expr = this.getLineExpr(p);
this.code.line(`${expr},`);
}
});
}
emitObjectEntries(prop) {
this.code.block(`${JSON.stringify(prop.propKey)}: {`, '},', () => {
for (const p of prop.getEntries()) {
const expr = this.getLineExpr(p);
this.code.line(`${JSON.stringify(p.key)}: ${expr},`);
}
});
}
emitSingleProp(prop) {
const expr = this.getLineExpr(prop);
this.code.line(`${JSON.stringify(prop.propKey)}: ${expr},`);
}
getLineExpr(line) {
const lineId = line.lineUid;
const expr = this.lineExprMap.get(lineId);
if (!expr) {
throw new CompilerError(`Line expression not found: ${lineId}`);
}
return expr;
}
convertTypeExpr(async = false, expr, sourceSchema, targetSchema) {
const schemaCompatible = isSchemaCompatible(targetSchema, sourceSchema);
return schemaCompatible ?
expr :
this.code.compose(async, expr, _ => `ctx.convertType(${_}, ${JSON.stringify(targetSchema)})`);
}
emitComment(str) {
if (this.options.comments) {
this.code.line(`// ${str}`);
}
}
isNodeCached(node) {
const cache = node.getModuleSpec().cacheMode ?? 'auto';
switch (cache) {
case 'auto': {
if (node.getEvalMode() === 'manual') {
return true;
}
const links = this.linkMap.get(node.localId);
if (links.size > 1) {
return true;
}
return node.isExpanded();
}
case 'always':
return true;
case 'never':
return false;
}
}
escapeValue(value) {
if (value === undefined) {
return 'undefined';
}
return JSON.stringify(value);
}
asyncSym(node) {
return node.isAsync() ? 'async ' : '';
}
awaitSym(node) {
return node.isAsync() ? `await ` : '';
}
}
//# sourceMappingURL=CompilerScope.js.map