From 67356d904ffd5f772d050a2b90a88a82b100b728 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Tue, 10 Nov 2020 14:05:28 -0600 Subject: [PATCH 01/63] Add methods for calling interpreted functions --- interpreter.js | 122 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/interpreter.js b/interpreter.js index a9709e2c..3b12cef7 100644 --- a/interpreter.js +++ b/interpreter.js @@ -353,6 +353,102 @@ Interpreter.prototype.run = function() { return this.paused_; }; +/** + * Call an interpreted function, returning result value if run immediately + * @param {Interpreter.Object} fn Interpreted function + * @param {Interpreter.Object} thisFn Interpreted Object to use a "this" + * @param {Boolean} [immediate=false] Execute immediately, returning result, or queue for later + * @param {Interpreter.Object} var_args Interpreted Objects to pass as arguments + */ +Interpreter.prototype.callFunction = function ( + fn, + thisFn, + immediate, + var_args +) { + const args = Array.prototype.slice.call(arguments, 3, arguments.length); + const callbackObject = this.getProperty(this.globalObject, this.CALLBACK_KEY); + // const argsArray = this.getProperty(callbackObject, 'args'); + const argsArray = this.createArray(); + for (let i = 0, l = args.length; i < l; i++) { + argsArray.properties[i] = args[i]; + } + argsArray.properties.length = args.length; + this.setProperty(callbackObject, "fn", fn); + this.setProperty(callbackObject, "thisFn", thisFn); + this.setProperty(callbackObject, "args", argsArray); + const currentIndex = this.stateStack.length; + this.appendScopedCode(this.CALLBACK_AST, this.getScope(), immediate); + if (!immediate) return; + const origValue = this.value; + const origPaused = this.paused_; + this.value = undefined; + let lastValue = undefined; + this.paused_ = false; + while ( + !this.paused_ && + this.stateStack.length > currentIndex && + this.step() + ) { + if (this.value !== undefined) { + lastValue = this.value; + } + } + this.value = origValue; + this.paused_ = origPaused; + return lastValue; +}; + +/** + * Convenience method to pre-parse raw JavaScript to AST + * @param {string|!Object} code Raw JavaScript text or AST. + */ +Interpreter.prototype.parseCode = function (code) { + if (typeof code === "string") { + code = acorn.parse(code, Interpreter.PARSE_OPTIONS); + } + if (!code || code["type"] !== "Program") { + throw Error("Expecting new AST to start with a Program node."); + } + return code; +}; + +/** + * Add pre-scoped code to the interpreter. + * Allows a previously stored scope to be applied to new code. + * @param {string|!Object} code Raw JavaScript text or AST. + * @param {Interpreter.Scope} scope Scope to run the code in. + * @param {Boolean} [immediate=false] Queue as next or last + */ +Interpreter.prototype.appendScopedCode = function (code, scope, immediate) { + if (typeof code === "string") { + code = acorn.parse(code, Interpreter.PARSE_OPTIONS); + } + const type = code && code.type; + if (!type) { + throw Error("Expecting AST"); + } + this.nodeConstructor = code.constructor; + // Clone the root 'Program' node so that the AST may be modified. + var ast = new this.nodeConstructor({ options: {} }); + for (var prop in code) { + ast[prop] = Array.isArray(prop) ? code[prop].slice() : code[prop]; + } + this.populateScope_(ast, scope); + if (type === "Program") ast.type = "BlockStatement"; // Convert program to block statement + const scopedState = new Interpreter.State(ast, scope); + scopedState.scope = scope; + console.log("scopedState", scopedState, this.stateStack); + if (immediate) { + // Queue as next item to execute + this.stateStack.push(scopedState); + } else { + // Queue as last item to execute + this.stateStack.splice(1, 0, scopedState); + } +}; + + /** * Initialize the global object with buitin properties and functions. * @param {!Interpreter.Object} globalObject Global object. @@ -393,6 +489,7 @@ Interpreter.prototype.initGlobal = function(globalObject) { this.initError(globalObject); this.initMath(globalObject); this.initJSON(globalObject); + this.initCallbackManager(globalObject); // Initialize global functions. var thisInterpreter = this; @@ -446,6 +543,31 @@ Interpreter.prototype.initGlobal = function(globalObject) { } }; +/** + * Initialize callback manager + * @param {!Interpreter.Object} globalObject Global object. + */ +Interpreter.prototype.initCallbackManager = function (globalObject) { + const key = "_JSICallback"; + this.CALLBACK_KEY = key; + this.CALLBACK_AST = this.parseCode(key + ".run();"); + // Interpreted object for handling callbacks + this.polyfills_.push( + key + " = {", + " fn: null,", + " args: null,", + " thisFn: null,", + " run: function() {", + " var result = " + key + ".fn.apply(" + key + ".thisFn, " + key + ".args);", + " " + key + ".fn = null;", + " " + key + ".args = null;", + " " + key + ".thisFn = null;", + " return result;", + " }", + "}" + ); +}; + /** * Initialize the Function class. * @param {!Interpreter.Object} globalObject Global object. From a1122eb08aad6bd16a3b4af28365acb30019eab3 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Wed, 11 Nov 2020 08:27:15 -0600 Subject: [PATCH 02/63] Remove ES6 --- interpreter.js | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/interpreter.js b/interpreter.js index 3b12cef7..f4a9297e 100644 --- a/interpreter.js +++ b/interpreter.js @@ -366,10 +366,13 @@ Interpreter.prototype.callFunction = function ( immediate, var_args ) { - const args = Array.prototype.slice.call(arguments, 3, arguments.length); - const callbackObject = this.getProperty(this.globalObject, this.CALLBACK_KEY); + if (this.paused_) { + throw new Error('Unable to call function while in paused state') + } + var args = Array.prototype.slice.call(arguments, 3, arguments.length); + var callbackObject = this.getProperty(this.globalObject, this.CALLBACK_KEY); // const argsArray = this.getProperty(callbackObject, 'args'); - const argsArray = this.createArray(); + var argsArray = this.createArray(); for (let i = 0, l = args.length; i < l; i++) { argsArray.properties[i] = args[i]; } @@ -377,26 +380,19 @@ Interpreter.prototype.callFunction = function ( this.setProperty(callbackObject, "fn", fn); this.setProperty(callbackObject, "thisFn", thisFn); this.setProperty(callbackObject, "args", argsArray); - const currentIndex = this.stateStack.length; + var currentIndex = this.stateStack.length; this.appendScopedCode(this.CALLBACK_AST, this.getScope(), immediate); if (!immediate) return; - const origValue = this.value; - const origPaused = this.paused_; + var origValue = this.value; this.value = undefined; - let lastValue = undefined; - this.paused_ = false; while ( !this.paused_ && this.stateStack.length > currentIndex && this.step() - ) { - if (this.value !== undefined) { - lastValue = this.value; - } - } + ) {} + var result = this.value; this.value = origValue; - this.paused_ = origPaused; - return lastValue; + return result; }; /** @@ -424,7 +420,7 @@ Interpreter.prototype.appendScopedCode = function (code, scope, immediate) { if (typeof code === "string") { code = acorn.parse(code, Interpreter.PARSE_OPTIONS); } - const type = code && code.type; + var type = code && code.type; if (!type) { throw Error("Expecting AST"); } @@ -436,7 +432,7 @@ Interpreter.prototype.appendScopedCode = function (code, scope, immediate) { } this.populateScope_(ast, scope); if (type === "Program") ast.type = "BlockStatement"; // Convert program to block statement - const scopedState = new Interpreter.State(ast, scope); + var scopedState = new Interpreter.State(ast, scope); scopedState.scope = scope; console.log("scopedState", scopedState, this.stateStack); if (immediate) { @@ -548,7 +544,7 @@ Interpreter.prototype.initGlobal = function(globalObject) { * @param {!Interpreter.Object} globalObject Global object. */ Interpreter.prototype.initCallbackManager = function (globalObject) { - const key = "_JSICallback"; + var key = "_JSICallback"; this.CALLBACK_KEY = key; this.CALLBACK_AST = this.parseCode(key + ".run();"); // Interpreted object for handling callbacks @@ -4141,4 +4137,5 @@ Interpreter.prototype['createNativeFunction'] = Interpreter.prototype['getProperty'] = Interpreter.prototype.getProperty; Interpreter.prototype['setProperty'] = Interpreter.prototype.setProperty; Interpreter.prototype['nativeToPseudo'] = Interpreter.prototype.nativeToPseudo; -Interpreter.prototype['pseudoToNative'] = Interpreter.prototype.pseudoToNative; +Interpreter.prototype['pseudoToNative'] = Interpreter.prototype.pseudoToNative;dCode; +Interpreter.prototype['callFunction'] = Interpreter.prototype.callFunction; From 27cc69699dfc03d84173c4595feb5928704aeae5 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Wed, 11 Nov 2020 08:30:49 -0600 Subject: [PATCH 03/63] Don't allow immediate callFunction when paused --- interpreter.js | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/interpreter.js b/interpreter.js index f4a9297e..12a5923c 100644 --- a/interpreter.js +++ b/interpreter.js @@ -360,20 +360,15 @@ Interpreter.prototype.run = function() { * @param {Boolean} [immediate=false] Execute immediately, returning result, or queue for later * @param {Interpreter.Object} var_args Interpreted Objects to pass as arguments */ -Interpreter.prototype.callFunction = function ( - fn, - thisFn, - immediate, - var_args -) { - if (this.paused_) { - throw new Error('Unable to call function while in paused state') +Interpreter.prototype.callFunction = function (fn, thisFn, immediate, var_args) { + if (this.paused_ && immediate) { + throw new Error("Unable to call pseudo function immediately when paused."); } var args = Array.prototype.slice.call(arguments, 3, arguments.length); var callbackObject = this.getProperty(this.globalObject, this.CALLBACK_KEY); // const argsArray = this.getProperty(callbackObject, 'args'); var argsArray = this.createArray(); - for (let i = 0, l = args.length; i < l; i++) { + for (var i = 0, l = args.length; i < l; i++) { argsArray.properties[i] = args[i]; } argsArray.properties.length = args.length; @@ -4137,5 +4132,4 @@ Interpreter.prototype['createNativeFunction'] = Interpreter.prototype['getProperty'] = Interpreter.prototype.getProperty; Interpreter.prototype['setProperty'] = Interpreter.prototype.setProperty; Interpreter.prototype['nativeToPseudo'] = Interpreter.prototype.nativeToPseudo; -Interpreter.prototype['pseudoToNative'] = Interpreter.prototype.pseudoToNative;dCode; -Interpreter.prototype['callFunction'] = Interpreter.prototype.callFunction; +Interpreter.prototype['pseudoToNative'] = Interpreter.prototype.pseudoToNative; From c85abaeeb8168c3b34c78d5bc4199e8a930d689c Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Wed, 11 Nov 2020 08:32:44 -0600 Subject: [PATCH 04/63] Remove debug --- interpreter.js | 1 - 1 file changed, 1 deletion(-) diff --git a/interpreter.js b/interpreter.js index 12a5923c..555a1883 100644 --- a/interpreter.js +++ b/interpreter.js @@ -429,7 +429,6 @@ Interpreter.prototype.appendScopedCode = function (code, scope, immediate) { if (type === "Program") ast.type = "BlockStatement"; // Convert program to block statement var scopedState = new Interpreter.State(ast, scope); scopedState.scope = scope; - console.log("scopedState", scopedState, this.stateStack); if (immediate) { // Queue as next item to execute this.stateStack.push(scopedState); From e028f9a4ff31f5e9056efb5e57a0484a8f21e61a Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Wed, 11 Nov 2020 08:53:14 -0600 Subject: [PATCH 05/63] Simplify callback polyfill --- interpreter.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/interpreter.js b/interpreter.js index 555a1883..798f1bc4 100644 --- a/interpreter.js +++ b/interpreter.js @@ -548,10 +548,11 @@ Interpreter.prototype.initCallbackManager = function (globalObject) { " args: null,", " thisFn: null,", " run: function() {", - " var result = " + key + ".fn.apply(" + key + ".thisFn, " + key + ".args);", - " " + key + ".fn = null;", - " " + key + ".args = null;", - " " + key + ".thisFn = null;", + " var obj = " + key +";", + " var result = obj.fn.apply(obj.thisFn, obj.args);", + " obj.fn = null;", + " obj.args = null;", + " obj.thisFn = null;", " return result;", " }", "}" From 8949d78ca5d03c931b7fb5160fb6a59db5bb0686 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Wed, 11 Nov 2020 09:03:29 -0600 Subject: [PATCH 06/63] Add exports --- interpreter.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/interpreter.js b/interpreter.js index 798f1bc4..805ff5cc 100644 --- a/interpreter.js +++ b/interpreter.js @@ -4133,3 +4133,6 @@ Interpreter.prototype['getProperty'] = Interpreter.prototype.getProperty; Interpreter.prototype['setProperty'] = Interpreter.prototype.setProperty; Interpreter.prototype['nativeToPseudo'] = Interpreter.prototype.nativeToPseudo; Interpreter.prototype['pseudoToNative'] = Interpreter.prototype.pseudoToNative; +Interpreter.prototype['callFunction'] = Interpreter.prototype.callFunction; +Interpreter.prototype['parseCode'] = Interpreter.prototype.parseCode; +Interpreter.prototype['appendScopedCode'] = Interpreter.prototype.appendScopedCode; From b863237dd9b47e04f1a5442afeda201146644e52 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Wed, 11 Nov 2020 09:09:57 -0600 Subject: [PATCH 07/63] Cleanup --- interpreter.js | 1 - 1 file changed, 1 deletion(-) diff --git a/interpreter.js b/interpreter.js index 805ff5cc..41b17ca6 100644 --- a/interpreter.js +++ b/interpreter.js @@ -366,7 +366,6 @@ Interpreter.prototype.callFunction = function (fn, thisFn, immediate, var_args) } var args = Array.prototype.slice.call(arguments, 3, arguments.length); var callbackObject = this.getProperty(this.globalObject, this.CALLBACK_KEY); - // const argsArray = this.getProperty(callbackObject, 'args'); var argsArray = this.createArray(); for (var i = 0, l = args.length; i < l; i++) { argsArray.properties[i] = args[i]; From 4d01f8e1aca8a24de1c3f24a69ce39c4ff1a9de3 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Wed, 11 Nov 2020 10:48:34 -0600 Subject: [PATCH 08/63] Fi issue with non-immediate callbacks --- interpreter.js | 95 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 57 insertions(+), 38 deletions(-) diff --git a/interpreter.js b/interpreter.js index 41b17ca6..fe632c75 100644 --- a/interpreter.js +++ b/interpreter.js @@ -364,29 +364,7 @@ Interpreter.prototype.callFunction = function (fn, thisFn, immediate, var_args) if (this.paused_ && immediate) { throw new Error("Unable to call pseudo function immediately when paused."); } - var args = Array.prototype.slice.call(arguments, 3, arguments.length); - var callbackObject = this.getProperty(this.globalObject, this.CALLBACK_KEY); - var argsArray = this.createArray(); - for (var i = 0, l = args.length; i < l; i++) { - argsArray.properties[i] = args[i]; - } - argsArray.properties.length = args.length; - this.setProperty(callbackObject, "fn", fn); - this.setProperty(callbackObject, "thisFn", thisFn); - this.setProperty(callbackObject, "args", argsArray); - var currentIndex = this.stateStack.length; - this.appendScopedCode(this.CALLBACK_AST, this.getScope(), immediate); - if (!immediate) return; - var origValue = this.value; - this.value = undefined; - while ( - !this.paused_ && - this.stateStack.length > currentIndex && - this.step() - ) {} - var result = this.value; - this.value = origValue; - return result; + return this.addCallback.apply(this, arguments); }; /** @@ -537,24 +515,65 @@ Interpreter.prototype.initGlobal = function(globalObject) { * @param {!Interpreter.Object} globalObject Global object. */ Interpreter.prototype.initCallbackManager = function (globalObject) { - var key = "_JSICallback"; - this.CALLBACK_KEY = key; - this.CALLBACK_AST = this.parseCode(key + ".run();"); + var thisInterpreter = this; + this.CALLBACK_QUEUE = []; + var name = "_JSICallback"; + var callbackAst = this.parseCode(name + ".run();"); + this.addCallback = function (fn, thisFn, immediate, var_args) { + var args = Array.prototype.slice.call(arguments, 3, arguments.length); + var callback = this.createObjectProto(this.OBJECT_PROTO); + var argsArray = this.createArray(); + for (var i = 0, l = args.length; i < l; i++) { + var arg = args[i]; + if (arg instanceof Interpreter.Object) { + argsArray.properties[i] = arg; + } else { + argsArray.properties[i] = this.nativeToPseudo(arg); + } + } + argsArray.properties.length = args.length; + this.setProperty(callback, "fn", fn); + this.setProperty(callback, "thisFn", thisFn); + this.setProperty(callback, "args", argsArray); + if (immediate) { + this.CALLBACK_QUEUE.push(callback); + // We want to run function immediately and get its result + var currentIndex = this.stateStack.length; + this.appendScopedCode(callbackAst, this.getScope(), true); + var origValue = this.value; + this.value = undefined; + while ( + !this.paused_ && + this.stateStack.length > currentIndex && + this.step() + ) {} + var result = this.value; + this.value = origValue; + return result; + } else { + // Queue for later, first in, last out + this.CALLBACK_QUEUE.unshift(callback); + this.appendScopedCode(callbackAst, this.getScope(), true); + } + } + var callbackManager = this.createObjectProto(this.OBJECT_PROTO); + this.setProperty(globalObject, name, callbackManager); + + this.setProperty(callbackManager, "next", + this.createNativeFunction(function() { + return thisInterpreter.CALLBACK_QUEUE.pop(); + }, false), + Interpreter.NONENUMERABLE_DESCRIPTOR); + // Interpreted object for handling callbacks this.polyfills_.push( - key + " = {", - " fn: null,", - " args: null,", - " thisFn: null,", - " run: function() {", - " var obj = " + key +";", - " var result = obj.fn.apply(obj.thisFn, obj.args);", - " obj.fn = null;", - " obj.args = null;", - " obj.thisFn = null;", - " return result;", + name + ".run = function() {", + " var callback = " + name +".next();", + " if (callback) {", + " return callback.fn.apply(callback.thisFn, callback.args);", " }", - "}" + " console.warn('No callbacks found.');", + "};" ); }; From 6f59cd630c42de8b335c18904ec6f4470b2326da Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Wed, 11 Nov 2020 13:05:06 -0600 Subject: [PATCH 09/63] Fix unwind issue with non-immediate callbacks --- interpreter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interpreter.js b/interpreter.js index fe632c75..b4cfa0d1 100644 --- a/interpreter.js +++ b/interpreter.js @@ -553,7 +553,7 @@ Interpreter.prototype.initCallbackManager = function (globalObject) { } else { // Queue for later, first in, last out this.CALLBACK_QUEUE.unshift(callback); - this.appendScopedCode(callbackAst, this.getScope(), true); + this.appendCode(callbackAst); } } var callbackManager = this.createObjectProto(this.OBJECT_PROTO); From 1930937bf3ccb86f5e96f9f983f2304df8cba3b1 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Wed, 11 Nov 2020 17:15:31 -0600 Subject: [PATCH 10/63] Remove need for callbacks --- interpreter.js | 104 +++++++++++++++---------------------------------- 1 file changed, 32 insertions(+), 72 deletions(-) diff --git a/interpreter.js b/interpreter.js index b4cfa0d1..6cd32af8 100644 --- a/interpreter.js +++ b/interpreter.js @@ -355,16 +355,44 @@ Interpreter.prototype.run = function() { /** * Call an interpreted function, returning result value if run immediately - * @param {Interpreter.Object} fn Interpreted function - * @param {Interpreter.Object} thisFn Interpreted Object to use a "this" + * @param {Interpreter.Object} func Interpreted function + * @param {Interpreter.Object} funcThis Interpreted Object to use a "this" * @param {Boolean} [immediate=false] Execute immediately, returning result, or queue for later * @param {Interpreter.Object} var_args Interpreted Objects to pass as arguments */ -Interpreter.prototype.callFunction = function (fn, thisFn, immediate, var_args) { +Interpreter.prototype.callFunction = function (func, funcThis, immediate, var_args) { if (this.paused_ && immediate) { throw new Error("Unable to call pseudo function immediately when paused."); } - return this.addCallback.apply(this, arguments); + var args = []; + for (var i = 3, l = arguments.length; i < l; i++) { + var arg = arguments[i]; + if (arg instanceof Interpreter.Object) { + args.push(arg); + } else { + args.push(this.nativeToPseudo(arg)); + } + } + var node = new this.nodeConstructor({options:{}}); + node['type'] = 'CallExpression'; + var state = new Interpreter.State(node, + this.stateStack[this.stateStack.length - 1].scope); + state.doneCallee_ = true; + state.funcThis_ = funcThis; + state.func_ = func; + state.doneArgs_ = true; + state.arguments_ = args; + + var currentIndex = this.stateStack.length; + this.stateStack.push(state); + if (immediate) { + while ( + !this.paused_ && + this.stateStack.length > currentIndex && + this.step() + ) {} + return state.value; + } }; /** @@ -456,7 +484,6 @@ Interpreter.prototype.initGlobal = function(globalObject) { this.initError(globalObject); this.initMath(globalObject); this.initJSON(globalObject); - this.initCallbackManager(globalObject); // Initialize global functions. var thisInterpreter = this; @@ -510,73 +537,6 @@ Interpreter.prototype.initGlobal = function(globalObject) { } }; -/** - * Initialize callback manager - * @param {!Interpreter.Object} globalObject Global object. - */ -Interpreter.prototype.initCallbackManager = function (globalObject) { - var thisInterpreter = this; - this.CALLBACK_QUEUE = []; - var name = "_JSICallback"; - var callbackAst = this.parseCode(name + ".run();"); - this.addCallback = function (fn, thisFn, immediate, var_args) { - var args = Array.prototype.slice.call(arguments, 3, arguments.length); - var callback = this.createObjectProto(this.OBJECT_PROTO); - var argsArray = this.createArray(); - for (var i = 0, l = args.length; i < l; i++) { - var arg = args[i]; - if (arg instanceof Interpreter.Object) { - argsArray.properties[i] = arg; - } else { - argsArray.properties[i] = this.nativeToPseudo(arg); - } - } - argsArray.properties.length = args.length; - this.setProperty(callback, "fn", fn); - this.setProperty(callback, "thisFn", thisFn); - this.setProperty(callback, "args", argsArray); - if (immediate) { - this.CALLBACK_QUEUE.push(callback); - // We want to run function immediately and get its result - var currentIndex = this.stateStack.length; - this.appendScopedCode(callbackAst, this.getScope(), true); - var origValue = this.value; - this.value = undefined; - while ( - !this.paused_ && - this.stateStack.length > currentIndex && - this.step() - ) {} - var result = this.value; - this.value = origValue; - return result; - } else { - // Queue for later, first in, last out - this.CALLBACK_QUEUE.unshift(callback); - this.appendCode(callbackAst); - } - } - var callbackManager = this.createObjectProto(this.OBJECT_PROTO); - this.setProperty(globalObject, name, callbackManager); - - this.setProperty(callbackManager, "next", - this.createNativeFunction(function() { - return thisInterpreter.CALLBACK_QUEUE.pop(); - }, false), - Interpreter.NONENUMERABLE_DESCRIPTOR); - - // Interpreted object for handling callbacks - this.polyfills_.push( - name + ".run = function() {", - " var callback = " + name +".next();", - " if (callback) {", - " return callback.fn.apply(callback.thisFn, callback.args);", - " }", - " console.warn('No callbacks found.');", - "};" - ); -}; - /** * Initialize the Function class. * @param {!Interpreter.Object} globalObject Global object. From 9024e3dd40db26f27fe5f7b7ed13a827be6d8427 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Wed, 11 Nov 2020 17:20:55 -0600 Subject: [PATCH 11/63] Remove unsused methods --- interpreter.js | 51 -------------------------------------------------- 1 file changed, 51 deletions(-) diff --git a/interpreter.js b/interpreter.js index 6cd32af8..d98beffd 100644 --- a/interpreter.js +++ b/interpreter.js @@ -395,55 +395,6 @@ Interpreter.prototype.callFunction = function (func, funcThis, immediate, var_ar } }; -/** - * Convenience method to pre-parse raw JavaScript to AST - * @param {string|!Object} code Raw JavaScript text or AST. - */ -Interpreter.prototype.parseCode = function (code) { - if (typeof code === "string") { - code = acorn.parse(code, Interpreter.PARSE_OPTIONS); - } - if (!code || code["type"] !== "Program") { - throw Error("Expecting new AST to start with a Program node."); - } - return code; -}; - -/** - * Add pre-scoped code to the interpreter. - * Allows a previously stored scope to be applied to new code. - * @param {string|!Object} code Raw JavaScript text or AST. - * @param {Interpreter.Scope} scope Scope to run the code in. - * @param {Boolean} [immediate=false] Queue as next or last - */ -Interpreter.prototype.appendScopedCode = function (code, scope, immediate) { - if (typeof code === "string") { - code = acorn.parse(code, Interpreter.PARSE_OPTIONS); - } - var type = code && code.type; - if (!type) { - throw Error("Expecting AST"); - } - this.nodeConstructor = code.constructor; - // Clone the root 'Program' node so that the AST may be modified. - var ast = new this.nodeConstructor({ options: {} }); - for (var prop in code) { - ast[prop] = Array.isArray(prop) ? code[prop].slice() : code[prop]; - } - this.populateScope_(ast, scope); - if (type === "Program") ast.type = "BlockStatement"; // Convert program to block statement - var scopedState = new Interpreter.State(ast, scope); - scopedState.scope = scope; - if (immediate) { - // Queue as next item to execute - this.stateStack.push(scopedState); - } else { - // Queue as last item to execute - this.stateStack.splice(1, 0, scopedState); - } -}; - - /** * Initialize the global object with buitin properties and functions. * @param {!Interpreter.Object} globalObject Global object. @@ -4112,5 +4063,3 @@ Interpreter.prototype['setProperty'] = Interpreter.prototype.setProperty; Interpreter.prototype['nativeToPseudo'] = Interpreter.prototype.nativeToPseudo; Interpreter.prototype['pseudoToNative'] = Interpreter.prototype.pseudoToNative; Interpreter.prototype['callFunction'] = Interpreter.prototype.callFunction; -Interpreter.prototype['parseCode'] = Interpreter.prototype.parseCode; -Interpreter.prototype['appendScopedCode'] = Interpreter.prototype.appendScopedCode; From b499bd23db01a5135698318605dcd0066a72429b Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Wed, 11 Nov 2020 19:48:51 -0600 Subject: [PATCH 12/63] Add callAsyncFunction and appendFunction methods --- interpreter.js | 63 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 15 deletions(-) diff --git a/interpreter.js b/interpreter.js index d98beffd..6d424223 100644 --- a/interpreter.js +++ b/interpreter.js @@ -354,18 +354,57 @@ Interpreter.prototype.run = function() { }; /** - * Call an interpreted function, returning result value if run immediately + * Call a pseudo function, returning result value * @param {Interpreter.Object} func Interpreted function * @param {Interpreter.Object} funcThis Interpreted Object to use a "this" - * @param {Boolean} [immediate=false] Execute immediately, returning result, or queue for later * @param {Interpreter.Object} var_args Interpreted Objects to pass as arguments */ -Interpreter.prototype.callFunction = function (func, funcThis, immediate, var_args) { - if (this.paused_ && immediate) { - throw new Error("Unable to call pseudo function immediately when paused."); +Interpreter.prototype.callFunction = function (func, funcThis, var_args) { + if (this.paused_) { + throw new Error("Unable to call pseudo function when paused."); } + var currentIndex = this.stateStack.length; + var state = this.appendFunction.apply(this, arguments); + while ( + !this.paused_ && + this.stateStack.length > currentIndex && + this.step() + ) {} + return state.value; +}; + +/** + * Call a pseudo function, returning result value + * @param {Function} callback Function to call after async call has completed + * @param {Interpreter.Object} func Interpreted function + * @param {Interpreter.Object} funcThis Interpreted Object to use a "this" + * @param {Interpreter.Object} var_args Interpreted Objects to pass as arguments + */ +Interpreter.prototype.callAsyncFunction = function (callback, func, funcThis, var_args) { + var args = Array.prototype.slice.call(arguments, 1) + var currentIndex = this.stateStack.length; + // Append callback to be called after func + this.appendFunction.call(this, + this.nativeToPseudo(callback), funcThis); + // Append func + var state = this.appendFunction.apply(this, args); + while ( + !this.paused_ && + this.stateStack.length > currentIndex && + this.step() + ) {} + return state; +}; + +/** + * Queue a pseudo function for execution on next step + * @param {Interpreter.Object} func Interpreted function + * @param {Interpreter.Object} funcThis Interpreted Object to use a "this" + * @param {Interpreter.Object} var_args Interpreted Objects to pass as arguments + */ +Interpreter.prototype.appendFunction = function (func, funcThis, var_args) { var args = []; - for (var i = 3, l = arguments.length; i < l; i++) { + for (var i = 2, l = arguments.length; i < l; i++) { var arg = arguments[i]; if (arg instanceof Interpreter.Object) { args.push(arg); @@ -383,16 +422,8 @@ Interpreter.prototype.callFunction = function (func, funcThis, immediate, var_ar state.doneArgs_ = true; state.arguments_ = args; - var currentIndex = this.stateStack.length; this.stateStack.push(state); - if (immediate) { - while ( - !this.paused_ && - this.stateStack.length > currentIndex && - this.step() - ) {} - return state.value; - } + return state; }; /** @@ -4062,4 +4093,6 @@ Interpreter.prototype['getProperty'] = Interpreter.prototype.getProperty; Interpreter.prototype['setProperty'] = Interpreter.prototype.setProperty; Interpreter.prototype['nativeToPseudo'] = Interpreter.prototype.nativeToPseudo; Interpreter.prototype['pseudoToNative'] = Interpreter.prototype.pseudoToNative; +Interpreter.prototype['appendFunction'] = Interpreter.prototype.appendFunction; Interpreter.prototype['callFunction'] = Interpreter.prototype.callFunction; +Interpreter.prototype['callAsyncFunction'] = Interpreter.prototype.callAsyncFunction; From bc179e680e6baee7bf0364e42544e31a935a6a9c Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Thu, 12 Nov 2020 01:38:12 -0600 Subject: [PATCH 13/63] Add getter to async function call callback --- interpreter.js | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/interpreter.js b/interpreter.js index 6d424223..d1fda574 100644 --- a/interpreter.js +++ b/interpreter.js @@ -353,11 +353,22 @@ Interpreter.prototype.run = function() { return this.paused_; }; +/** + * Check if interpreter is paused + * @param {Interpreter.Object} func Interpreted function + * @param {Interpreter.Object} funcThis Interpreted Object to use a "this" + * @param {Boolean} var_args Interpreted Objects to pass as arguments + */ +Interpreter.prototype.isPaused = function () { + return this.paused_; +} + /** * Call a pseudo function, returning result value * @param {Interpreter.Object} func Interpreted function * @param {Interpreter.Object} funcThis Interpreted Object to use a "this" * @param {Interpreter.Object} var_args Interpreted Objects to pass as arguments + * @return {Interpreter.Object} Value pseudo function returned */ Interpreter.prototype.callFunction = function (func, funcThis, var_args) { if (this.paused_) { @@ -379,15 +390,20 @@ Interpreter.prototype.callFunction = function (func, funcThis, var_args) { * @param {Interpreter.Object} func Interpreted function * @param {Interpreter.Object} funcThis Interpreted Object to use a "this" * @param {Interpreter.Object} var_args Interpreted Objects to pass as arguments + * @return {Interpreter.State} State object for running pseudo function */ Interpreter.prototype.callAsyncFunction = function (callback, func, funcThis, var_args) { var args = Array.prototype.slice.call(arguments, 1) var currentIndex = this.stateStack.length; // Append callback to be called after func - this.appendFunction.call(this, - this.nativeToPseudo(callback), funcThis); + if(!(callback instanceof Interpreter.Object)) { + callback = this.createNativeFunction(callback) + } + var cbState = this.appendFunction.call(this, callback, funcThis); // Append func var state = this.appendFunction.apply(this, args); + // Pass value getter to callback + cbState.arguments_ = [function(){return state.value}]; while ( !this.paused_ && this.stateStack.length > currentIndex && @@ -401,17 +417,13 @@ Interpreter.prototype.callAsyncFunction = function (callback, func, funcThis, va * @param {Interpreter.Object} func Interpreted function * @param {Interpreter.Object} funcThis Interpreted Object to use a "this" * @param {Interpreter.Object} var_args Interpreted Objects to pass as arguments + * @return {Interpreter.State} State object for running pseudo function */ Interpreter.prototype.appendFunction = function (func, funcThis, var_args) { - var args = []; - for (var i = 2, l = arguments.length; i < l; i++) { - var arg = arguments[i]; - if (arg instanceof Interpreter.Object) { - args.push(arg); - } else { - args.push(this.nativeToPseudo(arg)); - } - } + var thisInterpreter = this + var args = Array.prototype.slice.call(arguments, 2).map(function (arg) { + return arg instanceof Interpreter.Object ? arg : thisInterpreter.nativeToPseudo(arg) + }); var node = new this.nodeConstructor({options:{}}); node['type'] = 'CallExpression'; var state = new Interpreter.State(node, @@ -4096,3 +4108,4 @@ Interpreter.prototype['pseudoToNative'] = Interpreter.prototype.pseudoToNative; Interpreter.prototype['appendFunction'] = Interpreter.prototype.appendFunction; Interpreter.prototype['callFunction'] = Interpreter.prototype.callFunction; Interpreter.prototype['callAsyncFunction'] = Interpreter.prototype.callAsyncFunction; +Interpreter.prototype['isPaused'] = Interpreter.prototype.isPaused; From 3fa1672f119c031e9104ccf693075e9f26f9caa4 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Thu, 12 Nov 2020 01:56:12 -0600 Subject: [PATCH 14/63] Wrap value getter in pseudo if needed --- interpreter.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/interpreter.js b/interpreter.js index d1fda574..6a789792 100644 --- a/interpreter.js +++ b/interpreter.js @@ -396,14 +396,21 @@ Interpreter.prototype.callAsyncFunction = function (callback, func, funcThis, va var args = Array.prototype.slice.call(arguments, 1) var currentIndex = this.stateStack.length; // Append callback to be called after func - if(!(callback instanceof Interpreter.Object)) { + var pseudoCallback = callback instanceof Interpreter.Object; + if(!pseudoCallback) { callback = this.createNativeFunction(callback) } var cbState = this.appendFunction.call(this, callback, funcThis); // Append func var state = this.appendFunction.apply(this, args); // Pass value getter to callback - cbState.arguments_ = [function(){return state.value}]; + var valueGetter = function(){return state.value}; + if (pseudoCallback) { + // If we were passed a pseudo callback + // return a pseudo getter + valueGetter = this.createNativeFunction(valueGetter); + } + cbState.arguments_ = [valueGetter]; while ( !this.paused_ && this.stateStack.length > currentIndex && From 2934a11942338145d3300f43c083479436386f7e Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Thu, 12 Nov 2020 09:19:16 -0600 Subject: [PATCH 15/63] Add state.asyncWait_ and interpreter.strictAsync_ --- interpreter.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/interpreter.js b/interpreter.js index 6a789792..060fe788 100644 --- a/interpreter.js +++ b/interpreter.js @@ -411,6 +411,9 @@ Interpreter.prototype.callAsyncFunction = function (callback, func, funcThis, va valueGetter = this.createNativeFunction(valueGetter); } cbState.arguments_ = [valueGetter]; + this.strictAsync_ = true; // Don't allow async funcs waiting for callback to step + this.paused_ = false; // Allow stepper to run + // Only step states that we just added while ( !this.paused_ && this.stateStack.length > currentIndex && @@ -3383,6 +3386,7 @@ Interpreter.prototype['stepCallExpression'] = function(stack, state, node) { } else if (func.asyncFunc) { var thisInterpreter = this; var callback = function(value) { + state.asyncWait_ = false; state.value = value; thisInterpreter.paused_ = false; }; @@ -3392,6 +3396,7 @@ Interpreter.prototype['stepCallExpression'] = function(stack, state, node) { new Array(argLength)).slice(0, argLength); argsWithCallback.push(callback); this.paused_ = true; + state.asyncWait_ = true; func.asyncFunc.apply(state.funcThis_, argsWithCallback); return; } else { @@ -3404,6 +3409,11 @@ Interpreter.prototype['stepCallExpression'] = function(stack, state, node) { this.throwException(this.TYPE_ERROR, func.class + ' is not callable'); } } else { + if (this.strictAsync_ && state.asyncWait_) { + // Waiting for async call to complete. Someone un-paused us too early. + this.paused_ = true; + return; + } // Execution complete. Put the return value on the stack. stack.pop(); if (state.isConstructor && typeof state.value !== 'object') { From 9e741a155a405a26eb1fbd9ad35f2e1c5c291906 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Thu, 12 Nov 2020 10:14:08 -0600 Subject: [PATCH 16/63] Expose runUntil and getStateStackSize methods --- interpreter.js | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/interpreter.js b/interpreter.js index 060fe788..ed274016 100644 --- a/interpreter.js +++ b/interpreter.js @@ -354,13 +354,21 @@ Interpreter.prototype.run = function() { }; /** - * Check if interpreter is paused - * @param {Interpreter.Object} func Interpreted function - * @param {Interpreter.Object} funcThis Interpreted Object to use a "this" - * @param {Boolean} var_args Interpreted Objects to pass as arguments + * Execute the interpreter until it is size of index + * @return {boolean} True if a execution is asynchronously blocked, + * false if no more instructions. */ -Interpreter.prototype.isPaused = function () { +Interpreter.prototype.runUntil = function(index) { + while (!this.paused_ && this.stateStack.length > index && this.step()) {} return this.paused_; +}; + +/** + * Returns current state stack size + * @return {number} current state stack size + */ +Interpreter.prototype.getStateStackSize = function () { + return this.stateStack.length; } /** @@ -371,16 +379,12 @@ Interpreter.prototype.isPaused = function () { * @return {Interpreter.Object} Value pseudo function returned */ Interpreter.prototype.callFunction = function (func, funcThis, var_args) { - if (this.paused_) { - throw new Error("Unable to call pseudo function when paused."); - } var currentIndex = this.stateStack.length; var state = this.appendFunction.apply(this, arguments); - while ( - !this.paused_ && - this.stateStack.length > currentIndex && - this.step() - ) {} + this.strictAsync_ = true; // Don't allow async funcs waiting for callback to step + this.paused_ = false; // Allow stepper to run even in async + // Only step states that we just added + this.runUntil(currentIndex); return state.value; }; @@ -414,11 +418,7 @@ Interpreter.prototype.callAsyncFunction = function (callback, func, funcThis, va this.strictAsync_ = true; // Don't allow async funcs waiting for callback to step this.paused_ = false; // Allow stepper to run // Only step states that we just added - while ( - !this.paused_ && - this.stateStack.length > currentIndex && - this.step() - ) {} + this.runUntil(currentIndex); return state; }; @@ -3409,7 +3409,7 @@ Interpreter.prototype['stepCallExpression'] = function(stack, state, node) { this.throwException(this.TYPE_ERROR, func.class + ' is not callable'); } } else { - if (this.strictAsync_ && state.asyncWait_) { + if (state.asyncWait_ && this.strictAsync_) { // Waiting for async call to complete. Someone un-paused us too early. this.paused_ = true; return; @@ -4125,4 +4125,5 @@ Interpreter.prototype['pseudoToNative'] = Interpreter.prototype.pseudoToNative; Interpreter.prototype['appendFunction'] = Interpreter.prototype.appendFunction; Interpreter.prototype['callFunction'] = Interpreter.prototype.callFunction; Interpreter.prototype['callAsyncFunction'] = Interpreter.prototype.callAsyncFunction; -Interpreter.prototype['isPaused'] = Interpreter.prototype.isPaused; +Interpreter.prototype['getStateStackSize'] = Interpreter.prototype.getStateStackSize; +Interpreter.prototype['runUntil'] = Interpreter.prototype.runUntil; From 18c85430efe8463cdf937858227b6a1abe8e8629 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Thu, 12 Nov 2020 10:31:09 -0600 Subject: [PATCH 17/63] Expose isPaused --- interpreter.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/interpreter.js b/interpreter.js index ed274016..63b83533 100644 --- a/interpreter.js +++ b/interpreter.js @@ -363,6 +363,14 @@ Interpreter.prototype.runUntil = function(index) { return this.paused_; }; +/** + * Return current pause state + * @return {boolean} True if a execution is asynchronously blocked. + */ +Interpreter.prototype.isPaused = function(index) { + return this.paused_; +}; + /** * Returns current state stack size * @return {number} current state stack size @@ -4127,3 +4135,4 @@ Interpreter.prototype['callFunction'] = Interpreter.prototype.callFunction; Interpreter.prototype['callAsyncFunction'] = Interpreter.prototype.callAsyncFunction; Interpreter.prototype['getStateStackSize'] = Interpreter.prototype.getStateStackSize; Interpreter.prototype['runUntil'] = Interpreter.prototype.runUntil; +Interpreter.prototype['isPaused'] = Interpreter.prototype.isPaused; From 04dec5cee8d3a5cf889090b1c63c7fceff6fb70f Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Thu, 12 Nov 2020 12:29:00 -0600 Subject: [PATCH 18/63] Add ExpressionStatement before CallExpression --- interpreter.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/interpreter.js b/interpreter.js index 63b83533..b9d959a9 100644 --- a/interpreter.js +++ b/interpreter.js @@ -442,16 +442,26 @@ Interpreter.prototype.appendFunction = function (func, funcThis, var_args) { var args = Array.prototype.slice.call(arguments, 2).map(function (arg) { return arg instanceof Interpreter.Object ? arg : thisInterpreter.nativeToPseudo(arg) }); + var scope = this.stateStack[this.stateStack.length - 1].scope; // This may be wrong + // Create node and state for function call var node = new this.nodeConstructor({options:{}}); node['type'] = 'CallExpression'; var state = new Interpreter.State(node, - this.stateStack[this.stateStack.length - 1].scope); + scope); state.doneCallee_ = true; state.funcThis_ = funcThis; state.func_ = func; state.doneArgs_ = true; state.arguments_ = args; - + // Create node and state for function's return value + var expNode = new this.nodeConstructor({options:{}}); + expNode['type'] = 'ExpressionStatement'; + var expState = new Interpreter.State(expNode, + scope); + expState.done_ = true; + // Add return value holder to stop overwriting previous state value + this.stateStack.push(expState); + // Add function call this.stateStack.push(state); return state; }; From 4834769ab402d9a60cf8f7b1629acc5915b54b22 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Thu, 12 Nov 2020 12:36:26 -0600 Subject: [PATCH 19/63] Change ExpressionStatement to EmptyStatement --- interpreter.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/interpreter.js b/interpreter.js index b9d959a9..85e2bf98 100644 --- a/interpreter.js +++ b/interpreter.js @@ -455,11 +455,10 @@ Interpreter.prototype.appendFunction = function (func, funcThis, var_args) { state.arguments_ = args; // Create node and state for function's return value var expNode = new this.nodeConstructor({options:{}}); - expNode['type'] = 'ExpressionStatement'; + expNode['type'] = 'EmptyStatement'; var expState = new Interpreter.State(expNode, scope); - expState.done_ = true; - // Add return value holder to stop overwriting previous state value + // Add EmptyStatement to stop overwriting previous state value this.stateStack.push(expState); // Add function call this.stateStack.push(state); From d7a87a09c70ff656cea6cbd4d3038338741ec58e Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Thu, 12 Nov 2020 15:58:49 -0600 Subject: [PATCH 20/63] Add queueFunction method --- interpreter.js | 41 +++++++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/interpreter.js b/interpreter.js index 85e2bf98..477e8e03 100644 --- a/interpreter.js +++ b/interpreter.js @@ -431,13 +431,13 @@ Interpreter.prototype.callAsyncFunction = function (callback, func, funcThis, va }; /** - * Queue a pseudo function for execution on next step + * Generate state objects for running pseudo function * @param {Interpreter.Object} func Interpreted function * @param {Interpreter.Object} funcThis Interpreted Object to use a "this" * @param {Interpreter.Object} var_args Interpreted Objects to pass as arguments - * @return {Interpreter.State} State object for running pseudo function + * @return {[Interpreter.State]} State objects for running pseudo function */ -Interpreter.prototype.appendFunction = function (func, funcThis, var_args) { +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) @@ -458,11 +458,35 @@ Interpreter.prototype.appendFunction = function (func, funcThis, var_args) { expNode['type'] = 'EmptyStatement'; var expState = new Interpreter.State(expNode, scope); - // Add EmptyStatement to stop overwriting previous state value - this.stateStack.push(expState); - // Add function call - this.stateStack.push(state); - return state; + return [expState, state]; +}; + +/** + * Queue a pseudo function for execution on next step + * @param {Interpreter.Object} func Interpreted function + * @param {Interpreter.Object} funcThis Interpreted Object to use a "this" + * @param {Interpreter.Object} var_args Interpreted Objects to pass as arguments + * @return {Interpreter.State} State object for running pseudo function + */ +Interpreter.prototype.appendFunction = function (func, funcThis, var_args) { + var states = this.buildFunctionCaller.apply(this, arguments); + // Add function call states to end of stack so they are executed next + Array.prototype.push.apply(this.stateStack, states); + return states[1]; +}; + +/** + * 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 a "this" + * @param {Interpreter.Object} var_args Interpreted Objects to pass as arguments + * @return {Interpreter.State} State object for running pseudo function + */ +Interpreter.prototype.queueFunction = function (func, funcThis, var_args) { + var states = this.buildFunctionCaller.apply(this, arguments); + // Add function call states right after root "Program" state, so they are executed last + this.stateStack.splice(1, 0, states[1], states[0]); + return states[1]; }; /** @@ -4145,3 +4169,4 @@ Interpreter.prototype['callAsyncFunction'] = Interpreter.prototype.callAsyncFunc Interpreter.prototype['getStateStackSize'] = Interpreter.prototype.getStateStackSize; Interpreter.prototype['runUntil'] = Interpreter.prototype.runUntil; Interpreter.prototype['isPaused'] = Interpreter.prototype.isPaused; +Interpreter.prototype['queueFunction'] = Interpreter.prototype.queueFunction; From 8f9854835afd9b01def147ffacbe688ddc10ab4c Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Sat, 14 Nov 2020 08:08:59 -0600 Subject: [PATCH 21/63] Wrap function calls in CallExpressionFunc_ node --- interpreter.js | 59 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/interpreter.js b/interpreter.js index 477e8e03..fac6c1b5 100644 --- a/interpreter.js +++ b/interpreter.js @@ -442,23 +442,17 @@ Interpreter.prototype.buildFunctionCaller = function (func, funcThis, var_args) var args = Array.prototype.slice.call(arguments, 2).map(function (arg) { return arg instanceof Interpreter.Object ? arg : thisInterpreter.nativeToPseudo(arg) }); - var scope = this.stateStack[this.stateStack.length - 1].scope; // This may be wrong - // Create node and state for function call - var node = new this.nodeConstructor({options:{}}); - node['type'] = 'CallExpression'; - var state = new Interpreter.State(node, - scope); - state.doneCallee_ = true; - state.funcThis_ = funcThis; - state.func_ = func; - state.doneArgs_ = true; - state.arguments_ = args; + // Create node for CallExpression with pre-embedded function and arguments + var ceNode = new this.nodeConstructor({options:{}}); + ceNode['type'] = 'CallExpressionFunc_'; + ceNode.funcThis_ = funcThis; + ceNode.func_ = func; + ceNode.arguments_ = args; // Create node and state for function's return value var expNode = new this.nodeConstructor({options:{}}); - expNode['type'] = 'EmptyStatement'; - var expState = new Interpreter.State(expNode, - scope); - return [expState, state]; + expNode['type'] = 'ExpressionStatement'; + expNode.expression = ceNode; + return expNode; }; /** @@ -469,10 +463,12 @@ Interpreter.prototype.buildFunctionCaller = function (func, funcThis, var_args) * @return {Interpreter.State} State object for running pseudo function */ Interpreter.prototype.appendFunction = function (func, funcThis, var_args) { - var states = this.buildFunctionCaller.apply(this, arguments); - // Add function call states to end of stack so they are executed next - Array.prototype.push.apply(this.stateStack, states); - return states[1]; + var expNode = this.buildFunctionCaller.apply(this, arguments); + // Add function call state to end of stack so they are executed next + var scope = this.stateStack[this.stateStack.length - 1].scope; // This may be wrong + var state = new Interpreter.State(expNode, scope); + this.stateStack.push(state); + return state; }; /** @@ -483,10 +479,9 @@ Interpreter.prototype.appendFunction = function (func, funcThis, var_args) { * @return {Interpreter.State} State object for running pseudo function */ Interpreter.prototype.queueFunction = function (func, funcThis, var_args) { - var states = this.buildFunctionCaller.apply(this, arguments); - // Add function call states right after root "Program" state, so they are executed last - this.stateStack.splice(1, 0, states[1], states[0]); - return states[1]; + var expNode = this.buildFunctionCaller.apply(this, arguments); + // Add function call to root Program state + this.stateStack[0].node['body'].push(expNode); }; /** @@ -3552,6 +3547,24 @@ Interpreter.prototype['stepEvalProgram_'] = function(stack, state, node) { stack[stack.length - 1].value = this.value; }; +Interpreter.prototype['stepCallExpressionFunc_'] = function(stack, state, node) { + if (!state.done_) { + state.done_ = true; + var ceNode = new this.nodeConstructor({options:{}}); + ceNode['type'] = 'CallExpression'; + var ceSate = new Interpreter.State(ceNode, state.scope); + ceSate.doneCallee_ = true; + ceSate.funcThis_ = node.funcThis_; + ceSate.func_ = node.func_; + ceSate.doneArgs_ = true; + ceSate.arguments_ = node.arguments_; + return ceSate; + } + stack.pop(); + // Save this value to the previous state, just like CallExpression would have done + stack[stack.length - 1].value = state.value; +}; + Interpreter.prototype['stepExpressionStatement'] = function(stack, state, node) { if (!state.done_) { state.done_ = true; From e36756e67a9fe1f105dc42184b7167802fe6c3dd Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Sat, 14 Nov 2020 10:36:02 -0600 Subject: [PATCH 22/63] Executeimmediate functions in correct order --- interpreter.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/interpreter.js b/interpreter.js index fac6c1b5..2e5397bb 100644 --- a/interpreter.js +++ b/interpreter.js @@ -435,7 +435,7 @@ Interpreter.prototype.callAsyncFunction = function (callback, func, funcThis, va * @param {Interpreter.Object} func Interpreted function * @param {Interpreter.Object} funcThis Interpreted Object to use a "this" * @param {Interpreter.Object} var_args Interpreted Objects to pass as arguments - * @return {[Interpreter.State]} State objects for running pseudo function + * @return {nodeConstructor} node for running pseudo function */ Interpreter.prototype.buildFunctionCaller = function (func, funcThis, var_args) { var thisInterpreter = this @@ -467,7 +467,12 @@ Interpreter.prototype.appendFunction = function (func, funcThis, var_args) { // Add function call state to end of stack so they are executed next var scope = this.stateStack[this.stateStack.length - 1].scope; // This may be wrong var state = new Interpreter.State(expNode, scope); - this.stateStack.push(state); + state.immediateAppend_ = true + // Insert before chain of immediateAppend_ to execute in correct order + for (var i = this.stateStack.length - 1; i > 1; i--) { + if (!this.stateStack[i].immediateAppend_) break; + } + this.stateStack.splice(i, 0, state); return state; }; From a513475ea4fdb817a47374c6e561d91b88d8f066 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Sat, 14 Nov 2020 10:37:04 -0600 Subject: [PATCH 23/63] Fix index limit --- interpreter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interpreter.js b/interpreter.js index 2e5397bb..e99d1b59 100644 --- a/interpreter.js +++ b/interpreter.js @@ -469,7 +469,7 @@ Interpreter.prototype.appendFunction = function (func, funcThis, var_args) { var state = new Interpreter.State(expNode, scope); state.immediateAppend_ = true // Insert before chain of immediateAppend_ to execute in correct order - for (var i = this.stateStack.length - 1; i > 1; i--) { + for (var i = this.stateStack.length - 1; i > 0; i--) { if (!this.stateStack[i].immediateAppend_) break; } this.stateStack.splice(i, 0, state); From 32ca98da73ea4de5f641efb916ae00aa290fc7f7 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Sat, 14 Nov 2020 10:38:12 -0600 Subject: [PATCH 24/63] No return --- interpreter.js | 1 - 1 file changed, 1 deletion(-) diff --git a/interpreter.js b/interpreter.js index e99d1b59..117c1680 100644 --- a/interpreter.js +++ b/interpreter.js @@ -481,7 +481,6 @@ Interpreter.prototype.appendFunction = function (func, funcThis, var_args) { * @param {Interpreter.Object} func Interpreted function * @param {Interpreter.Object} funcThis Interpreted Object to use a "this" * @param {Interpreter.Object} var_args Interpreted Objects to pass as arguments - * @return {Interpreter.State} State object for running pseudo function */ Interpreter.prototype.queueFunction = function (func, funcThis, var_args) { var expNode = this.buildFunctionCaller.apply(this, arguments); From 81c83d53973f01c29a11d19c2e1614870b3607bc Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Sat, 14 Nov 2020 10:59:43 -0600 Subject: [PATCH 25/63] Rollback immediate call execution order --- interpreter.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/interpreter.js b/interpreter.js index 117c1680..94b13528 100644 --- a/interpreter.js +++ b/interpreter.js @@ -467,12 +467,7 @@ Interpreter.prototype.appendFunction = function (func, funcThis, var_args) { // Add function call state to end of stack so they are executed next var scope = this.stateStack[this.stateStack.length - 1].scope; // This may be wrong var state = new Interpreter.State(expNode, scope); - state.immediateAppend_ = true - // Insert before chain of immediateAppend_ to execute in correct order - for (var i = this.stateStack.length - 1; i > 0; i--) { - if (!this.stateStack[i].immediateAppend_) break; - } - this.stateStack.splice(i, 0, state); + this.stateStack.push(state); return state; }; From 86bd2099c32540a4cb7ba18af80be67e0f3f7a0e Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Sat, 14 Nov 2020 13:11:57 -0600 Subject: [PATCH 26/63] Restart program on function queue --- interpreter.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/interpreter.js b/interpreter.js index 94b13528..5427627d 100644 --- a/interpreter.js +++ b/interpreter.js @@ -450,7 +450,7 @@ Interpreter.prototype.buildFunctionCaller = function (func, funcThis, var_args) ceNode.arguments_ = args; // Create node and state for function's return value var expNode = new this.nodeConstructor({options:{}}); - expNode['type'] = 'ExpressionStatement'; + expNode['type'] = 'EmptyStatement'; expNode.expression = ceNode; return expNode; }; @@ -478,9 +478,11 @@ Interpreter.prototype.appendFunction = function (func, funcThis, var_args) { * @param {Interpreter.Object} var_args Interpreted Objects to pass as arguments */ 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 - this.stateStack[0].node['body'].push(expNode); + state.node['body'].push(expNode); + state.done = false; }; /** From 59117965500968e833d703f6a0f921a8190857a7 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Sat, 14 Nov 2020 13:51:06 -0600 Subject: [PATCH 27/63] Fix bugs --- interpreter.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/interpreter.js b/interpreter.js index 5427627d..4de5c88e 100644 --- a/interpreter.js +++ b/interpreter.js @@ -448,11 +448,7 @@ Interpreter.prototype.buildFunctionCaller = function (func, funcThis, var_args) ceNode.funcThis_ = funcThis; ceNode.func_ = func; ceNode.arguments_ = args; - // Create node and state for function's return value - var expNode = new this.nodeConstructor({options:{}}); - expNode['type'] = 'EmptyStatement'; - expNode.expression = ceNode; - return expNode; + return ceNode; }; /** @@ -3563,7 +3559,7 @@ Interpreter.prototype['stepCallExpressionFunc_'] = function(stack, state, node) } stack.pop(); // Save this value to the previous state, just like CallExpression would have done - stack[stack.length - 1].value = state.value; + // stack[stack.length - 1].value = state.value; }; Interpreter.prototype['stepExpressionStatement'] = function(stack, state, node) { From 151b8988d43e91c7efb118014cb3f64af8490224 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Fri, 20 Nov 2020 21:55:47 -0600 Subject: [PATCH 28/63] Make pseudo fn callbacks use state machine --- interpreter.js | 208 ++++++++++++++++++++++++------------------------- 1 file changed, 102 insertions(+), 106 deletions(-) diff --git a/interpreter.js b/interpreter.js index 4de5c88e..3f435ed9 100644 --- a/interpreter.js +++ b/interpreter.js @@ -354,80 +354,29 @@ Interpreter.prototype.run = function() { }; /** - * Execute the interpreter until it is size of index - * @return {boolean} True if a execution is asynchronously blocked, - * false if no more instructions. - */ -Interpreter.prototype.runUntil = function(index) { - while (!this.paused_ && this.stateStack.length > index && this.step()) {} - return this.paused_; -}; - -/** - * Return current pause state - * @return {boolean} True if a execution is asynchronously blocked. - */ -Interpreter.prototype.isPaused = function(index) { - return this.paused_; -}; - -/** - * Returns current state stack size - * @return {number} current state stack size - */ -Interpreter.prototype.getStateStackSize = function () { - return this.stateStack.length; -} - -/** - * Call a pseudo function, returning result value + * Queue a pseudo function for execution on next step * @param {Interpreter.Object} func Interpreted function * @param {Interpreter.Object} funcThis Interpreted Object to use a "this" * @param {Interpreter.Object} var_args Interpreted Objects to pass as arguments - * @return {Interpreter.Object} Value pseudo function returned + * @return {Interpreter.NativeState} State object for running pseudo function */ Interpreter.prototype.callFunction = function (func, funcThis, var_args) { - var currentIndex = this.stateStack.length; - var state = this.appendFunction.apply(this, arguments); - this.strictAsync_ = true; // Don't allow async funcs waiting for callback to step - this.paused_ = false; // Allow stepper to run even in async - // Only step states that we just added - this.runUntil(currentIndex); - return state.value; + var expNode = this.buildFunctionCaller_.apply(this, arguments); + return new Interpreter.NativeState(expNode); }; /** - * Call a pseudo function, returning result value - * @param {Function} callback Function to call after async call has completed + * 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 a "this" * @param {Interpreter.Object} var_args Interpreted Objects to pass as arguments - * @return {Interpreter.State} State object for running pseudo function */ -Interpreter.prototype.callAsyncFunction = function (callback, func, funcThis, var_args) { - var args = Array.prototype.slice.call(arguments, 1) - var currentIndex = this.stateStack.length; - // Append callback to be called after func - var pseudoCallback = callback instanceof Interpreter.Object; - if(!pseudoCallback) { - callback = this.createNativeFunction(callback) - } - var cbState = this.appendFunction.call(this, callback, funcThis); - // Append func - var state = this.appendFunction.apply(this, args); - // Pass value getter to callback - var valueGetter = function(){return state.value}; - if (pseudoCallback) { - // If we were passed a pseudo callback - // return a pseudo getter - valueGetter = this.createNativeFunction(valueGetter); - } - cbState.arguments_ = [valueGetter]; - this.strictAsync_ = true; // Don't allow async funcs waiting for callback to step - this.paused_ = false; // Allow stepper to run - // Only step states that we just added - this.runUntil(currentIndex); - return state; +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; }; /** @@ -437,50 +386,22 @@ Interpreter.prototype.callAsyncFunction = function (callback, func, funcThis, va * @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) { +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_'; ceNode.funcThis_ = funcThis; ceNode.func_ = func; ceNode.arguments_ = args; + ceNode.scope_ = scope; return ceNode; }; -/** - * Queue a pseudo function for execution on next step - * @param {Interpreter.Object} func Interpreted function - * @param {Interpreter.Object} funcThis Interpreted Object to use a "this" - * @param {Interpreter.Object} var_args Interpreted Objects to pass as arguments - * @return {Interpreter.State} State object for running pseudo function - */ -Interpreter.prototype.appendFunction = function (func, funcThis, var_args) { - var expNode = this.buildFunctionCaller.apply(this, arguments); - // Add function call state to end of stack so they are executed next - var scope = this.stateStack[this.stateStack.length - 1].scope; // This may be wrong - var state = new Interpreter.State(expNode, scope); - this.stateStack.push(state); - return state; -}; - -/** - * 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 a "this" - * @param {Interpreter.Object} var_args Interpreted Objects to pass as arguments - */ -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; -}; - /** * Initialize the global object with buitin properties and functions. * @param {!Interpreter.Object} globalObject Global object. @@ -3022,6 +2943,58 @@ Interpreter.Scope = function(parentScope, strict, object) { this.object = object; }; +/** + * Class for tracking native function states. + * @param {nodeConstructor} callFnState State that's being tracked + * @constructor + */ +Interpreter.NativeState = function(callFnNode) { + this.node_ = callFnNode + this.handler_ = null +}; + +/** + * Add handler for return of pseudo function's value + * @param {Function} handler Function to handle value + * @return {Interpreter.NativeState} State object for running pseudo function + */ +Interpreter.NativeState.prototype['then'] = function(handler) { + if (typeof handler !== 'function') { + throw new Error('Expected function for then handler'); + } + if (this.handlers_) { + throw new Error('Then handler already defined'); + } + this.handler_ = handler; + return this; +}; + +/** + * Add pseudo function callback to statStack + * @param {Interpreter} interpreter Interpreter instance + * @param {Interpreter.Scope} scope Function's scope. + */ +Interpreter.NativeState.prototype.pushState_ = function(interpreter, scope) { + var state = new Interpreter.State(this.node_, scope); + interpreter.stateStack.push(state); + this.state_ = 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.NativeState.prototype.doNext_ = function(asyncCallback) { + if (this.handler_) { + return this.handler_(this.state_.value, asyncCallback); + } else if(asyncCallback) { + asyncCallback(this.state_.value) + } else { + return this.state_.value + } +}; + /** * Class for an object. * @param {Interpreter.Object} proto Prototype object or null. @@ -3415,21 +3388,51 @@ Interpreter.prototype['stepCallExpression'] = function(stack, state, node) { return new Interpreter.State(evalNode, scope); } } else if (func.nativeFunc) { - state.value = func.nativeFunc.apply(state.funcThis_, state.arguments_); + var value; + if (state.callbackState_) { + // Do next step of nativeFunc + value = state.callbackState_.doNext_(); + state.callbackState_ = null; + } else { + // Call the initial nativeFunc + value = func.nativeFunc.apply(state.funcThis_, state.arguments_); + } + if (value instanceof Interpreter.NativeState) { + // We have a request for a pseudo function callback + state.callbackState_ = value; + value.pushState_(this, scope); + state.doneExec_ = false; + } else { + // We have a final value + state.value = value; + } } else if (func.asyncFunc) { + this.paused_ = true; var thisInterpreter = this; var callback = function(value) { - state.asyncWait_ = false; - state.value = value; thisInterpreter.paused_ = false; + if (value instanceof Interpreter.NativeState) { + // We have a request for a pseudo function callback + state.callbackState_ = value; + value.pushState_(thisInterpreter, scope); + state.doneExec_ = false; + } else { + // Final value + state.value = value; + } }; + if (state.callbackState_) { + // Do next step of native async func + state.callbackState_.doNext_(callback); + state.callbackState_ = null; + state.doneExec_ = false; + 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; - state.asyncWait_ = true; func.asyncFunc.apply(state.funcThis_, argsWithCallback); return; } else { @@ -3442,11 +3445,6 @@ Interpreter.prototype['stepCallExpression'] = function(stack, state, node) { this.throwException(this.TYPE_ERROR, func.class + ' is not callable'); } } else { - if (state.asyncWait_ && this.strictAsync_) { - // Waiting for async call to complete. Someone un-paused us too early. - this.paused_ = true; - return; - } // Execution complete. Put the return value on the stack. stack.pop(); if (state.isConstructor && typeof state.value !== 'object') { @@ -3549,7 +3547,7 @@ Interpreter.prototype['stepCallExpressionFunc_'] = function(stack, state, node) state.done_ = true; var ceNode = new this.nodeConstructor({options:{}}); ceNode['type'] = 'CallExpression'; - var ceSate = new Interpreter.State(ceNode, state.scope); + var ceSate = new Interpreter.State(ceNode, node.scope_ || state.scope); ceSate.doneCallee_ = true; ceSate.funcThis_ = node.funcThis_; ceSate.func_ = node.func_; @@ -3558,8 +3556,6 @@ Interpreter.prototype['stepCallExpressionFunc_'] = function(stack, state, node) return ceSate; } stack.pop(); - // Save this value to the previous state, just like CallExpression would have done - // stack[stack.length - 1].value = state.value; }; Interpreter.prototype['stepExpressionStatement'] = function(stack, state, node) { From b9e1b81df1726850a692d15af363e5cf3b0f76a0 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Fri, 20 Nov 2020 22:02:22 -0600 Subject: [PATCH 29/63] Remove unused exports --- interpreter.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/interpreter.js b/interpreter.js index 3f435ed9..0e52333e 100644 --- a/interpreter.js +++ b/interpreter.js @@ -4171,8 +4171,4 @@ Interpreter.prototype['nativeToPseudo'] = Interpreter.prototype.nativeToPseudo; Interpreter.prototype['pseudoToNative'] = Interpreter.prototype.pseudoToNative; Interpreter.prototype['appendFunction'] = Interpreter.prototype.appendFunction; Interpreter.prototype['callFunction'] = Interpreter.prototype.callFunction; -Interpreter.prototype['callAsyncFunction'] = Interpreter.prototype.callAsyncFunction; -Interpreter.prototype['getStateStackSize'] = Interpreter.prototype.getStateStackSize; -Interpreter.prototype['runUntil'] = Interpreter.prototype.runUntil; -Interpreter.prototype['isPaused'] = Interpreter.prototype.isPaused; Interpreter.prototype['queueFunction'] = Interpreter.prototype.queueFunction; From 8a0ff210b0abb8f436ba6b0c1fdbfb8fdf7b0f92 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Fri, 20 Nov 2020 22:03:23 -0600 Subject: [PATCH 30/63] Remove unused export --- interpreter.js | 1 - 1 file changed, 1 deletion(-) diff --git a/interpreter.js b/interpreter.js index 0e52333e..80e36947 100644 --- a/interpreter.js +++ b/interpreter.js @@ -4169,6 +4169,5 @@ Interpreter.prototype['getProperty'] = Interpreter.prototype.getProperty; Interpreter.prototype['setProperty'] = Interpreter.prototype.setProperty; Interpreter.prototype['nativeToPseudo'] = Interpreter.prototype.nativeToPseudo; Interpreter.prototype['pseudoToNative'] = Interpreter.prototype.pseudoToNative; -Interpreter.prototype['appendFunction'] = Interpreter.prototype.appendFunction; Interpreter.prototype['callFunction'] = Interpreter.prototype.callFunction; Interpreter.prototype['queueFunction'] = Interpreter.prototype.queueFunction; From ca4f121e082bd64247f30c03baea0429f4379cd4 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Sat, 21 Nov 2020 08:11:43 -0600 Subject: [PATCH 31/63] Rafactor NativeState to Callback --- interpreter.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/interpreter.js b/interpreter.js index 80e36947..1964a7ad 100644 --- a/interpreter.js +++ b/interpreter.js @@ -358,11 +358,11 @@ Interpreter.prototype.run = function() { * @param {Interpreter.Object} func Interpreted function * @param {Interpreter.Object} funcThis Interpreted Object to use a "this" * @param {Interpreter.Object} var_args Interpreted Objects to pass as arguments - * @return {Interpreter.NativeState} State object for running pseudo function + * @return {Interpreter.Callback} State object for running pseudo function callback */ Interpreter.prototype.callFunction = function (func, funcThis, var_args) { var expNode = this.buildFunctionCaller_.apply(this, arguments); - return new Interpreter.NativeState(expNode); + return new Interpreter.Callback(expNode); }; /** @@ -2948,7 +2948,7 @@ Interpreter.Scope = function(parentScope, strict, object) { * @param {nodeConstructor} callFnState State that's being tracked * @constructor */ -Interpreter.NativeState = function(callFnNode) { +Interpreter.Callback = function(callFnNode) { this.node_ = callFnNode this.handler_ = null }; @@ -2956,9 +2956,9 @@ Interpreter.NativeState = function(callFnNode) { /** * Add handler for return of pseudo function's value * @param {Function} handler Function to handle value - * @return {Interpreter.NativeState} State object for running pseudo function + * @return {Interpreter.Callback} State object for running pseudo function */ -Interpreter.NativeState.prototype['then'] = function(handler) { +Interpreter.Callback.prototype['then'] = function(handler) { if (typeof handler !== 'function') { throw new Error('Expected function for then handler'); } @@ -2974,7 +2974,7 @@ Interpreter.NativeState.prototype['then'] = function(handler) { * @param {Interpreter} interpreter Interpreter instance * @param {Interpreter.Scope} scope Function's scope. */ -Interpreter.NativeState.prototype.pushState_ = function(interpreter, scope) { +Interpreter.Callback.prototype.pushState_ = function(interpreter, scope) { var state = new Interpreter.State(this.node_, scope); interpreter.stateStack.push(state); this.state_ = state; @@ -2985,7 +2985,7 @@ Interpreter.NativeState.prototype.pushState_ = function(interpreter, scope) { * @param {Interpreter.Object} value Object containing pseudo function callback's result. * @param {Function} asyncCallback Function for asyncFunc callback */ -Interpreter.NativeState.prototype.doNext_ = function(asyncCallback) { +Interpreter.Callback.prototype.doNext_ = function(asyncCallback) { if (this.handler_) { return this.handler_(this.state_.value, asyncCallback); } else if(asyncCallback) { @@ -3397,7 +3397,7 @@ Interpreter.prototype['stepCallExpression'] = function(stack, state, node) { // Call the initial nativeFunc value = func.nativeFunc.apply(state.funcThis_, state.arguments_); } - if (value instanceof Interpreter.NativeState) { + if (value instanceof Interpreter.Callback) { // We have a request for a pseudo function callback state.callbackState_ = value; value.pushState_(this, scope); @@ -3411,7 +3411,7 @@ Interpreter.prototype['stepCallExpression'] = function(stack, state, node) { var thisInterpreter = this; var callback = function(value) { thisInterpreter.paused_ = false; - if (value instanceof Interpreter.NativeState) { + if (value instanceof Interpreter.Callback) { // We have a request for a pseudo function callback state.callbackState_ = value; value.pushState_(thisInterpreter, scope); From 3f8aa1104bb2512db8497d6625de833e669b5edc Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Sat, 21 Nov 2020 08:13:03 -0600 Subject: [PATCH 32/63] Remove unnecessary callbackState_ = null --- interpreter.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/interpreter.js b/interpreter.js index 1964a7ad..a0529867 100644 --- a/interpreter.js +++ b/interpreter.js @@ -3392,7 +3392,6 @@ Interpreter.prototype['stepCallExpression'] = function(stack, state, node) { if (state.callbackState_) { // Do next step of nativeFunc value = state.callbackState_.doNext_(); - state.callbackState_ = null; } else { // Call the initial nativeFunc value = func.nativeFunc.apply(state.funcThis_, state.arguments_); @@ -3424,7 +3423,6 @@ Interpreter.prototype['stepCallExpression'] = function(stack, state, node) { if (state.callbackState_) { // Do next step of native async func state.callbackState_.doNext_(callback); - state.callbackState_ = null; state.doneExec_ = false; return; } From 34f4f598e3adcf59317a7de1a2aa15e7d81d4116 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Sat, 21 Nov 2020 08:29:16 -0600 Subject: [PATCH 33/63] Use ternary operator --- interpreter.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/interpreter.js b/interpreter.js index a0529867..ccf12d39 100644 --- a/interpreter.js +++ b/interpreter.js @@ -3388,14 +3388,9 @@ Interpreter.prototype['stepCallExpression'] = function(stack, state, node) { return new Interpreter.State(evalNode, scope); } } else if (func.nativeFunc) { - var value; - if (state.callbackState_) { - // Do next step of nativeFunc - value = state.callbackState_.doNext_(); - } else { - // Call the initial nativeFunc - value = func.nativeFunc.apply(state.funcThis_, state.arguments_); - } + var value = state.callbackState_ + ? state.callbackState_.doNext_() + : func.nativeFunc.apply(state.funcThis_, state.arguments_); if (value instanceof Interpreter.Callback) { // We have a request for a pseudo function callback state.callbackState_ = value; From fb0fd76192cdc684fc6146b168f465a87f216cba Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Sat, 21 Nov 2020 08:34:02 -0600 Subject: [PATCH 34/63] Put pause back where it belongs --- interpreter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interpreter.js b/interpreter.js index ccf12d39..c9f7bc0c 100644 --- a/interpreter.js +++ b/interpreter.js @@ -3401,7 +3401,6 @@ Interpreter.prototype['stepCallExpression'] = function(stack, state, node) { state.value = value; } } else if (func.asyncFunc) { - this.paused_ = true; var thisInterpreter = this; var callback = function(value) { thisInterpreter.paused_ = false; @@ -3426,6 +3425,7 @@ Interpreter.prototype['stepCallExpression'] = function(stack, state, node) { var argsWithCallback = state.arguments_.concat( new Array(argLength)).slice(0, argLength); argsWithCallback.push(callback); + this.paused_ = true; func.asyncFunc.apply(state.funcThis_, argsWithCallback); return; } else { From 3bcc33030a6c606af3d21d224b41f0c7522fd6b2 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Sat, 21 Nov 2020 08:35:02 -0600 Subject: [PATCH 35/63] Move pause again --- interpreter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interpreter.js b/interpreter.js index c9f7bc0c..b1f5abbf 100644 --- a/interpreter.js +++ b/interpreter.js @@ -3414,6 +3414,7 @@ Interpreter.prototype['stepCallExpression'] = function(stack, state, node) { state.value = value; } }; + this.paused_ = true; if (state.callbackState_) { // Do next step of native async func state.callbackState_.doNext_(callback); @@ -3425,7 +3426,6 @@ Interpreter.prototype['stepCallExpression'] = function(stack, state, node) { var argsWithCallback = state.arguments_.concat( new Array(argLength)).slice(0, argLength); argsWithCallback.push(callback); - this.paused_ = true; func.asyncFunc.apply(state.funcThis_, argsWithCallback); return; } else { From 298fa89f1e9d8dfd378f88615a03f43c93690581 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Sat, 21 Nov 2020 10:33:11 -0600 Subject: [PATCH 36/63] Allow asyncFunctions to throw errors --- interpreter.js | 78 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 58 insertions(+), 20 deletions(-) diff --git a/interpreter.js b/interpreter.js index b1f5abbf..11b33977 100644 --- a/interpreter.js +++ b/interpreter.js @@ -2785,6 +2785,38 @@ 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 + */ +Interpreter.prototype.handleNativeReturn_ = function(state, scope, value) { + if (value instanceof Interpreter.Callback) { + // We have a request for a pseudo function callback + state.callbackState_ = value; + value.pushState_(this, scope); + state.doneExec_ = false; + } else if (value instanceof Interpreter.Throwable) { + // Result was an error + value.throw_(this); + } else { + // We have a final value + state.value = value; + } +}; + + /** * Unwind the stack to the innermost relevant enclosing TryStatement, * For/ForIn/WhileStatement or Call/NewExpression. If this results in @@ -2943,6 +2975,28 @@ 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 @@ -3388,31 +3442,14 @@ Interpreter.prototype['stepCallExpression'] = function(stack, state, node) { return new Interpreter.State(evalNode, scope); } } else if (func.nativeFunc) { - var value = state.callbackState_ + this.handleNativeReturn_(state, scope, state.callbackState_ ? state.callbackState_.doNext_() - : func.nativeFunc.apply(state.funcThis_, state.arguments_); - if (value instanceof Interpreter.Callback) { - // We have a request for a pseudo function callback - state.callbackState_ = value; - value.pushState_(this, scope); - state.doneExec_ = false; - } else { - // We have a final value - state.value = value; - } + : func.nativeFunc.apply(state.funcThis_, state.arguments_)); } else if (func.asyncFunc) { var thisInterpreter = this; var callback = function(value) { thisInterpreter.paused_ = false; - if (value instanceof Interpreter.Callback) { - // We have a request for a pseudo function callback - state.callbackState_ = value; - value.pushState_(thisInterpreter, scope); - state.doneExec_ = false; - } else { - // Final value - state.value = value; - } + thisInterpreter.handleNativeReturn_(state, scope, value); }; this.paused_ = true; if (state.callbackState_) { @@ -4164,3 +4201,4 @@ Interpreter.prototype['nativeToPseudo'] = Interpreter.prototype.nativeToPseudo; Interpreter.prototype['pseudoToNative'] = Interpreter.prototype.pseudoToNative; Interpreter.prototype['callFunction'] = Interpreter.prototype.callFunction; Interpreter.prototype['queueFunction'] = Interpreter.prototype.queueFunction; +Interpreter.prototype['createThrowable'] = Interpreter.prototype.createThrowable; From fbfc8c07a399706a1fe07043869e70b9e5c385ce Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Sat, 21 Nov 2020 22:29:24 -0600 Subject: [PATCH 37/63] Add .this(...) callback for queueFunction value --- interpreter.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/interpreter.js b/interpreter.js index 11b33977..52fa1d30 100644 --- a/interpreter.js +++ b/interpreter.js @@ -377,6 +377,10 @@ Interpreter.prototype.queueFunction = function (func, funcThis, var_args) { // Add function call to root Program state state.node['body'].push(expNode); state.done = false; + expNode['then'] = function(callback) { + if (typeof callback === 'function') expNode.callback_ = callback; + } + return expNode; }; /** @@ -3586,6 +3590,14 @@ Interpreter.prototype['stepCallExpressionFunc_'] = function(stack, state, node) return ceSate; } stack.pop(); + if (this.stateStack.length === 1) { + // Save value as return value if we were the last to be executed + this.value = state.value; + } + if (node.callback_) { + // Callback a 'then' handler + node.callback_(state.value); + } }; Interpreter.prototype['stepExpressionStatement'] = function(stack, state, node) { @@ -3772,7 +3784,7 @@ Interpreter.prototype['stepIdentifier'] = function(stack, state, node) { if (this.getterStep_) { // Call the getter function. var scope = state.scope; - while (!this.hasProperty(scope, node['name'])) { + while (scope !== this.globalScope && !this.hasProperty(scope, node['name'])) { scope = scope.parentScope; } var func = /** @type {!Interpreter.Object} */ (value); From 9be4aa081f9aad10adc924390e9bb12eaf61d310 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Sun, 22 Nov 2020 10:23:44 -0600 Subject: [PATCH 38/63] Add catch/finally to callFunction/queueFunction --- interpreter.js | 61 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/interpreter.js b/interpreter.js index 52fa1d30..e15c6328 100644 --- a/interpreter.js +++ b/interpreter.js @@ -379,6 +379,15 @@ Interpreter.prototype.queueFunction = function (func, funcThis, var_args) { state.done = false; expNode['then'] = function(callback) { if (typeof callback === 'function') expNode.callback_ = callback; + return expNode; + } + expNode['catch'] = function(callback) { + if (typeof callback === 'function') expNode.catch_ = callback; + return expNode; + } + expNode['finally'] = function(callback) { + if (typeof callback === 'function') expNode.finally_ = callback; + return expNode; } return expNode; }; @@ -2846,6 +2855,17 @@ Interpreter.prototype.unwind = function(type, value, label) { if (type === Interpreter.Completion.RETURN) { state.value = value; return; + } else if (state.catch_ && type === Interpreter.Completion.THROW) { + // Native catch handler + var result = state.catch_(value); + if (result instanceof Interpreter.Throwable) { + // Catch re-threw an exception + this.throwException(result.errorClass, result.opt_message); + return; + } + // Use catch's return value + state.value = result; + return; } else if (type !== Interpreter.Completion.THROW) { throw Error('Unsynatctic break/continue not rejected by Acorn'); } @@ -3018,15 +3038,47 @@ Interpreter.Callback = function(callFnNode) { */ Interpreter.Callback.prototype['then'] = function(handler) { if (typeof handler !== 'function') { - throw new Error('Expected function for then handler'); + throw new Error('Expected function for "then" handler'); } if (this.handlers_) { - throw new Error('Then handler already defined'); + throw new Error('"then" already defined'); } this.handler_ = 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.node_.catch_) { + throw new Error('"catch" already defined'); + } + this.node_.catch_ = handler; + return this; +}; + +/** + * Add finally 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['finally'] = function(handler) { + if (typeof handler !== 'function') { + throw new Error('Expected function for "finally" handler'); + } + if (this.node_.finally_) { + throw new Error('"finally" already defined'); + } + this.node_.finally_ = handler; + return this; +}; + /** * Add pseudo function callback to statStack * @param {Interpreter} interpreter Interpreter instance @@ -3587,6 +3639,7 @@ Interpreter.prototype['stepCallExpressionFunc_'] = function(stack, state, node) ceSate.func_ = node.func_; ceSate.doneArgs_ = true; ceSate.arguments_ = node.arguments_; + ceSate.catch_ = node.catch_; return ceSate; } stack.pop(); @@ -3598,6 +3651,10 @@ Interpreter.prototype['stepCallExpressionFunc_'] = function(stack, state, node) // Callback a 'then' handler node.callback_(state.value); } + if (node.finally_) { + // Callback a 'finally' handler + state.node.finally_(); + } }; Interpreter.prototype['stepExpressionStatement'] = function(stack, state, node) { From b4c8132b93bbdb44da5c22ba8a270e0e0071b5f2 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Sun, 22 Nov 2020 11:10:47 -0600 Subject: [PATCH 39/63] Don't call 'then' after 'catch' --- interpreter.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/interpreter.js b/interpreter.js index e15c6328..63e4a3c2 100644 --- a/interpreter.js +++ b/interpreter.js @@ -2863,8 +2863,12 @@ Interpreter.prototype.unwind = function(type, value, label) { this.throwException(result.errorClass, result.opt_message); return; } - // Use catch's return value - state.value = result; + // Skip to next step + stack.pop(); + if (stack.length > 1) { + // Tell next step that handled an exception + stack[stack.length - 1].caughtException_ = true + } return; } else if (type !== Interpreter.Completion.THROW) { throw Error('Unsynatctic break/continue not rejected by Acorn'); @@ -3096,6 +3100,10 @@ Interpreter.Callback.prototype.pushState_ = function(interpreter, scope) { * @param {Function} asyncCallback Function for asyncFunc callback */ Interpreter.Callback.prototype.doNext_ = function(asyncCallback) { + if (this.state_.caughtException_) { + // Native catch handled error. We're done. + return; + } if (this.handler_) { return this.handler_(this.state_.value, asyncCallback); } else if(asyncCallback) { @@ -3647,7 +3655,7 @@ Interpreter.prototype['stepCallExpressionFunc_'] = function(stack, state, node) // Save value as return value if we were the last to be executed this.value = state.value; } - if (node.callback_) { + if (node.callback_ && !state.caughtException_) { // Callback a 'then' handler node.callback_(state.value); } From 8d42446f9fcc7dcfc128050c4216da32c9201252 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Sun, 22 Nov 2020 11:30:16 -0600 Subject: [PATCH 40/63] Fix typo --- interpreter.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/interpreter.js b/interpreter.js index 63e4a3c2..66b1c97f 100644 --- a/interpreter.js +++ b/interpreter.js @@ -3641,14 +3641,14 @@ Interpreter.prototype['stepCallExpressionFunc_'] = function(stack, state, node) state.done_ = true; var ceNode = new this.nodeConstructor({options:{}}); ceNode['type'] = 'CallExpression'; - var ceSate = new Interpreter.State(ceNode, node.scope_ || state.scope); - ceSate.doneCallee_ = true; - ceSate.funcThis_ = node.funcThis_; - ceSate.func_ = node.func_; - ceSate.doneArgs_ = true; - ceSate.arguments_ = node.arguments_; - ceSate.catch_ = node.catch_; - return ceSate; + 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_; + ceState.catch_ = node.catch_; + return ceState; } stack.pop(); if (this.stateStack.length === 1) { From cd43acacd564c45f4a5eb5977cf1053e650a7203 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Sun, 22 Nov 2020 12:43:03 -0600 Subject: [PATCH 41/63] Simplify --- interpreter.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/interpreter.js b/interpreter.js index 66b1c97f..4fc89a95 100644 --- a/interpreter.js +++ b/interpreter.js @@ -2857,8 +2857,7 @@ Interpreter.prototype.unwind = function(type, value, label) { return; } else if (state.catch_ && type === Interpreter.Completion.THROW) { // Native catch handler - var result = state.catch_(value); - if (result instanceof Interpreter.Throwable) { + if (state.catch_(value) instanceof Interpreter.Throwable) { // Catch re-threw an exception this.throwException(result.errorClass, result.opt_message); return; From 170fc840180781a587d6f5f15d4a46f8b13caf74 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Sun, 22 Nov 2020 12:43:48 -0600 Subject: [PATCH 42/63] Rollback simplify --- interpreter.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/interpreter.js b/interpreter.js index 4fc89a95..66b1c97f 100644 --- a/interpreter.js +++ b/interpreter.js @@ -2857,7 +2857,8 @@ Interpreter.prototype.unwind = function(type, value, label) { return; } else if (state.catch_ && type === Interpreter.Completion.THROW) { // Native catch handler - if (state.catch_(value) instanceof Interpreter.Throwable) { + var result = state.catch_(value); + if (result instanceof Interpreter.Throwable) { // Catch re-threw an exception this.throwException(result.errorClass, result.opt_message); return; From db7ee6c428f661a7fafd62f25c48c488030d07ea Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Sun, 22 Nov 2020 12:51:34 -0600 Subject: [PATCH 43/63] Fix typo --- interpreter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interpreter.js b/interpreter.js index 66b1c97f..5224ae27 100644 --- a/interpreter.js +++ b/interpreter.js @@ -2866,7 +2866,7 @@ Interpreter.prototype.unwind = function(type, value, label) { // Skip to next step stack.pop(); if (stack.length > 1) { - // Tell next step that handled an exception + // Tell next step that we handled an exception stack[stack.length - 1].caughtException_ = true } return; From 2d8b51f56be8402608308dae0ddb2ab0e89ad548 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Sun, 22 Nov 2020 14:34:30 -0600 Subject: [PATCH 44/63] Allow catch to return value --- interpreter.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/interpreter.js b/interpreter.js index 5224ae27..4a9fb93a 100644 --- a/interpreter.js +++ b/interpreter.js @@ -2863,6 +2863,7 @@ Interpreter.prototype.unwind = function(type, value, label) { this.throwException(result.errorClass, result.opt_message); return; } + state.value = result; // Skip to next step stack.pop(); if (stack.length > 1) { @@ -3650,19 +3651,19 @@ Interpreter.prototype['stepCallExpressionFunc_'] = function(stack, state, node) ceState.catch_ = node.catch_; return ceState; } - stack.pop(); - if (this.stateStack.length === 1) { - // Save value as return value if we were the last to be executed - this.value = state.value; - } if (node.callback_ && !state.caughtException_) { // Callback a 'then' handler - node.callback_(state.value); + state.value = node.callback_(state.value); } if (node.finally_) { // Callback a 'finally' handler state.node.finally_(); } + 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) { From bd7c067a274eab11b24e4e2a06cd2991aeec1070 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Sun, 22 Nov 2020 14:37:48 -0600 Subject: [PATCH 45/63] Return proper value --- interpreter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interpreter.js b/interpreter.js index 4a9fb93a..56cb0bb1 100644 --- a/interpreter.js +++ b/interpreter.js @@ -2863,12 +2863,12 @@ Interpreter.prototype.unwind = function(type, value, label) { this.throwException(result.errorClass, result.opt_message); return; } - state.value = result; // Skip to next step stack.pop(); if (stack.length > 1) { // Tell next step that we handled an exception stack[stack.length - 1].caughtException_ = true + stack[stack.length - 1].value = result; } return; } else if (type !== Interpreter.Completion.THROW) { @@ -3103,7 +3103,7 @@ Interpreter.Callback.prototype.pushState_ = function(interpreter, scope) { Interpreter.Callback.prototype.doNext_ = function(asyncCallback) { if (this.state_.caughtException_) { // Native catch handled error. We're done. - return; + return this.state_.value; } if (this.handler_) { return this.handler_(this.state_.value, asyncCallback); From f87d96c99502f0fb4bb5acdd3dfacb3026f59020 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Sun, 22 Nov 2020 14:50:46 -0600 Subject: [PATCH 46/63] Change where exception is thrown --- interpreter.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/interpreter.js b/interpreter.js index 56cb0bb1..741f963d 100644 --- a/interpreter.js +++ b/interpreter.js @@ -2858,17 +2858,16 @@ Interpreter.prototype.unwind = function(type, value, label) { } else if (state.catch_ && type === Interpreter.Completion.THROW) { // Native catch handler var result = state.catch_(value); - if (result instanceof Interpreter.Throwable) { - // Catch re-threw an exception - this.throwException(result.errorClass, result.opt_message); - return; - } // Skip to next step stack.pop(); if (stack.length > 1) { // Tell next step that we handled an exception stack[stack.length - 1].caughtException_ = true stack[stack.length - 1].value = result; + } else if (result instanceof Interpreter.Throwable) { + // Catch re-threw an exception + this.throwException(result.errorClass, result.opt_message); + return; } return; } else if (type !== Interpreter.Completion.THROW) { From 46d9af95ab48a27ba427ac15897e3c7894e6d427 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Sun, 22 Nov 2020 19:29:56 -0600 Subject: [PATCH 47/63] Fix issue with async callbacks --- interpreter.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/interpreter.js b/interpreter.js index 741f963d..b6492a70 100644 --- a/interpreter.js +++ b/interpreter.js @@ -2856,18 +2856,21 @@ Interpreter.prototype.unwind = function(type, value, label) { state.value = value; return; } else if (state.catch_ && type === Interpreter.Completion.THROW) { - // Native catch handler + // Native function catch handler var result = state.catch_(value); // Skip to next step stack.pop(); - if (stack.length > 1) { + var nextStep = stack[stack.length - 1]; + if (nextStep && nextStep.node['type'] === 'CallExpressionFunc_') { // Tell next step that we handled an exception - stack[stack.length - 1].caughtException_ = true - stack[stack.length - 1].value = result; + nextStep.caughtException_ = true + nextStep.value = result; } else if (result instanceof Interpreter.Throwable) { - // Catch re-threw an exception + // Re-throw exception this.throwException(result.errorClass, result.opt_message); return; + } else { + throw new Error('Invalid native function catch'); } return; } else if (type !== Interpreter.Completion.THROW) { @@ -3519,7 +3522,6 @@ Interpreter.prototype['stepCallExpression'] = function(stack, state, node) { if (state.callbackState_) { // Do next step of native async func state.callbackState_.doNext_(callback); - state.doneExec_ = false; return; } // Force the argument lengths to match, then append the callback. From e88a691b719be9ed878edec1a25d3325ffea9c4b Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Mon, 23 Nov 2020 01:10:34 -0600 Subject: [PATCH 48/63] Refactor native catch, remove finally --- interpreter.js | 102 ++++++++++++++++++++++++------------------------- 1 file changed, 50 insertions(+), 52 deletions(-) diff --git a/interpreter.js b/interpreter.js index b6492a70..33002e48 100644 --- a/interpreter.js +++ b/interpreter.js @@ -385,10 +385,6 @@ Interpreter.prototype.queueFunction = function (func, funcThis, var_args) { if (typeof callback === 'function') expNode.catch_ = callback; return expNode; } - expNode['finally'] = function(callback) { - if (typeof callback === 'function') expNode.finally_ = callback; - return expNode; - } return expNode; }; @@ -2817,12 +2813,26 @@ Interpreter.prototype.createThrowable = function(errorClass, opt_message) { Interpreter.prototype.handleNativeReturn_ = function(state, scope, value) { if (value instanceof Interpreter.Callback) { // We have a request for a pseudo function callback - state.callbackState_ = value; + var _this = this; value.pushState_(this, scope); + state.callbackObj_ = value; + state.catch_ = value.catch_ && function(error) { + // Wrap catch function as a callback and set as next callback + // Only use the handler portion of the callback + var catchCb = _this.callFunction(null, state.funcThis_, error); + catchCb.handler_ = value.catch_; // Set handler to catch handler + catchCb + state.callbackObj_ = catchCb; + catchCb.pushed_ = true; // Only create a state, don't push + catchCb.pushState_(_this, scope); + catchCb.state_.value = error; + state.catch_ = null; // Don't do this again. + state.doneExec_ = false; // Allow stepCallExpression to run callback. + } state.doneExec_ = false; } else if (value instanceof Interpreter.Throwable) { // Result was an error - value.throw_(this); + value.throw_(value); } else { // We have a final value state.value = value; @@ -2855,23 +2865,12 @@ Interpreter.prototype.unwind = function(type, value, label) { if (type === Interpreter.Completion.RETURN) { state.value = value; return; + } else if (state.skipThrow_) { + // One of our parents will catch this, so allow unwind + continue; } else if (state.catch_ && type === Interpreter.Completion.THROW) { // Native function catch handler - var result = state.catch_(value); - // Skip to next step - stack.pop(); - var nextStep = stack[stack.length - 1]; - if (nextStep && nextStep.node['type'] === 'CallExpressionFunc_') { - // Tell next step that we handled an exception - nextStep.caughtException_ = true - nextStep.value = result; - } else if (result instanceof Interpreter.Throwable) { - // Re-throw exception - this.throwException(result.errorClass, result.opt_message); - return; - } else { - throw new Error('Invalid native function catch'); - } + state.catch_(value); return; } else if (type !== Interpreter.Completion.THROW) { throw Error('Unsynatctic break/continue not rejected by Acorn'); @@ -3036,6 +3035,7 @@ Interpreter.Throwable.prototype.throw_ = function(interpreter) { Interpreter.Callback = function(callFnNode) { this.node_ = callFnNode this.handler_ = null + this.catch_ = null }; /** @@ -3047,7 +3047,7 @@ Interpreter.Callback.prototype['then'] = function(handler) { if (typeof handler !== 'function') { throw new Error('Expected function for "then" handler'); } - if (this.handlers_) { + if (this.handler_) { throw new Error('"then" already defined'); } this.handler_ = handler; @@ -3063,26 +3063,11 @@ Interpreter.Callback.prototype['catch'] = function(handler) { if (typeof handler !== 'function') { throw new Error('Expected function for "catch" handler'); } - if (this.node_.catch_) { + if (this.catch_) { throw new Error('"catch" already defined'); } - this.node_.catch_ = handler; - return this; -}; - -/** - * Add finally 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['finally'] = function(handler) { - if (typeof handler !== 'function') { - throw new Error('Expected function for "finally" handler'); - } - if (this.node_.finally_) { - throw new Error('"finally" already defined'); - } - this.node_.finally_ = handler; + this.node_.skipThrow_ = true; + this.catch_ = handler; return this; }; @@ -3092,9 +3077,12 @@ Interpreter.Callback.prototype['finally'] = function(handler) { * @param {Interpreter.Scope} scope Function's scope. */ Interpreter.Callback.prototype.pushState_ = function(interpreter, scope) { - var state = new Interpreter.State(this.node_, scope); - interpreter.stateStack.push(state); - this.state_ = state; + if (!this.state_) { + this.state_ = new Interpreter.State(this.node_, scope); + } + if (this.pushed_) return; + interpreter.stateStack.push(this.state_); + this.pushed_ = true; }; /** @@ -3509,8 +3497,8 @@ Interpreter.prototype['stepCallExpression'] = function(stack, state, node) { return new Interpreter.State(evalNode, scope); } } else if (func.nativeFunc) { - this.handleNativeReturn_(state, scope, state.callbackState_ - ? state.callbackState_.doNext_() + this.handleNativeReturn_(state, scope, state.callbackObj_ + ? state.callbackObj_.doNext_() : func.nativeFunc.apply(state.funcThis_, state.arguments_)); } else if (func.asyncFunc) { var thisInterpreter = this; @@ -3519,9 +3507,9 @@ Interpreter.prototype['stepCallExpression'] = function(stack, state, node) { thisInterpreter.handleNativeReturn_(state, scope, value); }; this.paused_ = true; - if (state.callbackState_) { + if (state.callbackObj_) { // Do next step of native async func - state.callbackState_.doNext_(callback); + state.callbackObj_.doNext_(callback); return; } // Force the argument lengths to match, then append the callback. @@ -3649,17 +3637,27 @@ Interpreter.prototype['stepCallExpressionFunc_'] = function(stack, state, node) ceState.func_ = node.func_; ceState.doneArgs_ = true; ceState.arguments_ = node.arguments_; - ceState.catch_ = node.catch_; + ceState.catch_ = node.catch_ && function(error) { + var result = node.catch_(error); + // Skip to next step + stack.pop(); + if (result instanceof Interpreter.Throwable) { + // Re-throw exception + this.throwException(result.errorClass, result.opt_message); + return; + } + var nextState = stack[stack.length - 1]; + // Tell next step that we handled an exception + nextState.caughtException_ = true + nextState.value = result; + }; + ceState.skipThrow_ = node.skipThrow_; return ceState; } if (node.callback_ && !state.caughtException_) { // Callback a 'then' handler state.value = node.callback_(state.value); } - if (node.finally_) { - // Callback a 'finally' handler - state.node.finally_(); - } stack.pop(); if (this.stateStack.length === 1) { // Save value as return value if we were the last to be executed From fe24f3f7765649e9e9df0246acd01534c24c2cf0 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Mon, 23 Nov 2020 01:21:14 -0600 Subject: [PATCH 49/63] Cleanup --- interpreter.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/interpreter.js b/interpreter.js index 33002e48..5b09f02b 100644 --- a/interpreter.js +++ b/interpreter.js @@ -2819,13 +2819,12 @@ Interpreter.prototype.handleNativeReturn_ = function(state, scope, value) { state.catch_ = value.catch_ && function(error) { // Wrap catch function as a callback and set as next callback // Only use the handler portion of the callback - var catchCb = _this.callFunction(null, state.funcThis_, error); - catchCb.handler_ = value.catch_; // Set handler to catch handler - catchCb - state.callbackObj_ = catchCb; - catchCb.pushed_ = true; // Only create a state, don't push - catchCb.pushState_(_this, scope); - catchCb.state_.value = error; + var catcher = _this.callFunction(null, state.funcThis_, error); + catcher.handler_ = value.catch_; // Set handler to catch handler + state.callbackObj_ = catcher; + catcher.pushed_ = true; // Only create a state, don't push + catcher.pushState_(_this, scope); + catcher.state_.value = error; state.catch_ = null; // Don't do this again. state.doneExec_ = false; // Allow stepCallExpression to run callback. } From bba6491b8925f735a468f771969dff88763f68a4 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Mon, 23 Nov 2020 10:39:05 -0600 Subject: [PATCH 50/63] Refactor callback catch --- interpreter.js | 99 ++++++++++++++++++++++++-------------------------- 1 file changed, 47 insertions(+), 52 deletions(-) diff --git a/interpreter.js b/interpreter.js index 5b09f02b..ddd30fa2 100644 --- a/interpreter.js +++ b/interpreter.js @@ -2813,21 +2813,8 @@ Interpreter.prototype.createThrowable = function(errorClass, opt_message) { Interpreter.prototype.handleNativeReturn_ = function(state, scope, value) { if (value instanceof Interpreter.Callback) { // We have a request for a pseudo function callback - var _this = this; value.pushState_(this, scope); - state.callbackObj_ = value; - state.catch_ = value.catch_ && function(error) { - // Wrap catch function as a callback and set as next callback - // Only use the handler portion of the callback - var catcher = _this.callFunction(null, state.funcThis_, error); - catcher.handler_ = value.catch_; // Set handler to catch handler - state.callbackObj_ = catcher; - catcher.pushed_ = true; // Only create a state, don't push - catcher.pushState_(_this, scope); - catcher.state_.value = error; - state.catch_ = null; // Don't do this again. - state.doneExec_ = false; // Allow stepCallExpression to run callback. - } + state.cb_ = value; state.doneExec_ = false; } else if (value instanceof Interpreter.Throwable) { // Result was an error @@ -2864,17 +2851,16 @@ Interpreter.prototype.unwind = function(type, value, label) { if (type === Interpreter.Completion.RETURN) { state.value = value; return; - } else if (state.skipThrow_) { - // One of our parents will catch this, so allow unwind - continue; - } else if (state.catch_ && type === Interpreter.Completion.THROW) { - // Native function catch handler - state.catch_(value); - return; } else if (type !== Interpreter.Completion.THROW) { 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; + } case 'Program': // Don't pop the stateStack. // Leave the root scope on the tree in case the program is appended to. @@ -3035,6 +3021,7 @@ Interpreter.Callback = function(callFnNode) { this.node_ = callFnNode this.handler_ = null this.catch_ = null + this.node_.cb_ = this // For async catch handling }; /** @@ -3065,8 +3052,8 @@ Interpreter.Callback.prototype['catch'] = function(handler) { if (this.catch_) { throw new Error('"catch" already defined'); } - this.node_.skipThrow_ = true; - this.catch_ = handler; + // this.node_.skipThrow_ = true; + this.node_.catch_ = handler; return this; }; @@ -3079,9 +3066,8 @@ Interpreter.Callback.prototype.pushState_ = function(interpreter, scope) { if (!this.state_) { this.state_ = new Interpreter.State(this.node_, scope); } - if (this.pushed_) return; + if (this.indirect_) return; // Only uses handler_ interpreter.stateStack.push(this.state_); - this.pushed_ = true; }; /** @@ -3090,13 +3076,11 @@ Interpreter.Callback.prototype.pushState_ = function(interpreter, scope) { * @param {Function} asyncCallback Function for asyncFunc callback */ Interpreter.Callback.prototype.doNext_ = function(asyncCallback) { - if (this.state_.caughtException_) { - // Native catch handled error. We're done. - return this.state_.value; - } if (this.handler_) { - return this.handler_(this.state_.value, asyncCallback); - } else if(asyncCallback) { + return this.handler_(this.indirect_ ? this.value : this.state_.value, asyncCallback); + } + if (this.indirect_) return; // Callback will not directly return its value + if(asyncCallback) { asyncCallback(this.state_.value) } else { return this.state_.value @@ -3436,6 +3420,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)) { @@ -3496,8 +3485,8 @@ Interpreter.prototype['stepCallExpression'] = function(stack, state, node) { return new Interpreter.State(evalNode, scope); } } else if (func.nativeFunc) { - this.handleNativeReturn_(state, scope, state.callbackObj_ - ? state.callbackObj_.doNext_() + this.handleNativeReturn_(state, scope, state.cb_ + ? state.cb_.doNext_() : func.nativeFunc.apply(state.funcThis_, state.arguments_)); } else if (func.asyncFunc) { var thisInterpreter = this; @@ -3506,9 +3495,9 @@ Interpreter.prototype['stepCallExpression'] = function(stack, state, node) { thisInterpreter.handleNativeReturn_(state, scope, value); }; this.paused_ = true; - if (state.callbackObj_) { + if (state.cb_) { // Do next step of native async func - state.callbackObj_.doNext_(callback); + state.cb_.doNext_(callback); return; } // Force the argument lengths to match, then append the callback. @@ -3636,26 +3625,32 @@ Interpreter.prototype['stepCallExpressionFunc_'] = function(stack, state, node) ceState.func_ = node.func_; ceState.doneArgs_ = true; ceState.arguments_ = node.arguments_; - ceState.catch_ = node.catch_ && function(error) { - var result = node.catch_(error); - // Skip to next step - stack.pop(); - if (result instanceof Interpreter.Throwable) { - // Re-throw exception - this.throwException(result.errorClass, result.opt_message); - return; - } - var nextState = stack[stack.length - 1]; - // Tell next step that we handled an exception - nextState.caughtException_ = true - nextState.value = result; - }; - ceState.skipThrow_ = node.skipThrow_; + state.catch_ = node.catch_; return ceState; } - if (node.callback_ && !state.caughtException_) { + if (node.callback_ && !state.throw_) { // Callback a 'then' handler state.value = node.callback_(state.value); + node.callback_ = null; + return; + } + if (state.catch_ && state.throw_) { + // Callback a 'catch' handler + var cb = node.cb_; + if (cb) { + // Immediate callback from CallExpression + // Modify existing Callback object to execute catch steps + cb.indirect_ = true; // Callback can only use its handler for return value + cb.handler_ = state.catch_; + cb.value = state.throw_; // Set value to pass to handler + cb.force_ = true; // Force callback to run again + } else { + // Called via queued callback + // Just call the callback directly + state.value = state.catch_(state.throw_); + state.catch_ = null; + return; + } } stack.pop(); if (this.stateStack.length === 1) { From b322959d53b90cbf470b805b3b913ebeb83774b9 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Mon, 23 Nov 2020 10:44:32 -0600 Subject: [PATCH 51/63] cleanup --- interpreter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interpreter.js b/interpreter.js index ddd30fa2..e946f3af 100644 --- a/interpreter.js +++ b/interpreter.js @@ -2853,7 +2853,7 @@ Interpreter.prototype.unwind = function(type, value, label) { return; } else if (type !== Interpreter.Completion.THROW) { throw Error('Unsynatctic break/continue not rejected by Acorn'); - } + } break; case 'CallExpressionFunc_': if (type === Interpreter.Completion.THROW && state.catch_) { From 8ea6fa5f687cba3e11b6920f9ef15c94b527bab7 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Mon, 23 Nov 2020 11:33:32 -0600 Subject: [PATCH 52/63] Cleanup --- interpreter.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/interpreter.js b/interpreter.js index e946f3af..4c55396e 100644 --- a/interpreter.js +++ b/interpreter.js @@ -3066,7 +3066,7 @@ Interpreter.Callback.prototype.pushState_ = function(interpreter, scope) { if (!this.state_) { this.state_ = new Interpreter.State(this.node_, scope); } - if (this.indirect_) return; // Only uses handler_ + if (this.stateless_) return; // Only uses handler_ interpreter.stateStack.push(this.state_); }; @@ -3077,9 +3077,9 @@ Interpreter.Callback.prototype.pushState_ = function(interpreter, scope) { */ Interpreter.Callback.prototype.doNext_ = function(asyncCallback) { if (this.handler_) { - return this.handler_(this.indirect_ ? this.value : this.state_.value, asyncCallback); + return this.handler_(this.stateless_ ? this.value : this.state_.value, asyncCallback); } - if (this.indirect_) return; // Callback will not directly return its value + if (this.stateless_) return; // Callback does not have a state value if(asyncCallback) { asyncCallback(this.state_.value) } else { @@ -3640,9 +3640,9 @@ Interpreter.prototype['stepCallExpressionFunc_'] = function(stack, state, node) if (cb) { // Immediate callback from CallExpression // Modify existing Callback object to execute catch steps - cb.indirect_ = true; // Callback can only use its handler for return value + cb.stateless_ = true; // Callback can only use its handler for return value cb.handler_ = state.catch_; - cb.value = state.throw_; // Set value to pass to handler + cb.value = state.throw_; // Set stateless value to pass to handler cb.force_ = true; // Force callback to run again } else { // Called via queued callback From 392556e3d068a471ce467c83f21d76b6e49714ff Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Mon, 23 Nov 2020 11:35:44 -0600 Subject: [PATCH 53/63] Don't create state if not needed --- interpreter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interpreter.js b/interpreter.js index 4c55396e..83003030 100644 --- a/interpreter.js +++ b/interpreter.js @@ -3063,10 +3063,10 @@ Interpreter.Callback.prototype['catch'] = function(handler) { * @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); } - if (this.stateless_) return; // Only uses handler_ interpreter.stateStack.push(this.state_); }; From d73e5df5dbb112cda83884891534532ff25f6749 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Mon, 23 Nov 2020 12:37:35 -0600 Subject: [PATCH 54/63] Fix typos --- interpreter.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/interpreter.js b/interpreter.js index 83003030..17788baf 100644 --- a/interpreter.js +++ b/interpreter.js @@ -356,7 +356,7 @@ Interpreter.prototype.run = function() { /** * Queue a pseudo function for execution on next step * @param {Interpreter.Object} func Interpreted function - * @param {Interpreter.Object} funcThis Interpreted Object to use a "this" + * @param {Interpreter.Object} funcThis Interpreted Object to use as "this" * @param {Interpreter.Object} var_args Interpreted Objects to pass as arguments * @return {Interpreter.Callback} State object for running pseudo function callback */ @@ -368,7 +368,7 @@ Interpreter.prototype.callFunction = function (func, funcThis, var_args) { /** * 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 a "this" + * @param {Interpreter.Object} funcThis Interpreted Object to use as "this" * @param {Interpreter.Object} var_args Interpreted Objects to pass as arguments */ Interpreter.prototype.queueFunction = function (func, funcThis, var_args) { @@ -391,7 +391,7 @@ Interpreter.prototype.queueFunction = function (func, funcThis, var_args) { /** * Generate state objects for running pseudo function * @param {Interpreter.Object} func Interpreted function - * @param {Interpreter.Object} funcThis Interpreted Object to use a "this" + * @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 */ From 2df2c2e644737bb80152690aaa9903a600f30a31 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Mon, 23 Nov 2020 12:38:44 -0600 Subject: [PATCH 55/63] Cleanup --- interpreter.js | 1 + 1 file changed, 1 insertion(+) diff --git a/interpreter.js b/interpreter.js index 17788baf..01405ae0 100644 --- a/interpreter.js +++ b/interpreter.js @@ -370,6 +370,7 @@ Interpreter.prototype.callFunction = function (func, funcThis, var_args) { * @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.queueFunction = function (func, funcThis, var_args) { var state = this.stateStack[0]; From a9fafcbcd785b14d3d565e9842af4ae8bcdd846a Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Mon, 23 Nov 2020 13:02:52 -0600 Subject: [PATCH 56/63] Unify queued and immediate callback catch handlers --- interpreter.js | 49 +++++++++++++++++++++++-------------------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/interpreter.js b/interpreter.js index 01405ae0..19b1627f 100644 --- a/interpreter.js +++ b/interpreter.js @@ -358,7 +358,7 @@ Interpreter.prototype.run = 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 {Interpreter.Callback} State object for running pseudo function callback + * @return {Interpreter.Callback} Object for running pseudo function callback */ Interpreter.prototype.callFunction = function (func, funcThis, var_args) { var expNode = this.buildFunctionCaller_.apply(this, arguments); @@ -370,7 +370,7 @@ Interpreter.prototype.callFunction = function (func, funcThis, var_args) { * @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 + * @return {Interpreter.Callback} Object for running pseudo function callback */ Interpreter.prototype.queueFunction = function (func, funcThis, var_args) { var state = this.stateStack[0]; @@ -378,15 +378,7 @@ Interpreter.prototype.queueFunction = function (func, funcThis, var_args) { // Add function call to root Program state state.node['body'].push(expNode); state.done = false; - expNode['then'] = function(callback) { - if (typeof callback === 'function') expNode.callback_ = callback; - return expNode; - } - expNode['catch'] = function(callback) { - if (typeof callback === 'function') expNode.catch_ = callback; - return expNode; - } - return expNode; + return new Interpreter.Callback(expNode, true); // Allows adding then/catch }; /** @@ -3016,13 +3008,15 @@ Interpreter.Throwable.prototype.throw_ = function(interpreter) { /** * 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) { +Interpreter.Callback = function(callFnNode, opt_queued) { this.node_ = callFnNode this.handler_ = null this.catch_ = null - this.node_.cb_ = this // For async catch handling + this.node_.cb_ = this + this.queued_ = opt_queued }; /** @@ -3054,7 +3048,7 @@ Interpreter.Callback.prototype['catch'] = function(handler) { throw new Error('"catch" already defined'); } // this.node_.skipThrow_ = true; - this.node_.catch_ = handler; + this.catch_ = handler; return this; }; @@ -3616,6 +3610,8 @@ Interpreter.prototype['stepEvalProgram_'] = function(stack, state, node) { }; 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:{}}); @@ -3626,31 +3622,32 @@ Interpreter.prototype['stepCallExpressionFunc_'] = function(stack, state, node) ceState.func_ = node.func_; ceState.doneArgs_ = true; ceState.arguments_ = node.arguments_; - state.catch_ = node.catch_; + state.catch_ = cb.catch_; + state.handler_ = cb.handler_; return ceState; } - if (node.callback_ && !state.throw_) { - // Callback a 'then' handler + if (queued && state.handler_ && !state.throw_) { + // Called via queued callback + // Callback a 'then' handler now (non-queued are called in setCallExpression) state.value = node.callback_(state.value); - node.callback_ = null; + state.handler_ = null; return; } if (state.catch_ && state.throw_) { // Callback a 'catch' handler - var cb = node.cb_; - if (cb) { + if (queued) { + // Called via queued callback + // Just call the callback directly + state.value = 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.handler_ = state.catch_; cb.value = state.throw_; // Set stateless value to pass to handler cb.force_ = true; // Force callback to run again - } else { - // Called via queued callback - // Just call the callback directly - state.value = state.catch_(state.throw_); - state.catch_ = null; - return; } } stack.pop(); From 3c5a508b217287e448670449ad0cbc99bf9aee55 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Mon, 23 Nov 2020 13:27:30 -0600 Subject: [PATCH 57/63] Allow queued call's then/catch to call-back --- interpreter.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/interpreter.js b/interpreter.js index 19b1627f..429b6c7e 100644 --- a/interpreter.js +++ b/interpreter.js @@ -2802,13 +2802,15 @@ Interpreter.prototype.createThrowable = function(errorClass, opt_message) { * @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.handleNativeReturn_ = function(state, scope, value) { +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_(value); @@ -3480,14 +3482,14 @@ Interpreter.prototype['stepCallExpression'] = function(stack, state, node) { return new Interpreter.State(evalNode, scope); } } else if (func.nativeFunc) { - this.handleNativeReturn_(state, scope, state.cb_ + this.handleNativeResult_(state, scope, state.cb_ ? state.cb_.doNext_() : func.nativeFunc.apply(state.funcThis_, state.arguments_)); } else if (func.asyncFunc) { var thisInterpreter = this; var callback = function(value) { thisInterpreter.paused_ = false; - thisInterpreter.handleNativeReturn_(state, scope, value); + thisInterpreter.handleNativeResult_(state, scope, value); }; this.paused_ = true; if (state.cb_) { @@ -3629,7 +3631,7 @@ Interpreter.prototype['stepCallExpressionFunc_'] = function(stack, state, node) if (queued && state.handler_ && !state.throw_) { // Called via queued callback // Callback a 'then' handler now (non-queued are called in setCallExpression) - state.value = node.callback_(state.value); + this.handleNativeResult_(state, node.funcThis_, state.handler_(state.value)); state.handler_ = null; return; } @@ -3638,7 +3640,7 @@ Interpreter.prototype['stepCallExpressionFunc_'] = function(stack, state, node) if (queued) { // Called via queued callback // Just call the callback directly - state.value = state.catch_(state.throw_); + this.handleNativeResult_(state, node.funcThis_, state.catch_(state.throw_)); state.catch_ = null; return; } else { From 0da3a8052ce9d71974b1d29effff8b4ab6902265 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Mon, 23 Nov 2020 14:11:24 -0600 Subject: [PATCH 58/63] Allow chain of thens in callbacks --- interpreter.js | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/interpreter.js b/interpreter.js index 429b6c7e..b36e8375 100644 --- a/interpreter.js +++ b/interpreter.js @@ -3015,7 +3015,7 @@ Interpreter.Throwable.prototype.throw_ = function(interpreter) { */ Interpreter.Callback = function(callFnNode, opt_queued) { this.node_ = callFnNode - this.handler_ = null + this.handlers_ = [] this.catch_ = null this.node_.cb_ = this this.queued_ = opt_queued @@ -3030,10 +3030,7 @@ Interpreter.Callback.prototype['then'] = function(handler) { if (typeof handler !== 'function') { throw new Error('Expected function for "then" handler'); } - if (this.handler_) { - throw new Error('"then" already defined'); - } - this.handler_ = handler; + this.handlers_.push(handler); return this; }; @@ -3073,8 +3070,10 @@ Interpreter.Callback.prototype.pushState_ = function(interpreter, scope) { * @param {Function} asyncCallback Function for asyncFunc callback */ Interpreter.Callback.prototype.doNext_ = function(asyncCallback) { - if (this.handler_) { - return this.handler_(this.stateless_ ? this.value : this.state_.value, 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) { @@ -3625,14 +3624,13 @@ Interpreter.prototype['stepCallExpressionFunc_'] = function(stack, state, node) ceState.doneArgs_ = true; ceState.arguments_ = node.arguments_; state.catch_ = cb.catch_; - state.handler_ = cb.handler_; return ceState; } - if (queued && state.handler_ && !state.throw_) { + if (queued && cb.handlers_.length && !state.throw_) { // Called via queued callback // Callback a 'then' handler now (non-queued are called in setCallExpression) - this.handleNativeResult_(state, node.funcThis_, state.handler_(state.value)); - state.handler_ = null; + var handler = cb.handlers_.shift(); + this.handleNativeResult_(state, node.funcThis_, handler(state.value)); return; } if (state.catch_ && state.throw_) { @@ -3647,7 +3645,7 @@ Interpreter.prototype['stepCallExpressionFunc_'] = function(stack, state, node) // 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.handler_ = state.catch_; + cb.handlers_ = [state.catch_]; cb.value = state.throw_; // Set stateless value to pass to handler cb.force_ = true; // Force callback to run again } From 01d73555b5666f52bc579d5619133d16d160b6d1 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Mon, 23 Nov 2020 14:34:38 -0600 Subject: [PATCH 59/63] fix result in then chain --- interpreter.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/interpreter.js b/interpreter.js index b36e8375..efb42fed 100644 --- a/interpreter.js +++ b/interpreter.js @@ -2816,6 +2816,14 @@ Interpreter.prototype.handleNativeResult_ = function(state, scope, value) { value.throw_(value); } 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; } }; @@ -3484,6 +3492,7 @@ Interpreter.prototype['stepCallExpression'] = function(stack, state, node) { 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) { From 26232691c9ecbf276ee80ae9830055b06c3ee38e Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Tue, 24 Nov 2020 09:20:38 -0600 Subject: [PATCH 60/63] Cleanup / comments --- interpreter.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/interpreter.js b/interpreter.js index efb42fed..63a59714 100644 --- a/interpreter.js +++ b/interpreter.js @@ -397,6 +397,8 @@ Interpreter.prototype.buildFunctionCaller_ = function (func, funcThis, var_args) 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; @@ -3001,8 +3003,8 @@ Interpreter.Scope = function(parentScope, strict, object) { * @constructor */ Interpreter.Throwable = function(errorClass, opt_message) { - this.errorClass = errorClass - this.opt_message = opt_message + this.errorClass = errorClass; + this.opt_message = opt_message; }; /** @@ -3022,11 +3024,11 @@ Interpreter.Throwable.prototype.throw_ = function(interpreter) { * @constructor */ Interpreter.Callback = function(callFnNode, opt_queued) { - this.node_ = callFnNode - this.handlers_ = [] - this.catch_ = null - this.node_.cb_ = this - this.queued_ = 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; }; /** From 3d0499c1757b9378402efc3ce354fd514f3a45be Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Tue, 24 Nov 2020 15:33:42 -0600 Subject: [PATCH 61/63] Fix case fallthrough --- interpreter.js | 1 + 1 file changed, 1 insertion(+) diff --git a/interpreter.js b/interpreter.js index 63a59714..75c537bd 100644 --- a/interpreter.js +++ b/interpreter.js @@ -2866,6 +2866,7 @@ Interpreter.prototype.unwind = function(type, value, label) { 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. From 8acfcee1812dd4f91950969f61d0de0c76cc0ec6 Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Tue, 1 Dec 2020 18:45:00 -0600 Subject: [PATCH 62/63] Fix throwing exception --- interpreter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interpreter.js b/interpreter.js index 75c537bd..94968880 100644 --- a/interpreter.js +++ b/interpreter.js @@ -2815,7 +2815,7 @@ Interpreter.prototype.handleNativeResult_ = function(state, scope, value) { return value.state_; } else if (value instanceof Interpreter.Throwable) { // Result was an error - value.throw_(value); + value.throw_(this); } else { // We have a final value var cb = state.cb_; From 79da8204e4f3e73674e36a2c7eb3888aa76ec66f Mon Sep 17 00:00:00 2001 From: John Kielkopf Date: Fri, 10 Sep 2021 17:26:49 -0500 Subject: [PATCH 63/63] Merge upstream changes --- interpreter.js | 940 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 647 insertions(+), 293 deletions(-) diff --git a/interpreter.js b/interpreter.js index 94968880..0a23ba22 100644 --- a/interpreter.js +++ b/interpreter.js @@ -20,7 +20,7 @@ */ var Interpreter = function(code, opt_initFunc) { if (typeof code === 'string') { - code = acorn.parse(code, Interpreter.PARSE_OPTIONS); + code = this.parse_(code, 'code'); } // Get a handle on Acorn's node_t object. this.nodeConstructor = code.constructor; @@ -50,7 +50,7 @@ var Interpreter = function(code, opt_initFunc) { this.globalScope = this.createScope(this.ast, null); this.globalObject = this.globalScope.object; // Run the polyfills. - this.ast = acorn.parse(this.polyfills_.join('\n'), Interpreter.PARSE_OPTIONS); + this.ast = this.parse_(this.polyfills_.join('\n'), 'polyfills'); this.polyfills_ = undefined; // Allow polyfill strings to garbage collect. Interpreter.stripLocations_(this.ast, undefined, undefined); var state = new Interpreter.State(this.ast, this.globalScope); @@ -85,7 +85,8 @@ var Interpreter = function(code, opt_initFunc) { * @const {!Object} Configuration used for all Acorn parsing. */ Interpreter.PARSE_OPTIONS = { - ecmaVersion: 5 + 'locations': true, + 'ecmaVersion': 5 }; /** @@ -115,6 +116,16 @@ Interpreter.READONLY_NONENUMERABLE_DESCRIPTOR = { writable: false }; +/** + * Property descriptor of non-configurable, readonly, non-enumerable properties. + * E.g. NaN, Infinity. + */ +Interpreter.NONCONFIGURABLE_READONLY_NONENUMERABLE_DESCRIPTOR = { + configurable: false, + enumerable: false, + writable: false +}; + /** * Property descriptor of variables. */ @@ -162,6 +173,11 @@ Interpreter.toStringCycles_ = []; */ Interpreter.vm = null; +/** + * The global object (`window` in a browser, `global` in node.js) is `this`. + */ +Interpreter.nativeGlobal = typeof globalThis === 'undefined' ? this : globalThis; + /** * Code for executing regular expressions in a thread. */ @@ -244,7 +260,7 @@ Interpreter.stripLocations_ = function(node, start, end) { delete node['end']; } for (var name in node) { - if (node.hasOwnProperty(name)) { + if (name !== 'loc' && node.hasOwnProperty(name)) { var prop = node[name]; if (prop && typeof prop === 'object') { Interpreter.stripLocations_(prop, start, end); @@ -268,6 +284,15 @@ Interpreter.prototype['REGEXP_MODE'] = 2; */ Interpreter.prototype['REGEXP_THREAD_TIMEOUT'] = 1000; +/** + * Length of time (in ms) to allow a polyfill to run before ending step. + * If set to 0, polyfills will execute step by step. + * If set to 1000, polyfills will run for up to a second per step + * (execution will resume in the polyfill in the next step). + * If set to Infinity, polyfills will run to completion in a single step. + */ +Interpreter.prototype['POLYFILL_TIMEOUT'] = 1000; + /** * Flag indicating that a getter function needs to be called immediately. * @private @@ -280,6 +305,29 @@ Interpreter.prototype.getterStep_ = false; */ Interpreter.prototype.setterStep_ = false; +/** + * Number of code chunks appended to the interpreter. + * @private + */ +Interpreter.prototype.appendCodeNumber_ = 0; + +/** + * Parse JavaScript code into an AST using Acorn. + * @param {string} code Raw JavaScript text. + * @param {string} sourceFile Name of filename (for stack trace). + * @return {!Object} AST. + * @private + */ +Interpreter.prototype.parse_ = function(code, sourceFile) { + // Create a new options object, since Acorn will modify this object. + var options = {}; + for (var name in Interpreter.PARSE_OPTIONS) { + options[name] = Interpreter.PARSE_OPTIONS[name]; + } + options['sourceFile'] = sourceFile; + return acorn.parse(code, options); +}; + /** * Add more code to the interpreter. * @param {string|!Object} code Raw JavaScript text or AST. @@ -290,7 +338,7 @@ Interpreter.prototype.appendCode = function(code) { throw Error('Expecting original AST to start with a Program node.'); } if (typeof code === 'string') { - code = acorn.parse(code, Interpreter.PARSE_OPTIONS); + code = this.parse_(code, 'appendCode' + (this.appendCodeNumber_++)); } if (!code || code['type'] !== 'Program') { throw Error('Expecting new AST to start with a Program node.'); @@ -307,6 +355,7 @@ Interpreter.prototype.appendCode = function(code) { */ Interpreter.prototype.step = function() { var stack = this.stateStack; + var startTime = Date.now(); do { var state = stack[stack.length - 1]; if (!state) { @@ -339,7 +388,7 @@ Interpreter.prototype.step = function() { throw Error('Setter not supported in this context'); } // This may be polyfill code. Keep executing until we arrive at user code. - } while (!node['end']); + } while (!node['end'] && startTime + this['POLYFILL_TIMEOUT'] > Date.now()); return true; }; @@ -413,15 +462,15 @@ Interpreter.prototype.buildFunctionCaller_ = function (func, funcThis, var_args) Interpreter.prototype.initGlobal = function(globalObject) { // Initialize uneditable global properties. this.setProperty(globalObject, 'NaN', NaN, - Interpreter.READONLY_DESCRIPTOR); + Interpreter.NONCONFIGURABLE_READONLY_NONENUMERABLE_DESCRIPTOR); this.setProperty(globalObject, 'Infinity', Infinity, - Interpreter.READONLY_DESCRIPTOR); + Interpreter.NONCONFIGURABLE_READONLY_NONENUMERABLE_DESCRIPTOR); this.setProperty(globalObject, 'undefined', undefined, - Interpreter.READONLY_DESCRIPTOR); + Interpreter.NONCONFIGURABLE_READONLY_NONENUMERABLE_DESCRIPTOR); this.setProperty(globalObject, 'window', globalObject, - Interpreter.READONLY_DESCRIPTOR); + Interpreter.READONLY_DESCRIPTOR); this.setProperty(globalObject, 'this', globalObject, - Interpreter.READONLY_DESCRIPTOR); + Interpreter.NONCONFIGURABLE_READONLY_NONENUMERABLE_DESCRIPTOR); this.setProperty(globalObject, 'self', globalObject); // Editable. // Create the objects which will become Object.prototype and @@ -436,7 +485,7 @@ Interpreter.prototype.initGlobal = function(globalObject) { // be `Object`. This interpreter is closer to Node in that it has no DOM. globalObject.proto = this.OBJECT_PROTO; this.setProperty(globalObject, 'constructor', this.OBJECT, - Interpreter.NONENUMERABLE_DESCRIPTOR); + Interpreter.NONENUMERABLE_DESCRIPTOR); this.initArray(globalObject); this.initString(globalObject); this.initBoolean(globalObject); @@ -452,18 +501,23 @@ Interpreter.prototype.initGlobal = function(globalObject) { var func = this.createNativeFunction( function(x) {throw EvalError("Can't happen");}, false); func.eval = true; - this.setProperty(globalObject, 'eval', func); + this.setProperty(globalObject, 'eval', func, + Interpreter.NONENUMERABLE_DESCRIPTOR); this.setProperty(globalObject, 'parseInt', - this.createNativeFunction(parseInt, false)); + this.createNativeFunction(parseInt, false), + Interpreter.NONENUMERABLE_DESCRIPTOR); this.setProperty(globalObject, 'parseFloat', - this.createNativeFunction(parseFloat, false)); + this.createNativeFunction(parseFloat, false), + Interpreter.NONENUMERABLE_DESCRIPTOR); this.setProperty(globalObject, 'isNaN', - this.createNativeFunction(isNaN, false)); + this.createNativeFunction(isNaN, false), + Interpreter.NONENUMERABLE_DESCRIPTOR); this.setProperty(globalObject, 'isFinite', - this.createNativeFunction(isFinite, false)); + this.createNativeFunction(isFinite, false), + Interpreter.NONENUMERABLE_DESCRIPTOR); var strFunctions = [ [escape, 'escape'], [unescape, 'unescape'], @@ -499,6 +553,12 @@ Interpreter.prototype.initGlobal = function(globalObject) { } }; +/** + * Number of functions created by the interpreter. + * @private + */ +Interpreter.prototype.functionCodeNumber_ = 0; + /** * Initialize the Function class. * @param {!Interpreter.Object} globalObject Global object. @@ -508,7 +568,7 @@ Interpreter.prototype.initFunction = function(globalObject) { var wrapper; var identifierRegexp = /^[A-Za-z_$][\w$]*$/; // Function constructor. - wrapper = function(var_args) { + wrapper = function Function(var_args) { if (arguments.length) { var code = String(arguments[arguments.length - 1]); } else { @@ -529,8 +589,8 @@ Interpreter.prototype.initFunction = function(globalObject) { // Acorn needs to parse code in the context of a function or else `return` // statements will be syntax errors. try { - var ast = acorn.parse('(function(' + argsStr + ') {' + code + '})', - Interpreter.PARSE_OPTIONS); + var ast = this.parse_('(function(' + argsStr + ') {' + code + '})', + 'function' + (this.functionCodeNumber_++)); } catch (e) { // Acorn threw a SyntaxError. Rethrow as a trappable error. thisInterpreter.throwException(thisInterpreter.SYNTAX_ERROR, @@ -546,48 +606,33 @@ Interpreter.prototype.initFunction = function(globalObject) { // object created by stepCallExpression and assigned to `this` is discarded. // Interestingly, the scope for constructed functions is the global scope, // even if they were constructed in some other scope. - return thisInterpreter.createFunction(node, thisInterpreter.globalScope); + return thisInterpreter.createFunction(node, thisInterpreter.globalScope, 'anonymous'); }; this.FUNCTION = this.createNativeFunction(wrapper, true); - this.setProperty(globalObject, 'Function', this.FUNCTION); + this.setProperty(globalObject, 'Function', this.FUNCTION, + Interpreter.NONENUMERABLE_DESCRIPTOR); // Throw away the created prototype and use the root prototype. this.setProperty(this.FUNCTION, 'prototype', this.FUNCTION_PROTO, - Interpreter.NONENUMERABLE_DESCRIPTOR); + Interpreter.NONENUMERABLE_DESCRIPTOR); // Configure Function.prototype. this.setProperty(this.FUNCTION_PROTO, 'constructor', this.FUNCTION, - Interpreter.NONENUMERABLE_DESCRIPTOR); + Interpreter.NONENUMERABLE_DESCRIPTOR); this.FUNCTION_PROTO.nativeFunc = function() {}; this.FUNCTION_PROTO.nativeFunc.id = this.functionCounter_++; + this.FUNCTION_PROTO.illegalConstructor = true; this.setProperty(this.FUNCTION_PROTO, 'length', 0, Interpreter.READONLY_NONENUMERABLE_DESCRIPTOR); + this.FUNCTION_PROTO.class = 'Function'; - var boxThis = function(value) { - // In non-strict mode `this` must be an object. - if (!(value instanceof Interpreter.Object) && - !thisInterpreter.getScope().strict) { - if (value === undefined || value === null) { - // `Undefined` and `null` are changed to the global object. - value = thisInterpreter.globalObject; - } else { - // Primitives must be boxed in non-strict mode. - var box = thisInterpreter.createObjectProto( - thisInterpreter.getPrototype(value)); - box.data = value; - value = box; - } - } - return value; - }; - - wrapper = function(thisArg, args) { + wrapper = function apply(thisArg, args) { var state = thisInterpreter.stateStack[thisInterpreter.stateStack.length - 1]; // Rewrite the current CallExpression state to apply a different function. state.func_ = this; // Assign the `this` object. - state.funcThis_ = boxThis(thisArg); + state.funcThis_ = thisArg; // Bind any provided arguments. state.arguments_ = []; if (args !== null && args !== undefined) { @@ -602,13 +647,13 @@ Interpreter.prototype.initFunction = function(globalObject) { }; this.setNativeFunctionPrototype(this.FUNCTION, 'apply', wrapper); - wrapper = function(thisArg /*, var_args */) { + wrapper = function call(thisArg /*, var_args */) { var state = thisInterpreter.stateStack[thisInterpreter.stateStack.length - 1]; // Rewrite the current CallExpression state to call a different function. state.func_ = this; // Assign the `this` object. - state.funcThis_ = boxThis(thisArg); + state.funcThis_ = thisArg; // Bind any provided arguments. state.arguments_ = []; for (var i = 1; i < arguments.length; i++) { @@ -623,7 +668,7 @@ Interpreter.prototype.initFunction = function(globalObject) { // developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_objects/Function/bind "Object.defineProperty(Function.prototype, 'bind',", "{configurable: true, writable: true, value:", - "function(oThis) {", + "function bind(oThis) {", "if (typeof this !== 'function') {", "throw TypeError('What is trying to be bound is not callable');", "}", @@ -647,14 +692,14 @@ Interpreter.prototype.initFunction = function(globalObject) { // Function has no parent to inherit from, so it needs its own mandatory // toString and valueOf functions. - wrapper = function() { + wrapper = function toString() { return String(this); }; this.setNativeFunctionPrototype(this.FUNCTION, 'toString', wrapper); this.setProperty(this.FUNCTION, 'toString', this.createNativeFunction(wrapper, false), Interpreter.NONENUMERABLE_DESCRIPTOR); - wrapper = function() { + wrapper = function valueOf() { return this.valueOf(); }; this.setNativeFunctionPrototype(this.FUNCTION, 'valueOf', wrapper); @@ -671,7 +716,7 @@ Interpreter.prototype.initObject = function(globalObject) { var thisInterpreter = this; var wrapper; // Object constructor. - wrapper = function(value) { + wrapper = function Object(value) { if (value === undefined || value === null) { // Create a new object. if (thisInterpreter.calledWithNew()) { @@ -698,7 +743,8 @@ Interpreter.prototype.initObject = function(globalObject) { Interpreter.NONENUMERABLE_DESCRIPTOR); this.setProperty(this.OBJECT_PROTO, 'constructor', this.OBJECT, Interpreter.NONENUMERABLE_DESCRIPTOR); - this.setProperty(globalObject, 'Object', this.OBJECT); + this.setProperty(globalObject, 'Object', this.OBJECT, + Interpreter.NONENUMERABLE_DESCRIPTOR); /** * Checks if the provided value is null or undefined. @@ -713,7 +759,7 @@ Interpreter.prototype.initObject = function(globalObject) { }; // Static methods on Object. - wrapper = function(obj) { + wrapper = function getOwnPropertyNames(obj) { throwIfNullUndefined(obj); var props = (obj instanceof Interpreter.Object) ? obj.properties : obj; return thisInterpreter.arrayNativeToPseudo( @@ -723,7 +769,7 @@ Interpreter.prototype.initObject = function(globalObject) { this.createNativeFunction(wrapper, false), Interpreter.NONENUMERABLE_DESCRIPTOR); - wrapper = function(obj) { + wrapper = function keys(obj) { throwIfNullUndefined(obj); if (obj instanceof Interpreter.Object) { obj = obj.properties; @@ -734,7 +780,7 @@ Interpreter.prototype.initObject = function(globalObject) { this.createNativeFunction(wrapper, false), Interpreter.NONENUMERABLE_DESCRIPTOR); - wrapper = function(proto) { + wrapper = function create_(proto) { // Support for the second argument is the responsibility of a polyfill. if (proto === null) { return thisInterpreter.createObjectProto(null); @@ -753,7 +799,7 @@ Interpreter.prototype.initObject = function(globalObject) { this.polyfills_.push( "(function() {", "var create_ = Object.create;", - "Object.create = function(proto, props) {", + "Object.create = function create(proto, props) {", "var obj = create_(proto);", "props && Object.defineProperties(obj, props);", "return obj;", @@ -761,7 +807,7 @@ Interpreter.prototype.initObject = function(globalObject) { "})();", ""); - wrapper = function(obj, prop, descriptor) { + wrapper = function defineProperty(obj, prop, descriptor) { prop = String(prop); if (!(obj instanceof Interpreter.Object)) { thisInterpreter.throwException(thisInterpreter.TYPE_ERROR, @@ -789,7 +835,7 @@ Interpreter.prototype.initObject = function(globalObject) { // Flatten the descriptor to remove any inheritance or getter functions. "(function() {", "var defineProperty_ = Object.defineProperty;", - "Object.defineProperty = function(obj, prop, d1) {", + "Object.defineProperty = function defineProperty(obj, prop, d1) {", "var d2 = {};", "if ('configurable' in d1) d2.configurable = d1.configurable;", "if ('enumerable' in d1) d2.enumerable = d1.enumerable;", @@ -803,7 +849,7 @@ Interpreter.prototype.initObject = function(globalObject) { "Object.defineProperty(Object, 'defineProperties',", "{configurable: true, writable: true, value:", - "function(obj, props) {", + "function defineProperties(obj, props) {", "var keys = Object.keys(props);", "for (var i = 0; i < keys.length; i++) {", "Object.defineProperty(obj, keys[i], props[keys[i]]);", @@ -813,7 +859,7 @@ Interpreter.prototype.initObject = function(globalObject) { "});", ""); - wrapper = function(obj, prop) { + wrapper = function getOwnPropertyDescriptor(obj, prop) { if (!(obj instanceof Interpreter.Object)) { thisInterpreter.throwException(thisInterpreter.TYPE_ERROR, 'Object.getOwnPropertyDescriptor called on non-object'); @@ -847,7 +893,7 @@ Interpreter.prototype.initObject = function(globalObject) { this.createNativeFunction(wrapper, false), Interpreter.NONENUMERABLE_DESCRIPTOR); - wrapper = function(obj) { + wrapper = function getPrototypeOf(obj) { throwIfNullUndefined(obj); return thisInterpreter.getPrototype(obj); }; @@ -855,14 +901,14 @@ Interpreter.prototype.initObject = function(globalObject) { this.createNativeFunction(wrapper, false), Interpreter.NONENUMERABLE_DESCRIPTOR); - wrapper = function(obj) { + wrapper = function isExtensible(obj) { return Boolean(obj) && !obj.preventExtensions; }; this.setProperty(this.OBJECT, 'isExtensible', this.createNativeFunction(wrapper, false), Interpreter.NONENUMERABLE_DESCRIPTOR); - wrapper = function(obj) { + wrapper = function preventExtensions(obj) { if (obj instanceof Interpreter.Object) { obj.preventExtensions = true; } @@ -880,7 +926,7 @@ Interpreter.prototype.initObject = function(globalObject) { this.setNativeFunctionPrototype(this.OBJECT, 'valueOf', Interpreter.Object.prototype.valueOf); - wrapper = function(prop) { + wrapper = function hasOwnProperty(prop) { throwIfNullUndefined(this); if (this instanceof Interpreter.Object) { return String(prop) in this.properties; @@ -890,7 +936,7 @@ Interpreter.prototype.initObject = function(globalObject) { }; this.setNativeFunctionPrototype(this.OBJECT, 'hasOwnProperty', wrapper); - wrapper = function(prop) { + wrapper = function propertyIsEnumerable(prop) { throwIfNullUndefined(this); if (this instanceof Interpreter.Object) { return Object.prototype.propertyIsEnumerable.call(this.properties, prop); @@ -900,7 +946,7 @@ Interpreter.prototype.initObject = function(globalObject) { }; this.setNativeFunctionPrototype(this.OBJECT, 'propertyIsEnumerable', wrapper); - wrapper = function(obj) { + wrapper = function isPrototypeOf(obj) { while (true) { // Note, circular loops shouldn't be possible. obj = thisInterpreter.getPrototype(obj); @@ -924,7 +970,7 @@ Interpreter.prototype.initArray = function(globalObject) { var thisInterpreter = this; var wrapper; // Array constructor. - wrapper = function(var_args) { + wrapper = function Array(var_args) { if (thisInterpreter.calledWithNew()) { // Called as `new Array()`. var newArray = this; @@ -949,10 +995,11 @@ Interpreter.prototype.initArray = function(globalObject) { }; this.ARRAY = this.createNativeFunction(wrapper, true); this.ARRAY_PROTO = this.ARRAY.properties['prototype']; - this.setProperty(globalObject, 'Array', this.ARRAY); + this.setProperty(globalObject, 'Array', this.ARRAY, + Interpreter.NONENUMERABLE_DESCRIPTOR); // Static methods on Array. - wrapper = function(obj) { + wrapper = function isArray(obj) { return obj && obj.class === 'Array'; }; this.setProperty(this.ARRAY, 'isArray', @@ -964,110 +1011,271 @@ Interpreter.prototype.initArray = function(globalObject) { {configurable: false, enumerable: false, writable: true}); this.ARRAY_PROTO.class = 'Array'; - wrapper = function() { - return Array.prototype.pop.call(this.properties); - }; - this.setNativeFunctionPrototype(this.ARRAY, 'pop', wrapper); + this.polyfills_.push( +"Object.defineProperty(Array.prototype, 'pop',", + "{configurable: true, writable: true, value:", + "function pop() {", + "if (!this) throw TypeError();", + "var o = Object(this);", + "var len = o.length >>> 0;", + "if (!len || len < 0) {", + "o.length = 0;", + "return undefined;", + "}", + "len--;", + "var x = o[len];", + "delete o[len];", // Needed for non-arrays. + "o.length = len;", + "return x;", + "}", +"});", - wrapper = function(var_args) { - return Array.prototype.push.apply(this.properties, arguments); - }; - this.setNativeFunctionPrototype(this.ARRAY, 'push', wrapper); +"Object.defineProperty(Array.prototype, 'push',", + "{configurable: true, writable: true, value:", + "function push(var_args) {", + "if (!this) throw TypeError();", + "var o = Object(this);", + "var len = o.length >>> 0;", + "for (var i = 0; i < arguments.length; i++) {", + "o[len] = arguments[i];", + "len++;", + "}", + "o.length = len;", + "return len;", + "}", +"});", - wrapper = function() { - return Array.prototype.shift.call(this.properties); - }; - this.setNativeFunctionPrototype(this.ARRAY, 'shift', wrapper); +"Object.defineProperty(Array.prototype, 'shift',", + "{configurable: true, writable: true, value:", + "function shift() {", + "if (!this) throw TypeError();", + "var o = Object(this);", + "var len = o.length >>> 0;", + "if (!len || len < 0) {", + "o.length = 0;", + "return undefined;", + "}", + "var value = o[0];", + "for (var i = 0; i < len - 1; i++) {", + "o[i] = o[i + 1];", + "}", + "delete o[i];", // Needed for non-arrays. + "o.length = len - 1;", + "return value;", + "}", +"});", - wrapper = function(var_args) { - return Array.prototype.unshift.apply(this.properties, arguments); - }; - this.setNativeFunctionPrototype(this.ARRAY, 'unshift', wrapper); +"Object.defineProperty(Array.prototype, 'unshift',", + "{configurable: true, writable: true, value:", + "function unshift(var_args) {", + "if (!this) throw TypeError();", + "var o = Object(this);", + "var len = o.length >>> 0;", + "if (!len || len < 0) {", + "len = 0;", + "}", + "for (var i = len - 1; i >= 0; i--) {", + "o[i + arguments.length] = o[i];", + "}", + "for (var i = 0; i < arguments.length; i++) {", + "o[i] = arguments[i];", + "}", + "return o.length = len + arguments.length;", + "}", +"});", - wrapper = function() { - Array.prototype.reverse.call(this.properties); - return this; - }; - this.setNativeFunctionPrototype(this.ARRAY, 'reverse', wrapper); +"Object.defineProperty(Array.prototype, 'reverse',", + "{configurable: true, writable: true, value:", + "function reverse() {", + "if (!this) throw TypeError();", + "var o = Object(this);", + "var len = o.length >>> 0;", + "if (!len || len < 2) {", + "return o;", // Not an array, or too short to reverse. + "}", + "for (var i = 0; i < len / 2 - 0.5; i++) {", + "var x = o[i];", + "o[i] = o[len - i - 1];", + "o[len - i - 1] = x;", + "}", + "return o;", + "}", +"});", - wrapper = function(index, howmany /*, var_args*/) { - var list = Array.prototype.splice.apply(this.properties, arguments); - return thisInterpreter.arrayNativeToPseudo(list); - }; - this.setNativeFunctionPrototype(this.ARRAY, 'splice', wrapper); +"Object.defineProperty(Array.prototype, 'indexOf',", + "{configurable: true, writable: true, value:", + "function indexOf(searchElement, fromIndex) {", + "if (!this) throw TypeError();", + "var o = Object(this);", + "var len = o.length >>> 0;", + "var n = fromIndex | 0;", + "if (!len || n >= len) {", + "return -1;", + "}", + "var i = Math.max(n >= 0 ? n : len - Math.abs(n), 0);", + "while (i < len) {", + "if (i in o && o[i] === searchElement) {", + "return i;", + "}", + "i++;", + "}", + "return -1;", + "}", +"});", - wrapper = function(opt_begin, opt_end) { - var list = Array.prototype.slice.call(this.properties, opt_begin, opt_end); - return thisInterpreter.arrayNativeToPseudo(list); - }; - this.setNativeFunctionPrototype(this.ARRAY, 'slice', wrapper); +"Object.defineProperty(Array.prototype, 'lastIndexOf',", + "{configurable: true, writable: true, value:", + "function lastIndexOf(searchElement, fromIndex) {", + "if (!this) throw TypeError();", + "var o = Object(this);", + "var len = o.length >>> 0;", + "if (!len) {", + "return -1;", + "}", + "var n = len - 1;", + "if (arguments.length > 1) {", + "n = fromIndex | 0;", + "if (n) {", + "n = (n > 0 || -1) * Math.floor(Math.abs(n));", + "}", + "}", + "var i = n >= 0 ? Math.min(n, len - 1) : len - Math.abs(n);", + "while (i >= 0) {", + "if (i in o && o[i] === searchElement) {", + "return i;", + "}", + "i--;", + "}", + "return -1;", + "}", +"});", - wrapper = function(opt_separator) { - return Array.prototype.join.call(this.properties, opt_separator); - }; - this.setNativeFunctionPrototype(this.ARRAY, 'join', wrapper); - - wrapper = function(var_args) { - var list = []; - var length = 0; - // Start by copying the current array. - var iLength = thisInterpreter.getProperty(this, 'length'); - for (var i = 0; i < iLength; i++) { - if (thisInterpreter.hasProperty(this, i)) { - var element = thisInterpreter.getProperty(this, i); - list[length] = element; - } - length++; - } - // Loop through all arguments and copy them in. - for (var i = 0; i < arguments.length; i++) { - var value = arguments[i]; - if (thisInterpreter.isa(value, thisInterpreter.ARRAY)) { - var jLength = thisInterpreter.getProperty(value, 'length'); - for (var j = 0; j < jLength; j++) { - if (thisInterpreter.hasProperty(value, j)) { - list[length] = thisInterpreter.getProperty(value, j); - } - length++; - } - } else { - list[length] = value; - } - } - return thisInterpreter.arrayNativeToPseudo(list); - }; - this.setNativeFunctionPrototype(this.ARRAY, 'concat', wrapper); +"Object.defineProperty(Array.prototype, 'slice',", + "{configurable: true, writable: true, value:", + "function slice(start, end) {", + "if (!this) throw TypeError();", + "var o = Object(this);", + "var len = o.length >>> 0;", + // Handle negative value for "start" + "start |= 0;", + "start = (start >= 0) ? start : Math.max(0, len + start);", + // Handle negative value for "end" + "if (typeof end !== 'undefined') {", + "if (end !== Infinity) {", + "end |= 0;", + "}", + "if (end < 0) {", + "end = len + end;", + "} else {", + "end = Math.min(end, len);", + "}", + "} else {", + "end = len;", + "}", + "var size = end - start;", + "var cloned = [];", + "for (var i = 0; i < size; i++) {", + "cloned[i] = o[start + i];", + "}", + "return cloned;", + "}", +"});", - wrapper = function(searchElement, opt_fromIndex) { - return Array.prototype.indexOf.apply(this.properties, arguments); - }; - this.setNativeFunctionPrototype(this.ARRAY, 'indexOf', wrapper); +"Object.defineProperty(Array.prototype, 'splice',", + "{configurable: true, writable: true, value:", + "function splice(start, deleteCount, var_args) {", + "if (!this) throw TypeError();", + "var o = Object(this);", + "var len = o.length >>> 0;", + "start |= 0;", + "if (start < 0) {", + "start = Math.max(len + start, 0);", + "} else {", + "start = Math.min(start, len);", + "}", + "if (arguments.length < 1) {", + "deleteCount = len - start;", + "} else {", + "deleteCount |= 0;", + "deleteCount = Math.max(0, Math.min(deleteCount, len - start));", + "}", + "var removed = [];", + // Remove specified elements. + "for (var i = start; i < start + deleteCount; i++) {", + "removed[removed.length++] = o[i];", + "o[i] = o[i + deleteCount];", + "}", + // Move other element to fill the gap. + "for (var i = start + deleteCount; i < len - deleteCount; i++) {", + "o[i] = o[i + deleteCount];", + "}", + // Delete superfluous properties. + "for (var i = len - deleteCount; i < len; i++) {", + "delete o[i];", + "}", + "len -= deleteCount;", + // Insert specified items. + "for (var i = len - 1; i >= start; i--) {", + "o[i + arguments.length - 2] = o[i];", + "}", + "len += arguments.length - 2;", + "for (var i = 2; i < arguments.length; i++) {", + "o[start + i - 2] = arguments[i];", + "}", + "o.length = len;", + "return removed;", + "}", +"});", - wrapper = function(searchElement, opt_fromIndex) { - return Array.prototype.lastIndexOf.apply(this.properties, arguments); - }; - this.setNativeFunctionPrototype(this.ARRAY, 'lastIndexOf', wrapper); +"Object.defineProperty(Array.prototype, 'concat',", + "{configurable: true, writable: true, value:", + "function concat(var_args) {", + "if (!this) throw TypeError();", + "var o = Object(this);", + "var cloned = [];", + "for (var i = -1; i < arguments.length; i++) {", + "var value = (i === -1) ? o : arguments[i];", + "if (Array.isArray(value)) {", + "cloned.push.apply(cloned, value);", + "} else {", + "cloned.push(value);", + "}", + "}", + "return cloned;", + "}", +"});", - wrapper = function() { - Array.prototype.sort.call(this.properties); - return this; - }; - this.setNativeFunctionPrototype(this.ARRAY, 'sort', wrapper); +"Object.defineProperty(Array.prototype, 'join',", + "{configurable: true, writable: true, value:", + "function join(opt_separator) {", + "if (!this) throw TypeError();", + "var o = Object(this);", + "var sep = typeof opt_separator === 'undefined' ?", + "',' : ('' + opt_separator);", + "var str = '';", + "for (var i = 0; i < o.length; i++) {", + "if (i && sep) {", + "str += sep;", + "}", + "str += o[i];", + "}", + "return str;", + "}", +"});", - this.polyfills_.push( // Polyfill copied from: // developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/every "Object.defineProperty(Array.prototype, 'every',", "{configurable: true, writable: true, value:", - "function(callbackfn, thisArg) {", + "function every(callbackfn, thisArg) {", "if (!this || typeof callbackfn !== 'function') throw TypeError();", - "var T, k;", - "var O = Object(this);", - "var len = O.length >>> 0;", - "if (arguments.length > 1) T = thisArg;", + "var t, k;", + "var o = Object(this);", + "var len = o.length >>> 0;", + "if (arguments.length > 1) t = thisArg;", "k = 0;", "while (k < len) {", - "if (k in O && !callbackfn.call(T, O[k], k, O)) return false;", + "if (k in o && !callbackfn.call(t, o[k], k, o)) return false;", "k++;", "}", "return true;", @@ -1078,16 +1286,16 @@ Interpreter.prototype.initArray = function(globalObject) { // developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/filter "Object.defineProperty(Array.prototype, 'filter',", "{configurable: true, writable: true, value:", - "function(fun/*, thisArg*/) {", + "function filter(fun, var_args) {", "if (this === void 0 || this === null || typeof fun !== 'function') throw TypeError();", - "var t = Object(this);", - "var len = t.length >>> 0;", + "var o = Object(this);", + "var len = o.length >>> 0;", "var res = [];", "var thisArg = arguments.length >= 2 ? arguments[1] : void 0;", "for (var i = 0; i < len; i++) {", - "if (i in t) {", - "var val = t[i];", - "if (fun.call(thisArg, val, i, t)) res.push(val);", + "if (i in o) {", + "var val = o[i];", + "if (fun.call(thisArg, val, i, o)) res.push(val);", "}", "}", "return res;", @@ -1098,15 +1306,15 @@ Interpreter.prototype.initArray = function(globalObject) { // developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach "Object.defineProperty(Array.prototype, 'forEach',", "{configurable: true, writable: true, value:", - "function(callback, thisArg) {", + "function forEach(callback, thisArg) {", "if (!this || typeof callback !== 'function') throw TypeError();", - "var T, k;", - "var O = Object(this);", - "var len = O.length >>> 0;", - "if (arguments.length > 1) T = thisArg;", + "var t, k;", + "var o = Object(this);", + "var len = o.length >>> 0;", + "if (arguments.length > 1) t = thisArg;", "k = 0;", "while (k < len) {", - "if (k in O) callback.call(T, O[k], k, O);", + "if (k in o) callback.call(t, o[k], k, o);", "k++;", "}", "}", @@ -1116,19 +1324,19 @@ Interpreter.prototype.initArray = function(globalObject) { // developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/map "Object.defineProperty(Array.prototype, 'map',", "{configurable: true, writable: true, value:", - "function(callback, thisArg) {", - "if (!this || typeof callback !== 'function') new TypeError;", - "var T, A, k;", - "var O = Object(this);", - "var len = O.length >>> 0;", - "if (arguments.length > 1) T = thisArg;", - "A = new Array(len);", + "function map(callback, thisArg) {", + "if (!this || typeof callback !== 'function') throw TypeError();", + "var t, a, k;", + "var o = Object(this);", + "var len = o.length >>> 0;", + "if (arguments.length > 1) t = thisArg;", + "a = new Array(len);", "k = 0;", "while (k < len) {", - "if (k in O) A[k] = callback.call(T, O[k], k, O);", + "if (k in o) a[k] = callback.call(t, o[k], k, o);", "k++;", "}", - "return A;", + "return a;", "}", "});", @@ -1136,20 +1344,20 @@ Interpreter.prototype.initArray = function(globalObject) { // developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce "Object.defineProperty(Array.prototype, 'reduce',", "{configurable: true, writable: true, value:", - "function(callback /*, initialValue*/) {", + "function reduce(callback /*, initialValue*/) {", "if (!this || typeof callback !== 'function') throw TypeError();", - "var t = Object(this), len = t.length >>> 0, k = 0, value;", + "var o = Object(this), len = o.length >>> 0, k = 0, value;", "if (arguments.length === 2) {", "value = arguments[1];", "} else {", - "while (k < len && !(k in t)) k++;", + "while (k < len && !(k in o)) k++;", "if (k >= len) {", "throw TypeError('Reduce of empty array with no initial value');", "}", - "value = t[k++];", + "value = o[k++];", "}", "for (; k < len; k++) {", - "if (k in t) value = callback(value, t[k], k, t);", + "if (k in o) value = callback(value, o[k], k, o);", "}", "return value;", "}", @@ -1159,20 +1367,20 @@ Interpreter.prototype.initArray = function(globalObject) { // developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/ReduceRight "Object.defineProperty(Array.prototype, 'reduceRight',", "{configurable: true, writable: true, value:", - "function(callback /*, initialValue*/) {", + "function reduceRight(callback /*, initialValue*/) {", "if (null === this || 'undefined' === typeof this || 'function' !== typeof callback) throw TypeError();", - "var t = Object(this), len = t.length >>> 0, k = len - 1, value;", + "var o = Object(this), len = o.length >>> 0, k = len - 1, value;", "if (arguments.length >= 2) {", "value = arguments[1];", "} else {", - "while (k >= 0 && !(k in t)) k--;", + "while (k >= 0 && !(k in o)) k--;", "if (k < 0) {", "throw TypeError('Reduce of empty array with no initial value');", "}", - "value = t[k--];", + "value = o[k--];", "}", "for (; k >= 0; k--) {", - "if (k in t) value = callback(value, t[k], k, t);", + "if (k in o) value = callback(value, o[k], k, o);", "}", "return value;", "}", @@ -1182,13 +1390,13 @@ Interpreter.prototype.initArray = function(globalObject) { // developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/some "Object.defineProperty(Array.prototype, 'some',", "{configurable: true, writable: true, value:", - "function(fun/*, thisArg*/) {", + "function some(fun/*, thisArg*/) {", "if (!this || typeof fun !== 'function') throw TypeError();", - "var t = Object(this);", - "var len = t.length >>> 0;", + "var o = Object(this);", + "var len = o.length >>> 0;", "var thisArg = arguments.length >= 2 ? arguments[1] : void 0;", "for (var i = 0; i < len; i++) {", - "if (i in t && fun.call(thisArg, t[i], i, t)) {", + "if (i in o && fun.call(thisArg, o[i], i, o)) {", "return true;", "}", "}", @@ -1197,18 +1405,18 @@ Interpreter.prototype.initArray = function(globalObject) { "});", -"(function() {", - "var sort_ = Array.prototype.sort;", - "Array.prototype.sort = function(opt_comp) {", - // Fast native sort. +"Object.defineProperty(Array.prototype, 'sort',", + "{configurable: true, writable: true, value:", + "function sort(opt_comp) {", // Bubble sort! + "if (!this) throw TypeError();", "if (typeof opt_comp !== 'function') {", - "return sort_.call(this);", + "opt_comp = undefined;", "}", - // Slow bubble sort. "for (var i = 0; i < this.length; i++) {", "var changes = 0;", "for (var j = 0; j < this.length - i - 1; j++) {", - "if (opt_comp(this[j], this[j + 1]) > 0) {", + "if (opt_comp ? (opt_comp(this[j], this[j + 1]) > 0) :", + "(String(this[j]) > String(this[j + 1]))) {", "var swap = this[j];", "this[j] = this[j + 1];", "this[j + 1] = swap;", @@ -1218,15 +1426,17 @@ Interpreter.prototype.initArray = function(globalObject) { "if (!changes) break;", "}", "return this;", - "};", -"})();", + "}", +"});", "Object.defineProperty(Array.prototype, 'toLocaleString',", "{configurable: true, writable: true, value:", - "function() {", + "function toLocaleString() {", + "if (!this) throw TypeError();", + "var o = Object(this);", "var out = [];", - "for (var i = 0; i < this.length; i++) {", - "out[i] = (this[i] === null || this[i] === undefined) ? '' : this[i].toLocaleString();", + "for (var i = 0; i < o.length; i++) {", + "out[i] = (o[i] === null || o[i] === undefined) ? '' : o[i].toLocaleString();", "}", "return out.join(',');", "}", @@ -1242,8 +1452,8 @@ Interpreter.prototype.initString = function(globalObject) { var thisInterpreter = this; var wrapper; // String constructor. - wrapper = function(value) { - value = arguments.length ? String(value) : ''; + wrapper = function String(value) { + value = arguments.length ? Interpreter.nativeGlobal.String(value) : ''; if (thisInterpreter.calledWithNew()) { // Called as `new String()`. this.data = value; @@ -1254,7 +1464,8 @@ Interpreter.prototype.initString = function(globalObject) { } }; this.STRING = this.createNativeFunction(wrapper, true); - this.setProperty(globalObject, 'String', this.STRING); + this.setProperty(globalObject, 'String', this.STRING, + Interpreter.NONENUMERABLE_DESCRIPTOR); // Static methods on String. this.setProperty(this.STRING, 'fromCharCode', @@ -1271,14 +1482,19 @@ Interpreter.prototype.initString = function(globalObject) { String.prototype[functions[i]]); } - wrapper = function(compareString, locales, options) { - locales = locales ? thisInterpreter.pseudoToNative(locales) : undefined; - options = options ? thisInterpreter.pseudoToNative(options) : undefined; - return String(this).localeCompare(compareString, locales, options); + wrapper = function localeCompare(compareString, locales, options) { + locales = thisInterpreter.pseudoToNative(locales); + options = thisInterpreter.pseudoToNative(options); + try { + return String(this).localeCompare(compareString, locales, options); + } catch (e) { + thisInterpreter.throwException(thisInterpreter.ERROR, + 'localeCompare: ' + e.message); + } }; this.setNativeFunctionPrototype(this.STRING, 'localeCompare', wrapper); - wrapper = function(separator, limit, callback) { + wrapper = function split(separator, limit, callback) { var string = String(this); limit = limit ? Number(limit) : undefined; // Example of catastrophic split RegExp: @@ -1320,7 +1536,7 @@ Interpreter.prototype.initString = function(globalObject) { }; this.setAsyncFunctionPrototype(this.STRING, 'split', wrapper); - wrapper = function(regexp, callback) { + wrapper = function match(regexp, callback) { var string = String(this); if (thisInterpreter.isa(regexp, thisInterpreter.REGEXP)) { regexp = regexp.data; @@ -1360,7 +1576,7 @@ Interpreter.prototype.initString = function(globalObject) { }; this.setAsyncFunctionPrototype(this.STRING, 'match', wrapper); - wrapper = function(regexp, callback) { + wrapper = function search(regexp, callback) { var string = String(this); if (thisInterpreter.isa(regexp, thisInterpreter.REGEXP)) { regexp = regexp.data; @@ -1399,7 +1615,7 @@ Interpreter.prototype.initString = function(globalObject) { }; this.setAsyncFunctionPrototype(this.STRING, 'search', wrapper); - wrapper = function(substr, newSubstr, callback) { + wrapper = function replace_(substr, newSubstr, callback) { // Support for function replacements is the responsibility of a polyfill. var string = String(this); newSubstr = String(newSubstr); @@ -1443,7 +1659,7 @@ Interpreter.prototype.initString = function(globalObject) { this.polyfills_.push( "(function() {", "var replace_ = String.prototype.replace;", - "String.prototype.replace = function(substr, newSubstr) {", + "String.prototype.replace = function replace(substr, newSubstr) {", "if (typeof newSubstr !== 'function') {", // string.replace(string|regexp, string) "return replace_.call(this, substr, newSubstr);", @@ -1484,8 +1700,8 @@ Interpreter.prototype.initBoolean = function(globalObject) { var thisInterpreter = this; var wrapper; // Boolean constructor. - wrapper = function(value) { - value = Boolean(value); + wrapper = function Boolean(value) { + value = Interpreter.nativeGlobal.Boolean(value); if (thisInterpreter.calledWithNew()) { // Called as `new Boolean()`. this.data = value; @@ -1496,7 +1712,8 @@ Interpreter.prototype.initBoolean = function(globalObject) { } }; this.BOOLEAN = this.createNativeFunction(wrapper, true); - this.setProperty(globalObject, 'Boolean', this.BOOLEAN); + this.setProperty(globalObject, 'Boolean', this.BOOLEAN, + Interpreter.NONENUMERABLE_DESCRIPTOR); }; /** @@ -1507,8 +1724,8 @@ Interpreter.prototype.initNumber = function(globalObject) { var thisInterpreter = this; var wrapper; // Number constructor. - wrapper = function(value) { - value = arguments.length ? Number(value) : 0; + wrapper = function Number(value) { + value = arguments.length ? Interpreter.nativeGlobal.Number(value) : 0; if (thisInterpreter.calledWithNew()) { // Called as `new Number()`. this.data = value; @@ -1519,17 +1736,18 @@ Interpreter.prototype.initNumber = function(globalObject) { } }; this.NUMBER = this.createNativeFunction(wrapper, true); - this.setProperty(globalObject, 'Number', this.NUMBER); + this.setProperty(globalObject, 'Number', this.NUMBER, + Interpreter.NONENUMERABLE_DESCRIPTOR); var numConsts = ['MAX_VALUE', 'MIN_VALUE', 'NaN', 'NEGATIVE_INFINITY', 'POSITIVE_INFINITY']; for (var i = 0; i < numConsts.length; i++) { this.setProperty(this.NUMBER, numConsts[i], Number[numConsts[i]], - Interpreter.READONLY_NONENUMERABLE_DESCRIPTOR); + Interpreter.NONCONFIGURABLE_READONLY_NONENUMERABLE_DESCRIPTOR); } // Instance methods on Number. - wrapper = function(fractionDigits) { + wrapper = function toExponential(fractionDigits) { try { return Number(this).toExponential(fractionDigits); } catch (e) { @@ -1539,7 +1757,7 @@ Interpreter.prototype.initNumber = function(globalObject) { }; this.setNativeFunctionPrototype(this.NUMBER, 'toExponential', wrapper); - wrapper = function(digits) { + wrapper = function toFixed(digits) { try { return Number(this).toFixed(digits); } catch (e) { @@ -1549,7 +1767,7 @@ Interpreter.prototype.initNumber = function(globalObject) { }; this.setNativeFunctionPrototype(this.NUMBER, 'toFixed', wrapper); - wrapper = function(precision) { + wrapper = function toPrecision(precision) { try { return Number(this).toPrecision(precision); } catch (e) { @@ -1559,7 +1777,7 @@ Interpreter.prototype.initNumber = function(globalObject) { }; this.setNativeFunctionPrototype(this.NUMBER, 'toPrecision', wrapper); - wrapper = function(radix) { + wrapper = function toString(radix) { try { return Number(this).toString(radix); } catch (e) { @@ -1569,7 +1787,7 @@ Interpreter.prototype.initNumber = function(globalObject) { }; this.setNativeFunctionPrototype(this.NUMBER, 'toString', wrapper); - wrapper = function(locales, options) { + wrapper = function toLocaleString(locales, options) { locales = locales ? thisInterpreter.pseudoToNative(locales) : undefined; options = options ? thisInterpreter.pseudoToNative(options) : undefined; return Number(this).toLocaleString(locales, options); @@ -1585,20 +1803,22 @@ Interpreter.prototype.initDate = function(globalObject) { var thisInterpreter = this; var wrapper; // Date constructor. - wrapper = function(value, var_args) { + wrapper = function Date(value, var_args) { if (!thisInterpreter.calledWithNew()) { // Called as `Date()`. // Calling Date() as a function returns a string, no arguments are heeded. - return Date(); + return Interpreter.nativeGlobal.Date(); } // Called as `new Date()`. var args = [null].concat(Array.from(arguments)); - this.data = new (Function.prototype.bind.apply(Date, args)); + this.data = new (Function.prototype.bind.apply( + Interpreter.nativeGlobal.Date, args)); return this; }; this.DATE = this.createNativeFunction(wrapper, true); this.DATE_PROTO = this.DATE.properties['prototype']; - this.setProperty(globalObject, 'Date', this.DATE); + this.setProperty(globalObject, 'Date', this.DATE, + Interpreter.NONENUMERABLE_DESCRIPTOR); // Static methods on Date. this.setProperty(this.DATE, 'now', this.createNativeFunction(Date.now, false), @@ -1627,11 +1847,16 @@ Interpreter.prototype.initDate = function(globalObject) { for (var i = 0; i < functions.length; i++) { wrapper = (function(nativeFunc) { return function(var_args) { + var date = this.data; + if (!(date instanceof Date)) { + thisInterpreter.throwException(thisInterpreter.TYPE_ERROR, + nativeFunc + ' not called on a Date'); + } var args = []; for (var i = 0; i < arguments.length; i++) { args[i] = thisInterpreter.pseudoToNative(arguments[i]); } - return this.data[nativeFunc].apply(this.data, args); + return date[nativeFunc].apply(date, args); }; })(functions[i]); this.setNativeFunctionPrototype(this.DATE, functions[i], wrapper); @@ -1646,22 +1871,29 @@ Interpreter.prototype.initRegExp = function(globalObject) { var thisInterpreter = this; var wrapper; // RegExp constructor. - wrapper = function(pattern, flags) { + wrapper = function RegExp(pattern, flags) { if (thisInterpreter.calledWithNew()) { // Called as `new RegExp()`. var rgx = this; } else { // Called as `RegExp()`. + if (flags === undefined && + thisInterpreter.isa(pattern, thisInterpreter.REGEXP)) { + // Regexp(/foo/) returns the same obj. + return pattern; + } var rgx = thisInterpreter.createObjectProto(thisInterpreter.REGEXP_PROTO); } - pattern = pattern ? String(pattern) : ''; + pattern = pattern === undefined ? '' : String(pattern); flags = flags ? String(flags) : ''; - thisInterpreter.populateRegExp(rgx, new RegExp(pattern, flags)); + thisInterpreter.populateRegExp(rgx, + new Interpreter.nativeGlobal.RegExp(pattern, flags)); return rgx; }; this.REGEXP = this.createNativeFunction(wrapper, true); this.REGEXP_PROTO = this.REGEXP.properties['prototype']; - this.setProperty(globalObject, 'RegExp', this.REGEXP); + this.setProperty(globalObject, 'RegExp', this.REGEXP, + Interpreter.NONENUMERABLE_DESCRIPTOR); this.setProperty(this.REGEXP.properties['prototype'], 'global', undefined, Interpreter.READONLY_NONENUMERABLE_DESCRIPTOR); @@ -1676,12 +1908,12 @@ Interpreter.prototype.initRegExp = function(globalObject) { this.polyfills_.push( "Object.defineProperty(RegExp.prototype, 'test',", "{configurable: true, writable: true, value:", - "function(str) {", - "return String(str).search(this) !== -1", + "function test(str) {", + "return !!this.exec(str);", "}", "});"); - wrapper = function(string, callback) { + wrapper = function exec(string, callback) { var regexp = this.data; string = String(string); // Get lastIndex from wrapped regexp, since this is settable. @@ -1745,7 +1977,7 @@ Interpreter.prototype.initRegExp = function(globalObject) { Interpreter.prototype.initError = function(globalObject) { var thisInterpreter = this; // Error constructor. - this.ERROR = this.createNativeFunction(function(opt_message) { + this.ERROR = this.createNativeFunction(function Error(opt_message) { if (thisInterpreter.calledWithNew()) { // Called as `new Error()`. var newError = this; @@ -1753,13 +1985,11 @@ Interpreter.prototype.initError = function(globalObject) { // Called as `Error()`. var newError = thisInterpreter.createObject(thisInterpreter.ERROR); } - if (opt_message) { - thisInterpreter.setProperty(newError, 'message', String(opt_message), - Interpreter.NONENUMERABLE_DESCRIPTOR); - } + thisInterpreter.populateError(newError, opt_message); return newError; }, true); - this.setProperty(globalObject, 'Error', this.ERROR); + this.setProperty(globalObject, 'Error', this.ERROR, + Interpreter.NONENUMERABLE_DESCRIPTOR); this.setProperty(this.ERROR.properties['prototype'], 'message', '', Interpreter.NONENUMERABLE_DESCRIPTOR); this.setProperty(this.ERROR.properties['prototype'], 'name', 'Error', @@ -1775,10 +2005,7 @@ Interpreter.prototype.initError = function(globalObject) { // Called as `XyzError()`. var newError = thisInterpreter.createObject(constructor); } - if (opt_message) { - thisInterpreter.setProperty(newError, 'message', - String(opt_message), Interpreter.NONENUMERABLE_DESCRIPTOR); - } + thisInterpreter.populateError(newError, opt_message); return newError; }, true); thisInterpreter.setProperty(constructor, 'prototype', @@ -1786,7 +2013,8 @@ Interpreter.prototype.initError = function(globalObject) { Interpreter.NONENUMERABLE_DESCRIPTOR); thisInterpreter.setProperty(constructor.properties['prototype'], 'name', name, Interpreter.NONENUMERABLE_DESCRIPTOR); - thisInterpreter.setProperty(globalObject, name, constructor); + thisInterpreter.setProperty(globalObject, name, constructor, + Interpreter.NONENUMERABLE_DESCRIPTOR); return constructor; }; @@ -1805,7 +2033,8 @@ Interpreter.prototype.initError = function(globalObject) { */ Interpreter.prototype.initMath = function(globalObject) { var myMath = this.createObjectProto(this.OBJECT_PROTO); - this.setProperty(globalObject, 'Math', myMath); + this.setProperty(globalObject, 'Math', myMath, + Interpreter.NONENUMERABLE_DESCRIPTOR); var mathConsts = ['E', 'LN2', 'LN10', 'LOG2E', 'LOG10E', 'PI', 'SQRT1_2', 'SQRT2']; for (var i = 0; i < mathConsts.length; i++) { @@ -1829,9 +2058,10 @@ Interpreter.prototype.initMath = function(globalObject) { Interpreter.prototype.initJSON = function(globalObject) { var thisInterpreter = this; var myJSON = thisInterpreter.createObjectProto(this.OBJECT_PROTO); - this.setProperty(globalObject, 'JSON', myJSON); + this.setProperty(globalObject, 'JSON', myJSON, + Interpreter.NONENUMERABLE_DESCRIPTOR); - var wrapper = function(text) { + var wrapper = function parse(text) { try { var nativeObj = JSON.parse(String(text)); } catch (e) { @@ -1841,7 +2071,7 @@ Interpreter.prototype.initJSON = function(globalObject) { }; this.setProperty(myJSON, 'parse', this.createNativeFunction(wrapper, false)); - wrapper = function(value, replacer, space) { + wrapper = function stringify(value, replacer, space) { if (replacer && replacer.class === 'Function') { thisInterpreter.throwException(thisInterpreter.TYPE_ERROR, 'Function replacer on JSON.stringify not supported'); @@ -1919,6 +2149,50 @@ Interpreter.prototype.populateRegExp = function(pseudoRegexp, nativeRegexp) { Interpreter.READONLY_NONENUMERABLE_DESCRIPTOR); }; +/** + * Initialize a pseudo error object. + * @param {!Interpreter.Object} pseudoError The existing object to set. + * @param {string=} opt_message Error's message. + */ +Interpreter.prototype.populateError = function(pseudoError, opt_message) { + if (opt_message) { + this.setProperty(pseudoError, 'message', String(opt_message), + Interpreter.NONENUMERABLE_DESCRIPTOR); + } + var tracebackData = []; + for (var i = this.stateStack.length - 1; i >= 0; i--) { + var state = this.stateStack[i]; + var node = state.node; + if (node['type'] === 'CallExpression') { + var func = state.func_; + if (func && tracebackData.length) { + tracebackData[tracebackData.length - 1].name = + this.getProperty(func, 'name'); + } + } + if (node['loc'] && + (!tracebackData.length || node['type'] === 'CallExpression')) { + tracebackData.push({loc: node['loc']}); + } + } + var name = String(this.getProperty(pseudoError, 'name')); + var message = String(this.getProperty(pseudoError, 'message')); + var stackString = name + ': ' + message + '\n'; + for (var i = 0; i < tracebackData.length; i++) { + var loc = tracebackData[i].loc; + var name = tracebackData[i].name; + var locString = loc['source'] + ':' + + loc['start']['line'] + ':' + loc['start']['column']; + if (name) { + stackString += ' at ' + name + ' (' + locString + ')\n'; + } else { + stackString += ' at ' + locString + '\n'; + } + } + this.setProperty(pseudoError, 'stack', stackString.trim(), + Interpreter.NONENUMERABLE_DESCRIPTOR); +}; + /** * Create a Web Worker to execute regular expressions. * Using a separate file fails in Chrome when run locally on a file:// URI. @@ -2082,6 +2356,8 @@ Interpreter.prototype.createFunctionBase_ = function(argumentLength, this.setProperty(func, 'length', argumentLength, Interpreter.READONLY_NONENUMERABLE_DESCRIPTOR); func.class = 'Function'; + // When making changes to this function, check to see if those changes also + // need to be made to the creation of FUNCTION_PROTO in initFunction. return func; }; @@ -2089,12 +2365,22 @@ Interpreter.prototype.createFunctionBase_ = function(argumentLength, * Create a new interpreted function. * @param {!Object} node AST node defining the function. * @param {!Interpreter.Scope} scope Parent scope. + * @param {string=} opt_name Optional name for function. * @return {!Interpreter.Object} New function. */ -Interpreter.prototype.createFunction = function(node, scope) { +Interpreter.prototype.createFunction = function(node, scope, opt_name) { var func = this.createFunctionBase_(node['params'].length, true); func.parentScope = scope; func.node = node; + // Choose a name for this function. + // function foo() {} -> 'foo' + // var bar = function() {}; -> 'bar' + // var bar = function foo() {}; -> 'foo' + // foo.bar = function() {}; -> '' + // var bar = new Function(''); -> 'anonymous' + var name = node['id'] ? String(node['id']['name']) : (opt_name || ''); + this.setProperty(func, 'name', name, + Interpreter.READONLY_NONENUMERABLE_DESCRIPTOR); return func; }; @@ -2109,6 +2395,8 @@ Interpreter.prototype.createNativeFunction = function(nativeFunc, var func = this.createFunctionBase_(nativeFunc.length, isConstructor); func.nativeFunc = nativeFunc; nativeFunc.id = this.functionCounter_++; + this.setProperty(func, 'name', nativeFunc.name, + Interpreter.READONLY_NONENUMERABLE_DESCRIPTOR); return func; }; @@ -2121,6 +2409,8 @@ Interpreter.prototype.createAsyncFunction = function(asyncFunc) { var func = this.createFunctionBase_(asyncFunc.length, true); func.asyncFunc = asyncFunc; asyncFunc.id = this.functionCounter_++; + this.setProperty(func, 'name', asyncFunc.name, + Interpreter.READONLY_NONENUMERABLE_DESCRIPTOR); return func; }; @@ -2224,8 +2514,8 @@ Interpreter.prototype.pseudoToNative = function(pseudoObj, opt_cycles) { if (this.isa(pseudoObj, this.ARRAY)) { // Array. nativeObj = []; cycles.native.push(nativeObj); - var length = this.getProperty(pseudoObj, 'length'); - for (var i = 0; i < length; i++) { + var len = this.getProperty(pseudoObj, 'length'); + for (var i = 0; i < len; i++) { if (this.hasProperty(pseudoObj, i)) { nativeObj[i] = this.pseudoToNative(this.getProperty(pseudoObj, i), cycles); @@ -2430,7 +2720,7 @@ Interpreter.prototype.setProperty = function(obj, name, value, opt_descriptor) { } if (obj.class === 'Array') { // Arrays have a magic length variable that is bound to the elements. - var length = obj.properties.length; + var len = obj.properties.length; var i; if (name === 'length') { // Delete elements if length is smaller. @@ -2444,7 +2734,7 @@ Interpreter.prototype.setProperty = function(obj, name, value, opt_descriptor) { if (isNaN(value)) { this.throwException(this.RANGE_ERROR, 'Invalid array length'); } - if (value < length) { + if (value < len) { for (i in obj.properties) { i = Interpreter.legalArrayIndex(i); if (!isNaN(i) && value <= i) { @@ -2454,7 +2744,7 @@ Interpreter.prototype.setProperty = function(obj, name, value, opt_descriptor) { } } else if (!isNaN(i = Interpreter.legalArrayIndex(name))) { // Increase length if this index is larger. - obj.properties.length = Math.max(length, i + 1); + obj.properties.length = Math.max(len, i + 1); } } if (obj.preventExtensions && !(name in obj.properties)) { @@ -2466,21 +2756,15 @@ Interpreter.prototype.setProperty = function(obj, name, value, opt_descriptor) { } if (opt_descriptor) { // Define the property. - if ('get' in opt_descriptor) { - if (opt_descriptor.get) { - obj.getter[name] = opt_descriptor.get; - } else { - delete obj.getter[name]; - } + var descriptor = {}; + if ('get' in opt_descriptor && opt_descriptor.get) { + obj.getter[name] = opt_descriptor.get; + descriptor.get = this.setProperty.placeholderGet_; } - if ('set' in opt_descriptor) { - if (opt_descriptor.set) { - obj.setter[name] = opt_descriptor.set; - } else { - delete obj.setter[name]; - } + if ('set' in opt_descriptor && opt_descriptor.set) { + obj.setter[name] = opt_descriptor.set; + descriptor.set = this.setProperty.placeholderSet_; } - var descriptor = {}; if ('configurable' in opt_descriptor) { descriptor.configurable = opt_descriptor.configurable; } @@ -2506,6 +2790,13 @@ Interpreter.prototype.setProperty = function(obj, name, value, opt_descriptor) { } catch (e) { this.throwException(this.TYPE_ERROR, 'Cannot redefine property: ' + name); } + // Now that the definition has suceeded, clean up any obsolete get/set funcs. + if ('get' in opt_descriptor && !opt_descriptor.get) { + delete obj.getter[name]; + } + if ('set' in opt_descriptor && !opt_descriptor.set) { + delete obj.setter[name]; + } } else { // Set the property. if (value === Interpreter.VALUE_IN_DESCRIPTOR) { @@ -2544,6 +2835,9 @@ Interpreter.prototype.setProperty = function(obj, name, value, opt_descriptor) { } }; +Interpreter.prototype.setProperty.placeholderGet_ = function() {throw Error('Placeholder getter')}; +Interpreter.prototype.setProperty.placeholderSet_ = function() {throw Error('Placeholder setter')}; + /** * Convenience method for adding a native function as a non-enumerable property * onto an object's prototype. @@ -2781,8 +3075,7 @@ Interpreter.prototype.throwException = function(errorClass, opt_message) { var error = errorClass; // This is a value to throw, not an error class. } else { var error = this.createObject(errorClass); - this.setProperty(error, 'message', opt_message, - Interpreter.NONENUMERABLE_DESCRIPTOR); + this.populateError(error, opt_message); } this.unwind(Interpreter.Completion.THROW, error, undefined); // Abort anything related to the current step. @@ -2902,6 +3195,7 @@ Interpreter.prototype.unwind = function(type, value, label) { var message = this.getProperty(value, 'message').valueOf(); var errorConstructor = errorTable[name] || Error; realError = errorConstructor(message); + realError.stack = String(this.getProperty(value, 'stack')); } else { realError = String(value); } @@ -2965,6 +3259,27 @@ Interpreter.prototype.createSetter_ = function(func, left, value) { return state; }; +/** + * In non-strict mode `this` must be an object. + * Must not be called in strict mode. + * @param {Interpreter.Value} value Proposed value for `this`. + * @return {!Interpreter.Object} Final value for `this`. + * @private + */ +Interpreter.prototype.boxThis_ = function(value) { + if (value === undefined || value === null) { + // `Undefined` and `null` are changed to the global object. + return this.globalObject; + } + if (!(value instanceof Interpreter.Object)) { + // Primitives must be boxed. + var box = this.createObjectProto(this.getPrototype(value)); + box.data = value; + return box; + } + return value; +}; + /** * Typedef for JS values. * @typedef {!Interpreter.Object|boolean|number|string|undefined|null} @@ -3132,11 +3447,22 @@ Interpreter.Object.prototype.toString = function() { cycles.push(this); try { var strs = []; - for (var i = 0; i < this.properties.length; i++) { + // Truncate very long strings. This is not part of the spec, + // but it prevents hanging the interpreter for gigantic arrays. + var maxLength = this.properties.length; + var truncated = false; + if (maxLength > 1024) { + maxLength = 1000; + truncated = true; + } + for (var i = 0; i < maxLength; i++) { var value = this.properties[i]; strs[i] = ((value instanceof Interpreter.Object) && cycles.indexOf(value) !== -1) ? '...' : value; } + if (truncated) { + strs.push('...'); + } } finally { cycles.pop(); } @@ -3251,6 +3577,13 @@ Interpreter.prototype['stepAssignmentExpression'] = } } state.doneRight_ = true; + // When assigning an unnamed function to a variable, the function's name + // is set to the variable name. Record the variable name in case the + // right side is a functionExpression. + // E.g. foo = function() {}; + if (node['operator'] === '=' && node['left']['type'] === 'Identifier') { + state.destinationName = node['left']['name']; + } return new Interpreter.State(node['right'], state.scope); } if (state.doneSetter_) { @@ -3360,6 +3693,12 @@ Interpreter.prototype['stepBreakStatement'] = function(stack, state, node) { this.unwind(Interpreter.Completion.BREAK, undefined, label); }; +/** + * Number of evals called by the interpreter. + * @private + */ +Interpreter.prototype.evalCodeNumber_ = 0; + Interpreter.prototype['stepCallExpression'] = function(stack, state, node) { if (!state.doneCallee_) { state.doneCallee_ = 1; @@ -3405,7 +3744,7 @@ Interpreter.prototype['stepCallExpression'] = function(stack, state, node) { } // Determine value of `this` in function. if (node['type'] === 'NewExpression') { - if (func.illegalConstructor) { + if (!(func instanceof Interpreter.Object) || func.illegalConstructor) { // Illegal: new escape(); this.throwException(this.TYPE_ERROR, func + ' is not a constructor'); } @@ -3421,9 +3760,6 @@ Interpreter.prototype['stepCallExpression'] = function(stack, state, node) { state.funcThis_ = this.createObjectProto(proto); } state.isConstructor = true; - } else if (state.funcThis_ === undefined) { - // Global function, `this` is global object (or `undefined` if strict). - state.funcThis_ = state.scope.strict ? undefined : this.globalObject; } state.doneArgs_ = true; } @@ -3458,6 +3794,9 @@ Interpreter.prototype['stepCallExpression'] = function(stack, state, node) { if (name) { this.setProperty(scope.object, name, func); } + if (!scope.strict) { + state.funcThis_ = this.boxThis_(state.funcThis_); + } this.setProperty(scope.object, 'this', state.funcThis_, Interpreter.READONLY_DESCRIPTOR); state.value = undefined; // Default value if no explicit return. @@ -3470,7 +3809,8 @@ Interpreter.prototype['stepCallExpression'] = function(stack, state, node) { state.value = code; } else { try { - var ast = acorn.parse(String(code), Interpreter.PARSE_OPTIONS); + var ast = this.parse_(String(code), + 'eval' + (this.evalCodeNumber_++)); } catch (e) { // Acorn threw a SyntaxError. Rethrow as a trappable error. this.throwException(this.SYNTAX_ERROR, 'Invalid code: ' + e.message); @@ -3492,6 +3832,9 @@ Interpreter.prototype['stepCallExpression'] = function(stack, state, node) { return new Interpreter.State(evalNode, scope); } } else if (func.nativeFunc) { + if (!state.scope.strict) { + state.funcThis_ = this.boxThis_(state.funcThis_); + } this.handleNativeResult_(state, scope, state.cb_ ? state.cb_.doNext_() : func.nativeFunc.apply(state.funcThis_, state.arguments_)); @@ -3513,6 +3856,9 @@ Interpreter.prototype['stepCallExpression'] = function(stack, state, node) { var argsWithCallback = state.arguments_.concat( new Array(argLength)).slice(0, argLength); argsWithCallback.push(callback); + if (!state.scope.strict) { + state.funcThis_ = this.boxThis_(state.funcThis_); + } func.asyncFunc.apply(state.funcThis_, argsWithCallback); return; } else { @@ -3839,7 +4185,8 @@ Interpreter.prototype['stepFunctionDeclaration'] = Interpreter.prototype['stepFunctionExpression'] = function(stack, state, node) { stack.pop(); - stack[stack.length - 1].value = this.createFunction(node, state.scope); + state = stack[stack.length - 1]; + state.value = this.createFunction(node, state.scope, state.destinationName); }; Interpreter.prototype['stepIdentifier'] = function(stack, state, node) { @@ -3852,10 +4199,6 @@ Interpreter.prototype['stepIdentifier'] = function(stack, state, node) { // An identifier could be a getter if it's a property on the global object. if (this.getterStep_) { // Call the getter function. - var scope = state.scope; - while (scope !== this.globalScope && !this.hasProperty(scope, node['name'])) { - scope = scope.parentScope; - } var func = /** @type {!Interpreter.Object} */ (value); return this.createGetter_(func, this.globalObject); } @@ -3954,16 +4297,8 @@ Interpreter.prototype['stepObjectExpression'] = function(stack, state, node) { state.object_ = this.createObjectProto(this.OBJECT_PROTO); state.properties_ = Object.create(null); } else { - // Determine property name. - var key = property['key']; - if (key['type'] === 'Identifier') { - var propName = key['name']; - } else if (key['type'] === 'Literal') { - var propName = key['value']; - } else { - throw SyntaxError('Unknown object structure: ' + key['type']); - } // Set the property computed in the previous execution. + var propName = state.destinationName; if (!state.properties_[propName]) { // Create temp object to collect value, getter, and/or setter. state.properties_[propName] = {}; @@ -3973,6 +4308,20 @@ Interpreter.prototype['stepObjectExpression'] = function(stack, state, node) { property = node['properties'][n]; } if (property) { + // Determine property name. + var key = property['key']; + if (key['type'] === 'Identifier') { + var propName = key['name']; + } else if (key['type'] === 'Literal') { + var propName = key['value']; + } else { + throw SyntaxError('Unknown object structure: ' + key['type']); + } + // When assigning an unnamed function to a property, the function's name + // is set to the property name. Record the property name in case the + // value is a functionExpression. + // E.g. {foo: function() {}} + state.destinationName = propName; return new Interpreter.State(property['value'], state.scope); } for (var key in state.properties_) { @@ -4239,6 +4588,11 @@ Interpreter.prototype['stepVariableDeclaration'] = function(stack, state, node) if (declarationNode['init']) { state.n_ = n; state.init_ = true; + // When assigning an unnamed function to a variable, the function's name + // is set to the variable name. Record the variable name in case the + // right side is a functionExpression. + // E.g. var foo = function() {}; + state.destinationName = declarationNode['id']['name']; return new Interpreter.State(declarationNode['init'], state.scope); } declarationNode = declarations[++n];