-
Notifications
You must be signed in to change notification settings - Fork 1
Transformation
This page will describe the value class transformation.
Let us start with an example:
$ cd Workspace/value-plugin/sandbox/
$ cat complex.scala
@value class Complex(val re: Double, val im: Double) {
def +(c: Complex) = new Complex(re + c.re, im + c.im)
override def toString: String = s"$re + $im * i"
}
object Test {
def main(args: Array[String]): Unit = {
val c1 = new Complex(1.0, 2.0)
val c2 = new Complex(2.0, 1.0)
val c = c1 + c2
println(c.toString)
}
}
$ va-scalac complex.scala
$ va-scala Test
3.0 + 3.0 * iSurprisingly, this execution creates a single instance of a class Complex! It is required for the + operation return, as the Java Virtual Machine specification does not allow multiple returns.
The translated low-level code is (simplified, only showing the Test object):
$ va-scalac -Xprint:valium-commit complex.scala
[[syntax trees at end of valium-commit]] // complex.scala
package <empty> {
...
object Test extends Object {
def main(args: Array[String]): Unit = {
val c1$re: Double = 1.0;
val c1$im: Double = 2.0;
val c2$re: Double = 2.0;
val c2$im: Double = 1.0;
val $$21: Complex = Complex.this.+$xtension(c1$re, c1$im, c2$re, c2$im);
val c$re: Double = $$21.re;
val c$im: Double = $$21.im;
println(Complex.this.toString$xtension(c$re, c$im))
}
}
}As you can see, the Complex class has been inlined and instead of calling methods on the class itself, the code calls extension methods, which don't require creating the object. The single instance of Complex in the execution is produced by the call to the +$xtension extension method, which cannot return the individual fields.
How does the value class plugin get the code from the original form to the optimized form? Well, in 6 successive phases, using the Unified Data Representation Transformation:
$ va-scalac -Xshow-phases
phase name id description
---------- -- -----------
parser 1 parse source into ASTs, perform simple desugaring
namer 2 resolve names, attach symbols to named trees
packageobjects 3 load package objects
typer 4 the meat and potatoes: type the trees
patmat 5 translate match expressions
superaccessors 6 add super accessors in traits and nested classes
extmethods 7 add extension methods for inline classes
pickler 8 serialize symbol tables
refchecks 9 reference/override checking, translate nested objects
uncurry 10 uncurry, translate function values to anonymous classes
tailcalls 11 replace tail calls by jumps
specialize 12 @specialized-driven class and method specialization
valium-prepare 13
valium-verify 14
valium-addext 15
valium-inject 16
valium-coerce 17
valium-commit 18
explicitouter 19 this refs to outer pointers
erasure 20 erase types, add interfaces for traits
posterasure 21 clean up erased inline classes
lazyvals 22 allocate bitmaps, translate lazy vals into lazified defs
lambdalift 23 move nested functions to top level
constructors 24 move field definitions into constructors
flatten 25 eliminate inner classes
mixin 26 mixin composition
cleanup 27 platform-specific cleanups, generate reflective calls
delambdafy 28 remove lambdas
icode 29 generate portable intermediate code
jvm 30 generate JVM bytecode
terminal 31 the last phase during a compilation run
The following subsections will describe each of the phases.
This phase is just a technical requirement, as some of the Scala compiler abstract syntax tree patterns result in suboptimal code after the translation, and in this phase we can rewrite them to equivalent but more optimal cases. The most obvious example is this:
val i = new Complex(0.0, 0.1)
i.asInstanceOf[Complex] // requires boxing
In this code we know statically that i has type Complex, thus the cast is redundant. Still, keeping the cast forces boxing i into an object, which is suboptimal. For such a case, we remove the cast altogether. It should be noted that the cast needs not be written by the user -- earlier phases in the compiler also introduce casts to avoid type errors for complex expressions.
The verification phase checks the properties of the value classes. For example, given the following class:
@value class Complex(var re: Double, var im: Double)The valium-verify phase will complain about the mutable fields:
$ cd Workspace/value-plugin/sandbox/
$ va-scalac complex-bad.scala
complex-bad.scala:1: error: there can only be immutable fields in valium classes
@value class Complex(var re: Double, var im: Double)
^
complex-bad.scala:1: error: there can only be immutable fields in valium classes
@value class Complex(var re: Double, var im: Double)
^
two errors foundThis phase will verify all the restrictions on value classes:
- that value classes are classes and not traits of objects
- that value classes are not abstract
- that all fields are public
- that all fields are immutable
- that all fields are non-value classes (otherwise we may have infinite expansions)
- that value classes don't contain type members
- that value classes don't define additional parameters
- that value classes don't define secondary constructors
- that value classes don't define their own
hashCodeandequalsimplementations - that the value class constructor is side-effect-free
- that no class extends a value class (and the final flag is set)
So far, the compiler phases focused on preparing and verifying the source code for transformation. It is only now that they actually start transforming the tree: The next phase moves value class methods to an object, thus making them extension methods. At this point, the extension methods still need boxing. Getting back to our running example in complex.scala (output simplified):
$ va-scalac complex.scala -Xprint:valium-addext
[[syntax trees at end of valium-addext]] // complex.scala
package <empty> {
@value final class Complex extends Object {
private[this] val re: Double = _;
def re(): Double = Complex.this.re;
private[this] val im: Double = _;
def im(): Double = Complex.this.im;
def +(c: Complex): Complex = Complex.+$xtension(Complex.this, c);
override def toString(): String = Complex.toString$xtension(Complex.this);
override def equals(x$1: Any): Boolean = Complex.equals$xtension(Complex.this, x$1);
override def hashCode(): Int = Complex.hashCode$xtension(Complex.this)
};
object Complex extends Object {
final def +$xtension($this: Complex, c: Complex): Complex = new Complex($this.re().+(c.re()), $this.im().+(c.im()));
final def toString$xtension($this: Complex): String = new StringContext(scala.this.Predef.wrapRefArray[String](Array[String]{"", " + ", " * i"})).s(scala.this.Predef.genericWrapArray[Any](Array[Any]{$this.re(), $this.im()}));
final def equals$xtension($this: Complex, x$1: Any): Boolean = x$1.isInstanceOf[Complex]().&&({
<synthetic> val Complex$1: Complex = x$1.asInstanceOf[Complex]();
$this.re().==(Complex$1.re()).&&($this.im().==(Complex$1.im()))
});
final def hashCode$xtension($this: Complex): Int = -1679819632.+($this.re.hashCode()).+($this.im.hashCode())
};
object Test extends Object {
def main(args: Array[String]): Unit = {
val c1: Complex = new Complex(1.0, 2.0);
val c2: Complex = new Complex(2.0, 1.0);
val c: Complex = c1.+(c2);
println(c.toString())
}
}
}Once the extension methods have been extracted, the next step is applying the unified data representation transformation.
The value class plugin follows exactly the patterns from the unified data representation transformation, which have been explained in miniboxing. In this tutorial, we will only show what each phase does and comment on it.
Unlike miniboxing, the injection phase on the value class plugin is very simple: if a value has type V, where V is a value class, then mark it for unboxing: V @unboxed. Note that it will not transform deep inside generics, thus List[V] will remain List[V]:
$ va-scalac complex.scala -Xprint:valium-inject
[[syntax trees at end of valium-inject]] // complex.scala
package <empty> {
@value final class Complex extends Object {
private[this] val re: Double = _;
def re(): Double = Complex.this.re;
private[this] val im: Double = _;
def im(): Double = Complex.this.im;
def +(c: Complex @unboxed): Complex = Complex.+$xtension(Complex.this, c);
override def toString(): String = Complex.toString$xtension(Complex.this);
override def equals(x$1: Any): Boolean = Complex.equals$xtension(Complex.this, x$1);
override def hashCode(): Int = Complex.hashCode$xtension(Complex.this)
};
object Complex extends Object {
final def +$xtension($this: Complex @unboxed, c: Complex @unboxed): Complex = new Complex($this.re().+(c.re()), $this.im().+(c.im()));
final def toString$xtension($this: Complex @unboxed): String = new StringContext(scala.this.Predef.wrapRefArray[String](Array[String]{"", " + ", " * i"})).s(scala.this.Predef.genericWrapArray[Any](Array[Any]{$this.re(), $this.im()}));
final def equals$xtension($this: Complex @unboxed, x$1: Any): Boolean = x$1.isInstanceOf[Complex]().&&({
val Complex$1: Complex @unboxed = x$1.asInstanceOf[Complex]();
$this.re().==(Complex$1.re()).&&($this.im().==(Complex$1.im()))
});
final def hashCode$xtension($this: Complex @unboxed): Int = -1679819632.+($this.re.hashCode()).+($this.im.hashCode())
};
object Test extends Object {
def main(args: Array[String]): Unit = {
val c1: Complex @unboxed = new Complex(1.0, 2.0);
val c2: Complex @unboxed = new Complex(2.0, 1.0);
val c: Complex @unboxed = c1.+(c2);
println(c.toString())
}
}
}This is a very simple transformation, with little secret sauce to it.
The coerce phase is a bit more complex, in that it takes care of four aspects:
- it adds coercions when required by subtyping and by generics
- it boxes method receivers when necessary
- but if method receivers have extension methods, it redirects to the extension method instead of boxing
- it eliminates unstable unboxed expressions from the tree by boxing them (unboxed unstable expressions cannot be transformed to bytecode equivalents)
While the first 3 are common to all data representation transformations, the last one is specific to value classes. Unstable expressions are defined in the Scala language specification and intuitively are expressions that may change their value in time:
$ cat stable.scala
object Test {
val c = 3
import c._
def d = 4 // may change value in time
// no stability guarantee!
import d._
}
$ scalac stable.scala
stable.scala:7: error: stable identifier required, but Test.this.d found.
import d._
^
one error foundThe coercions introduced by the valium-coerce phase to our running example are:
$ va-scalac complex.scala -Xprint:valium-coerce
[[syntax trees at end of valium-coerce]] // complex.scala
package <empty> {
@value final class Complex extends Object {
private[this] val re: Double = _;
def re(): Double = Complex.this.re;
private[this] val im: Double = _;
def im(): Double = Complex.this.im;
def +(c: Complex @unboxed): Complex = Complex.+$xtension(scala.this.box2unbox[Complex](Complex.this), c);
override def toString(): String = Complex.toString$xtension(scala.this.box2unbox[Complex](Complex.this));
override def equals(x$1: Any): Boolean = Complex.equals$xtension(scala.this.box2unbox[Complex](Complex.this), x$1);
override def hashCode(): Int = Complex.hashCode$xtension(scala.this.box2unbox[Complex](Complex.this))
};
object Complex extends Object {
final def +$xtension($this: Complex @unboxed, c: Complex @unboxed): Complex = new Complex(scala.this.unbox2box[Complex]($this).re().+(scala.this.unbox2box[Complex](c).re()), scala.this.unbox2box[Complex]($this).im().+(scala.this.unbox2box[Complex](c).im()));
final def toString$xtension($this: Complex @unboxed): String = new StringContext(scala.this.Predef.wrapRefArray[String](Array[String]{"", " + ", " * i"})).s(scala.this.Predef.genericWrapArray[Any](Array[Any]{scala.this.unbox2box[Complex]($this).re(), scala.this.unbox2box[Complex]($this).im()}));
final def equals$xtension($this: Complex @unboxed, x$1: Any): Boolean = x$1.isInstanceOf[Complex]().&&({
val Complex$1: Complex @unboxed = scala.this.box2unbox[Complex](x$1.asInstanceOf[Complex]());
scala.this.unbox2box[Complex]($this).re().==(scala.this.unbox2box[Complex](Complex$1).re()).&&(scala.this.unbox2box[Complex]($this).im().==(scala.this.unbox2box[Complex](Complex$1).im()))
});
final def hashCode$xtension($this: Complex @unboxed): Int = -1679819632.+(scala.this.unbox2box[Complex]($this).re().hashCode()).+(scala.this.unbox2box[Complex]($this).im().hashCode())
};
object Test extends Object {
def main(args: Array[String]): Unit = {
val c1: Complex @unboxed = scala.this.box2unbox[Complex](new Complex(1.0, 2.0));
val c2: Complex @unboxed = scala.this.box2unbox[Complex](new Complex(2.0, 1.0));
val c: Complex @unboxed = scala.this.box2unbox[Complex](Complex.this.+$xtension(c1, c2));
scala.this.Predef.println(Complex.this.toString$xtension(c))
}
}
}We can also test the stable expression transformation:
$ cat complex2.scala
@value class Complex(val re: Double, val im: Double)
object Test {
def main(args: Array[String]): Unit = {
val c1 = new Complex(1.0, 2.0)
val c2 = new Complex(2.0, 1.0)
def cond = true
val c = if (cond) c1 else c2 //unstable
}
}
$ va-scalac complex2.scala -Xprint:valium-coerce
[[syntax trees at end of valium-coerce]] // complex2.scala
package <empty> {
...
object Test extends Object {
def <init>(): Test.type = {
Test.super.<init>();
()
};
def main(args: Array[String]): Unit = {
val c1: Complex @unboxed = scala.this.box2unbox[Complex](new Complex(1.0, 2.0));
val c2: Complex @unboxed = scala.this.box2unbox[Complex](new Complex(2.0, 1.0));
def cond(): Boolean = true;
val c: Complex @unboxed = scala.this.box2unbox[Complex](if (cond())
scala.this.unbox2box[Complex](c1)
else
scala.this.unbox2box[Complex](c2));
()
}
}
}In this example, the unstable expression would have the type Complex @unboxed:
val c =
if (cond)
c1
else
c2Yet, since this is unstable, the valium-coerce phase choses to box and unbox it:
val c: Complex @unboxed =
scala.this.box2unbox[Complex](
if (cond)
scala.this.unbox2box[Complex](c1)
else
scala.this.unbox2box[Complex](c2)
)This transformation is done since again, the JVM specification does not allow multiple returns, thus the commit phase would not have a proper lowering for such expressions.
In the case of value classes, the commit phase is the most complex. It takes care of expanding unboxed value classes to their fields and transforming expressions accordingly. The rules for the transformation are documented here: https://github.com/miniboxing/value-plugin/blob/master/components/plugin/src/valium/plugin/transform/commit/ValiumCommitTreeTransformer.scala#L14-L74
Getting back to our running example, we get:
$ va-scalac complex.scala -Xprint:valium-commit
[[syntax trees at end of valium-commit]] // complex.scala
package {
@value final class Complex extends Object {
private[this] val re: Double = _;
def re(): Double = Complex.this.re;
private[this] val im: Double = _;
def im(): Double = Complex.this.im;
def +(c$re: Double, c$im: Double): Complex = {
val $$6: Complex = Complex.this;
val 1$re: Double = $$6.re;
val 1$im: Double = $$6.im;
val this$re$2: Double = 1$re;
val this$im$3: Double = 1$im;
val arg$c$re$4: Double = c$re;
val arg$c$im$5: Double = c$im;
Complex.+$xtension(this$re$2, this$im$3, arg$c$re$4, arg$c$im$5)
};
override def toString(): String = {
val $$10: Complex = Complex.this;
val 7$re: Double = $$10.re;
val 7$im: Double = $$10.im;
val this$re$8: Double = 7$re;
val this$im$9: Double = 7$im;
Complex.toString$xtension(this$re$8, this$im$9)
};
override def equals(x$1: Any): Boolean = {
val $$15: Complex = Complex.this;
val 11$re: Double = $$15.re;
val 11$im: Double = $$15.im;
val this$re$12: Double = 11$re;
val this$im$13: Double = 11$im;
val 14: Any = x$1;
Complex.equals$xtension(this$re$12, this$im$13, 14)
};
override def hashCode(): Int = {
val $$19: Complex = Complex.this;
val 16$re: Double = $$19.re;
val 16$im: Double = $$19.im;
val this$re$17: Double = 16$re;
val this$im$18: Double = 16$im;
Complex.hashCode$xtension(this$re$17, this$im$18)
}
};
object Complex extends Object {
final def +$xtension($this$re: Double, $this$im: Double, c$re: Double, c$im: Double): Complex = new Complex($this$re.+(c$re), $this$im.+(c$im));
final def toString$xtension($this$re: Double, $this$im: Double): String = new StringContext(scala.this.Predef.wrapRefArray[String](Array[String]{"", " + ", " * i"})).s(scala.this.Predef.genericWrapArray[Any](Array[Any]{$this$re, $this$im}));
final def equals$xtension($this$re: Double, $this$im: Double, x$1: Any): Boolean = x$1.isInstanceOf[Complex]().&&({
val $$20: Complex = x$1.asInstanceOf[Complex]();
val Complex$1$re: Double = $$20.re;
val Complex$1$im: Double = $$20.im;
$this$re.==(Complex$1$re).&&($this$im.==(Complex$1$im))
});
final def hashCode$xtension($this$re: Double, $this$im: Double): Int = -1679819632.+($this$re.hashCode()).+($this$im.hashCode())
};
object Test extends Object {
def <init>(): Test.type = {
Test.super.<init>();
()
};
def main(args: Array[String]): Unit = {
val c1$re: Double = 1.0;
val c1$im: Double = 2.0;
val c2$re: Double = 2.0;
val c2$im: Double = 1.0;
val $$21: Complex = Complex.this.+$xtension(c1$re, c1$im, c2$re, c2$im);
val c$re: Double = $$21.re;
val c$im: Double = $$21.im;
scala.this.Predef.println(Complex.this.toString$xtension(c$re, c$im))
}
}
}Which is exactly what we have seen in the beginning of the article. While the code output could certainly be improved by a simple copy propagation phase, it is not necessary to do this in the plugin, since the backend already contains copy propagation and dead code elimination phases.
We encourage the readers to try out different examples. We are aware of an important bug which prevents value classes from working under separate compilation. It is a technical issue, not a fundamental limitation, but you will need to compile the value classes along with the code that uses them in the same file (or in different files, all compiled at the same time): https://github.com/miniboxing/value-plugin/issues/37
This said, the benchmarks show how well this transformation works to eliminate the overhead of the object-oriented representation.