scala / scala3

The Scala 3 compiler, also known as Dotty.
https://dotty.epfl.ch
Apache License 2.0
5.85k stars 1.06k forks source link

Invariant SAM type that's not a platform SAM: AbstractMethodError (JVM) / Referring to non-existent method (JS) #16388

Closed justcoon closed 1 year ago

justcoon commented 1 year ago

Compiler version

scala 3.2.0, scalajs 1.11.0

Minimized code

import java.util.{function, Map => JMap}

trait Ops {

  private val comparatorCache: JMap[(Int, Int), Boolean] =
    new java.util.HashMap()

  def isLess(left: Int, right: Int): Boolean = {

    // working

//    val f = new function.Function[(Int, Int), Boolean] {
//      override def apply(t: (Int, Int)): Boolean = left.compareTo(right) <= 0
//    }
//
//    comparatorCache.computeIfAbsent((left, right), f)

    // NOT working
    comparatorCache.computeIfAbsent(
      (left, right),
      _ => left.compareTo(right) <= 0
    )
  }
}

object Test1 extends Ops {}

println(Test1.isLess(1, 2))
println(Test1.isLess(1, 2))
println(Test1.isLess(2, 1))

https://scastie.scala-lang.org/kwQJBQRlSneSBEP93zzz8w

Output

[error] Referring to non-existent method Ops$$anon$1.apply(java.lang.Object)java.lang.Object
[error]   called from java.util.Map.computeIfAbsent(java.lang.Object,java.util.function.Function)java.lang.Object
[error]   called from Ops.isLess(int,int)boolean
[error]   called from Test1$.isLess(int,int)boolean
[error]   called from private Test1Tests$.tests$$anonfun$1$$anonfun$1$$anonfun$1(scala.Function1)boolean
[error]   called from private Test1Tests$.tests$$anonfun$1$$anonfun$1()scala.util.Left
[error]   called from private Test1Tests$.tests$$anonfun$1()scala.util.Right
[error]   called from Test1Tests$.tests()utest.Tests
[error]   called from utest.runner.BaseRunner.runSuite(scala.collection.immutable.Seq,java.lang.String,sbt.testing.EventHandler,sbt.testing.TaskDef)scala.concurrent.Future
[error]   called from private utest.runner.BaseRunner.makeTask$$anonfun$1(sbt.testing.TaskDef,scala.collection.immutable.Seq,sbt.testing.EventHandler)scala.concurrent.Future
[error]   called from private utest.runner.BaseRunner.makeTask(sbt.testing.TaskDef)sbt.testing.Task
[error]   called from utest.runner.BaseRunner.deserializeTask(java.lang.String,scala.Function1)sbt.testing.Task
[error]   called from org.scalajs.testing.bridge.TaskInfoBuilder$.attachTask(org.scalajs.testing.common.TaskInfo,sbt.testing.Runner)sbt.testing.Task
[error]   called from private org.scalajs.testing.bridge.TestAdapterBridge$.$anonfun$executeFun$1(sbt.testing.Runner,int,org.scalajs.testing.common.ExecuteRequest)scala.concurrent.Future
[error]   called from private org.scalajs.testing.bridge.TestAdapterBridge$.executeFun(int,sbt.testing.Runner)scala.Function1
[error]   called from private org.scalajs.testing.bridge.TestAdapterBridge$.$anonfun$createRunnerFun$1(boolean,org.scalajs.testing.common.RunnerArgs)void
[error]   called from private org.scalajs.testing.bridge.TestAdapterBridge$.createRunnerFun(boolean)scala.Function1
[error]   called from org.scalajs.testing.bridge.TestAdapterBridge$.start()void
[error]   called from org.scalajs.testing.bridge.Bridge$.start()void
[error]   called from static org.scalajs.testing.bridge.Bridge.start()void
[error]   called from core module module initializers

Expectation

code will run without issues

sjrd commented 1 year ago

Thanks for the report.

sjrd commented 1 year ago

I could reproduce the issue without Scala.js nor Java classes. All that's really needed is an invariant SAM type that is not a platform SAM type:

abstract class Parent

trait Function[A, B] extends Parent {
  def apply(a: A): B
}

object Test {
  def test(x: (Int, Int), f: Function[? >: (Int, Int), ? <: Boolean]): Boolean =
    f.apply(x)

  def isLess(left: Int, right: Int): Boolean = {
    // working
    val f = new Function[(Int, Int), Boolean] {
      override def apply(t: (Int, Int)): Boolean = left.compareTo(right) <= 0
    }
    test((left, right), f)

    // NOT working
    test(
      (left, right),
      _ => left.compareTo(right) <= 0
    )
  }

  def main(args: Array[String]): Unit = {
    println(isLess(1, 2))
    println(isLess(1, 2))
    println(isLess(2, 1))
  }
}
> scalac tests/run/hello.scala
> scala Test
Exception in thread "main" java.lang.AbstractMethodError
        at Test$.test(hello.scala:9)
        at Test$.isLess(hello.scala:21)
        at Test$.main(hello.scala:26)
        at Test.main(hello.scala)

So this is not Scala.js-specific.

I think ExpandSAMs is doing something bad because it generates:

        Test.test(Tuple2.apply[Int, Int](left, right),
          {
            def $anonfun(_$1: (Int, Int)): Boolean =
              int2Integer(left).compareTo(int2Integer(right)).<=(0)
            {
              final class $anon() extends Parent(), 
                Function[? >: (Int, Int), ? <: Boolean] {
                final def apply(_$1: (Int, Int)): Boolean = $anonfun(_$1)
              }
              new Parent with Function[? >: (Int, Int), ? <: Boolean] {...}()
            }
          }
        )

Notice the ? type parameters in the extends clause. That's illegal. It should generate extends Function[(Int, Int), Boolean] instead.

sjrd commented 1 year ago

In fact, I wonder whether typer is not to blame instead. It already generates:

        Test.test(Tuple2.apply[Int, Int](left, right),
          {
            def $anonfun(_$1: (Int, Int)): Boolean =
              int2Integer(left).compareTo(int2Integer(right)).<=(0)
            closure($anonfun:Function[? >: (Int, Int), ? <: Boolean])
          }
        )

Not downright illegal, but since it obviously can correctly construct (Int, Int) and Boolean in def $anonfun (as opposed to ? >: (Int, Int) and ? <: Boolean), perhaps it is already equipped to use the correct types in the closure(...) node.

sjrd commented 1 year ago

Also, if Function itself is an abstract class, instead of a trait, then the typer refuses the code:

-- Error: tests/run/hello.scala:21:37 ------------------------------------------
21 |      _ => left.compareTo(right) <= 0
   |                                     ^
   |result type of lambda is an underspecified SAM type Function[? >: (Int, Int), ? <: Boolean]

so apparently it is already trying to protect itself from this issue, but it only does that for classes and not for traits. That's clearly broken.

sjrd commented 1 year ago

Aaaand ... I knew I had seen this somewhere before. It's a duplicate of #16065.