Skip to content

Commit 700a8da

Browse files
sjrdWojciechMazur
authored andcommitted
Opt: Get rid of the LiftTry phase; instead handle things in the back-end.
When we enter a `try-catch` at the JVM level, we have to make sure that the stack is empty. That's because, upon exception, the JVM wipes the stack, and we must not lose operands that are already on the stack that we will still use. Previously, this was achieved with a transformation phase, `LiftTry`, which lifted problematic `try-catch`es in local `def`s, called `liftedTree$x`. It analyzed the tree to predict which `try-catch`es would execute on a non-empty stack when eventually compiled to the JVM. This approach has several shortcomings. It exhibits performance cliffs, as the generated def can then cause more variables to be boxed in to `XRef`s. These were the only extra defs created for implementation reasons rather than for language reasons. As a user of the language, it is hard to predict when such a lifted def will be needed. The additional `liftedTree` methods also show up on stack traces and obfuscate them. Debugging can be severely hampered as well. Phases executing after `LiftTry`, notably `CapturedVars`, also had to take care not to create more problematic situations as a result of their transformations, which is hard to predict and to remember. Finally, Scala.js and Scala Native do not have the same restriction, so they received suboptimal code for no reason. In this commit, we entirely remove the `LiftTry` phase. Instead, we enhance the JVM back-end to deal with the situation. When starting a `try-catch` on a non-empty stack, we stash the entire contents of the stack into local variables. After the `try-catch`, we pop all those local variables back onto the stack. We also null out the leftover vars not to prevent garbage collection. This new approach solves all of the problems mentioned above. [Cherry-picked 52e8e74]
1 parent c5c4e48 commit 700a8da

File tree

12 files changed

+75
-152
lines changed

12 files changed

+75
-152
lines changed

compiler/src/dotty/tools/backend/jvm/BCodeSkelBuilder.scala

+12
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ trait BCodeSkelBuilder extends BCodeHelpers {
4545
private var stack = new Array[BType](32)
4646
private var size = 0
4747

48+
def isEmpty: Boolean = size == 0
49+
4850
def push(btype: BType): Unit =
4951
if size == stack.length then
5052
stack = java.util.Arrays.copyOf(stack, stack.length * 2)
@@ -81,6 +83,16 @@ trait BCodeSkelBuilder extends BCodeHelpers {
8183

8284
def clear(): Unit =
8385
size = 0
86+
87+
def acquireFullStack(): IArray[BType] =
88+
val res = IArray.unsafeFromArray(stack.slice(0, size))
89+
size = 0
90+
res
91+
92+
def restoreFullStack(fullStack: IArray[BType]): Unit =
93+
assert(size == 0 && stack.length >= fullStack.length)
94+
fullStack.copyToArray(stack)
95+
size = fullStack.length
8496
end BTypesStack
8597

8698
object BTypesStack:

compiler/src/dotty/tools/backend/jvm/BCodeSyncAndTry.scala

+57-1
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ trait BCodeSyncAndTry extends BCodeBodyBuilder {
118118

119119
/*
120120
* Emitting try-catch is easy, emitting try-catch-finally not quite so.
121+
*
122+
* For a try-catch, the only thing we need to care about is to stash the stack away
123+
* in local variables and load them back in afterwards, in case the incoming stack
124+
* is not empty.
125+
*
121126
* A finally-block (which always has type Unit, thus leaving the operand stack unchanged)
122127
* affects control-transfer from protected regions, as follows:
123128
*
@@ -190,7 +195,7 @@ trait BCodeSyncAndTry extends BCodeBodyBuilder {
190195
}
191196
}
192197

193-
// ------ (0) locals used later ------
198+
// ------ locals used later ------
194199

195200
/*
196201
* `postHandlers` is a program point denoting:
@@ -203,6 +208,13 @@ trait BCodeSyncAndTry extends BCodeBodyBuilder {
203208
*/
204209
val postHandlers = new asm.Label
205210

211+
// stack stash
212+
val needStackStash = !stack.isEmpty && !caseHandlers.isEmpty
213+
val acquiredStack = if needStackStash then stack.acquireFullStack() else null
214+
val stashLocals =
215+
if acquiredStack == null then null
216+
else acquiredStack.uncheckedNN.filter(_ != UNIT).map(btpe => locals.makeTempLocal(btpe))
217+
206218
val hasFinally = (finalizer != tpd.EmptyTree)
207219

208220
/*
@@ -222,6 +234,17 @@ trait BCodeSyncAndTry extends BCodeBodyBuilder {
222234
*/
223235
val finCleanup = if (hasFinally) new asm.Label else null
224236

237+
/* ------ (0) Stash the stack into local variables, if necessary.
238+
* From top of the stack down to the bottom.
239+
* ------
240+
*/
241+
242+
if stashLocals != null then
243+
val stashLocalsNN = stashLocals.uncheckedNN // why is this necessary?
244+
for i <- (stashLocalsNN.length - 1) to 0 by -1 do
245+
val local = stashLocalsNN(i)
246+
bc.store(local.idx, local.tk)
247+
225248
/* ------ (1) try-block, protected by:
226249
* (1.a) the EHs due to case-clauses, emitted in (2),
227250
* (1.b) the EH due to finally-clause, emitted in (3.A)
@@ -367,6 +390,39 @@ trait BCodeSyncAndTry extends BCodeBodyBuilder {
367390
emitFinalizer(finalizer, tmp, isDuplicate = false) // the only invocation of emitFinalizer with `isDuplicate == false`
368391
}
369392

393+
/* ------ (5) Unstash the stack, if it was stashed before.
394+
* From bottom of the stack to the top.
395+
* If there is a non-UNIT result, we need to temporarily store
396+
* that one in a local variable while we unstash.
397+
* ------
398+
*/
399+
400+
if stashLocals != null then
401+
val stashLocalsNN = stashLocals.uncheckedNN // why is this necessary?
402+
403+
val resultLoc =
404+
if kind == UNIT then null
405+
else if tmp != null then locals(tmp) // reuse the same local
406+
else locals.makeTempLocal(kind)
407+
if resultLoc != null then
408+
bc.store(resultLoc.idx, kind)
409+
410+
for i <- 0 until stashLocalsNN.size do
411+
val local = stashLocalsNN(i)
412+
bc.load(local.idx, local.tk)
413+
if local.tk.isRef then
414+
bc.emit(asm.Opcodes.ACONST_NULL)
415+
bc.store(local.idx, local.tk)
416+
417+
stack.restoreFullStack(acquiredStack.nn)
418+
419+
if resultLoc != null then
420+
bc.load(resultLoc.idx, kind)
421+
if kind.isRef then
422+
bc.emit(asm.Opcodes.ACONST_NULL)
423+
bc.store(resultLoc.idx, kind)
424+
end if // stashLocals != null
425+
370426
kind
371427
} // end of genLoadTry()
372428

compiler/src/dotty/tools/dotc/Compiler.scala

-1
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,6 @@ class Compiler {
101101
new Getters, // Replace non-private vals and vars with getter defs (fields are added later)
102102
new SpecializeFunctions, // Specialized Function{0,1,2} by replacing super with specialized super
103103
new SpecializeTuples, // Specializes Tuples by replacing tuple construction and selection trees
104-
new LiftTry, // Put try expressions that might execute on non-empty stacks into their own methods
105104
new CollectNullableFields, // Collect fields that can be nulled out after use in lazy initialization
106105
new ElimOuterSelect, // Expand outer selections
107106
new ResolveSuper, // Implement super accessors

compiler/src/dotty/tools/dotc/core/NameKinds.scala

-1
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,6 @@ object NameKinds {
314314
val TailTempName: UniqueNameKind = new UniqueNameKind("$tmp")
315315
val ExceptionBinderName: UniqueNameKind = new UniqueNameKind("ex")
316316
val SkolemName: UniqueNameKind = new UniqueNameKind("?")
317-
val LiftedTreeName: UniqueNameKind = new UniqueNameKind("liftedTree")
318317
val SuperArgName: UniqueNameKind = new UniqueNameKind("$superArg$")
319318
val DocArtifactName: UniqueNameKind = new UniqueNameKind("$doc")
320319
val UniqueInlineName: UniqueNameKind = new UniqueNameKind("$i")

compiler/src/dotty/tools/dotc/transform/CapturedVars.scala

+4-34
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@ class CapturedVars extends MiniPhase with IdentityDenotTransformer:
2525

2626
override def description: String = CapturedVars.description
2727

28-
override def runsAfterGroupsOf: Set[String] = Set(LiftTry.name)
29-
// lifting tries changes what variables are considered to be captured
30-
3128
private[this] var Captured: Store.Location[util.ReadOnlySet[Symbol]] = _
3229
private def captured(using Context) = ctx.store(Captured)
3330

@@ -131,42 +128,15 @@ class CapturedVars extends MiniPhase with IdentityDenotTransformer:
131128
*
132129
* intRef.elem = expr
133130
*
134-
* rewrite using a temporary var to
135-
*
136-
* val ev$n = expr
137-
* intRef.elem = ev$n
138-
*
139-
* That way, we avoid the problem that `expr` might contain a `try` that would
140-
* run on a non-empty stack (which is illegal under JVM rules). Note that LiftTry
141-
* has already run before, so such `try`s would not be eliminated.
142-
*
143-
* If the ref type lhs is followed by a cast (can be an artifact of nested translation),
144-
* drop the cast.
145-
*
146-
* If the ref type is `ObjectRef` or `VolatileObjectRef`, immediately assign `null`
147-
* to the temporary to make the underlying target of the reference available for
148-
* garbage collection. Nullification is omitted if the `expr` is already `null`.
149-
*
150-
* var ev$n: RHS = expr
151-
* objRef.elem = ev$n
152-
* ev$n = null.asInstanceOf[RHS]
131+
* the lhs can be followed by a cast as an artifact of nested translation.
132+
* In that case, drop the cast.
153133
*/
154134
override def transformAssign(tree: Assign)(using Context): Tree =
155-
def absolved: Boolean = tree.rhs match
156-
case Literal(Constant(null)) | Typed(Literal(Constant(null)), _) => true
157-
case _ => false
158-
def recur(lhs: Tree): Tree = lhs match
135+
tree.lhs match
159136
case TypeApply(Select(qual@Select(_, nme.elem), nme.asInstanceOf_), _) =>
160-
recur(qual)
161-
case Select(_, nme.elem) if refInfo.boxedRefClasses.contains(lhs.symbol.maybeOwner) =>
162-
val tempDef = transformFollowing(SyntheticValDef(TempResultName.fresh(), tree.rhs, flags = Mutable))
163-
val update = cpy.Assign(tree)(lhs, ref(tempDef.symbol))
164-
def reset = cpy.Assign(tree)(ref(tempDef.symbol), nullLiteral.cast(tempDef.symbol.info))
165-
val res = if refInfo.objectRefClasses(lhs.symbol.maybeOwner) && !absolved then reset else unitLiteral
166-
transformFollowing(Block(tempDef :: update :: Nil, res))
137+
cpy.Assign(tree)(qual, tree.rhs)
167138
case _ =>
168139
tree
169-
recur(tree.lhs)
170140

171141
object CapturedVars:
172142
val name: String = "capturedVars"

compiler/src/dotty/tools/dotc/transform/LiftTry.scala

-88
This file was deleted.

docs/_docs/internals/overall-structure.md

-1
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,6 @@ phases. The current list of phases is specified in class [Compiler] as follows:
151151
new InterceptedMethods, // Special handling of `==`, `|=`, `getClass` methods
152152
new Getters, // Replace non-private vals and vars with getter defs (fields are added later)
153153
new SpecializeFunctions, // Specialized Function{0,1,2} by replacing super with specialized super
154-
new LiftTry, // Put try expressions that might execute on non-empty stacks into their own methods
155154
new CollectNullableFields, // Collect fields that can be nulled out after use in lazy initialization
156155
new ElimOuterSelect, // Expand outer selections
157156
new ResolveSuper, // Implement super accessors

tests/neg/t1672b.scala

-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ object Test1772B {
4141
}
4242
}
4343

44-
// the `liftedTree` local method will prevent a tail call here.
4544
@tailrec
4645
def bar(i : Int) : Int = {
4746
if (i == 0) 0

tests/run/i1692.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class LazyNullable(a: => Int) {
2222
lazy val l4Inf = eInf
2323

2424
private[this] val i = "I"
25-
// null out i even though the try ends up lifted, because the LazyVals phase runs before the LiftTry phase
25+
// null out i even though the try needs stack stashing
2626
lazy val l5 = try i catch { case e: Exception => () }
2727
}
2828

tests/run/i1692b.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class LazyNullable(a: => Int) {
2424
@threadUnsafe lazy val l4Inf = try eInf finally () // null out e, since private[this] is inferred
2525

2626
private[this] val i = "I"
27-
// null out i even though the try ends up lifted, because the LazyVals phase runs before the LiftTry phase
27+
// null out i even though the try needs stack stashing
2828
@threadUnsafe lazy val l5 = try i catch { case e: Exception => () }
2929
}
3030

tests/run/i4866.check

-2
This file was deleted.

tests/run/i4866.scala

-21
This file was deleted.

0 commit comments

Comments
 (0)