scala / bug

Scala 2 bug reports only. Please, no questions — proper bug reports only.
https://scala-lang.org
232 stars 21 forks source link

"Error while emitting" from scalac on pattern matching with AnyVal result #13043

Open iogrm opened 1 week ago

iogrm commented 1 week ago

Reproduction steps

openjdk 17 and 21 scala version 2.13.10 and 2.13.14 and 2.13.15 sbt version 1.10.1 and 1.10.2

build.sbt

scalaVersion := "2.13.14" // or other 2.13.x versions

trait ParentTrait extends Any {
  def value: String
}
class Child2(val value: String) extends AnyVal with ParentTrait

object Sample {
    def test( sample: String): ParentTrait = 
      sample match {
        case "0" if true   =>  ???
        case _             => new Child2(???)
      }
}

I also tested with case classes and sealed traits. Also, the predicate in if statement (true in this sample code) can be anything, the issue will be the same.

Problem

Compilation fails with unhelpful message:

[info] compiling 1 Scala source to .../target/scala-2.13/classes ...
[error] Error while emitting Sample$
[error] Index 0 out of bounds for length 0
[error] one error found
[error] (Compile / compileIncremental) Compilation failed

And sometimes

[info] compiling 1 Scala source to .../target/scala-2.13/classes ...
[error] Error while emitting Sample$
[error] null
[error] one error found
[error] (Compile / compileIncremental) Compilation failed
lrytz commented 1 week ago

Thanks @iogrm for the minimization!

Turns out this is very similar to https://github.com/scala/scala/pull/10775 / https://github.com/scala/bug/issues/12990 which I recently looked at. I simplified the example a tiny bit:

class C(val x: String) extends AnyVal

abstract class Test {
  def c: C
  def test(v: Int): Any = 
    v match {
      case 1 if true => c
      case _ => c
    }
}

AST after patmat:

    <method> def test(v: scala.this.Int): scala.this.Any = {
      case <synthetic> val x1: scala.this.Int = v;
      x1 match {
        case 1 => if (true)
          Test.this.c
        else
          default3()
        case _ => default3(){
          Test.this.c
        }
      }
    }

The match tree is left in place because it's a match that can be translated to a switch bytecode by the backend. In the original example, the match is on a string, which is also translated to a switch thanks to https://github.com/scala/scala/pull/8451.

Now erasure comes along and realizes that the result has to be boxed: Test.this.c returns a String (the value class is erased), but the return type of test is Any. So erasure adds new C:

    <method> def test(v: scala.this.Int): lang.this.Object = {
      case <synthetic> val x1: scala.this.Int = v;
      (x1: scala.this.Int) match {
        case 1 => if (true)
          new C.<init>(Test.this.c())
        else
          new C.<init>(default3())
        case _ => default3(){
          new C.<init>(Test.this.c())
        }
      }
    }

The resulting AST new C.<init>(default3()) is not correct, as default3() is not a method call that returns a value. It's a jump to a "label".

I guess this is a bug in erasure, it should realize that the boxing is already added to the default case. The jump to default3() could just be left as it is.