diff --git a/interpreter.js b/interpreter.js index fefc311..00b0251 100644 --- a/interpreter.js +++ b/interpreter.js @@ -433,6 +433,59 @@ Interpreter.prototype.run = function() { return this.paused_; }; +/** + * Queue a pseudo function for execution on next step + * @param {Interpreter.Object} func Interpreted function + * @param {Interpreter.Object} funcThis Interpreted Object to use as "this" + * @param {Interpreter.Object} var_args Interpreted Objects to pass as arguments + * @return {Interpreter.Callback} Object for running pseudo function callback + */ +Interpreter.prototype.callFunction = function (func, funcThis, var_args) { + var expNode = this.buildFunctionCaller_.apply(this, arguments); + return new Interpreter.Callback(expNode); +}; + +/** + * Queue a pseudo function for execution after all current instructions complete + * @param {Interpreter.Object} func Interpreted function + * @param {Interpreter.Object} funcThis Interpreted Object to use as "this" + * @param {Interpreter.Object} var_args Interpreted Objects to pass as arguments + * @return {Interpreter.Callback} Object for running pseudo function callback + */ +Interpreter.prototype.queueFunction = function (func, funcThis, var_args) { + var state = this.stateStack[0]; + var expNode = this.buildFunctionCaller_.apply(this, arguments); + // Add function call to root Program state + state.node['body'].push(expNode); + state.done = false; + return new Interpreter.Callback(expNode, true); // Allows adding then/catch +}; + +/** + * Generate state objects for running pseudo function + * @param {Interpreter.Object} func Interpreted function + * @param {Interpreter.Object} funcThis Interpreted Object to use as "this" + * @param {Interpreter.Object} var_args Interpreted Objects to pass as arguments + * @return {nodeConstructor} node for running pseudo function + */ +Interpreter.prototype.buildFunctionCaller_ = function (func, funcThis, var_args) { + var thisInterpreter = this + var args = Array.prototype.slice.call(arguments, 2).map(function (arg) { + return arg instanceof Interpreter.Object ? arg : thisInterpreter.nativeToPseudo(arg) + }); + // Create node for CallExpression with pre-embedded function and arguments + var scope = this.stateStack[this.stateStack.length - 1].scope; // This may be wrong + var ceNode = new this.nodeConstructor({options:{}}); + ceNode['type'] = 'CallExpressionFunc_'; + // Attach state settings to node, so we can retrieve them later. + // (Can only add a node to root program body.) + ceNode.funcThis_ = funcThis; + ceNode.func_ = func; + ceNode.arguments_ = args; + ceNode.scope_ = scope; + return ceNode; +}; + /** * Initialize the global object with buitin properties and functions. * @param {!Interpreter.Object} globalObject Global object. @@ -3142,6 +3195,48 @@ Interpreter.prototype.throwException = function(errorClass, opt_message) { throw Interpreter.STEP_ERROR; }; +/** + * Return a Throwable object for use in native function return values + * @param {!Interpreter.Object|Interpreter.Value} errorClass Type of error + * (if message is provided) or the value to throw (if no message). + * @param {string=} opt_message Message being thrown. + */ +Interpreter.prototype.createThrowable = function(errorClass, opt_message) { + return new Interpreter.Throwable(errorClass, opt_message); +}; + +/** + * Handle result of native function call + * @param {Interpreter.State} state CallExpression state + * @param {!Interpreter.Scope} scope CallExpression scope. + * @param {Interpreter.Object|String|Number} value Values returned from native function + * @return {Interpreter.State} New callback state added, if any + */ +Interpreter.prototype.handleNativeResult_ = function(state, scope, value) { + if (value instanceof Interpreter.Callback) { + // We have a request for a pseudo function callback + value.pushState_(this, scope); + state.cb_ = value; + state.doneExec_ = false; + return value.state_; + } else if (value instanceof Interpreter.Throwable) { + // Result was an error + value.throw_(this); + } else { + // We have a final value + var cb = state.cb_; + if (cb) { + if (cb.stateless_) { + cb.value = value; + } else { + cb.state_.value = value; + } + } + state.value = value; + } +}; + + /** * Unwind the stack to the innermost relevant enclosing TryStatement, * For/ForIn/WhileStatement or Call/NewExpression. If this results in @@ -3171,6 +3266,13 @@ Interpreter.prototype.unwind = function(type, value, label) { throw Error('Unsynatctic break/continue not rejected by Acorn'); } break; + case 'CallExpressionFunc_': + if (type === Interpreter.Completion.THROW && state.catch_) { + // Let stepCallExpressionFunc_ catch this throw + state.throw_ = value; + return; + } + continue; case 'Program': // Don't pop the stateStack. // Leave the root scope on the tree in case the program is appended to. @@ -3349,6 +3451,104 @@ Interpreter.Scope = function(parentScope, strict, object) { this.object = object; }; +/** + * Class for allowing async function throws + * @param {!Interpreter.Object|Interpreter.Value} errorClass Type of error + * (if message is provided) or the value to throw (if no message). + * @param {string=} opt_message Message being thrown. + * @constructor + */ +Interpreter.Throwable = function(errorClass, opt_message) { + this.errorClass = errorClass; + this.opt_message = opt_message; +}; + +/** + * Class for allowing async function throws + * @param {Interpreter} interpreter Interpreter instance + * @constructor + */ +Interpreter.Throwable.prototype.throw_ = function(interpreter) { + interpreter.throwException(this.errorClass, this.opt_message); +}; + + +/** + * Class for tracking native function states. + * @param {nodeConstructor} callFnState State that's being tracked + * @param {Boolean=} opt_queued Will this be queued or immediate + * @constructor + */ +Interpreter.Callback = function(callFnNode, opt_queued) { + this.node_ = callFnNode; + this.handlers_ = []; + this.catch_ = null; + this.node_.cb_ = this; // Attach callback to node so we can retrieve it later + this.queued_ = opt_queued; +}; + +/** + * Add handler for return of pseudo function's value + * @param {Function} handler Function to handle value + * @return {Interpreter.Callback} State object for running pseudo function + */ +Interpreter.Callback.prototype['then'] = function(handler) { + if (typeof handler !== 'function') { + throw new Error('Expected function for "then" handler'); + } + this.handlers_.push(handler); + return this; +}; + +/** + * Add exception catch handler for return of pseudo function's call + * @param {Function} handler Function to handle value + * @return {Interpreter.Callback} State object for running pseudo function + */ +Interpreter.Callback.prototype['catch'] = function(handler) { + if (typeof handler !== 'function') { + throw new Error('Expected function for "catch" handler'); + } + if (this.catch_) { + throw new Error('"catch" already defined'); + } + // this.node_.skipThrow_ = true; + this.catch_ = handler; + return this; +}; + +/** + * Add pseudo function callback to statStack + * @param {Interpreter} interpreter Interpreter instance + * @param {Interpreter.Scope} scope Function's scope. + */ +Interpreter.Callback.prototype.pushState_ = function(interpreter, scope) { + if (this.stateless_) return; // Only uses handler_ + if (!this.state_) { + this.state_ = new Interpreter.State(this.node_, scope); + } + interpreter.stateStack.push(this.state_); +}; + +/** + * Handle next step for native function callback + * @param {Interpreter.Object} value Object containing pseudo function callback's result. + * @param {Function} asyncCallback Function for asyncFunc callback + */ +Interpreter.Callback.prototype.doNext_ = function(asyncCallback) { + var handler = this.handlers_.shift(); + if (handler) { + this.force_ = this.handlers_.length; // Continue to run if we have more + return handler(this.stateless_ ? this.value : this.state_.value, asyncCallback); + } + if (this.stateless_) return; // Callback does not have a state value + if(asyncCallback) { + asyncCallback(this.state_.value) + } else { + return this.state_.value + } +}; + /** * Class for an object. * @param {Interpreter.Object} proto Prototype object or null. @@ -3712,6 +3912,11 @@ Interpreter.prototype['stepCallExpression'] = function(stack, state, node) { } state.doneArgs_ = true; } + if (state.cb_ && state.cb_.force_) { + // Callback wants to run again + state.doneExec_ = false; + state.cb_.force_ = false; + } if (!state.doneExec_) { state.doneExec_ = true; if (!(func instanceof Interpreter.Object)) { @@ -3774,19 +3979,27 @@ Interpreter.prototype['stepCallExpression'] = function(stack, state, node) { if (!state.scope.strict) { state.funcThis_ = this.boxThis_(state.funcThis_); } - state.value = func.nativeFunc.apply(state.funcThis_, state.arguments_); + this.handleNativeResult_(state, scope, state.cb_ + ? state.cb_.doNext_() + : func.nativeFunc.apply(state.funcThis_, state.arguments_)); + return; } else if (func.asyncFunc) { var thisInterpreter = this; var callback = function(value) { - state.value = value; thisInterpreter.paused_ = false; + thisInterpreter.handleNativeResult_(state, scope, value); }; + this.paused_ = true; + if (state.cb_) { + // Do next step of native async func + state.cb_.doNext_(callback); + return; + } // Force the argument lengths to match, then append the callback. var argLength = func.asyncFunc.length - 1; var argsWithCallback = state.arguments_.concat( new Array(argLength)).slice(0, argLength); argsWithCallback.push(callback); - this.paused_ = true; if (!state.scope.strict) { state.funcThis_ = this.boxThis_(state.funcThis_); } @@ -3887,6 +4100,53 @@ Interpreter.prototype['stepEvalProgram_'] = function(stack, state, node) { stack[stack.length - 1].value = this.value; }; +Interpreter.prototype['stepCallExpressionFunc_'] = function(stack, state, node) { + var cb = node.cb_; + var queued = cb.queued_; + if (!state.done_) { + state.done_ = true; + var ceNode = new this.nodeConstructor({options:{}}); + ceNode['type'] = 'CallExpression'; + var ceState = new Interpreter.State(ceNode, node.scope_ || state.scope); + ceState.doneCallee_ = true; + ceState.funcThis_ = node.funcThis_; + ceState.func_ = node.func_; + ceState.doneArgs_ = true; + ceState.arguments_ = node.arguments_; + state.catch_ = cb.catch_; + return ceState; + } + if (queued && cb.handlers_.length && !state.throw_) { + // Called via queued callback + // Callback a 'then' handler now (non-queued are called in setCallExpression) + var handler = cb.handlers_.shift(); + this.handleNativeResult_(state, node.funcThis_, handler(state.value)); + return; + } + if (state.catch_ && state.throw_) { + // Callback a 'catch' handler + if (queued) { + // Called via queued callback + // Just call the callback directly + this.handleNativeResult_(state, node.funcThis_, state.catch_(state.throw_)); + state.catch_ = null; + return; + } else { + // Immediate callback from CallExpression + // Modify existing Callback object to execute catch steps + cb.stateless_ = true; // Callback can only use its handler for return value + cb.handlers_ = [state.catch_]; + cb.value = state.throw_; // Set stateless value to pass to handler + cb.force_ = true; // Force callback to run again + } + } + stack.pop(); + if (this.stateStack.length === 1) { + // Save value as return value if we were the last to be executed + this.value = state.value; + } +}; + Interpreter.prototype['stepExpressionStatement'] = function(stack, state, node) { if (!state.done_) { this.value = undefined; @@ -4536,3 +4796,6 @@ Interpreter.prototype['getGlobalScope'] = Interpreter.prototype.getGlobalScope; Interpreter.prototype['getStateStack'] = Interpreter.prototype.getStateStack; Interpreter.prototype['setStateStack'] = Interpreter.prototype.setStateStack; Interpreter['VALUE_IN_DESCRIPTOR'] = Interpreter.VALUE_IN_DESCRIPTOR; +Interpreter.prototype['callFunction'] = Interpreter.prototype.callFunction; +Interpreter.prototype['queueFunction'] = Interpreter.prototype.queueFunction; +Interpreter.prototype['createThrowable'] = Interpreter.prototype.createThrowable;