Problem (?) with Nashorn management of Error prototype
David P. Caldwell
david at code.davidpcaldwell.com
Fri Oct 3 16:15:43 UTC 2014
So I have a big library of Rhino-compatible (and browser-compatible)
code and have recently been working through the processes (debugger,
profiler) and code changes necessary to get it working with Nashorn.
A bit of background: the project probably differs from the typical
early Nashorn use case in that it heavily manipulates scopes and
targeting (i.e., 'this') in order to achieve its modularization goals.
Much of this is abstracted into a "loader" layer whose innermost
Nashorn invocation is the following code:
this.compile = function(name,code,scope,target) {
var compiled =
Context.getContext().compileScript(toSource(name,code),scope);
return Packages.jdk.nashorn.internal.runtime.ScriptRuntime.apply(compiled,target);
};
Yes, internal interfaces, I've been warned, moving on ... :)
The name "compile" is deceptive; it's actually the name of the
strategy. (I had tried several, and they are still in the code, and
selected at runtime via an environment variable. "compile" is the
default. Feel free to comment.) The function would, if it were
standalone, probably be called something like "run" or "load."
toSource() calls the Source constructor or factory method, depending
which is present (looks like that changed in early 1.8.0 JDKs).
Here's the full source file:
https://bitbucket.org/davidpcaldwell/slime/src/d04bd814156d4e4cc520492e65876cf1235a55e8/loader/rhino/nashorn.js
For a while, this barely worked with Nashorn (requiring workarounds)
because somehow with complicated scope chains Nashorn would get
confused and variables from outer scopes would be missing inside inner
scopes, particularly when Nashorn had been re-entered (i.e., the
apply() call from which the inner scope was being executed was
different from the apply() call in which the outer scope was
executed).
I vaguely wrote to the list about this but I was never able to
reproduce the problem in a way that I could present or was small, and
then it was somehow fixed by the u20 release of JDK 1.8, and suddenly
everything worked.
Except objects that have Error in the prototype chain. They still have
strange behavior, and I've been poking at it for some time, and have a
few guesses about it.
The good news is, I have an easy way to reproduce it (instructions at
end of this message). The bad news is, it involves my whole JavaScript
shell because when I tried a minimal wrapper everything worked. The
good news is, I'm confident it's not a bug in my code, because:
1. The loader layer abstracts the differences between the four major
browsers, Rhino, and Nashorn, and everything except errors works the
same in all six, and errors work the same in the first five.
2. When I replace the Error global object with my own, the code works.
So it's something having to do with "your" Error object, or (my guess)
some conditional code you have somewhere inside Nashorn that checks
for it.
My guess is that there is some optimization somewhere that walks the
prototype chain and checks for Error. I think this because I tried a
number of strategies for working around this:
* Using Object.create
* Using an intermediary function ErrorType that had new Error() as its
prototype, and using new ErrorType() as the prototype for my function
... and they all had the same result if Error was anywhere in the
prototype chain of my error. But using a different prototype
(basically, writing my own Error) worked, also in several different
configurations.
Here's the code that fails (ignore the loader scaffolding):
$exports.Error.Type = function(name) {
var rv = function(message,properties) {
if (this instanceof arguments.callee) {
this.name = name;
this.message = message;
for (var x in properties) {
this[x] = properties[x];
}
} else {
return new arguments.callee(message);
}
};
rv.prototype = new Error();
return rv;
};
Here's some test code that exercises it:
var Unimplemented = jsh.js.Error.Type("Unimplemented");
try {
throw new Unimplemented("Not implemented.");
} catch (e) {
jsh.shell.echo("toString: " + e.toString());
jsh.shell.echo("Error?: " + Boolean(e instanceof Error));
jsh.shell.echo("Unimplemented?: " + Boolean(e instanceof Unimplemented));
jsh.shell.echo("name: " + e.name);
jsh.shell.echo("message: " + e.message);
jsh.shell.echo("stack: " + e.stack);
}
Here's the output if I use my own global Error object. That object is
implemented at the top of nashorn.js (same file as above):
https://bitbucket.org/davidpcaldwell/slime/src/d04bd814156d4e4cc520492e65876cf1235a55e8/loader/rhino/nashorn.js
toString: Unimplemented: Not implemented.
Error?: true
Unimplemented?: true
name: Unimplemented
message: Not implemented.
stack: at rhino/nashorn.js:35
at /Users/dcaldwell/tmp/nashorn.error/jsh/src/jsh/test/manual/errortype.jsh.js:11
at rhino/nashorn.js:131
at rhino/nashorn.js:173
at rhino/nashorn.js:203
at rhino/literal.js:65
at rhino/literal.js#36:9<eval>@6:132
at rhino/literal.js#36:9<eval>@6:217
at /Users/dcaldwell/tmp/nashorn.error/jsh/script/jsh/jsh.js#111:14<eval>@10:91
at /Users/dcaldwell/tmp/nashorn.error/jsh/script/jsh/jsh.js:252
That is what is intended. The output is roughly the same as that for
all four major browsers and for Rhino.
Here's the output if I don't replace the global Error object:
toString: Error
Error?: true
Unimplemented?: false
name: Error
message:
stack:
So that's where I am at the moment. Basically, it looks like I'm
getting the error subtype's prototype as my error, no matter what I
do.
A few comments:
* My workaround *mostly* works. The main problems are, a.) I need to
enumerate all my own error subtypes like TypeError or else TypeError
objects are not instanceof Error. On the other hand, if I do that, b.)
TypeErrors (presumably other errors, also) thrown from Nashorn itself
are not instanceof TypeError or Error. So it's a little messy.
* I haven't looked at the Nashorn source code surrounding any of this;
I've been debugging it as a black box. (Obviously the fact that I am
using ScriptRuntime, Context and Source means I've been in there a
little bit, though. I have not built it myself.)
* This may be a red herring, but developing with NetBeans, if I step
through the $exports.Error.Type code (using a patched copy; NetBeans
Nashorn support also isn't good enough yet to run my shell, though it
becomes minimally usable with a little bit of patching; see
https://netbeans.org/bugzilla/show_bug.cgi?id=247441), it is *very*
confused. Inside the error subtype constructor (called "rv" above),
the debugger (but not the code!) thinks that the global object is
"this" (even when executing via new). But when the code executes, it
does *not* treat the global object as "this." So don't know what the
problem is here, but the whole thing smells funny.
* My test environment is JDK 1.8.0u20. OS X Mavericks, though I can't
imagine that matters.
* Possibly unrelated, but another strange hard-coding symptom I seemed
to have with Error is that if I assign properties to *my* replacement
Error constructor -- not to Error objects, but to the constructor (I
tried to use Error.nashorn to act as a conditional flag) -- those
properties are invisible; not enumerable or accessible. But I haven't
investigated this as thoroughly and it might be something dumb I'm
doing; just reporting in case it rings a bell. Possibly the Error
property of the global scope is used *by name* somewhere and handled
specially, because that one symptom still seemed to impact my Error
object even when I replaced yours.
Anyway, I know this was long but I wanted to do my due diligence and
communicate all of it. Here's how to reproduce, starting in an empty
directory:
1. Build the shell to a subdirectory called "jsh":
jrunscript -e "load('https://bitbucket.org/davidpcaldwell/slime/raw/d04bd81/jsh/etc/jrunscript.js?relative=./install.jrunscript.js')"
jsh
2. To run the test case with my workaround enabled:
java -jar jsh/jsh.jar jsh/src/jsh/test/manual/errortype.jsh.js
3. To run the test case with my workaround disabled, exposing the
error (on a UNIX-like system; translate for Windows as necessary):
env DISABLE_NASHORN_ERROR_HACK=true java -jar jsh/jsh.jar
jsh/src/jsh/test/manual/errortype.jsh.js
Hopefully it's something simple. :)
-- David P. Caldwell
http://www.davidpcaldwell.com/
More information about the nashorn-dev
mailing list