wren.js

/**
 * @overview
 * The wren programming language in your browser!
 * <br>
 *
 * @example
 * import * as Wren from "../out/wren.js";
 *
 * let vm = new Wren.VM({
 *   resolveModuleFn     : function(importer, name) {...},
 *   loadModuleFn        : function(name) {...},
 *   bindForeignMethodFn : function(moduleName, className, isStatic, signature) {...},
 *   bindForeignClassFn  : function(moduleName, className) {...},
 *   writeFn             : function(toLog) {...},
 *   errorFn             : function(errorType, moduleName, line, msg) {...}
 * });
 *
 * vm.interpret("main", `
 *   System.print("Hello from Wren!")
 * `);
 */

/**
 * wrenjs is an ES6 module.
 *
 * @example
 * import * as Wren from "./path/to/wren.js";
 *
 * @module Wren
 */

import libwren from './generated/libwren.js';

/*
* 'libwren' represents our connection to the Wren C API, through emscripten's ccall function.
* We attach an empty object to it so that we can store and lookup JS Wren.VMs from C and JS.
*/
var Module = libwren();
Module._VMs = {};
Module._values = [];

/**
* Get the current wren version number.
*
* Can be used to range checks over versions.
* @return {number} A monotonically increasing numeric representation of the
*                  version number.
*/
export function getVersionNumber() {
  let result = Module.ccall('wrenGetVersionNumber',
      'number',
      [], []
  );

  return result;
}


/**
* The type of error returned by the [VM].
* @property {number} COMPILE A syntax or resolution error detected at compile time.
* @property {number} RUNTIME The error message for a runtime error.
* @property {number} STACK_TRACE One entry of a runtime error's stack trace.
*/
export var ErrorType = {
  COMPILE:      0,
  RUNTIME:      1,
  STACK_TRACE:  2
}

/**
* The result of a VM interpreting wren source.
* @property {number} SUCCESS the VM interpreted the source without error.
* @property {number} COMPILE_ERROR the VM experienced a compile error.
* @property {number} RUNTIME_ERROR the VM experienced a runtime error.
*/
export var Result = {
  SUCCESS:        0,
  COMPILE_ERROR:  1,
  RUNTIME_ERROR:  2
}


/**
* A single virtual machine for executing Wren code.
*/
export class VM {

    /**
    * Creates a new Wren virtual machine using the given [configuration].
    * If [configuration] is undefined, uses a default
    * configuration.
    * @param {Object} configuration
    * an object containing any or all of the following properties:
    * resolveModuleFn, loadModuleFn, bindForeignMethodFn, bindForeignClassFn,
    * writeFn, errorFn.
    *
    * let vm = new Wren.VM({
    *   resolveModuleFn     : function(importer, name) {...},
    *   loadModuleFn        : function(name) {...},
    *   bindForeignMethodFn : function(moduleName, className, isStatic, signature) {...},
    *   bindForeignClassFn  : function(moduleName, className) {...},
    *   writeFn             : function(toLog) {...},
    *   errorFn             : function(errorType, moduleName, line, msg) {...}
    * });
    */
    constructor(config) {
        // Replaces wrenInitConfiguration
        let default_config = {
            resolveModuleFn     : VM.defaultResolveModuleFn,
            loadModuleFn        : VM.defaultLoadModuleFn,
            bindForeignMethodFn : VM.defaultBindForeignMethodFn,
            bindForeignClassFn  : VM.defaultBindForeignClassFn,
            writeFn             : VM.defaultWriteFn,
            errorFn             : VM.defaultErrorFn
        }
        this.config = Object.assign(default_config, config);

        this._pointer = Module.ccall('shimNewVM',
          'number',
          [],[]
        );

        Module._VMs[this._pointer] = this;

        this._foreignClasses = {};
    }

    // Defaults //
    static defaultResolveModuleFn(importer, name) {
        return name;
    }

    static defaultLoadModuleFn(name) {
        return null;
    }

    static defaultBindForeignMethodFn(moduleName, className, isStatic, signature) {
        return null;
    }

    static defaultBindForeignClassFn(moduleName, className) {
        return null;
    }

    static defaultWriteFn(toLog) {
        let str = 'WRENJS: ';
        console.log(str + toLog);
    }

    static defaultErrorFn(errorType, moduleName, line, msg) {
        let str = 'WRENJS: ';
        if (errorType == 0) {
          console.warn(
              str + "["+moduleName+" line " +line+ "] [Error] "+msg+"\n"
          );
        }
        if (errorType == 1) {
          console.warn(
              str + "["+moduleName+" line "+line+"] in "+msg+"\n"
          );
        }
        if (errorType == 2) {
          console.warn(
              str + "[Runtime Error] "+msg+"\n"
          );
        }
    }

    /*
    * The following methods are called from C, and should not be relied upon in
    * the JS context.
    * --------------------------------------------------------------------------
    */

    _resolveModuleFn(importer, name) {
        return this.config.resolveModuleFn(importer, name);
    }

    _loadModuleFn(name) {
        return this.config.loadModuleFn(name);
    }

    _bindForeignMethod(moduleName, className, isStatic, signature) {
        // This should return a function looking for a Wren.VM as its only arg.
        let method = this.config.bindForeignMethodFn(moduleName, className,
          isStatic, signature
        );

        if (method == null) {
          return null;
        }

        // The wren C api expects a function looking for a pointer as its arg.
        let vm = this;
        let wrappedMethod = function(pointer) {
          method(vm);
        }
        return wrappedMethod;
    }

    _bindForeignClass(moduleName, className) {
        var methods =  this.config.bindForeignClassFn(moduleName, className);

        if (methods == null) {
          return null;
        }

        // Similar to the bindForeignMethod fn above, C expects to pass these
        // a pointer to the VM, and we need to convert that to a JS Wren.VM
        let vm = this;

        return {
            allocate: function() {
                methods.allocate(vm);
            },
            finalize: function() {
                methods.finalize(vm);

                let pointer = Module.ccall('wrenGetSlotForeign',
                  'number',
                  ['number', 'number'],
                  [vm._pointer, 0]
                );

                delete vm._foreignClasses[pointer];
            }
        }
    }

    _write(text) {
        this.config.writeFn(text);
    }

    _error(errorType, moduleName, line, msg) {
        this.config.errorFn(errorType, moduleName, line, msg);
    }

    /*
    * The following methods are implementations of the Wren C API, callable
    * from the JS context.
    * --------------------------------------------------------------------------
    */

    /**
    * Disposes of all resources in use by the VM.
    */
    free() {
        Module.ccall('wrenFreeVM',
          null,
          ['number'],
          [this._pointer]
        );
        delete VM[this._pointer]
        this._pointer = undefined;
    }

    /**
    * Immediately run the garbage collector to free unused memory.
    */
    collectGarbage() {
        Module.ccall('wrenCollectGarbage',
          null,
          ['number'],
          [this._pointer]
        );
    }

    /**
    * Runs [source], a string of Wren source code in a new fiber in [vm] in the
    * context of resolved [moduleName].
    * @return {string} whether the result was a success or an error.
    * @param {string} moduleName the name of the wren module.
    * @param {string} src the wren source code to interpret
    */
    interpret(moduleName, src) {
        let result = Module.ccall('wrenInterpret',
            'number',
            ['number', 'string', 'string'],
            [this._pointer, moduleName, src]);

        return result;
    }

    /**
    * Creates a handle that can be used to invoke a method with [signature] on
    * using a receiver and arguments that are set up on the stack.
    *
    * This handle can be used repeatedly to directly invoke that method from JS
    * code using [call].
    *
    * When you are done with this handle, it must be released using
    * [releaseHandle].
    * @return {number} a handle for use with [VM.call].
    * @param {string} signature a string depicting the signature of a wren method.
    */
    makeCallHandle(signature) {
        let handle = Module.ccall('wrenMakeCallHandle',
          'number',
          ['number', 'string'],
          [this._pointer, signature]
        );
        return handle;
    }

    /**
    * Calls [method], using the receiver and arguments previously set up on the
    * stack.
    *
    * [method] must have been created by a call to [makeCallHandle]. The
    * arguments to the method must be already on the stack. The receiver should be
    * in slot 0 with the remaining arguments following it, in order. It is an
    * error if the number of arguments provided does not match the method's
    * signature.
    *
    * After this returns, you can access the return value from slot 0 on the stack.
    * @return {string} whether the result was a success or error.
    * @param {number} method the handle returned from makeCallHandle.
    */
    call(method) {
        let result = Module.ccall('wrenCall',
          'number',
          ['number', 'number'],
          [this._pointer, method]
        );

        return result;
    }

    /**
    * Releases the reference stored in [handle]. After calling this, [handle] can
    * no longer be used.
    * @param {number} handle the handle returned from makeCallHandle.
    */
    releaseHandle(handle) {
        Module.ccall('wrenReleaseHandle',
          null,
          ['number', 'number'],
          [this._pointer, 'handle']
        );
    }

    /**
    * Returns the number of slots available to the current foreign method.
    * @return {number} the number of slots.
    */
    getSlotCount() {
        let count = Module.ccall('wrenGetSlotCount',
          'number',
          ['number'],
          [this._pointer]
        );
        return count;
    }

    /**
    * Ensures that the foreign method stack has at least [numSlots] available for
    * use, growing the stack if needed.
    *
    * Does not shrink the stack if it has more than enough slots.
    *
    * It is an error to call this from a finalizer.
    * @param {number} numSlots the number of slots needed.
    */
    ensureSlots(numSlots) {
        Module.ccall('wrenEnsureSlots',
          null,
          ['number', 'number'],
          [this._pointer, numSlots]
        );
    }

    /**
    * Gets the type of the object in [slot].
    * @return {string} the type of the object.
    * @param {number} slot the index of the slot.
    */
    getSlotType(slot) {
        let t = Module.ccall('wrenGetSlotType',
          'number',
          ['number', 'number'],
          [this._pointer, slot]
        );

        let types = [
          'WREN_TYPE_BOOL',
          'WREN_TYPE_NUM',
          'WREN_TYPE_FOREIGN',
          'WREN_TYPE_LIST',
          'WREN_TYPE_MAP',
          'WREN_TYPE_NULL',
          'WREN_TYPE_STRING',
          'WREN_TYPE_UNKNOWN'
        ]; // TODO: pull out into an enum.

        return types[t];
    }

    /**
    * Reads a boolean value from [slot].
    *
    * It is an error to call this if the slot does not contain a boolean value.
    * @return {boolean} the value stored in the slot.
    * @param {number} slot the index of the slot.
    */
    getSlotBool(slot) {
        let boolean = Module.ccall('wrenGetSlotBool',
          'boolean',
          ['number', 'number'],
          [this._pointer, slot]
        );
        return boolean;
    }

    /**
    * Reads a byte array from [slot].
    *
    * The memory for the returned string is owned by Wren. You can inspect it
    * while in your foreign method, but cannot keep a pointer to it after the
    * function returns, since the garbage collector may reclaim it.
    *
    * Returns a pointer to the first byte of the array and fill [length] with the
    * number of bytes in the array. TODO: does it?
    *
    * It is an error to call this if the slot does not contain a string.
    * @return {string} the bytes as a string.
    * @param {number} slot the index of the slot.
    * @param {number} length the length of the bytes.
    */
    getSlotBytes(slot, length) {
        let bytes = Module.ccall('wrenGetSlotBytes',
          'string',
          ['number', 'number', 'number'],
          [this._pointer, slot, length]
        );
        return bytes;
    }

    /**
    * Reads a number from [slot].
    *
    * It is an error to call this if the slot does not contain a number.
    * @return {number} the value stored in the slot.
    * @param {number} slot the index of the slot.
    */
    getSlotDouble(slot) {
        let double = Module.ccall('wrenGetSlotDouble',
          'number',
          ['number', 'number'],
          [this._pointer, slot]
        );
        return double;
    }

    /**
    * Reads a foreign object from [slot] and returns a pointer to the foreign data
    * stored with it.
    *
    * It is an error to call this if the slot does not contain an instance of a
    * foreign class.
    * @return {Object} the JavaScript Object stored in the slot.
    * @param {number} slot the index of the slot.
    */
    getSlotForeign(slot) {
        let pointer = Module.ccall('wrenGetSlotForeign',
          'number',
          ['number', 'number'],
          [this._pointer, slot]
        );

        return this._foreignClasses[pointer];
    }

    /**
    * Reads a string from [slot].
    *
    * The memory for the returned string is owned by Wren. You can inspect it
    * while in your foreign method, but cannot keep a pointer to it after the
    * function returns, since the garbage collector may reclaim it.
    * TODO: Is it?
    *
    * It is an error to call this if the slot does not contain a string.
    * @return {string} the string stored in the slot.
    * @param {number} slot the index of the slot.
    */
    getSlotString(slot) {
        let string = Module.ccall('wrenGetSlotString',
          'string',
          ['number', 'number'],
          [this._pointer, slot]
        );
        return string;
    }

    /**
    * Creates a handle for the value stored in [slot].
    *
    * This will prevent the object that is referred to from being garbage collected
    * until the handle is released by calling [releaseHandle()].
    * @return {number} a handle for use with [VM.call].
    * @param {number} slot the index of the slot.
    */
    getSlotHandle(slot) {
        let handle = Module.ccall('wrenGetSlotHandle',
          'number',
          ['number', 'number'],
          [this._pointer, slot]
        );
        return handle;
    }

    /**
    * Stores the boolean [value] in [slot].
    * @param {number} slot the index of the slot.
    * @param {boolean} value the boolean to store.
    */
    setSlotBool(slot, value) {
        Module.ccall('wrenSetSlotBool',
          null,
          ['number', 'number', 'boolean'],
          [this._pointer, slot, value]
        );
    }

    /**
    * Stores the array [length] of [bytes] in [slot].
    *
    * The bytes are copied to a new string within Wren's heap, so you can free
    * memory used by them after this is called.
    * @param {number} slot the index of the slot.
    * @param {string} bytes the bytes to store.
    * @param {number} length the length of the bytes.
    */
    setSlotBytes(slot, bytes, length) {
        Module.ccall('wrenSetSlotBytes',
          null,
          ['number', 'number', 'string', 'number'],
          [this._pointer, slot, bytes, length]
        );
    }

    /**
    * Stores the numeric [value] in [slot].
    * @param {number} slot the index of the slot.
    * @param {number} value the value to store.
    */
    setSlotDouble(slot, value) {
        Module.ccall('wrenSetSlotDouble',
          null,
          ['number', 'number', 'number'],
          [this._pointer, slot, value]
        );
    }

    /**
    * Creates a new instance of the foreign class stored in [classSlot] with [size]
    * bytes of raw storage and places the resulting object in [slot].
    *
    * This does not invoke the foreign class's constructor on the new instance. If
    * you need that to happen, call the constructor from Wren, which will then
    * call the allocator foreign method. In there, call this to create the object
    * and then the constructor will be invoked when the allocator returns.
    *
    * @return {Object} the same foreignObject you passed in.
    * @param {number} slot the index of the slot.
    * @param {number} classSlot the slot containing the foreign class.
    * @param {Object} foreignObject a JavaScript class.
    */
    setSlotNewForeign(slot, classSlot, foreignObject) {
        let pointer = Module.ccall('wrenSetSlotNewForeign',
          'number',
          ['number', 'number', 'number', 'number'],
          [this._pointer, slot, classSlot, 0]
        );

        this._foreignClasses[pointer] = foreignObject;

        return foreignObject;
    }

    /**
    * Stores a new empty list in [slot].
    * @param {number} slot the index of the slot.
    */
    setSlotNewList(slot) {
        Module.ccall('wrenSetSlotNewList',
          null,
          ['number', 'number'],
          [this._pointer, slot]
        );
    }

    /**
    * Stores a new empty map in [slot].
    * @param {number} slot the index of the slot.
    */
    setSlotNewMap(slot) {
        Module.ccall('wrenSetSlotNewMap',
          null,
          ['number', 'number'],
          [this._pointer, slot]
        );
    }

    /**
    * Stores null in [slot].
    * @param {number} slot the index of the slot.
    */
    setSlotNull(slot) {
        Module.ccall('wrenSetSlotNull',
          null,
          ['number', 'number'],
          [this._pointer, slot]
        );
    }

    /**
    * Stores the string [text] in [slot].
    *
    * The [text] is copied to a new string within Wren's heap, so you can free
    * memory used by it after this is called. The length is calculated using
    * [strlen()]. If the string may contain any null bytes in the middle, then you
    * should use [setSlotBytes()] instead.
    * @param {number} slot the index of the slot.
    * @param {string} text the string to store.
    */
    setSlotString(slot, text) {
        Module.ccall('wrenSetSlotString',
          null,
          ['number', 'number', 'string'],
          [this._pointer, slot, text]
        );
    }

    /**
    * Stores the value captured in [handle] in [slot].
    *
    * This does not release the handle for the value.
    * @param {number} slot the index of the slot.
    * @param {number} handle a handle returned from makeCallHandle.
    */
    setSlotHandle(slot, handle) {
        Module.ccall('wrenSetSlotHandle',
          null,
          ['number', 'number', 'number'],
          [this._pointer, slot, handle]
        );
    }

    /**
    * Returns the number of elements in the list stored in [slot].
    * @return {number} the number of elements in the list.
    * @param {number} slot the index of the slot.
    */
    getListCount(slot) {
        let count = Module.ccall('wrenGetListCount',
          'number',
          ['number', 'number'],
          [this._pointer, slot]
        );
        return count;
    }

    /**
    * Reads element [index] from the list in [listSlot] and stores it in
    * [elementSlot].
    * @param {number} listSlot
    * @param {number} index
    * @param {number} elementSlot
    */
    getListElement(listSlot, index, elementSlot) {
        Module.ccall('wrenGetListElement',
          null,
          ['number', 'number', 'number', 'number'],
          [this._pointer, listSlot, index, elementSlot]
        );
    }

    /**
    * Sets the value stored at [index] in the list at [listSlot],
    * to the value from [elementSlot].
    * @param {number} listSlot
    * @param {number} index
    * @param {number} elementSlot
    */
    setListElement(listSlot, index, elementSlot) {
        Module.ccall('wrenSetListElement',
          null,
          ['number', 'number', 'number', 'number'],
          [this._pointer, listSlot, index, elementSlot]
        );
    }

    /**
    * Takes the value stored at [elementSlot] and inserts it into the list stored
    * at [listSlot] at [index].
    *
    * As in Wren, negative indexes can be used to insert from the end. To append
    * an element, use `-1` for the index.
    * @param {number} listSlot
    * @param {number} index
    * @param {number} elementSlot
    */
    insertInList(listSlot, index, elementSlot) {
        Module.ccall('wrenInsertInList',
          null,
          ['number', 'number', 'number', 'number'],
          [this._pointer, listSlot, index, elementSlot]
        );
    }

    /**
    * Returns the number of entries in the map stored in [slot].
    * @return {number} the number of entries in the map.
    * @param {number} slot the index of the slot.
    */
    getMapCount(slot) {
        let count = Module.ccall('wrenGetMapCount',
          'number',
          ['number', 'number'],
          [this._pointer, slot]
        );
        return count;
    }

    /**
    * Returns true if the key in [keySlot] is found in the map placed in [mapSlot].
    * @return {boolean} whether the map contains the key.
    * @param {number} mapSlot a slot containing the map.
    * @param {number} keySlot a slot containing the key.
    */
    getMapContainsKey(mapSlot, keySlot) {
        let boolean = Module.ccall('wrenGetMapContainsKey',
          'boolean',
          ['number', 'number', 'number'],
          [this._pointer, mapSlot, keySlot]
        );
        return boolean;
    }

    /**
    * Retrieves a value with the key in [keySlot] from the map in [mapSlot] and
    * stores it in [valueSlot].
    * @param {number} mapSlot a slot containing the map.
    * @param {number} keySlot a slot containing the key.
    * @param {number} valueSlot a slot to place the value.
    */
    getMapValue(mapSlot, keySlot, valueSlot) {
        Module.ccall('wrenGetMapValue',
          null,
          ['number', 'number', 'number', 'number'],
          [this._pointer, mapSlot, keySlot, valueSlot]
        );
    }

    /**
    * Takes the value stored at [valueSlot] and inserts it into the map stored
    * at [mapSlot] with key [keySlot].
    * @param {number} mapSlot a slot containing the map.
    * @param {number} keySlot a slot containing the key to add.
    * @param {number} valueSlot a slot containing the key's value.
    */
    setMapValue(mapSlot, keySlot, valueSlot) {
        Module.ccall('wrenSetMapValue',
          null,
          ['number', 'number', 'number', 'number'],
          [this._pointer, mapSlot, keySlot, valueSlot]
        );
    }

    /**
    * Removes a value from the map in [mapSlot], with the key from [keySlot],
    * and place it in [removedValueSlot]. If not found, [removedValueSlot] is
    * set to null, the same behaviour as the Wren Map API.
    * @param {number} mapSlot a slot containing the map.
    * @param {number} keySlot a slot containing the key to remove.
    * @param {number} removedValueSlot a slot to contain the removed value.
    */
    removeMapValue(mapSlot, keySlot, removedValueSlot) {
        Module.ccall('wrenRemoveMapValue',
          null,
          ['number', 'number', 'number', 'number'],
          [this._pointer, mapSlot, keySlot, removedValueSlot]
        );
    }

    /**
    * Looks up the top level variable with [name] in resolved [moduleName] and stores
    * it in [slot].
    * @param {string} moduleName the name of the wren moduleName.
    * @param {string} name the name of the variable.
    * @param {number} slot the index of the slot.
    */
    getVariable(moduleName, name, slot) {
        Module.ccall('wrenGetVariable',
          null,
          ['number', 'string', 'string', 'number'],
          [this._pointer, moduleName, name, slot]
        );
    }

    /**
    * Looks up the top level variable with [name] in resolved [moduleName],
    * returns false if not found. The module must be imported at the time,
    * use wrenHasModule to ensure that before calling.
    * @return {boolean} whether the variable exists in module.
    * @param {string} moduleName the name of the wren module.
    * @param {string} name the name of the variable.
    */
    hasVariable(moduleName, name) {
        let boolean = Module.ccall('wrenHasVariable',
          'boolean',
          ['number', 'string', 'string'],
          [this._pointer, moduleName, name]
        );
        return boolean;
    }

    /**
    * Returns true if [moduleName] has been imported/resolved before, false if not.
    * @return {boolean} whether the module has been imported/resolved before.
    * @param {string} moduleName the name of the wren module.
    */
    hasModule(moduleName) {
        let boolean = Module.ccall('wrenHasModule',
          'boolean',
          ['number', 'string'],
          [this._pointer, moduleName]
        );
        return boolean;
    }

    /**
    * Sets the current fiber to be aborted, and uses the value in [slot] as the
    * runtime error object.
    * @param {number} slot the index of the slot.
    */
    abortFiber(slot) {
        Module.ccall('wrenAbortFiber',
          null,
          ['number', 'number'],
          [this._pointer, slot]
        );
    }

    // The following APIs are not implemented.
    //getUserData() {}
    //setUserData(userData) {}
}