You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Use Labeled blocks in TailRec, instead of label-defs.
It's easier to first explain on an example. Consider the following
tail-recursive method:
def fact(n: Int, acc: Int): Int =
if (n == 0) acc
else fact(n - 1, n * acc)
It is now translated as follows by the `tailrec` transform:
def fact(n: Int, acc: Int): Int = {
var n$tailLocal1: Int = n
var acc$tailLocal1: Int = acc
while (true) {
tailLabel1[Unit]: {
return {
if (n$tailLocal1 == 0) {
acc
} else {
val n$tailLocal1$tmp1: Int = n$tailLocal1 - 1
val acc$tailLocal1$tmp1: Int = n$tailLocal1 * acc$tailLocal1
n$tailLocal1 = n$tailLocal1$tmp1
acc$tailLocal1 = acc$tailLocal1$tmp1
(return[tailLabel1] ()): Int
}
}
}
}
throw null // unreachable code
}
First, we allocate local `var`s for every parameter, as well as `this`
if necessary.
When we find a tail-recursive call, we evaluate the arguments into
temporaries, then assign them to the `var`s. It is necessary to use
temporaries in order not to use the new contents of a param local when
computing the new value of another param local.
We avoid reassigning param locals if their rhs (i.e., the actual
argument to the recursive call) is itself, which does happen quite
often in practice. In particular, we thus avoid reassigning the local
var for `this` if the prefix is empty. We could further optimize this
by avoiding the reassignment if the prefix is non-empty but equivalent
to `this`.
If only one parameter ends up changing value in any particular
tail-recursive call, we can avoid the temporaries and directly assign
it. This is also a fairly common situation, especially after discarding
useless assignments to the local for `this`.
After all that, we `return` from a labeled block, which is right inside
an infinite `while` loop. The net result is to loop back to the
beginning, implementing the jump. The `return` node is explicitly
ascribed with the previous result type, so that lubs upstream are not
affected (not doing so can cause Ycheck errors).
For control flows that do *not* end up in a tail-recursive call, the
result value is given to an explicit `return` out of the enclosing
method, which prevents the looping.
There is one pretty ugly artifact: after the `while` loop, we must
insert a `throw null` for the body to still typecheck as an `Int` (the
result type of the `def`). This could be avoided if we dared type a
`WhileDo(Literal(Constant(true)), body)` as having type `Nothing`
rather than `Unit`. This is probably dangerous, though, as we have no
guarantee that further transformations will leave the `true` alone,
especially in the presence of compiler plugins. If the `true` gets
wrapped in any way, the type of the `WhileDo` will be altered, and
chaos will ensue.
In the future, we could enhance the codegen to avoid emitting that dead
code. This should not be too difficult:
* emitting a `WhileDo` whose argument is `true` would set the generated
`BType` to `Nothing`.
* then, when emitting a `Block`, we would drop any statements and expr
following a statement whose generated `BType` was `Nothing`.
This commit does not go to such lengths, however.
This change removes the last source of label-defs in the compiler.
After this commit, we will be able to entirely remove label-defs.
0 commit comments