metaES docs

About metaES as a meta-circular evaluator

metaES is a ECMAScript meta circular evaluator. The terms "evaluator" and "interpreter" are used interchangeably throughout this manual. You can learn more about such evaluator for example in SICP book that is available for free online at https://mitpress.mit.edu/sites/default/files/sicp/full-text/book/book.html

Meta-circular evaluator interprets the language that it is written in, but that interpretation process is easier, because many of the features are implemented in base evaluator already. Example features list includes:

  • basic operators like +, -, *, /,
  • parsing of literals including boolean, String, Number and more,
  • support for functions including closures, internal function calling (using [[Call]]) and function methods like bind, apply, call,
  • handling of prototypes – metaES doesn’t rewrite them,
  • objects creation with new, Object.create or {}/[] literals,
  • availability of global objects, like Object, Array, String, Date and so on.

As a result, a significant part of meta-circular interpretation in metaES involves reusing the capabilities of the original interpreter. However, metaES also provides users with additional first class state (available as objects) such as call stack, environment, functions' closures and more. In ECMAScript engines those are typically hidden during normal execution and may only be accessible through engine-specific debugger APIs or non-standard functions.

Additionally, metaES is designed to be eaisily extendible on multiple layers.

Installation

Follow README.md on GitHub.

Using metaES

Before beginning

We need to understand one thing outside of metaES business: parsing and ASTs (Abstract Syntax Trees).

Let's say we have a function parse that accepts string and returns AST object:

parse('2+2');

// returns
{
    "type": "Program",
    "body": [
        {
            "type": "ExpressionStatement",
            "expression": {
                "type": "BinaryExpression",
                "operator": "+",
                "left": {
                    "type": "Literal",
                    "value": 2,
                    "raw": "2"
                },
                "right": {
                    "type": "Literal",
                    "value": 2,
                    "raw": "2"
                }
            }
        }
    ],
    "sourceType": "script"
}

AST represents code in a structure that can be evaluated, compiled, transformed and so one. For metaES it doesn't matter as long produced AST is of the same shape defined by https://github.com/estree/estree spec. TODO: write that metaES uses meryiah.

Basic evaluation

In the simplest scenario you can write:

metaesEval("2+2", console.log);

which will eval via callback to 4 - a success value. If you want to get back result, you need to provide second callback:

metaesEval("x", null, console.error);

You can omit success and error callback entirely - metaesEval('doAnythingCorrect()') - but you won't get the result back.

Any script can be run this say:

let anyLongCode = `
    let input = [1,2,3];
    let squared = input.map(x=>x*x);`;
metaesEval(anyLongCode, console.log);

that parses. Otherwise:

metaesEval(`1x`, null, console.error);

will throw the parse error.

Remember that calling metaES in the following way:

metaesEval("throw 1");

will not throw. You have to provide error callback. It can throw only if error happens internally in metaES implementation. If you want to get result without callbacks and you know the program is synchronous, you can use uncps helper:

try {
  console.log("Result if success:", uncps(metaesEval)(`1x`));
} catch (e) {
  console.log("Error:", e);
}

You can also run AST directly:

metaesEval(
  {
    type: "BinaryExpression",
    operator: "+",
    left: {
      type: "Literal",
      value: 2,
      raw: "2"
    },
    right: {
      type: "Literal",
      value: 2,
      raw: "2"
    }
  },
  console.log
); // 4

That way metaES won't need to parse the source. The downside is there's no AST validator - a JavaScript parser was validating incorrect programs. metaES checks AST correctness neither.

metaesEval vs eval

metaesEval is similar to eval, but improved im some ways and limited in other. Let's compare. First, create wrapper for metaesEval to make experiments easier to read and write:

function eval2(source, env) {
  return uncps(metaesEval)(source, env);
}

Now, we can do:

eval("2+2") === eval2("2+2"); // true;

Errors work too:

let error1, error2;
try {
  eval("throw 1");
} catch (e) {
  error1 = e;
}
try {
  eval2("throw 1");
} catch (e) {
  error2 = e.value;
}
error1 === error2;

We had to unpack error thrown by eval2 - in metaES they're called exceptions and exceptions wrap original JavaScript error. TODO: what about cause?

Consider other example:

var a = 2;
eval("a+2"); // 4

standard eval captures surrounding scope, parses string end executes script. But:

var a = 2;
eval2("a+2"); // ReferenceError: a is not defined

doesn't work. There is no automatic closures or scope capturing in metaES. We can mitigate it if we know how to use continuations (a.k.a call-with-current-continuation, abbreviated as call/cc) in metaES. Read more about call/cc. Example:

eval3(function () {
  var a = 2;
  eval2("a+2");
});

We'd have to create eval3, where eval2 is predefined in its environment and is able to capture surrunding environment.

Final examples:

eval2("eval(1+2)"); // ReferenceError: eval is not defined.
eval2("eval(1+2)", { eval }); // 3
eval('eval2("eval(1+2)", {eval})'); // 3