Skip to content

Spurious Typed node in pattern match in obscure circumstances, affects Scala.js' js.Tuple's. #9588

Closed
@sjrd

Description

@sjrd

Minimized code

final class JSTuple2[+T1, +T2](val _1: T1, val _2: T2)

object JSTuple2 {
  def apply[T1, T2](_1: T1, _2: T2): JSTuple2[T1, T2] =
    new JSTuple2(_1, _2)

  def unapply[T1, T2](t: JSTuple2[T1, T2]): Option[(T1, T2)] =
    Some((t._1, t._2))
}

object Test {
  def main(args: Array[String]): Unit = {
    val ts: List[JSTuple2[Int, String]] = List(JSTuple2(42, "foo"))
    ts(0) match { // note ts(0) here
      case JSTuple2(x, y) => println(s"$x $y")
    }
    val JSTuple2(a, b) = ts(0) // and ts(0) here
    println(s"$a $b")
  }
}

Compile with -Xprint:typer:

sbt:dotty> dotc -Xprint:typer tests/run/hello.scala
[warn] Multiple main classes detected.  Run 'show discoveredMainClasses' to see the list
[info] running (fork) dotty.tools.dotc.Main -classpath C:\Users\sjrdo\AppData\Local\Coursier\cache\v1\https\repo1.maven.org\maven2\org\scala-lang\scala-library\2.13.3\scala-library-2.13.3.jar;C:\Users\sjrdo\Documents\Projets\dotty\library\..\out\bootstrap\dotty-library-bootstrapped\scala-0.27\dotty-library_0.27-0.27.0-bin-SNAPSHOT.jar -Xprint:typer tests/run/hello.scala
result of tests\run\hello.scala after typer:
package <empty> {
  final class JSTuple2[T1 >: Nothing <: Any, T2 >: Nothing <: Any](_1: T1,
    _2: T2
  ) extends Object() {
    +T1
    +T2
    val _1: T1
    val _2: T2
  }
  final lazy module val JSTuple2: JSTuple2$ = new JSTuple2$()
  final module class JSTuple2$() extends Object(), _root_.scala.Serializable {
    this: JSTuple2.type =>
    def apply[T1 >: Nothing <: Any, T2 >: Nothing <: Any](_1: T1, _2: T2):
      JSTuple2[T1, T2]
     = new JSTuple2[T1, T2](_1, _2)
    def unapply[T1 >: Nothing <: Any, T2 >: Nothing <: Any](t: JSTuple2[T1, T2])
      :
    Option[Tuple2[T1, T2]] =
      Some.apply[(T1, T2)](Tuple2.apply[T1, T2](t._1, t._2))
  }
  final lazy module val Test: Test$ = new Test$()
  final module class Test$() extends Object(), _root_.scala.Serializable {
    this: Test.type =>
    def main(args: Array[String]): Unit =
      {
        val ts: List[JSTuple2[Int, String]] =
          List.apply[JSTuple2[Int, String]](
            [JSTuple2.apply[Int, String](42, "foo") : JSTuple2[Int, String]]:
              JSTuple2[Int, String]*
          )
        ts.apply(0) match
          {
            case JSTuple2.unapply[Int, String](x @ _, y @ _) => // no : JSTuple2[Int, String]  here
              println(
                _root_.scala.StringContext.apply([""," ","" : String]:String*).s
                  (
                [x,y : Any]:Any*)
              )
          }
        val $1$: (Int, String) =
          ts.apply(0):JSTuple2[Int, String] @unchecked match
            {
              case
                JSTuple2.unapply[Int, String](a @ _, b @ _):
                  JSTuple2[Int, String] // Note the : JSTuple2[Int, String] here, it is spurious
               => Tuple2.apply[Int, String](a, b)
            }
        val a: Int = $1$._1
        val b: String = $1$._2
        println(
          _root_.scala.StringContext.apply([""," ","" : String]:String*).s(
            [a,b : Any]:Any*
          )
        )
      }
  }
}

In the extractor case, the typer inserts a spurious Typed node, printed as : JSTuple2[Int, String]. That Typed is not inserted when we do an equivalent extraction but using a match, as illustrated in the same example.

Funnier, this requires that the thing being extracted be something like ts(0). I can work around the problem by assigning val t = ts(0) first, and then use t:

final class JSTuple2[+T1, +T2](val _1: T1, val _2: T2)

object JSTuple2 {
  def apply[T1, T2](_1: T1, _2: T2): JSTuple2[T1, T2] =
    new JSTuple2(_1, _2)

  def unapply[T1, T2](t: JSTuple2[T1, T2]): Option[(T1, T2)] =
    Some((t._1, t._2))
}

object Test {
  def main(args: Array[String]): Unit = {
    val ts: List[JSTuple2[Int, String]] = List(JSTuple2(42, "foo"))
    val t = ts(0)
    t match { // note `t` here
      case JSTuple2(x, y) => println(s"$x $y")
    }
    val JSTuple2(a, b) = t // note `t` here
    println(s"$a $b")
  }
}

With that changed, printing after typer shows that neither occurrence has a Typed node:

sbt:dotty> dotc -Xprint:typer tests/run/hello.scala
[warn] Multiple main classes detected.  Run 'show discoveredMainClasses' to see the list
[info] running (fork) dotty.tools.dotc.Main -classpath C:\Users\sjrdo\AppData\Local\Coursier\cache\v1\https\repo1.maven.org\maven2\org\scala-lang\scala-library\2.13.3\scala-library-2.13.3.jar;C:\Users\sjrdo\Documents\Projets\dotty\library\..\out\bootstrap\dotty-library-bootstrapped\scala-0.27\dotty-library_0.27-0.27.0-bin-SNAPSHOT.jar -Xprint:typer tests/run/hello.scala
result of tests\run\hello.scala after typer:
package <empty> {
  final class JSTuple2[T1 >: Nothing <: Any, T2 >: Nothing <: Any](_1: T1,
    _2: T2
  ) extends Object() {
    +T1
    +T2
    val _1: T1
    val _2: T2
  }
  final lazy module val JSTuple2: JSTuple2$ = new JSTuple2$()
  final module class JSTuple2$() extends Object(), _root_.scala.Serializable {
    this: JSTuple2.type =>
    def apply[T1 >: Nothing <: Any, T2 >: Nothing <: Any](_1: T1, _2: T2):
      JSTuple2[T1, T2]
     = new JSTuple2[T1, T2](_1, _2)
    def unapply[T1 >: Nothing <: Any, T2 >: Nothing <: Any](t: JSTuple2[T1, T2])
      :
    Option[Tuple2[T1, T2]] =
      Some.apply[(T1, T2)](Tuple2.apply[T1, T2](t._1, t._2))
  }
  final lazy module val Test: Test$ = new Test$()
  final module class Test$() extends Object(), _root_.scala.Serializable {
    this: Test.type =>
    def main(args: Array[String]): Unit =
      {
        val ts: List[JSTuple2[Int, String]] =
          List.apply[JSTuple2[Int, String]](
            [JSTuple2.apply[Int, String](42, "foo") : JSTuple2[Int, String]]:
              JSTuple2[Int, String]*
          )
        val t: JSTuple2[Int, String] = ts.apply(0)
        t match
          {
            case JSTuple2.unapply[Int, String](x @ _, y @ _) => // No : JSTuple2[Int, String] here
              println(
                _root_.scala.StringContext.apply([""," ","" : String]:String*).s
                  (
                [x,y : Any]:Any*)
              )
          }
        val $1$: (Int, String) =
          t:(t : JSTuple2[Int, String]) @unchecked match
            {
              case JSTuple2.unapply[Int, String](a @ _, b @ _) => // and no : JSTuple2[Int, String] here either!
                Tuple2.apply[Int, String](a, b)
            }
        val a: Int = $1$._1
        val b: String = $1$._2
        println(
          _root_.scala.StringContext.apply([""," ","" : String]:String*).s(
            [a,b : Any]:Any*
          )
        )
      }
  }
}

There's clearly something funny that the type checker is doing here.

Expectation

I expect the type checker not to insert the Typed node in any of the above examples. scalac doesn't do it.

The insertion of the Typed node has a consequence: it means that PatternMatcher inserts an isInstanceOf[JSTuple[Int, String]] for that Typed, even though it is not necessary.

This is a mild consequence for the JVM, but it is critical for Scala.js if the type is a JS trait, like all js.TupleNs are. Because doing isInstanceOf[SomeJSTrait] is not valid and cannot be implemented.

Ultimately, this issue causes the test org.scalajs.testsuite.library.ObjectTest.entries_from_object() not to compile, with compile errors at lines
https://github.com/scala-js/scala-js/blob/e25e45bf55564fb9fc972dc7a9dbc34bcec9e5f4/test-suite/js/src/test/scala/org/scalajs/testsuite/library/ObjectTest.scala#L66
and
https://github.com/scala-js/scala-js/blob/e25e45bf55564fb9fc972dc7a9dbc34bcec9e5f4/test-suite/js/src/test/scala/org/scalajs/testsuite/library/ObjectTest.scala#L70

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions