JS-Interpreter Documentation

JS-Interpreter is a sandboxed JavaScript interpreter written in JavaScript. It allows for execution of arbitrary ES5 JavaScript code line by line. Execution is completely isolated from the main JavaScript environment. Multiple instances of the JS-Interpreter allow for multi-threaded concurrent JavaScript without the use of Web Workers.

Play with the JS-Interpreter demo.

Get the source code.

Usage

Start by including the two JavaScript source files:

    <script src="acorn.js"></script>
    <script src="interpreter.js"></script>
  

Alternatively, use the compressed bundle (80kb):

    <script src="acorn_interpreter.js"></script>
  

Next, instantiate an interpreter with the JavaScript code that needs to be parsed:

    var myCode = 'var a=1; for(var i=0;i<4;i++){a*=i;} a;';
    var myInterpreter = new Interpreter(myCode);
  

Additional JavaScript code may be added at any time (frequently used to interactively call previously defined functions):

    myInterpreter.appendCode('foo();');
  

To run the code step by step, call the step function repeatedly until it returns false:

    function nextStep() {
      if (myInterpreter.step()) {
        window.setTimeout(nextStep, 0);
      }
    }
    nextStep();
  

By default, one step is a single operation. For an example of how to step line by line, see the line step demo.

Alternatively, if the code is known to be safe from infinite loops, it may be executed to completion by calling the run function once:

    myInterpreter.run();
  

In cases where the code encounters asynchronous API calls (see below), or there are pending tasks created by setTimeout/setInterval, run will return true if it is blocked and needs to be called again at a later time.

External API

Similar to the eval function, the result of the last statement executed is available in myInterpreter.value:

    var myInterpreter = new Interpreter('6 * 7');
    myInterpreter.run();
    alert(myInterpreter.value);
  

Additionally, API calls may be added to the interpreter's global object during creation. Here is the addition of alert() and a url variable:

    var myCode = 'alert(url);';
    var initFunc = function(interpreter, globalObject) {
      interpreter.setProperty(globalObject, 'url', String(location));

      var wrapper = function alert(text) {
        return window.alert(text);
      };
      interpreter.setProperty(globalObject, 'alert',
          interpreter.createNativeFunction(wrapper));
    };
    var myInterpreter = new Interpreter(myCode, initFunc);
  

Alternatively, API calls may be added to an object:

    var myCode = 'robot.forwards(robot.fast);';
    var initFunc = function(interpreter, globalObject) {
      // Create 'robot' global object.
      var robot = interpreter.nativeToPseudo({});
      interpreter.setProperty(globalObject, 'robot', robot);

      // Define 'robot.fast' property.
      interpreter.setProperty(robot, 'fast', 99);

      // Define 'robot.forwards' function.
      var wrapper = function forwards(speed) {
        return realRobot.forwards(speed);
      };
      interpreter.setProperty(robot, 'forwards',
          interpreter.createNativeFunction(wrapper));
    };
    var myInterpreter = new Interpreter(myCode, initFunc);
  

Only primitives (numbers, strings, booleans, null and undefined) may be passed in and out of the interpreter during execution. Objects and functions are not compatible between native and interpreted code. See the JSON demo for an example of exchanging JSON between the browser and the interpreter.

Asynchronous API functions may wrapped so that they appear to be synchronous to interpreter. For example, a getXhr(url) function that returns the contents of an XMLHttpRequest could be defined in initFunc like this:

    var wrapper = function getXhr(href, callback) {
      var req = new XMLHttpRequest();
      req.open('GET', href, true);
      req.onreadystatechange = function() {
        if (req.readyState === 4 && req.status === 200) {
          callback(req.responseText);
        }
      };
      req.send(null);
    };
    interpreter.setProperty(globalObject, 'getXhr',
        interpreter.createAsyncFunction(wrapper));
  

This snippet uses createAsyncFunction in the same way that createNativeFunction was used earlier. The difference is that the wrapped asynchronous function's return value is ignored. Instead, an extra callback function is passed in when the wrapper is called. When the wrapper is ready to return, it calls the callback function with the value it wishes to return. From the point of view of the code running inside the JS-Interpreter, a function call was made and the result was returned immediately.

For a working example, see the async demo.

Serialization

A unique feature of the JS-Interpreter is its ability to pause execution, serialize the current state, then resume the execution at that point at a later time. Loops, variables, closures, and all other state is preserved.

Uses of this feature include continuously executing programs that survive a server reboot, or loading a stack image that has been computed up to a certain point, or forking execution, or rolling back to a stored state.

Serialization has several limitations. One is that the serialized format is not human-readable. It is also not guaranteed that future versions of the JS-Interpreter will be able to parse the serialization from older versions. Another limitation is that serialization reaches beyond the public API, and thus does not work with the compressed bundle (acorn_interpreter.js). Yet another limitation is that the serialization format is rather large; it has a 300 kb overhead due to the standard polyfills.

For a working example, see the serialization demo.

Another example shows how to use serialization of every step to enable stepping backwards.

Threading

JavaScript is single-threaded, but the JS-Interpreter allows one to run multiple threads at the same time. Creating two or more completely independent threads that run separately from each other is trivial: just create two or more instances of the Interpreter, each with its own code, and alternate calling each interpreter's step function. They may communicate indirectly with each other through any external APIs that are provided.

A more complex case is where two or more threads should share the same global scope. To implement this,

  1. create one main JS-Interpreter with no code but with any needed API definitions,
  2. create an empty array to store the thread stacks,
  3. for each thread:
    1. create a empty, temporary JS-Interpreter (no code, no APIs).
    2. replace this interpreter's global scope with the main interpreter's global (using getGlobalScope and setGlobalScope),
    3. inject the thread's code into this interpreter (using appendCode),
    4. extract the stack from this interpreter (using getStateStack) and add the stack to the array of thread stacks,
  4. then assign the desired stack to the main interpreter (using setStateStack) before calling step.

For a working example, see the thread demo.

Status

A JS-Intepreter instance has a getStatus function that returns information about what the interpreter is doing. The return values are one of:

Interpreter.Status.DONE
The interpreter has finished executing all code. Unless appendCode is used, it's done.
Interpreter.Status.STEP
The interpreter has code to be executed and is ready to take another step.
Interpreter.Status.TASK
The interpreter doesn't have any code to be executed at the moment, but there's a pending setTimeout/setInterval task which will provide code at some future time.
Interpreter.Status.ASYNC
The interpreter's execution is blocked by an asynchonous function call (created with createAsyncFunction) which is currently executing.

This status information might be used to determine whether to take another step immediately, or call setTimeout and step later, or stop.

Security

A common use-case of the JS-Interpreter is to sandbox potentially hostile code. The interpreter is secure by default: it does not use blacklists to prevent dangerous actions, instead it creates its own virtual machine with no external APIs except as provided by the developer.

Infinite loops are handled by calling the step function a maximum number of times, or by calling step indefinitely many times but using a setTimeout between each call to ensure other tasks have a chance to execute.

Memory bombs (e.g. var x='X'; while(1) x=x+x;) can be detected by periodically measuring the size of the stack, and aborting execution if it grows too large. For a working example, see the size demo.

Regular Expressions

Pathological regular expressions can execute in geometric time. For example 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac'.match(/^(a+)+b/) will effectively crash many JavaScript runtimes. JS-Interpreter has a variety of methods for safely dealing with regular expressions depending on the platform.

Limitations

The version of JavaScript implemented by the interpreter has a few differences from that which executes in a browser:

API
None of the DOM APIs are exposed. That's the point of a sandbox. If you need these, write your own interfaces. For a working example, see the DOM demo.
ES6
More recent additions to JavaScript such as let or Set aren't implemented. Babel is a good method of transpiling newer versions of JavaScript to ES5. For a working example, see the Babel demo.
toString & valueOf
User-created functions are not called when casting objects to primitives.
Performance
The interpreter is not particularly efficient. It currently runs about 200 times slower than native JavaScript.

Node.js

JS-Intepreter can be run server-side using Node.js. For a minimal demo, just run: node demos/node.js

Dependency

The only dependency is Acorn, a beautifully written JavaScript parser by Marijn Haverbeke. A minimal version of Acorn for ES5 is included in the JS-Interpreter package.

Alternatively, one could use latest version from the Acorn repo. However, it's three times the size due to features (such as ES6+) which JS-Interpreter doesn't use. There's no benefit in doing so unless your project already uses Acorn for something else.

Compatibility

The limiting factor for browser support is the use of Object.create(null) to create hash objects in both Acorn and JS-Interpreter. This results in the following minimum browser requirements:

Additionally, if safely executing regular expressions is required, then IE 11 is the minimum version of IE that supports Web Workers via Blobs.