oracle / graaljs

A high-performance, ECMAScript compliant, and embeddable JavaScript runtime for Java
https://www.graalvm.org/javascript/
Universal Permissive License v1.0
1.8k stars 190 forks source link

does graal.js support java.long.Long in nashorn-compat mode? #97

Closed missedone closed 5 years ago

missedone commented 5 years ago

hi folks i'm got an issue that seems graal.js 1.0.0-rc10 does not support Long, even running in nashorn-compat mode:

// start graal js in nashorn-compat mode
 ~/dev/graalvm-ce-1.0.0-rc10/Contents/Home/bin $ ./js --jvm.Dgraal.ShowConfiguration=info --js.nashorn-compat=true
[Use -Dgraal.LogFile=<path> to redirect Graal log output to a file.]
Using Graal compiler configuration 'community' provided by org.graalvm.compiler.hotspot.CommunityCompilerConfigurationFactory loaded from jar:file:/Users/nick/dev/graalvm-ce-1.0.0-rc10/Contents/Home/jre/lib/jvmci/graal.jar!/org/graalvm/compiler/hotspot/CommunityCompilerConfigurationFactory.class

// define Long type
> var Long = Java.type("java.lang.Long");
> var Integer = Java.type("java.lang.Integer");

// define a long value
> var lval = new Long(123456789);
> print(lval);
123456789

// check type of Long?
> lval instanceof Long
false
// check type of Integer?
> lval instanceof Integer
true

// even new instantiated is not a Long type
> (new Long(1234567890) instanceof Long)
false

// no way to call Long.longValue()
> print(lval.longValue());
TypeError: (intermediate value).longValue is not a function
    at <js> :program(<shell>:8:1:6-21)

// what's the type of `lval`?
> print(lval.class);
undefined
// what's the type of Long?
> print(Long.class);
class java.lang.Long

// what the type if i define an Integer
> var ival = new Integer(123);
> ival.class
> ival instanceof Integer
true

// what happen if i define a bigdecimal
> var BigDecimal = Java.type('java.math.BigDecimal');
> var bd = new BigDecimal('10');
> print(bd);
10
> bd.class
JavaClass[java.math.BigDecimal]
wirthi commented 5 years ago

Hi @missedone

thanks for your question.

I think the question is: what do you understand as "supported"? ECMAScript does not have a long (or an integer) type, all values are basically doubles. We fully conform to that spec and its semantics regarding arithmetics. Internally, we might use other types (e.g., integers instead of doubles) to represent values, mostly for better performance - but that is an implementation detail you should not care about, you should not even have an opportunity to see that that (and you cannot, with pure ECMAScript). All calculations will be correct, regardless of input value or the type of the input value.

The Java interoperability breaks that separation of concern: As you experience, when you put in a long, you might expect to get out a long again. That, however, is conflicting with our other goals, i.e. to provide a fast implementation. If you do arithmetics on your long value (yourLong + 1), do you expect to get back a long? Why not an integer, if it fits? Supporting all possible primitive number types would mean we would need to provide all arithmetic operations on all possible combinations, probably using Java semantics what exact operation to use.

Our current take on this is that you have no guarantee on the type you get back, but the value is always compliant to the ECMAScript spec. That is true for Java's primitive types. Typically, you'll get back integers and doubles when you do calculation on Java primitives. BigDecimal is a different story. That is not a primitive, so we don't automatically convert it to such.

Another question is whether lval.longValue() should be supported. Note that lval.intValue() is not supported either. As we convert all primitive numbers to our internal (JavaScript!) values, we lose the ability to call (Java!) functions on them. This is something we could support in principle (we do e.g. in cornercases of the String type), but right now we don't. It would not make much sense anyway, at least in this very example: longValue would provide a long, that JavaScript would immediately convert back to either int or double.

Best, Christian

missedone commented 5 years ago

hi @wirthi thanks for the clarification.

we are migrating our app from Java 8 to Java 11, the javascript code are running well with Nashorn in Java 8. now we're evaluating migrating to Graal.js in Java 11, however, the Long data type lossy conversion to Integer is a blocker. as you may see, our js code relies on Java interoperability a lot, Ex. call the java api which requires Long params in the js, and i was expecting it can work in nashorn-compat mode in Graal.js but it doesn't.

so we have to seek other options. thanks anyway.

wirthi commented 5 years ago

Hi @missedone

Can you please help me understand what you mean by lossy conversion and how Nashorn is different on it? See the following example:

var Long = Java.type('java.lang.Long');
var Double = Java.type('java.lang.Double');
var l1 = new Long('9007199254740991'); //2^53 - 1

print(l1 instanceof Long);
print(l1 instanceof Double);
print(l1);

var l2 = l1+1;
print(l2 instanceof Long);
print(l2 instanceof Double);
print(l2);

var l3 = l1+l1;
print(l3 instanceof Long);
print(l3 instanceof Double);
print(l3);

Graal.js and Nashorn behave equally on the arithmetics: Nashorn, too, converts the l2 and l3 to doubles, as JavaScript semantic requires that. And yes, that might involve a lossy conversion.

There is one difference: Graal.js does the conversion eagerly. Even for l1, Graal.js converts it to a different datatype (actually, a boxed long with some range checks). This can lead to a lossy conversion that Nashorn does not have, iff you pass longs from Java to JavaScript and back, and don't do arithmetics in JavaScript. Only then Graal.js behaves differently to Nashorn. This limitation is something we might be able to fix in the future.

Edit: I see your requirement on the API calls. In combination with longValue() that could help you call the right method from your API. We need to investigate what our solution there could be.

Best, Christian

missedone commented 5 years ago

here is the pseudo-code of javascript in our app. the javascript is eval by the jsr223 api.

...
// obj.entityId is in type of java.lang.Long
var entityId = obj.entityId
// method validate() requires a Long param
// 1. below works with Nashorn in Java 8
// 2. below does not work with Nashorn in Java 11, got error java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.Long (java.lang.Integer and java.lang.Long are in module java.base of loader 'bootstrap')
// 3. below does not wokr with Graal.js in Java 11, got error java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.Long (java.lang.Integer and java.lang.Long are in module java.base of loader 'bootstrap'):
com.example.Util.validate(entityId)

the workaround for above code running in Nashorn in Java 11:

var Long = Java.type("java.lang.Long");
// explicitly create a Long object even obj.entityId is Long
var entityId = new Long(obj.entityId);
com.example.Util.validate(entityId)

however, the above workaround does not work in Graal.js in Java 11. and i was hoping obj.entityId.longValue() might work but it doesn't at all.

i can prepare a sample project if you think it can help you diagnose the behavior.