Home Reference Source

src/Engine.js

'use strict';

const esper = require('./index.js');
const Evaluator = require('./Evaluator');
const Realm = require('./Realm');
const Scope = require('./Scope');
const Value = require('./Value');
const BridgeValue = require('./values/BridgeValue');
const ASTPreprocessor = require('./ASTPreprocessor');
const FutureValue = require('./values/FutureValue');
const EasyNativeFunction = require('./values/EasyNativeFunction');
const ClosureValue = require('./values/ClosureValue');
const SmartLinkValue = require('./values/SmartLinkValue');
const DefaultRuntime = require('./DefaultRuntime')

let defaultOptions = {
	strict: false,
	foreignObjectMode: 'link',
	addInternalStack: false,
	executionLimit: Infinity,
	exposeEsperGlobal: true,
	extraErrorInfo: false,
	addExtraErrorInfoToStacks: false,
	bookmarkInvocationMode: 'error',
	yieldPower: 5,
	debug: false,
	compile: 'pre',
	language: 'javascript',
	runtime: false,
	rotateThreads: false 
};

/**
 * Container class for all of esper.
 */
class Engine {

	constructor(options, realm=false) {
		options = options || {};
		this.options = {};
		for ( var k in defaultOptions ) {
			if ( k in options ) this.options[k] = options[k];
			else this.options[k] = defaultOptions[k];
		}

		if ( realm )  {
			this.realm = realm;
		} else {
			this.realm = new Realm(this.options, this);
		}

		this.evaluator = new Evaluator(this.realm, null, this.globalScope);
		if ( this.options.debug ) {
			this.evaluator.debug = true;
		}

		this.evaluator.defaultYieldPower = this.options.yieldPower;
		this.evaluator.yieldPower = this.options.yieldPower;

		if ( this.language.startupCode && !realm ) {
			this.loadLangaugeStartupCode();
		}

		//options.runtime = true;
		if ( options.runtime ) {
			if ( "boolean" == typeof(options.runtime) ) {
				this.runtime = new DefaultRuntime();
			} else {
				this.runtime = options.runtime;
			}
		}

		this.threads = [];
		let that = this;
		if ( options.runtime ) {
			this.evloop = {next: function() {
				let promises = [];
				for ( let i = 0; i < that.threads.length; ++i ) {
					if ( that.threads[i] ) {
						let val = that.threads[i].next();
						if ( val.done ) {  that.threads.splice(i, 1); return {done: false, value: val.value}; }
						if ( !val.value || !val.value.then ) {
							if ( options.rotateThreads ) that.threads.push(that.threads.splice(i, 1)[0]);
							return {done: false, value: val.value};
						}
						else promises.push(val.value);
					}
				}
				if ( promises.length > 0 ) return {done: false, value: Promise.race(promises)};
				else return {done: true};

			}};
		} else {
			Object.defineProperty(this, "evloop", {
				get: () => this.threads[0],
				set: (v) => this.threads[0] = v // Supports CrazyJoshMode
			});
		}

		if ( this.language.setupEngine ) {
			this.language.setupEngine(esper, this);
		}

		for ( let hook of esper.hooks.setupEngine ) {
			hook(esper, this);
		}

	}

	//get evloop() { return this.generator; }
	get generator() { return this.evloop; }
	set generator(v) { return this.evloop = v; } // Supports CrazyJoshMode

	loadLangaugeStartupCode() {
		let past = this.preprocessAST(this.language.startupCode(), {markNonUser: true});
		let stdlib_eval = new Evaluator(this.realm, null, this.globalScope);
		stdlib_eval.frames = [];
		stdlib_eval.pushAST(past, this.globalScope);
		stdlib_eval.ast = past;

		let gen = stdlib_eval.generator();
		let val = gen.next();
		while ( !val.done ) val = gen.next();
	}

	get language() {
		if ( !(this.options.language in esper.languages) ) {
			throw new Error(`Unknown language ${this.options.language}. Load the lang-${this.options.language} plugin?`);
		}
		return esper.languages[this.options.language];
	}

	/**
	 * Evalute `code` and return a promise for the result.
	 *
	 * @access public
	 * @param {string} code - String of code to evaluate
	 * @return {Promise<Value>} - The result of execution, as a promise.
	 */
	eval(code) {
		let ast = this.realm.parser(code);
		return this.evalAST(ast, {source: code});
	}

	/**
	 * Evalute `code` and return a the result.
	 *
	 * @access public
	 * @param {string} code - String of code to evaluate
	 * @return {Value} - The result of execution
	 */
	evalSync(code) {
		let ast = this.realm.parser(code);
		return this.evalASTSync(ast, {source: code});
	}

	evalDetatched(code) {
		let ast = this.realm.parser(code);
		this.loadAST(ast, {source: code});
		let p = new Promise((res, rej) => {
			this.evaluator.onCompletion = res;
			this.evaluator.onError = rej;
		});
		setTimeout(() => this.run().catch((e) => { }), 0);
		return p
	}

	/**
	 * Evalute `ast` and return a promise for the result.
	 *
	 * @access public
	 * @param {Node} ast - ESTree AST representing the code to run.
	 * @param {string} codeRef - The code that was used to generate the AST.
	 * @return {Value} - The result of execution, as a promise.
	 */
	evalAST(ast, opts) {
		//console.log(escodegen.generate(ast));
		this.loadAST(ast, opts);
		let p = this.run();
		p.then(
			() => this.threads = [],
			() => this.threads = []
		);
		return p
	}

	evalASTSync(ast, opts) {
		this.loadAST(ast, opts);
		let value = this.runSync();
		this.threads[0] = [];
		return value;
	}

	preprocessAST(ast, opts) {
		opts = opts || {};
		opts.compile = this.options.compile;
		let past = ASTPreprocessor.process(ast, opts);
		return past;
	}

	loadAST(ast, opts) {
		let past = this.preprocessAST(ast, opts);
		this.evaluator.frames = [];
		this.evaluator.pushAST(past, this.globalScope);
		this.evaluator.ast = past;
		this.threads[0] = this.evaluator.generator();
	}

	load(code) {
		let ast = this.realm.parser(code);
		this.loadAST(ast, {source: code});
	}

	step() {
		if ( this.threads.length < 1 ) throw new Error('No code loaded to step');
		let value = this.evloop.next();
		return value.done;
	}

	run() {
		let that = this;
		let steps = 0;
		function handler(value) {
			while ( !value.done ) {
				value = that.evloop.next();
				if ( value.value && value.value.then ) {
					return value.value.then((v) => {
						return handler({done: false, value: v});
					});
				}
				if ( ++steps > that.options.executionLimit ) throw new Error('Execution Limit Reached');
			}
			if ( !that.options.runtime ) that.threads = [];
			return value;
		}
		return new Promise(function(resolve, reject) {
			try {
				let value = that.evloop.next();
				resolve(value);
			} catch ( e ) {
				reject(e);
			}
		}).then(handler).then((v) => v.value);
	}

	runSync() {
		let steps = 0;
		let value = this.evloop.next();
		while ( !value.done ) {
			value = this.evloop.next();
			if ( value.value && value.value.then ) throw new Error('Can\'t deal with futures when running in sync mode');
			if ( ++steps > this.options.executionLimit ) throw new Error('Execution Limit Reached');
		}
		return value.value;
	}



	/**
	 * Refrence to the global scope.
	 * @return {Scope}
	 */
	get globalScope() {
		return this.realm.globalScope;
	}

	addGlobal(name, what, opts) {
		opts = opts || {};
		if ( !(what instanceof Value) ) what = this.realm.makeForForeignObject(what);
		if ( !opts.const ) this.globalScope.add(name, what);
		else this.globalScope.addConst(name, what);
	}

	addGlobalFx(name, what, opts) {
		var x  = EasyNativeFunction.makeForNative(this.realm, what);
		x.makeThisForNew = function*(realm) {
			return Value.null;
		};
		this.addGlobal(name, x, opts);
	}

	addGlobalValue(name, what, opts) {
		this.addGlobal(name, Value.fromNative(what, this.realm), opts);
	}

	addGlobalBridge(name, what, opts) {
		this.addGlobal(name, new BridgeValue(what, this.realm), opts);
	}

	fetchFunctionSync(name, shouldYield) {
		var genfx = this.fetchFunction(name, shouldYield);
		return function() {
			let gen = genfx.apply(this, arguments);
			let val = gen.next();
			//TODO: Make sure we dont await as it will loop FOREVER.
			while (!val.done) val = gen.next();
			return val.value;
		};
	}

	fetchFunction(name, shouldYield) {
		var val = this.globalScope.get(name);
		return this.makeFunctionFromClosure(val, shouldYield);
	}

	functionFromSource(source, shouldYield) {
		let code = source;
		let ast = this.realm.parser(code, {inFunctionBody: true});
		return this.functionFromAST(ast, shouldYield, source);
	}

	functionFromAST(ast, shouldYield, source) {
		if ( ast.type === 'Program' ) ast = ast.body;
		if ( Array.isArray(ast) ) ast = {type: 'BlockStatement', body: ast};
		if ( ast.type !== 'BlockStatement' ) ast = {type: 'BlockStatement', body: [ast]};

		let past = {
			type: 'FunctionExpression',
			body: ast,
			params: []
		};
		past = ASTPreprocessor.process(past, {source: source});
		let fx = new ClosureValue(past, this.globalScope);
		return this.makeFunctionFromClosure(fx, shouldYield, this.evaluator);
	}

	functionFromSourceSync(source, shouldYield) {
		let genfx = this.functionFromSource(source, shouldYield);
		return function() {
			let gen = genfx.apply(this, arguments);
			let val = gen.next();
			//TODO: Make sure we dont await as it will loop FOREVER.
			while (!val.done) val = gen.next();
			return val.value;
		};
	}

	functionFromASTSync(ast, shouldYield, source) {
		let genfx = this.functionFromAST(ast, shouldYield, source);
		return function() {
			let gen = genfx.apply(this, arguments);
			let val = gen.next();
			//TODO: Make sure we dont await as it will loop FOREVER.
			while (!val.done) val = gen.next();
			return val.value;
		};
	}

	makeFunctionFromClosure(val, shouldYield, evalu) {

		var realm = this.realm;
		var scope = this.globalScope;
		var that = this;
		let evaluator = evalu || this.evaluator;
		if ( !evaluator ) throw new Error('Evaluator is falsey');
		if ( !val ) return;

		return function*() {
			var realThis = realm.makeForForeignObject(this);
			var realArgs = new Array(arguments.length);
			for ( let i = 0; i < arguments.length; ++i ) {
				realArgs[i] = realm.makeForForeignObject(arguments[i]);
			}

			if ( !val.isCallable ) {
				throw new TypeError(val.debugStr + ' is not a function.');
			}
			let c = val.call(realThis, realArgs, scope);
			evaluator.pushFrame({type: 'program', generator: c, scope: scope});
			let gen = evaluator.generator();

			let last = yield * that.filterGenerator(gen, shouldYield, evaluator);
			if ( last ) return last.toNative();
		};
	}

	/**
	 * Returns a new engine that executes in the same Realm.  Useful
	 * for creating threads / coroutines
	 * @return {Engine}
	 */
	fork() {
		let engine = new Engine(this.options, this.realm);
		var scope = this.globalScope;
		engine.evaluator = this.makeEvaluatorClone();
		return engine;
	}

	makeEvaluatorClone() {
		let evaluator = new Evaluator(this.realm, this.evaluator.ast, this.globalScope);
		evaluator.frames = [];
		if ( this.evaluator.insturment ) {
			evaluator.insturment = this.evaluator.insturment;
		}
		if ( this.evaluator.debug ) {
			evaluator.debug = true;
		}

		if ( SmartLinkValue.isThreadPrivileged(this.evaluator) ) {
			SmartLinkValue.makeThreadPrivileged(evaluator);
		}

		return evaluator;
	}

	*filterGenerator(gen, shouldYield, evaluator) {
		let value = gen.next();
		let steps = 0;
		if ( !evaluator ) throw new Error('Evaluator is falsey');
		while ( !value.done ) {
			if ( !shouldYield ) yield;
			else if ( evaluator.topFrame.type == 'await' ) {
				if ( !value.value.resolved ) yield;
			} else {
				var yieldValue = shouldYield(this, evaluator, value.value);
				if ( yieldValue !== false ) yield yieldValue;
			}
			value = gen.next(value.value);
			if ( ++steps > this.options.executionLimit ) throw new Error('Execution Limit Reached');
		}
		return value.value;
	}
}

module.exports = Engine;