As the author of JSCPP , I ran into the same problem when I ran a debugger that pauses and continues to interpret the fly program. In the end, I decided to use the generator functions from es6, but I would like to share my thought process here.
A general way is to first compile the target code into bytecode with a low level of recursive byte. You label each statement, and then process the entire control flow with unconditional jump and conditional jump . Then, the byte code interpreter is started instead. This is a good option if you do not mind that all of this compilation work is done.
Another way is the call stack / call stack workflow. When you need to pause the interpretation, you recursively push all arguments and all local variables into the custom stack all the way to the end. When you need to continue execution, you recursively load all of these arguments and local variables. Your code will be converted from
AddExpression.prototype.visit = function(param) { var leftVal = visit(this.left, param); var rightVal = visit(this.right, param); return leftVal + rightVal; }
to
AddExpression.prototype.visit = function(param) { if (needToStop) { stack.push({ method: AddExpression.prototype.visit, _this: this, params: [param], locals: {}, step: 0 }); return; } if (recoverFromStop && stack.top().step === 0) { var thisCall = stack.pop(); if (stack.length > 0) { var nextCall = stack.top(); nextCall.method.apply(nextCall._this, params); } } var leftvalue = visit(this.left, param); if (needToStop) { stack.push({ method: AddExpression.prototype.visit, _this: this, params: [], locals: { leftvalue: leftvalue }, step: 1 }); return; } if (recoverFromStop && stack.top().step === 1) { var thisCall = stack.pop(); leftvalue = thisCall.locals.leftvalue; if (stack.length > 0) { var nextCall = stack.top(); nextCall.method.apply(nextCall._this, params); } } var rightvalue = visit(this.right, param); if (needToStop) { stack.push({ method: AddExpression.prototype.visit, _this: this, params: [], locals: { leftvalue: leftvalue, rightvalue: rightvalue }, step: 2 }); return; } if (recoverFromStop && stack.top().step === 2) { var thisCall = stack.pop(); leftvalue = thisCall.locals.leftvalue; rightvalue = thisCall.locals.rightvalue; if (stack.length > 0) { var nextCall = stack.top(); nextCall.method.apply(nextCall._this, params); } } return leftvalue + rightvalue; };
This method does not change the core logic of your interpreter, but you can see for yourself how crazy the code is for the simple A + B syntax.
Finally, I decided to use generators. Generators are not designed for interactively changing program execution, but rather for lazy evaluation. But with some simple hacking, we can use lazy evaluations of our statements after receiving the βcontinueβ command.
function interpret(mainNode, param) { var step; var gen = visit(mainNode); do { step = gen.next(); } while(!step.done); return step.value; } function visit*(node, param) { return (yield* node.visit(param)); } AddExpression.prototype.visit = function*(param) { var leftvalue = yield* visit(this.left, param); var rightvalue = yield* visit(this.right, param); return leftvalue + rightvalue; }
Here function* indicates that the AddExpression.visit function should be a generator function. yield* , followed by visit call facility visit The function itself is a recursive generator function.
This solution seems perfect at first glance, but it suffers from a huge decrease in performance due to the use of generators ( http://jsperf.com/generator-performance ) and that it is from es6 and not many browsers support it.
In conclusion, you have three different ways to achieve intermittent execution:
- compile low level code:
- Pros: common practice, individual problems, easy to optimize and maintain.
- Cons: too much work
- save stack / boot stack:
- Pros: relatively fast, preserves interpretation logic
- Cons: hard to maintain
- generator:
- Pros: ease of maintenance, perfectly retains the logic of interpretation.
- Cons: slow, es6 required for es5 transpiling