views:

116

answers:

5

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.)

A: 

Is it possible that your function name "eval" is colliding with the eval function itself? Try this:

var evaler = {
  evalit: function (str)
  {
    return window.eval(str);
  },
};

eval("function t1() { return 1; }");
evaler.evalit("function t2() { return 2; }");

Edit
I modified to use @Matt's suggestion and tested. This works as intended.

Is it good? I frown on eval, personally. But it works.

Randolpho
Just tried it, and it didn't work. (Thanks, anyway!)
mschaef
@mschaef: try my modification. Should work for ya
Randolpho
I just tried it too, under Rhino, and I'm getting an error that 'window' is not defined. Based on what you're saying, this is the right approach, as long as I can figure out what the name is of the global scope in Rhino. (Edit: this is server-side, not in the browser...)
mschaef
@mschaef: you never mentioned Rhino before. I'd add that as a tag, and possibly edit your question to clarify. And yes, this definitely works in browser. I'd say finding where `eval` is defined will get you where you want to go.
Randolpho
@mschaef: you probably can't see @Matt's deleted answer, but it looks like `global.eval` is what you want.
Randolpho
@Randolpho - I deleted it because it was wrong, and I didn't want to spread misinformation. Scope is not the issue here at all. He's returning a function object that he never calls.
Matt
@Randolpho: Just added the Rhino tag. Thanks. I tried calling through 'global', and it doesn't appear to be defined.
mschaef
A: 
el.pescado
If that were true, wouldn't the definition of t1 fail for the same reason? (t1 does get defined in the initial code..)
mschaef
`function t2() { return 2; }` even under the *Eval Execution Context* `t2` is still parsed as a `FunctionDeclaration`, it is *not* a `FunctionExpression`...
CMS
CMS: You may be right.
el.pescado
@el.pescado: Just try `eval("function () {}");`, which is a valid `FunctionExpression`, will give you a `SyntaxError` because the name of a `FunctionDeclaration` is grammatically required.
CMS
+1  A: 

Your code actually is not failing at all. The eval is returning a function which you never invoke.

print(evaler.eval("function t2() { return 2; }")()); // prints 2

To spell it out a bit more:

x = evaler.eval("function t2() { return 2; }"); // this returns a function
y = x(); // this invokes it, and saves the return value
print(y); // this prints the result

EDIT

In response to:

Is there another way to create an interactive read-eval-print-loop than to use eval?

Since you're using Rhino.. I guess you could call Rhino with a java Process object to read a file with js?

Let's say I have this file:

test.js

function tf2() {
  return 2;
}

print(tf2());

Then I could run this code, which calls Rhino to evaluate that file:

process = java.lang.Runtime.getRuntime().exec('java -jar js.jar test.js');
result = java.io.BufferedReader(java.io.InputStreamReader(process.getInputStream()));
print(result.readLine()); // prints 2, believe it or not

So you could take this a step further by WRITING some code to eval to a file, THEN calling the above code ...

Yes, it's ridiculous.

Matt
Your original answer was more correct, IMO. Calling eval on a string such as "function t2() {return 2;}" will insert the function into the global scope. At least it will in most browsers.
Randolpho
Does not work on SpiderMonkey.
el.pescado
Agreed that it's creating the function... the question is where is it creating the binding of t2 to the newly created function? In the direct eval case, t2 is being bound in the global environment. In the evaler case, it's being bound 'elsewhere'.
mschaef
Hmm.. I don't think it is bound anywhere. You COULD manually bind it, but in either case, the `eval` behavior does not seem to be consistent between implementations (Rhino/Spidermonkey) .. another good reason to avoid using it..
Matt
@Matt... Is there another way to create an interactive read-eval-print-loop than to use eval? I'm thinking it's an essential part of my goal. (Maybe a REPL is a bad goal in and of itself, but that's a different question)
mschaef
Interesting finding... I agree, this is another reason not to use eval. :)
Randolpho
@mschaef - see my update and prepare for hackery.
Matt
+1  A: 

The problem you are running into is that JavaScript uses function level scoping.

When you call eval() from within the eval function you have defined, it is probably creating the function t2() in the scope of that eval: function(str) {} function.

You could use evaler.eval('global.t2 = function() { return 2; }'); t2();

You could also do something like this though:

t2 = evaler.eval("function t2() { return 2; }");
t2();

Or....

var someFunc = evaler.eval("function t2() { return 2; }");
// if we got a "named" function, lets drop it into our namespace:
if (someFunc.name) this[someFunc.name] = someFunc;
// now lets try calling it?
t2();
// returns 2

Even one step further:

var evaler = (function(global){
  return {
    eval: function (str)
    {
      var ret = eval(str);
      if (ret.name) global[ret.name] = ret;
      return ret;
    }
  };
})(this);

evaler.eval('function t2() { return 2; }');
t2(); // returns 2

With the DOM you could get around this function-level scoping issue by injecting "root level" script code instead of using eval(). You would create a <script> tag, set its text to the code you want to evaluate, and append it to the DOM somewhere.

gnarf
A: 

I got this answer from the Rhino mailing list, and it appears to work.

var window = this;

var evaler = {
    eval : function (str) {
         eval.call(window, str);
    }
};

The key is that call explicitly sets this, and this gets t2 defined in the proper spot.

mschaef
Thats strange, considering my version of Rhino (1.7 release 1 - yes, i know, old) complains about this method (as this was the first thing I tried...) `uncaught JavaScript runtime exception: EvalError: Function "eval" must be called directly, and not by way of a function of another name.`
gnarf