Home Reference Source

src/Evaluator.js

'use strict';

const Value = require('./Value');
const CompletionRecord = require('./CompletionRecord');
const ClosureValue = require('./values/ClosureValue');
const ObjectValue = require('./values/ObjectValue');
const FutureValue = require('./values/FutureValue');
const RegExpValue = require('./values/RegExpValue');
const PropertyDescriptor = require('./values/PropertyDescriptor');
const ErrorValue = require('./values/ErrorValue');
const ArrayValue = require('./values/ArrayValue');
const EvaluatorInstruction = require('./EvaluatorInstruction');

class Frame {
	constructor(type, o) {
		this.type = type;
		for ( var k in o ) this[k] = o[k];
	}
}

class Evaluator {
	constructor(realm, n, s) {
		this.realm = realm;
		let that = this;
		this.lastValue = null;
		this.ast = n;
		this.defaultYieldPower = 5;
		this.yieldPower = this.defaultYieldPower;
		this.debug = false;
		this.profile = false;
		this.lastASTNodeProcessed = null;
		/**
		 * @type {Object[]}
		 * @property {Generator} generator
		 * @property {string} type
		 * @property {ast} ast
		 */
		this.frames = [];
		if ( n ) this.pushAST(n, s);
	}

	pushAST(n, s) {
		let that = this;
		let gen = n ? this.branch(n, s) : (function*() {
			return yield EvaluatorInstruction.stepMinor;
		})();
		this.pushFrame({generator: gen, type: 'program', scope: s, ast: n});
	}
	processLostFrames(frames) {
		for ( let f of frames ) {
			if ( f.profileName ) {
				this.incrCtr('fxTime', f.profileName, Date.now() - f.entered);
			}
		}
	}
	unwindStack(target, canCrossFxBounds, label) {
		let finallyFrames = [];
		for ( let i = 0; i < this.frames.length; ++i ) {
			let t = this.frames[i].type;
			let match = t == target || (target == 'return' && t == 'function' );
			if ( match && label ) {
				match = label == this.frames[i].label;
			}

			if ( match ) {
				let j = i + 1;
				for (; j < this.frames.length; ++j ) if ( this.frames[j].type != 'finally' ) break;
				let fr = this.frames[j];
				this.processLostFrames(this.frames.splice(0, i + 1));
				this.saveFrameShortcuts();
				Array.prototype.unshift.apply(this.frames, finallyFrames);
				return fr;
			} else if ( target == 'return' && this.frames[i].retValue ) {
				let fr = this.frames[i];
				this.processLostFrames(this.frames.splice(0, i));
				this.saveFrameShortcuts();
				Array.prototype.unshift.apply(this.frames, finallyFrames);
				return fr;
			} else if ( !canCrossFxBounds && this.frames[i].type == 'function' ) {
				break;
			} else if ( t == 'finally' ) {
				finallyFrames.push(this.frames[i]);
			}
		}
		return false;
	}

	next(lastValueOveride) {
		let frames = this.frames;

		//Implement proper tailcalls by hand.
		do {
			let top = frames[0];
			let result;
			//console.log(top.type, top.ast && top.ast.type);

			if ( top.exception ) {
				this.lastValue = top.exception;
				delete top.exception;
			} else if ( top.retValue ) {
				this.lastValue = top.retValue;
				delete top.retValue;
			}

			result = top.generator.next(lastValueOveride || this.lastValue);
			lastValueOveride = undefined;
			let val = result.value;

			if ( val instanceof EvaluatorInstruction ) {
				switch ( val.type ) {
					case 'branch':
						this.branchFrame(val.kind, val.ast, val.scope, val.extra);
						continue;
					case 'getEvaluator':
						//lastValueOveride = this;
						//continue;
						return this.next(this);
					case 'waitForFramePop':
						continue;
					case 'framePushed':
						continue;
					case 'event':
					case 'step':
						if ( this.instrument ) this.instrument(this, val);
						return {done: false, value: val};
				}
			}

			if ( val instanceof CompletionRecord ) {
				this.processCompletionValueMeaning(val);
				this.lastValue = val.value;
				continue;
			}
			//if ( !val ) console.log("Bad val somewhere around", this.topFrame.type);
			if ( this.instrument ) this.instrument(this, val);

			if ( val && val.then ) {
				if ( top && top.type !== 'await' ) {
					this.pushAwaitFrame(val);
				}
				return {done: false, value: val};
			}

			this.lastValue = val;
			if ( result.done ) {
				let lastFrame = this.popFrame();

				if ( lastFrame.profileName ) {
					this.processLostFrames([lastFrame]);
				}

				// Latient values can't cross function calls.
				// Dont do this, and you get coffeescript mode.
				if ( lastFrame.type === 'function' && !lastFrame.returnLastValue ) {
					this.lastValue = Value.undef;
				}

				if ( frames.length === 0 ) {
					if ( this.debug ) {
						this.dumpProfilingInformation();
					}
					if ( this.onCompletion ) this.onCompletion(result.value);
					return {done: true, value: result.value};
				} else continue;
			}
		} while ( false );

		return {done: false, value: this.lastValue};
	}

	processCompletionValueMeaning(val) {
		if ( !(val.value instanceof Value) ) {
			if ( val.value instanceof Error ) {
				throw new Error('Value was an error: ' + val.value.stack);
			} else if ( val.value.type ) {
				switch ( val.value.type ) {
					case "TypeError": val.value = CompletionRecord.makeTypeError(this.realm, val.value.message).value;
				}
			} else {
				throw new Error('Value isnt of type Value, its: ' + val.value.toString());
			}
		}

		switch ( val.type ) {
			case CompletionRecord.CONTINUE:
				if ( this.unwindStack('continue', false, val.target) ) return true;
				throw new Error('Cant find matching loop frame for continue');
			case CompletionRecord.BREAK:
				if ( this.unwindStack('loop', false, val.target) ) return true;
				throw new Error('Cant find matching loop frame for break');
			case CompletionRecord.RETURN:
				let rfr = this.unwindStack('return', false);
				if ( !rfr ) throw new Error('Cant find function bounds.');
				rfr.retValue = val.value;
				return true;
			case CompletionRecord.THROW:
				//TODO: Fix this nonsense:
				let e = val.value.toNative();
				//val.value.native = e;

				let smallStack;
				if ( e && e.stack ) smallStack = e.stack.split(/\n/).slice(0, 4).join('\n');
				let stk = this.buildStacktrace(e).join('\n    ');
				let bestFrame;
				for ( let i = 0; i < this.frames.length; ++i ) {
					if ( this.frames[i].ast ) {
						bestFrame = this.frames[i];
						break;
					}
				}

				if ( val.value instanceof ErrorValue ) {
					if ( this.realm.options.addExtraErrorInfoToStacks && val.value.extra ) {
						stk += '\n-------------';
						for ( let key in val.value.extra ) {
							let vv = val.value.extra[key];
							if ( vv instanceof Value ) stk += `
${key} => ${vv.debugString}`;
							else stk += `
${key} => ${vv}`;
						}
					}
				}

				if ( e instanceof Error ) {
					e.stack = stk;
					if ( smallStack && this.realm.options.addInternalStack ) e.stack += '\n-------------\n' + smallStack;
					if ( bestFrame ) {
						e.range = bestFrame.ast.range;
						e.loc = bestFrame.ast.loc;
					}
				}

				if ( val.value instanceof ErrorValue ) {
					if ( !val.value.has('stack') ) {
						val.value.setImmediate('stack', Value.fromNative(stk));
						val.value.properties['stack'].enumerable = false;
					}
				}

				let tfr = this.unwindStack('catch', true);
				if ( tfr ) {
					tfr.exception = val;
					this.lastValue = val;
					return true;
				}
				let line = -1;
				if ( this.topFrame.ast && this.topFrame.ast.attr) {
					line = this.topFrame.ast.attr.pos.start_line;
				}
				//console.log(this.buildStacktrace(val.value.toNative()));
				let v = val.value.toNative();
				if ( this.onError ) this.onError(v);
				throw v;
			case CompletionRecord.NORMAL:
				return false;
		}
	}

	buildStacktrace(e) {
		let lines = e ? [e.toString()] : [];
		for ( var f of this.frames ) {
			//if ( f.type !== 'function' ) continue;
			if ( f.ast ) {
				let line = 'at ' + (f.ast.srcName || f.ast.type) + ' ';
				if ( f.ast.loc ) line += '(<src>:' + f.ast.loc.start.line + ':' + f.ast.loc.start.column + ')';
				lines.push(line);
			}
		}
		return lines;
	}
	pushFrame(frame) {
		frame.srcAst = frame.ast;
		if ( frame.yieldPower === undefined ) frame.yieldPower = this.defaultYieldPower;
		this.frames.unshift(new Frame(frame.type, frame));
		this.saveFrameShortcuts();
	}

	pushAwaitFrame(val) {
		this.pushFrame({
			generator: (function *(f) {
				while ( !f.resolved ) yield f;
				if ( f.successful ) {
					return f.value;
				} else {
					return new CompletionRecord(CompletionRecord.THROW, f.value);
				}
			})(val),
			type: 'await'
		});
	}

	popFrame() {
		let frame = this.frames.shift();
		this.saveFrameShortcuts();
		return frame;
	}

	saveFrameShortcuts() {
		let prev = this.yieldPower;
		if ( this.frames.length == 0 ) {
			this.topFrame = undefined;
			this.yieldPower = this.defaultYieldPower;
		} else {
			this.topFrame = this.frames[0];
			this.yieldPower = this.topFrame.yieldPower;
		}
	}

	fromNative(native, x) {
		return this.realm.fromNative(native, x);
	}

	generator() {
		return {
			next: this.next.bind(this), 
			throw: (e) => { throw e; },
			evaluator: this
		};
	}

	breakFrames() {

	}


	*resolveRef(n, s, create) {
		let oldAST = this.topFrame.ast;
		this.topFrame.ast = n;
		switch (n.type) {
			case 'Identifier':
				let iref = s.ref(n.name, s.realm);
				if ( !iref ) {
					iref = {
						getValue: function*() {
							let err = CompletionRecord.makeReferenceError(s.realm, `${n.name} is not defined`);
							yield * err.addExtra({code: 'UndefinedVariable', when: 'read', ident: n.name, strict: s.strict});
							return yield err;
						},
						del: function() {
							return true;
						}
					};
					if ( !create || s.strict ) {
						iref.setValue = function *() {
							let err = CompletionRecord.makeReferenceError(s.realm, `${n.name} is not defined`);
							yield * err.addExtra({code: 'UndefinedVariable', when: 'write', ident: n.name, strict: s.strict});
							return yield err;
						};
					} else {
						iref.setValue = function *(value) {
							s.global.set(n.name, value, s);
							let aref = s.global.ref(n.name, s);
							this.setValue = aref.setValue;
							this.getValue = aref.getValue;
							this.del = aref.delete;
						};
					}
				}
				this.topFrame.ast = oldAST;
				return iref;
			case 'MemberExpression':
				let idx;
				let ref = yield * this.branch(n.object, s);
				if ( n.computed ) {
					idx = (yield * this.branch(n.property, s)).toNative();
				} else {
					idx = n.property.name;
				}

				if ( !ref ) {
					return yield CompletionRecord.makeTypeError(s.realm, `Can't write property of undefined: ${idx}`);
				}

				if ( !ref.ref ) {
					return yield CompletionRecord.makeTypeError(s.realm, `Can't write property of non-object type: ${idx}`);
				}

				this.topFrame.ast = oldAST;
				return ref.ref(idx, s);

			default:
				return yield CompletionRecord.makeTypeError(s.realm, `Couldnt resolve ref component: ${n.type}`);
		}
	}

	*partialMemberExpression(left, n, s) {
		if ( n.computed ) {
			let right = yield * this.branch(n.property, s);
			return yield * left.get(right.toNative(), s.realm);
		} else if ( n.property.type == 'Identifier') {
			if ( !left ) throw `Cant index ${n.property.name} of undefined`;
			return yield * left.get(n.property.name, s.realm);
		} else {
			if ( !left ) throw `Cant index ${n.property.value.toString()} of undefined`;
			return yield * left.get(n.property.value.toString(), s.realm);
		}
	}

	//NOTE: Returns generator, fast return yield *;
	doBinaryEvaluation(operator, left, right, realm) {
		switch ( operator ) {
			case '==': return left.doubleEquals(right, realm);
			case '!=': return left.notEquals(right, realm);
			case '===': return left.tripleEquals(right, realm);
			case '!==': return left.doubleNotEquals(right, realm);
			case '+': return left.add(right, realm);
			case '-': return left.subtract(right, realm);
			case '*': return left.multiply(right, realm);
			case '/': return left.divide(right, realm);
			case '%': return left.mod(right, realm);
			case '|': return left.bitOr(right, realm);
			case '^': return left.bitXor(right, realm);
			case '&': return left.bitAnd(right, realm);
			case 'in': return right.inOperator(left, realm);
			case 'instanceof': return left.instanceOf(right, realm);
			case '>': return left.gt(right, realm);
			case '<': return left.lt(right, realm);
			case '>=': return left.gte(right, realm);
			case '<=': return left.lte(right, realm);
			case '<<': return left.shiftLeft(right, realm);
			case '>>': return left.shiftRight(right, realm);
			case '>>>': return left.shiftRightZF(right, realm);
			case '**': return left.pow(right, realm);
			default:
				throw new Error('Unknown binary operator: ' + operator);
		}
	}

	branchFrame(type, n, s, extra) {
		let frame = {generator: this.branch(n, s), type: type, scope: s, ast: n};

		if ( extra ) {
			for ( var k in extra ) {
				frame[k] = extra[k];
			}
			if ( extra.profileName ) {
				frame.entered = Date.now();
			}
		}
		this.pushFrame(frame);
		return EvaluatorInstruction.framePushed;
	}

	beforeNode(n) {
		let tf = this.topFrame;
		let state = {top: tf, ast: tf.ast, node: n};
		this.lastASTNodeProcessed = n;
		if ( this.debug ) this.incrCtr('astInvocationCount', n.type);
		tf.ast = n;
		return state;
	}

	afterNode(state, r) {
		let tf = this.topFrame;
		tf.value = r;
		tf.ast = state.ast;
	}

	/**
	 * @private
	 * @param {object} n - AST Node to dispatch
	 * @param {Scope} s - Current evaluation scope
	 */
	branch(n, s) {
		if ( !n.dispatch ) {
			let nextStep = this.findNextStep(n.type);

			n.dispatch = function*(that, n, s) {
				let state = that.beforeNode(n);

				let result = yield * nextStep(that, n, s);
				if ( result instanceof CompletionRecord ) result = yield result;
				if ( result && result.then ) result = yield result;

				that.afterNode(state, result);

				return result;
			};
		}
		return n.dispatch(this, n, s);
	}

	incrCtr(n, c, v) {
		if ( v === undefined ) v = 1;
		if ( !this.profile ) this.profile = {};
		let o = this.profile[n];
		if ( !o ) {
			o = {};
			this.profile[n] = o;
		}
		c = c || '???';
		if ( c in o ) o[c] += v;
		else o[c] = v;
	}


	dumpProfilingInformation() {
		function lpad(s, l) { return s + new Array(Math.max(l - s.length, 1)).join(' '); }

		if ( !this.profile ) {
			console.log('===== Profile: None collected =====');
			return;
		}

		console.log('===== Profile =====');
		for ( let  sec in this.profile ) {
			console.log(sec + ' Stats:');
			let o = this.profile[sec];
			let okeys = Object.keys(o).sort((a,b) => o[b] - o[a]);
			for ( let name of okeys ) {
				console.log(`  ${lpad(name, 20)}: ${o[name]}`);
			}
		}
		console.log('=================');
	}

	get insterment() { return this.instrument; }
	set insterment(v) { this.instrument = v; }
}

Evaluator.prototype.findNextStep = require('./EvaluatorHandlers').findNextStep;

module.exports = Evaluator;