NUDelta / orchestration-engine

Scripts to guide situated actions, powered by programming constructs that model ways of working
0 stars 0 forks source link

use a babel middleware to transform more human-readable OS code into execution code #16

Open kapil1garg opened 2 years ago

kapil1garg commented 2 years ago

Due to the way the execution environment works and the need for most of the OS language functions to be asynchronous (since they query an external Studio API), writing code for orchestration scripts can be syntactically messy, requiring the user to add async/await flags and this.keywords for any OS functions. For instance, this is what a simple trigger to send a message during a SIG meeting currently looks like:

async function feedbackOpportunity() {
  return await this.during(await this.venue("SIG"));
}

One way to clean this up is to create a custom plugin with Babel that takes code that is more human-readable and workable, and transforms it in a middleware layer before it's saved to the database. The user only ever interacts with the nice to read/work with code, while the backend system gets the representation that's easy for it to execute.

Here's a tester implementation that gets most of the way there (see TODOs in the code for what needs to still be done):

import babel from '@babel/core';
import * as t from "@babel/types";

// define a babel configuration
const babelTransformConfig = {
  plugins: [
    function orchestrationScriptTransformer() {
      return {
        visitor: {
          FunctionDeclaration(path) {
            path.node.async = true;
          },

          VariableDeclaration(path) {
            // TODO: may also need to add a check for variables that are stored as objects in the PL
          },

          Identifier(path) {
            // don't work on the highest-level identifier
            // TODO: is there a more elegant way to do this? (maybe check parent?)
            if (path.node.name === "detector") {
              return;
            }

            // TODO: need to check if the expressions are anything in our library
            // add this keyword to member expression
            if (t.isCallExpression(path.parentPath.node)) {
              path.replaceWith(
              t.memberExpression(
                t.thisExpression(),
                path.node
              )
            );
            }

            // skip children so we don't repeat
            path.skip();
          },

          CallExpression(path) {
            // TODO: need to check if the expressions are anything in our library
            // make any calls to OS functions async
            if (!t.isAwaitExpression(path.parentPath.node)) {
              path.replaceWith(
                t.awaitExpression(path.node)
              );
            }
          }
        },
      };
    },
  ],

  // keep any white space so code stays pretty
  retainLines: true
};

const transformOSCode = function (code, config) {
  let output = babel.transformSync(code, config);
  return output.code;
}

Going back to the example above, here's what the input code can now become, and the output generated though the transformation above:

// input code (what a mentor would write)
function feedbackOpportunity() {
    return during(venue("SIG"));
  }

// output code (what the engine will use to execute)
async function feedbackOpportunity() {
  return await this.during(await this.venue("SIG"));
}

Some helpful links:

Writing Transformations Writing a babel transform Custom plugins in babel Dealing with replaced nodes More on stopping code for added nodes

AST Explorers + Info Babel-specific General ASTs in JS

Babel Documentation Babel Plugin Handbook @babel/parser @babel/generator @babel/traverse @babel/types More on babel types Babel options

kapil1garg commented 2 years ago
// input code (what a mentor would write)
function feedbackOpportunity() {
    return during(venue("SIG"));
  }

With this input code, it's actually possible (and pretty easy) to remove the return as well and just have the expression code there. In the transform, a ReturnStatement with the node starting from FunctionDeclaration would need to be added

kapil1garg commented 2 years ago

See the orchestration-scripts-ast-transformer repo for the development code. It'll be easier to play around there and get the system working as intended before adding it into the engine.