Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Double/Integer in javascript body #350

Open
luthionb opened this issue Oct 23, 2017 · 5 comments
Open

Double/Integer in javascript body #350

luthionb opened this issue Oct 23, 2017 · 5 comments
Labels
Java Interop Issues related to the interaction between Java and JavaScript Triage Issues yet to be triaged

Comments

@luthionb
Copy link

luthionb commented Oct 23, 2017

I'm working with rhino library:

<groupId>org.mozilla</groupId>
<artifactId>rhino</artifactId>
<version>1.7R4</version>

To operate on numbers in javascript. Here I have a sample code:

Context ctx = Context.enter();
Scriptable scope = ctx.initStandardObjects();
ctx.getWrapFactory().setJavaPrimitiveWrap(false);
String script = "var x = 2.0; x;";
Object result = ctx.evaluateString(scope, script, "<cmd>", 1, null);
System.out.println(result.getClass());

And what suprised me is the fact that rhino is returning Integer, as according to ECMAScript specification all "primitive" javascript Number types should be float with double precision. What's even more interesting is the fact that changing script to:
String script = "var x = 1.0; x;";
Will return a double. So results are not consistent if no decimal values are provided.

And I think it's all due to implementation. Problem seems to be connected with class org.mozilla.javascript.optimizer.Codegen and method
pushNumberAsObject(ClassFileWriter cfw, double num)
For 0.0, 1.0, -1.0 we are using some predefined statics. For other we are creating static constants with type defined by method
String getStaticConstantWrapperType(double num)
And this method for number 2.0 will always return "Ljava/lang/Integer" object. Is it correct behavior? What's the reason for keeping such strange behavior?

And if it is something that cannot be resolved, can I somehow give a bytecode node additional number property?

I also verified this issue for latest released library 1.7-R5 and 1.7.7.2 but still I'm getting same behavior.

@sainaen
Copy link
Contributor

sainaen commented Oct 24, 2017

And what suprised me is the fact that rhino is returning Integer, as according to ECMAScript specification all "primitive" javascript Number types should be float with double precision.

ECMAScript spec only governs effects/behavior observable inside of the ECMAScript code. As far as I know, most ES implementations use an optimistic assumption that all numbers are regular 32-bit int-s, until they have to fallback to doubles. I assume that in this case the difference you get is a results of similar optimization done by Rhino.

And if it is something that cannot be resolved, can I somehow give a bytecode node additional number property?

I'm not sure I understand what do you mean here, but first, what is the problem you're trying to solve? If you want to always have doubles instead of int-s in the generated code, I don't think it's possible; even in the ES code some operations only work with integers.

@gbrail
Copy link
Collaborator

gbrail commented Oct 26, 2017

The optimization level may have an effect on this -- there are certain integer optimizations that only take affect at optimization level 9.

@luthionb
Copy link
Author

luthionb commented Oct 27, 2017

Sorry for late response. Use case I'm trying to apply is pretty complicated. But let me simplify it.

So let us assume that we have a simple java class that is receiving map of arguments passed from javascript code (as map) and relying on parameter types it's launching correct handler.

I have written simple java class that is simple receiving map of arguments and printing types to console. It will look like:

public class ArgumentsHandler {
 
 	public void handle(final Map<String, Object> args) {
 		args.entrySet().stream()
 			.forEach(arg -> System.out.println(
 			 	arg.getKey() + " [" + arg.getValue().getClass() + "]"));
 	}
 }

Now I have created three test cases and only in one everything is working fine.

  1. We are passing arguments as java.util.HashMap.
 Context ctx = Context.enter();
 Scriptable scope = ctx.initStandardObjects();
 String script = "var x = 2.0;" +
 		"var y = 1.0;" +
 		"var z = 1;" +
 		"var args = new java.util.HashMap();" +
 		"args.put(\"arg1\", x);" + 
 		"args.put(\"arg2\", y);" +
 		"args.put(\"arg3\", z);" +
 		"var handler = new com.mozilla.rhino.ArgumentsHandler();" +
 		"handler.handle(args);" +
 		"args.get(\"arg1\");";
 Object result = ctx.evaluateString(scope, script, "<cmd>", 1, null);
 System.out.println(result.getClass());

As a result I'm receiving:

arg3 [class java.lang.Double]
arg2 [class java.lang.Double]
arg1 [class java.lang.Double]
class org.mozilla.javascript.NativeJavaObject

And in my opinion it's correct behavior as all "primitive" types in javascript are float with double precision.

  1. We are passing arguments as javascript object.
Context ctx = Context.enter();
Scriptable scope = ctx.initStandardObjects();
String script = "var x = 2.0;" +
 		"var y = 1.0;" +
 		"var z = 1;" +
 		"var args = {};" +
 		"args.arg1 = x;" + 
 		"args.arg2 = y;" +
 		"args.arg3 = z;" +
 		"var handler = new com.mozilla.rhino.ArgumentsHandler();" +
 		"handler.handle(args);" +
 		"args.arg1;";
 Object result = ctx.evaluateString(scope, script, "<cmd>", 1, null);
 System.out.println(result.getClass());

Here as a result we are receiving:

arg1 [class java.lang.Integer]
arg2 [class java.lang.Double]
arg3 [class java.lang.Double]
class java.lang.Integer

In my opinion "arg1" should also be treated as Double and it's an incorrect behavior.

  1. We are passing arguments as json map.
 Context ctx = Context.enter();
 Scriptable scope = ctx.initStandardObjects();
 String script = "var x = 2.0;" +
 		"var y = 1.0;" +
 		"var z = 1;" +
 		"var handler = new com.mozilla.rhino.ArgumentsHandler();" +
 		"handler.handle({\"arg1\" : x, \"arg2\" : y, \"arg3\" : z});" +
 		"x;";
Object result = ctx.evaluateString(scope, script, "<cmd>", 1, null);
System.out.println(result.getClass());

Here we are receiving same result as in case 2.

arg1 [class java.lang.Integer]
arg2 [class java.lang.Double]
arg3 [class java.lang.Double]
class java.lang.Integer

I would expect javascript engine at least to behave the same in all cases. However I think in all cases it should always return java.lang.Double for "primitive" number type.

@luthionb
Copy link
Author

luthionb commented Oct 27, 2017

And if it is something that cannot be resolved, can I somehow give a bytecode node additional number property?

Here I was thinking about similar approach that we can find in java. I know that for javascript we are not able to define type for numbers but maybe rhino is allowing for something like:
var number = 1.0d;

The optimization level may have an effect on this -- there are certain integer optimizations that only take affect at optimization level 9.

Thanks for that hint. I've checked all optimization levels from -1 to 9. And for level -1 I'm receiving always Double from javascript (just as in case 1 presented above). Unfortunately I cannot afford lower runtime performance with interpretive mode turn on. I really feel that generating compiled bytecode classes for selected javascript makes difference.
For other optimization levels I'm always receiving result (the same as case 2 and 3 original results):

arg1 [class java.lang.Integer]
arg2 [class java.lang.Double]
arg3 [class java.lang.Double]
class java.lang.Integer

@sainaen
Copy link
Contributor

sainaen commented Oct 27, 2017

Thanks for the examples!

First of all, I think your second and third cases are essentially the same: we're creating an object and passing it to the handle(). So we have only the difference between the first and the second.

Now, the type conversion of arguments that are passed to Java methods from JavaScript seems to be done via call to NativeJavaObject#coerceTypeImpl(Class<?> desiredType, Object val).

  • in the first case, coerceTypeImpl() is called for each number separately (as part of a call to Map#put()) with a target type Object which turns them into Double-s, then handle() gets a map with already “correct” types
  • in the second case, coerceTypeImpl() is executed only once when we're passing an object to handle() method, it checks if desiredType.isInstance(val) and, because NativeObject implements Map interface, it's used as is, so now our handle() gets original NativeObject that contains both Integer-s and Double-s

I would expect javascript engine at least to behave the same in all cases.

I'm not sure I agree. The cases are different after all, even if that's not obvious.

It may be possible to achieve consistency by making coerceTypeImpl() recognize NativeObject as not just a Map. Though, this would require what looks like a good amount of changes to support method arguments with generic types, because then you'd need to recursively convert values inside an object.

@p-bakker p-bakker added Java Interop Issues related to the interaction between Java and JavaScript Triage Issues yet to be triaged labels Jul 5, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Java Interop Issues related to the interaction between Java and JavaScript Triage Issues yet to be triaged
Projects
None yet
Development

No branches or pull requests

4 participants