Today at work, I needed to try out some Javascript code in a test wrapper around Internet Explorer. IE proper has a Javascript debugger built in, which has a very handy "immediate window" in which you can type commands and have them take effect. IE8's tools are okay at best, but the IE9 Platform Preview Builds show a good deal of progress, and are quite usable. But I digress...
Anyway, I didn't have access to the usual set of Javascript tools that help with discovering functionality and trying out code before writing it up in an HTML page. I thought to myself, "wouldn't it be simple just to create a little Javascript tool that lets me type lines of code and show me the output?" And then I thought to myself, "Yeah, that ought to be pretty easy." So I set out to do just that.
My thinking was basically that the interpreter portion of the code would use eval
to interpret the line and then I'd format the result as a string and display it. So I started out with something like this:
function handleLine(line)
{
return (eval(line)).toString();
}
It's intuitive and straightforward. I tried it with this input:
>> var x = 1;
undefined
>> x
ReferenceError: 'x' is undefined
The first line is okay (var
doesn't return the value of the assignment, for some reason), but the second one? That's not what I expected at all... and hopefully not what you'd hope for. Perhaps you've guessed the problem already, that eval
injects code in the context in which it's called. Here, that context is the local variable scope of the function handleLine
. Once that function returns, all the local variables are, of course, blown away.
So in order to mimic the kind of persistent state from line to line that would be really useful, I need some way of preserving the mutated context in which the eval
executes.
I'm really lucky that I had taken a really rad programming languages class in the past. Having gone through some of the utterly mind bending homework assignments, I was at least somewhat equipped to reason through this problem and ultimately arrive at a working solution.
The solution revolves around the technique of using a closure to capture the current context and propagate it from line to line. It would be really nice just to be able to package up the current state of all variables, but you can't quite do that in Javascript, so we have to play some tricks.
The way to create a closure is simply to declare a new function. This function, whenever it is called, will be able to use all of the local variables available at the time it was defined. So after calling eval
, we will create a new function to capture the new state of the environment after whatever side effects the eval
had, and return it. This function will be used later to execute the next statement, using that new environment.
Enough talking! Let's code!
function evaler(str)
{
var ret = null;
try
{
ret = eval(str);
}
catch (ex)
{
ret = ex;
}
eval(evaler.toString());
return {
ret: ret,
context: evaler
};
}
var g_context = evaler;
function handleLine(line)
{
var o = g_context(line);
g_context = o.context;
return o.ret;
}
There are only a couple parts here that are fairly tricky (the bolded part). Firstly, within the body of evaler
sometime after the eval
call, it defines a new closure (also called evaler
, just for convenience). Then we return it. It has everything needed to preserve the current context and execute another line (and generate a new evaler
to do the same again, ad infinitum).
Some less interesting features include the try
/catch
to make sure we don't let exceptions escape, and that we also return the actual output (in ret
).
Now it works like we'd expect!
>> var x = 1;
undefined
>> x
1
I can even define functions and execute them. Kewl.
>> function whoa() { echo("sup"); }
undefined
>> whoa();
sup
undefined
Try it out if you'd like. I hope this was informative. I know I felt kind of clever afterwards.
0 comments:
Post a Comment