I've written (in JavaScript) an interactive read-eval-print-loop that is encapsulated within an object. However, I recently noticed that toplevel function definitions specified to the interpreter do not appear to be 'remembered' by the interpreter. After some diagnostic work, I've reduced the core problem to this:
var evaler = {
eval: function (str)
{
return eval(str);
},
};
eval("function t1() { return 1; }"); // GOOD
evaler.eval("function t2() { return 2; }"); // FAIL
At this point, I am hoping that the following two statements wil work as expected:
print(t1()); // => Results in 1 (This works)
print(t2()); // => Results in 2 (this fails with an error that t2 is undefined.)
What I get instead is the expected value for the t1
line, and the t2
line fails with an error that t2
is unbound.
IOW: After running this script, I have a definition for t1
, and no defintion for t2
. The act of calling eval from within evaler
is sufficiently different from the toplevel call that the global definition does not get recorded. What does happen is that the call to
evaler.eval
returns a function object, so I'm presuming that t2
is being defined and stored in some other set of bindings that I don't have access to. (It's not defined as a member in evaler
.)
Is there any easy fix for this? I've tried all sorts of fixes, and haven't stumbled upon one that works. (Most of what I've done has centered around putting the call to eval in an anonymous function, and altering the way that's called, chainging __parent__
, etc.)
Any thoughts on how to fix this?
Here's the result of a bit more investigation:
tl;dr: Rhino adds an intermediate scope to the scope chain when calling a method on an instance. t2
is being defined in this intermediate scope, which is immediately discarded. @Matt: Your 'hacky' approach might well be the best way to solve this.
I'm still doing some work on the root cause, but thanks to some quality time with jdb, I now have more understanding of what's happening. As has been discussed, a function statement like function t1() { return 42; }
does two things.
- It creates an anonymous instance of a function object, like you'd get with the expression
function() { return 42; }
- It binds that anonymous function to the current top scope with the name
t1
.
My initial question is about why I'm not seeing the second of these things happen when I call eval
from within a method of an object.
The code that actually performs the binding in Rhino appears to be in the function org.mozilla.javascript.ScriptRuntime.initFunction
.
if (type == FunctionNode.FUNCTION_STATEMENT) {
....
scope.put(name, scope, function);
For the t1
case above, scope
is what I've set to be my top-level scope. This is where I want my toplevel functions defined, so this is an expected result:
main[1] print function.getFunctionName()
function.getFunctionName() = "t1"
main[1] print scope
scope = "com.me.testprogram.Main@24148662"
However, in the t2
case, scope
is something else entirely:
main[1] print function.getFunctionName()
function.getFunctionName() = "t2"
main[1] print scope
scope = "org.mozilla.javascript.NativeCall@23abcc03"
And it's the parent scope of this NativeCall
that is my expected toplevel scope:
main[1] print scope.getParentScope()
scope.getParentScope() = "com.me.testprogram.Main@24148662"
This is more or less what I was afraid of when I wrote this above: " In the direct eval case, t2 is being bound in the global environment. In the evaler case, it's being bound 'elsewhere'" In this case, 'elsewhere' turns out to be the instance of NativeCall
... the t2
function gets created, bound to a t2
variable in the NativeCall
, and the NativeCall
goes away when the call to evaler.eval
returns.
And this is where things get a bit fuzzy... I haven't done as much analysis as I'd like, but my current working theory is that the NativeCall
scope is needed to ensure that this
points to evaler
when execution in the call to evaler.eval
. (Backing up the stack frame a bit, the NativeCall
gets added to the scope chain by Interpreter.initFrame
when the function 'needs activation' and has a non-zero function type. I'm assuming that these things are true for simple function invocations only, but haven't traced upstream enough to know for sure. Maybe tomorrow.)