diff --git a/interpreter.js b/interpreter.js index ccfe1777..ed6cf338 100644 --- a/interpreter.js +++ b/interpreter.js @@ -31,6 +31,7 @@ * global scope object. * @constructor */ +var acorn = acorn || require('./acorn'); var Interpreter = function(code, opt_initFunc) { if (typeof code == 'string') { code = acorn.parse(code); @@ -2151,22 +2152,22 @@ Interpreter.prototype['stepCallExpression'] = function() { }; Interpreter.prototype['stepCatchClause'] = function() { - var state = this.stateStack[0]; - var node = state.node; - if (!state.doneBody) { - state.doneBody = true; - var scope; - if (node.param) { - scope = this.createSpecialScope(this.getScope()); - // Add the argument. - var paramName = this.createPrimitive(node.param.name); - this.setProperty(scope, paramName, state.throwValue); + var state = this.stateStack[0]; + if(!state.done) { + state.done = true; + var scope; + if (state.node.param) { + scope = this.createSpecialScope(this.getScope()); + // Add the argument. + var paramName = this.createPrimitive(state.node.param.name); + this.setProperty(scope, paramName, state.node.parameter); + } + this.stateStack.unshift({node:state.node.body, scope: scope}); + }else{ + this.stateStack.shift(); } - this.stateStack.unshift({node: node.body, scope: scope}); - } else { - this.stateStack.shift(); - } -}; +} + Interpreter.prototype['stepConditionalExpression'] = function() { var state = this.stateStack[0]; @@ -2197,6 +2198,7 @@ Interpreter.prototype['stepContinueStatement'] = function() { label = node.label.name; } var state = this.stateStack[0]; + var position = 0; while (state && state.node.type != 'CallExpression' && state.node.type != 'NewExpression') { @@ -2205,8 +2207,18 @@ Interpreter.prototype['stepContinueStatement'] = function() { return; } } - this.stateStack.shift(); - state = this.stateStack[0]; + + if(state.node.type == 'CatchClause') { + // if an unhandled exception is on the stack when returning, rethrow + this.stateStack.splice(position, 1, state.node.thrower); + position++; + }else if(state.node.type === 'TryStatement' && state.node.finalizer && !state.done) { + //before returning finish finally blocks + position++; + }else{ + this.stateStack.splice(position, 1); + } + state = this.stateStack[position]; } // Syntax error, do not allow this error to be trapped. throw new SyntaxError('Illegal continue statement'); @@ -2450,8 +2462,7 @@ Interpreter.prototype['stepObjectExpression'] = function() { } }; -Interpreter.prototype['stepProgram'] = - Interpreter.prototype['stepBlockStatement']; +Interpreter.prototype['stepProgram'] = Interpreter.prototype['stepBlockStatement']; Interpreter.prototype['stepReturnStatement'] = function() { var state = this.stateStack[0]; @@ -2461,15 +2472,25 @@ Interpreter.prototype['stepReturnStatement'] = function() { this.stateStack.unshift({node: node.argument}); } else { var value = state.value || this.UNDEFINED; + var position = 0; do { - this.stateStack.shift(); + if(state.node.type == 'CatchClause') { + // if an unhandled exception is on the stack when returning, rethrow + this.stateStack.splice(position, 1, state.node.thrower); + position++; + }else if(state.node.type === 'TryStatement' && state.node.finalizer && !state.done) { + //before returning finish finally blocks + position++; + }else{ + this.stateStack.splice(position, 1); + } if (this.stateStack.length == 0) { // Syntax error, do not allow this error to be trapped. throw new SyntaxError('Illegal return statement'); } - state = this.stateStack[0]; + state = this.stateStack[position]; } while (state.node.type != 'CallExpression' && - state.node.type != 'NewExpression'); + state.node.type != 'NewExpression'); state.value = value; } }; @@ -2539,31 +2560,86 @@ Interpreter.prototype['stepThisExpression'] = function() { throw 'No this expression found.'; }; +Interpreter.prototype['stepTryStatement'] = function() { + var state = this.stateStack[0]; + if(!state.done) { + if(!state.tried) { + state.tried = true; + this.stateStack.unshift({node: state.node.block}) + }else{ + if(state.node.finalizer) { + this.stateStack.unshift({node: state.node.finalizer}); + } + state.done = true; + } + }else{ + this.stateStack.shift(); + this.stateStack[0].value = state.value; + } +} + Interpreter.prototype['stepThrowStatement'] = function() { var state = this.stateStack[0]; var node = state.node; - if (!state.argument) { + if (!state.argument) { //calculate error object state.argument = true; this.stateStack.unshift({node: node.argument}); } else { - this.throwException(state.value); - } -}; + /* + * find nearest try statements with only finally clauses + * delete their remaining try blocks and started finally blocks + * triger their finally blocks + * find the nearest try statement with a catch clause + * set that try statement to not trigger again + * trigger the catch block + * */ + var try_statement = null; + var finallys_todo = []; + for(var i = 0;i < this.stateStack.length; i++) { + if(this.stateStack[i].node.type === 'TryStatement' && this.stateStack[i].node.handler && !this.stateStack[i].triggered) { + try_statement = this.stateStack[i].node; + this.stateStack[i].triggered = true; + finallys_todo.push(this.stateStack[i]); + break; + } else if(this.stateStack[i].node.type === 'TryStatement' && !this.stateStack[i].done) { + finallys_todo.push(this.stateStack[i]); + } + } + var position = 0; + var count = 0; + if(try_statement) { + var handler = try_statement.handler; + handler.parameter = state.value; + handler.thrower = state; + this.stateStack.shift() + + for(var j = 0; j < finallys_todo.length; j++) { + while(this.stateStack[position] !== finallys_todo[j] && count < this.stateStack.length) { + count++; + this.stateStack.splice(position,1); + } + position++; + } -Interpreter.prototype['stepTryStatement'] = function() { - var state = this.stateStack[0]; - var node = state.node; - if (!state.doneBlock) { - state.doneBlock = true; - this.stateStack.unshift({node: node.block}); - } else if (!state.doneFinalizer && node.finalizer) { - state.doneFinalizer = true; - this.stateStack.unshift({node: node.finalizer}); - } else { - this.stateStack.shift(); + this.stateStack.splice(finallys_todo.length-1, 0, {node: handler}); + }else if(finallys_todo.length > 0) { + this.stateStack.shift() + + for(var j = 0; j < finallys_todo.length; j++) { + while(this.stateStack[position] !== finallys_todo[j] && count < this.stateStack.length) { + count++; + this.stateStack.splice(position,1); + } + position++; + } + this.stateStack.splice(finallys_todo.length, 0, state); + }else{ + throw new Error("Uncaught exception") + } } }; + Interpreter.prototype['stepUnaryExpression'] = function() { var state = this.stateStack[0]; var node = state.node; @@ -2674,9 +2750,69 @@ Interpreter.prototype['stepWithStatement'] = function() { Interpreter.prototype['stepWhileStatement'] = Interpreter.prototype['stepDoWhileStatement']; +Interpreter.prototype.extract = function extract(node) { + if(node.isPrimitive) { + return this.extractPrimitive(node); + }else if(node.parent === this.ARRAY) { + return this.extractArray(node); + }else if(node.parent === this.OBJECT) { + return this.extractObject(node); + }else if(node.parent === this.REGEXP) { + return this.extractPrimitive(node); + }else if(node.type === 'function') { + return this.extractFunction(node); + }else if(node.type === 'object' && node.parent.type === 'function') { + return this.extractClassObject(node); + } +} + +Interpreter.prototype.extractFunction = function extractFunction(node) { + return { + type: 'function', + funcText: escodegen.generate(node.node), + createdIn: node.parentScope.scopeName + } +} + +Interpreter.prototype.extractPrimitive = function extractPrimitive(node) { + return node.data; +} + +Interpreter.prototype.extractArray = function extractArray(node) { + var result = []; + for(var index in node.properties) { + result[index] = this.extract(node.properties[index]); + }; + return result; +} + +Interpreter.prototype.extractClassObject = function extractClassObject(node) { + var result = { + type: 'object', + constructor: node.parent.node.id.name, properties: {}}; + for(var prop in node.properties) { + result.properties[prop] = this.extract(node.properties[prop]); + } + return result; +} + +Interpreter.prototype.extractObject = function extractObject(node) { + var result = { + type: 'object', + constructor: 'object', properties: {}}; + for(var prop in node.properties) { + result.properties[prop] = this.extract(node.properties[prop]); + } + return result; +} + + // Preserve top-level API functions from being pruned by JS compilers. // Add others as needed. +var window = window || {}; window['Interpreter'] = Interpreter; Interpreter.prototype['appendCode'] = Interpreter.prototype.appendCode; Interpreter.prototype['step'] = Interpreter.prototype.step; Interpreter.prototype['run'] = Interpreter.prototype.run; +var module = module || {}; +module.exports = Interpreter; diff --git a/tests/test.js b/tests/test.js new file mode 100644 index 00000000..3cb10ee8 --- /dev/null +++ b/tests/test.js @@ -0,0 +1,311 @@ +var Interpreter = require('../interpreter.js'); +var expect = require('chai').expect; + +var setup_code = "function Writer() {\ + this.output = [];\ + this.log = function(text) {\ + this.output.push(text);\ + };\ +};\ + var c = new Writer();"; + + +function getOutput(code) { + var test = new Interpreter(setup_code + code); + var state = null; + var count = 0; + while(test.step()) { + //console.log(count , test.stateStack[0]); + if(test.stateStack.length > 0) { + state = test.stateStack[0]; + } + count ++; + } + var results = test.extract(state.scope.properties.c.properties.output) + return results; +} + +describe("try/catch/finally", () => { + it("Should print the results of the try block before the finally block", () => { + var test_code = '\ + try {\ + c.log("tried");\ + } finally {\ + c.log("finally");\ + }'; + var results = getOutput(test_code); + expect(results).to.deep.equal(["tried","finally"]); + }); + + it("Should have an uncaught exception", () => { + var test_code = '\ + try {\ + c.log("tried");\ + throw "oops";\ + } finally {\ + c.log("finally");\ + }'; + expect( () => { + var results = getOutput(test_code); + }).to.throw(Error); + }); + + it("Should print the results of the try block before the finally block, and not the catch branch", () => { + var test_code = '\ + try {\ + c.log("tried");\ + } catch (e) {\ + c.log("caught");\ + } finally {\ + c.log("finally");\ + }'; + var results = getOutput(test_code); + expect(results).to.deep.equal(["tried","finally"]); + }); + + + it("Should print the results of the caught block before the finally block, and not the rest of the try branch", () => { + var test_code = '\ + try {\ + c.log("tried");\ + throw false;\ + c.log("tried again");\ + } catch (e) {\ + c.log("caught");\ + } finally {\ + c.log("finally");\ + }'; + var results = getOutput(test_code); + expect(results).to.deep.equal(["tried","caught","finally"]); + }); + + it("Should handle the exception in the outer try catch exception, and finish inner finally", () => { + var test_code = '\ + try {\ + try {\ + throw "oops";\ + }\ + finally {\ + c.log("finally");\ + }\ + }\ + catch (ex) {\ + c.log("outer");\ + c.log(ex);\ + }'; + var results = getOutput(test_code); + expect(results).to.deep.equal(["finally","outer","oops"]); + }); + + it("Should handle inner try catch, and not call outer catch", () => { + var test_code = '\ + try {\ + try {\ + throw "oops";\ + }\ + catch (ex) {\ + c.log("inner");\ + c.log(ex);\ + }\ + finally {\ + c.log("finally");\ + }\ + }\ + catch (ex) {\ + c.log("outer");\ + c.log(ex);\ + }'; + var results = getOutput(test_code); + expect(results).to.deep.equal(["inner","oops","finally"]); + }); + + it("Should handle rethrowing the error, and catching in the outer", () => { + var test_code = '\ + try {\ + try {\ + throw "oops";\ + }\ + catch (ex) {\ + c.log("inner");\ + c.log(ex);\ + throw ex;\ + }\ + finally {\ + c.log("finally");\ + }\ + }\ + catch (ex) {\ + c.log("outer");\ + c.log(ex);\ + }'; + var results = getOutput(test_code); + expect(results).to.deep.equal(["inner","oops","finally","outer","oops"]); + }); + + it("Should not catch if returning early in a finally block", () => { + var test_code = '\ + function test() {\ + try {\ + try {\ + throw "oops";\ + }\ + catch (ex) {\ + c.log("inner");\ + c.log(ex);\ + throw ex;\ + }\ + finally {\ + c.log("finally");\ + return;\ + }\ + }\ + catch (ex) {\ + c.log("outer");\ + }\ + }\ + test();'; + + var test = new Interpreter(setup_code + test_code); + var state = null; + var count = 0; + expect(() => { + while(test.step()) { + //console.log(count , test.stateStack[0]); + if(test.stateStack.length > 0) { + state = test.stateStack[0]; + } + count++; + } + }).to.throw(Error); + var results = test.extract(test.getScope().properties.c.properties.output); + expect(results).to.deep.equal(['inner','oops','finally']); + }); + + it("It should not leave undone finally statements for uncaught exceptions", () => { + var test_code = '\ + function test() {\ + try {\ + c.log("try");\ + throw "oops";\ + c.log("shouldn\'t be here");\ + } finally {\ + c.log("finally");\ + }\ + c.log("got here?");\ + return 4;\ + }\ + var hmm = test();'; + + var test = new Interpreter(setup_code + test_code); + var state = null; + var count = 0; + expect(() => { + while(test.step()) { + //console.log(count , test.stateStack[0]); + if(test.stateStack.length > 0) { + state = test.stateStack[0]; + } + count++; + } + }).to.throw(Error); + var results = test.extract(test.stateStack[test.stateStack.length-1].scope.properties.c.properties.output); + expect(results).to.deep.equal(['try','finally']); + }) +}) + +describe('return', () => { + it("Should return a simple value", () => { + var test_code = '\ + function id(x) {\ + return x;\ + }\ + c.log(id(10));'; + var results = getOutput(test_code); + expect(results).to.deep.equal([10]); + }); + + it("Should return early", () => { + var test_code = '\ + function test(bool) {\ + if(bool) {\ + c.log(bool);\ + return bool;\ + c.log("nope");\ + }else{\ + c.log("negative");\ + return bool;\ + }\ + }\ + c.log(test(true));'; + var results = getOutput(test_code); + expect(results).to.deep.equal([true, true]); + }); + + it("Should respect finally", () => { + var test_code = '\ + function example() {\ + try {\ + return true;\ + }\ + finally {\ + return false;\ + }\ + }\ + c.log(example());'; + var results = getOutput(test_code); + expect(results).to.deep.equal([false]); + }); + + + it("Should respect catch and finally", () => { + var test_code = '\ + function example() {\ + try {\ + throw "oops";\ + } catch (e) {\ + return e;\ + } finally {\ + return false;\ + }\ + }\ + c.log(example());'; + var results = getOutput(test_code); + expect(results).to.deep.equal([false]); + }); + + it("Should handle continue statements", () => { + var test_code = '\ + for(var i = 0; i < 2; i++) {\ + try {\ + c.log(i);\ + continue;\ + } finally {\ + c.log("end");\ + }\ + }'; + var results = getOutput(test_code); + expect(results).to.deep.equal([0,'end', 1, 'end']) + }) + + it("should handle the scopes created by catch", () => { + var test_code = 'function capturedFoo() {return foo};\ + foo = "prior to throw";\ + try {\ + throw "Error";\ + }\ + catch (foo) {\ + var foo = "initializer in catch";\ + }'; + + var test = new Interpreter(setup_code + test_code); + var state = null; + while(test.step()) { + //console.log(count , test.stateStack[0]); + if(test.stateStack.length > 0) { + state = test.stateStack[0]; + } + } + var results = test.extract(state.scope.properties.foo); + expect(results).to.equal('prior to throw'); + }) +});