Skip to content

Linking error after the optimizer for someInt.toDouble.compareTo(double) #3085

@sjrd

Description

@sjrd

The following snippet:

object Main {
  def main(args: Array[String]): Unit =
    foo(5)

  def foo(x: Int): Unit =
    bar(x.toDouble)

  @inline def bar(x: Double): Unit =
    println(x.compareTo(5.5))
}

causes the following linking error after the optimizer has run:

[error] Referring to non-existent method java.lang.Integer.compareTo(java.lang.Double)scala.Int
[error]   called from helloworld.HelloWorld$.main([java.lang.String)scala.Unit
[error]   called from core module module initializers
[error] involving instantiated classes:
[error]   helloworld.HelloWorld$

This is because the type of x in bar() is refined to be int because of the inlining. And then we search for compareTo__D__B in j.l.Integer instead of j.l.Double, which causes the breakage.

This issue is similar to #2184 and #2780, and to the reason we define compareTo(Byte/Short) in j.l.Integer (here). However, the present issue shows that it is more severe than I previously thought. It is not limited to Byte and Short being treated as Ints because there are no bytes and shorts in the IR. In general, it is also problematic because int <: double (and float <: double) in the IR, which allows the optimizer to refine a double into an int.

This means that this issue would not be fixed simply by adding bytes and shorts in the IR. We need those ugly bridges in Integer (and Float) anyway.

Worse, there is a more annoying issue. The following snippet:

object Main {
  def main(args: Array[String]): Unit =
    foo(5)

  def foo(x: Int): Unit =
    bar(x.toDouble)

  @inline def bar(x: Double): Unit =
    foobar(x)

  @inline def foobar(x: Comparable[java.lang.Double]): Unit =
    println(x.compareTo(5.5))
}

links, but fails at run-time with a CCE:

[error] scala.scalajs.runtime.UndefinedBehaviorError: An undefined behavior was detected: 5.5 is not an instance of java.lang.Integer
[error]     at $c_sjsr_UndefinedBehaviorError.$c_jl_Throwable.fillInStackTrace__jl_Throwable (StackTrace.scala:52:51)
[error]     at $c_sjsr_UndefinedBehaviorError.fillInStackTrace__jl_Throwable (UndefinedBehaviorError.scala:24:34)
[error]     at $c_sjsr_UndefinedBehaviorError.$c_jl_Throwable.init___T__jl_Throwable (Throwables.scala:12:19)
[error]     at $c_sjsr_UndefinedBehaviorError.init___T__jl_Throwable (Throwables.scala:156:46)
[error]     at $c_sjsr_UndefinedBehaviorError.init___jl_Throwable (UndefinedBehaviorError.scala:20:5)
[error]     at $throwClassCastException (scalajsenv.js:172:1)
[error]     at $asInt (scalajsenv.js:619:1)
[error]     at $c_Lhelloworld_HelloWorld$.main__AT__V (Integer.scala:8:13)
[error]     at Object.<anonymous> (helloworld-fastopt.js:2912:30)
[error]     at Object.<anonymous> (helloworld-fastopt.js:2913:4)

Here, the problem is that we end up calling the bridge j.l.Integer.compareTo(Object) instead of j.l.Double.compareTo(Object). But the former does not accept a Double as its parameter; it only accepts Ints.

That problem cannot be fixed by adding the relevant method in j.l.Integer, because it already exists, but does the wrong thing.

I'm afraid this issue is unfixable while the IR specifies that int <: double and int <: Integer/double <: Double. Since int <: Integer is critical to JS interop, I think we have to abandon int <: double in the IR. Instead, explicit conversions of the form UnaryOp(UnaryOp.IntToDouble, x) will have to be inserted, even though FunctionEmitter would erase them at the end of the day.

Metadata

Metadata

Assignees

Labels

bugConfirmed bug. Needs to be fixed.

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions