Skip to content

Adding a lazy val to a trait emits backwards-compatible code with wrong semantics #16352

Open
@armanbilge

Description

@armanbilge

Compiler version

3.3.0-RC1-bin-20221115-e587a81-NIGHTLY

Minimized code

tl;dr sbt app/runMain Bar in armanbilge/sandbox@345b785

  1. Compile old Foo.
trait Foo {
  def main(args: Array[String]): Unit = ()
}
  1. Compile Bar.
object Bar extends Foo
  1. Compile new Foo.
trait Foo {
  lazy val sameSame: AnyRef = new AnyRef

  def main(args: Array[String]): Unit = println(sameSame eq sameSame)
}

Output

  1. Run Bar with new Foo.
sbt:sandbox> app/runMain Bar
[info] running Bar 
false

Expectation

Lazy val semantics dictate that sameSame eq sameSame should always be true.

Analysis

If we look at the decompiled bytecode for new Foo, we see it has emitted a default public Object sameSame() method which it is using as an accessor. However, because Bar was compiled against an old Foo, it is not overriding that method. Therefore the lazy val effectively has the semantics of a def.

It seems like that should not be a default method, since the default implementation does not have the correct semantics.

import scala.Predef$;
import scala.runtime.BoxesRunTime;

public interface Foo {
    public static void $init$(Foo $this) {
    }

    public static Object sameSame$(Foo $this) {
        return $this.sameSame();
    }

    default public Object sameSame() {
        return new Object();
    }

    public static void main$(Foo $this, String[] args) {
        $this.main(args);
    }

    default public void main(String[] args) {
        Predef$.MODULE$.println((Object)BoxesRunTime.boxToBoolean((this.sameSame() == this.sameSame() ? 1 : 0) != 0));
    }
}

Credits

@s5bug for stumbling on this by way of bkirwi/decline#461.

See also

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions