scala-dev
scala-dev copied to clipboard
Align Scala 2.14 and 3.x trait initialization
Dotty has implemented a new encoding for trait constructors. Scala 2.14 and 3.0 should align on this implementation detail.
The alignment could come from:
- Scala 2.14 adopting the new encoding
- Scala 3.x reverting to the old encoding
- Scala 3.x modifying the new encoding to allay concerns about binary fragility (discussion below), and Scala 2.x moving to the modified version.
Sample
trait T {
println("1")
val field1 = 1
println("2")
var field2 = 2
println("3")
}
class C extends T
Scala 2.13
public class C
implements T {
private int field1;
private int field2;
@Override
public int field1() {
return this.field1;
}
@Override
public int field2() {
return this.field2;
}
@Override
public void field2_$eq(int x$1) {
this.field2 = x$1;
}
@Override
public void T$_setter_$field1_$eq(int x$1) {
this.field1 = x$1;
}
public C() {
T.$init$(this);
Statics.releaseFence();
}
}
public interface T {
public void T$_setter_$field1_$eq(int var1);
public int field1();
public int field2();
public void field2_$eq(int var1);
public static void $init$(T $this) {
Predef$.MODULE$.println((Object)"1");
$this.T$_setter_$field1_$eq(1);
Predef$.MODULE$.println((Object)"2");
$this.field2_$eq(2);
Predef$.MODULE$.println((Object)"3");
}
}
Scala 3.x
public class C
implements T {
private final int field1 = T.super.initial$field1();
private int field2 = T.super.initial$field2();
public C() {
T.super.$init$();
}
@Override
public int field1() {
return this.field1;
}
@Override
public int field2() {
return this.field2;
}
@Override
public void field2_$eq(int x$1) {
this.field2 = x$1;
}
}
public interface T {
default public void $init$() {
Predef$.MODULE$.println((Object)"3");
}
public int field1();
default public int initial$field1() {
Predef$.MODULE$.println((Object)"1");
return 1;
}
public int field2();
default public int initial$field2() {
Predef$.MODULE$.println((Object)"2");
return 2;
}
public void field2_$eq(int var1);
}
Advantages of new encoding
- eager vals may be represented by JVM
finalfields. Scala 2's isn't able to do this because final fields assignments must appear lexically in the class constructor.
Binary fragility
Traits have a number of well-known binary fragilities. Most notably, when a field is added to a trait and a subclass is not recompiled, a LinkageError will happen when the trait constructor calls the trait setter.
The new encoding introduces new binary fragilities, one of which has a soft failure mode (execution order incorrect). This is because the subclass has the order of the fields "baked in" to its constructor which calls the intiaial$... methods sequentially.
Following are some failure modes when certain changes are made to the trait without recompiling the subclass.
| Edit | New Behaviour | Old Behaviour |
|---|---|---|
| Fields or side effects reordered | The subclass constructor will execute the old sequence of initialiation/effects. This could be benign but is dangerous in general | No need to recompile subclass, semantics okay |
| Field deleted | LinkageError |
wasted field in the subclass, but semantically okay |
Modified, fail-fast encoding
@smarter Has suggested that we could encode the index of the initializer into the initial$... method names.
| Edit | New Behaviour |
|---|---|
| Fields or side effects reordered | LinkageError |
| Field deleted | LinkageError |
(the original Scala source code is missing in the description)
@lrytz oops, fixed
I should have taken it as a reverse-engineering exercise 🙃
out of scope for Scala 2