JDK-8044000 : Access to undefined property yields "null" instead of "undefined"
  • Type: Bug
  • Component: core-libs
  • Sub-Component: jdk.nashorn
  • Affected Version: 8u20,9
  • Priority: P3
  • Status: Resolved
  • Resolution: Fixed
  • OS: windows_7
  • CPU: x86_64
  • Submitted: 2014-05-26
  • Updated: 2014-07-29
  • Resolved: 2014-05-27
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
JDK 8 JDK 9
8u20Fixed 9 b15Fixed
Description
FULL PRODUCT VERSION :
java version "1.9.0-ea"
Java(TM) SE Runtime Environment (build 1.9.0-ea-b13)
Java HotSpot(TM) 64-Bit Server VM (build 1.9.0-ea-b13, mixed mode)

java version "1.8.0_20-ea"
Java(TM) SE Runtime Environment (build 1.8.0_20-ea-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.20-b10, mixed mode)

java version "1.8.0_20-ea"
Java(TM) SE Runtime Environment (build 1.8.0_20-ea-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.20-b15, mixed mode)

ADDITIONAL OS VERSION INFORMATION :
Microsoft Windows [Version 6.1.7601]

A DESCRIPTION OF THE PROBLEM :
A small conversion script of mine is failing to correctly handle script objects after they have been transferred to a different Global. The immediate cause of this is the fact that access to undefined properties / functions does not return "undefined" as they should, instead returning null when the conversion script is run against the object in the second Global. This incorrectly triggers a conditional block in the conversion script that is designed to handle String-like values.

This issue manifests itself in a very peculiar constellation for which the causality escapes my understanding.
- a prior execution (in an unrelated Global) has to include an instance of a Map as the value of a property of the script object to be converted
- the conversion script has to contain a conditional block that handles conversion of native JavaScript objects into Map instances

If either of these parameters is altered (e.g. the Map instance in the prior execution replace by a simple Object) the issue does not occur.

The test case works correctly when run in JDK 1.8.0_05

REGRESSION.  Last worked in version 8

ADDITIONAL REGRESSION INFORMATION: 
java version "1.8.0_05"
Java(TM) SE Runtime Environment (build 1.8.0_05-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.5-b02, mixed mode)

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Execute the test case with assertions enabled (-ea)

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The program completes without failing any of the assertions.
ACTUAL -
The pogram fails the second to last of the assertion. The result of the conversion is an instance of java.lang.String instead of the expected java.util.Map

ERROR MESSAGES/STACK TRACES THAT OCCUR :
Exception in thread "main" java.lang.AssertionError
	at Test2.main(Test2.java:52)


REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------
import java.util.Collections;
import java.util.Map;

import javax.script.Bindings;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;

public class Test2
{

    private static final String TEST_SCRIPT =
            "function convert(o) {" +
                "var b = o, c, d, e, f, g;" +
                // test for native or Java String
                "if (typeof o === 'string' || o.toUpperCase!== undefined) {" +
                    "c = Java.type('java.lang.String');" +
                    "b = new c(o);" +
                "} else if (typeof o === 'object') {" +
                    "c = Java.type('java.util.HashMap');" +
                    "b = new c();" +
                    "for (d in o) { b.put(convert(d), convert(o[d])); }" +
                "} return b; }" +
            "javaObj = convert(nashornObj);";

    public static void main(final String[] args) throws Exception
    {
        final ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");

        // construct object with reference to a Java Map (this somehow triggers the assert to fail in the end)
        // Note: replacing map with Object => everything works fine again
        final Bindings globalBindings = engine.createBindings();
        globalBindings.put("map", Collections.<String, Object> emptyMap());
        engine.eval("nashornObj = { test: map };", globalBindings);

        engine.eval(TEST_SCRIPT, globalBindings);
        assert !(globalBindings.get("javaObj") instanceof String);
        assert globalBindings.get("javaObj") instanceof Map<?, ?>;

        // construct object in new Global
        final Bindings originalBindings = engine.createBindings();
        engine.eval("nashornObj = { test: 12 }", originalBindings);
        // show that it works in original Global
        engine.eval(TEST_SCRIPT, originalBindings);
        assert !(originalBindings.get("javaObj") instanceof String);
        assert originalBindings.get("javaObj") instanceof Map<?, ?>;

        // transfer to a different Global
        final Bindings secondaryBindings = engine.createBindings();
        secondaryBindings.put("nashornObj", originalBindings.get("nashornObj"));
        // it should also work in different Global, but doesn't
        engine.eval(TEST_SCRIPT, secondaryBindings);
        assert !(secondaryBindings.get("javaObj") instanceof String);
        assert secondaryBindings.get("javaObj") instanceof Map<?, ?>;
    }
}

---------- END SOURCE ----------

CUSTOMER SUBMITTED WORKAROUND :
The primary workaround for the test case is to always combine checks against undefined with checks against null, even though null is 100 % certain to either undefined or a function reference.


Comments
If you compile and run the above script as java -ea Main you'll see no failure with jdk8 GA. But with jdk8u-dev repo and jdk9 repo (or recent jdk8u or jdk9 builds), you'll see an assertion failure on the last line. With jdk8u and jdk9, the following removes the assertion: java -Dnashorn.args=--class-cache-size=0 -ea Main This is because with class sharing in jdk8u and jdk9, compiled code for "typeof obj.foo" is reused in both global objects. That step is needed for the current issue. Callsite for "obj.foo" uses "obj instanceof Map" as it's guard for the first time (when the code is evaluated in default global). With second global (newGlobal), the same callsite guard passes - because every ScriptObjectMirror is a Map! And so "obj.foo" becomes if (obj instanceof Map) { ((Map)obj).get("foo"); } Since guard passes second time (for ScriptObjectMirror), we'll continue to use Map.get - which returns null for missing mapping entry. We need to have the guard fail second time, so that fresh linkage will occur and ScriptObjectMirror/JSObject linking occurs to translate obj.foo as ((ScriptObjectMirror)obj).getMember("foo"). So, effectively, we need to have the guard to be if (obj instanceof Map && !(obj instanceof JSObject)) { } in the first time linking.
27-05-2014

Simplified test case is as follows: import javax.script.Bindings; import javax.script.ScriptContext; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; public class Main { private static final String TEST_SCRIPT = "typeof obj.foo"; public static void main(final String[] args) throws Exception { final ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn"); final Bindings global = engine.getContext().getBindings(ScriptContext.ENGINE_SCOPE); engine.eval("obj = java.util.Collections.emptyMap()"); engine.eval(TEST_SCRIPT, global); // redefine obj to be a script object engine.eval("obj = {}"); final Bindings newGlobal = engine.createBindings(); // transfer 'obj' from default global to new global newGlobal.put("obj", global.get("obj")); assert engine.eval(TEST_SCRIPT, newGlobal).equals("undefined"); } }
27-05-2014