Skip to content

Commit 04e7bc1

Browse files
committed
Suppress returns that cross different stack sizes.
This is needed to avoid verify errors
1 parent 4610201 commit 04e7bc1

File tree

4 files changed

+132
-67
lines changed

4 files changed

+132
-67
lines changed

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

Lines changed: 70 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ object DropBreaks:
2828
var otherRefs: Int = 0
2929

3030
private val LabelUsages = new Property.Key[Map[Symbol, LabelUsage]]
31-
private val LabelsShadowedByTry = new Property.Key[Set[Symbol]]
31+
private val ShadowedLabels = new Property.Key[Set[Symbol]]
3232

3333
/** Rewrites local Break throws to labeled returns.
3434
* Drops `try` statements on breaks if no other uses of its label remain.
@@ -38,7 +38,7 @@ object DropBreaks:
3838
* - the throw and the boundary are in the same method, and
3939
* - there is no try expression inside the boundary that encloses the throw.
4040
*/
41-
class DropBreaks extends MiniPhase:
41+
class DropBreaks extends MiniPhase, RecordStackChange:
4242
import DropBreaks.*
4343

4444
import tpd._
@@ -99,6 +99,31 @@ class DropBreaks extends MiniPhase:
9999
None
100100
end BreakBoundary
101101

102+
private object Break:
103+
104+
private def isBreak(sym: Symbol)(using Context): Boolean =
105+
sym.name == nme.apply && sym.owner == defn.breakModule.moduleClass
106+
107+
/** `(local, arg)` provided `tree` matches
108+
*
109+
* break[...](arg)(local)
110+
*
111+
* or `(local, ())` provided `tree` matches
112+
*
113+
* break()(local)
114+
*/
115+
def unapply(tree: Tree)(using Context): Option[(Symbol, Tree)] = tree match
116+
case Apply(Apply(fn, args), id :: Nil)
117+
if isBreak(fn.symbol) =>
118+
stripInlined(id) match
119+
case id: Ident =>
120+
val arg = (args: @unchecked) match
121+
case arg :: Nil => arg
122+
case Nil => Literal(Constant(())).withSpan(tree.span)
123+
Some((id.symbol, arg))
124+
case _ => None
125+
case _ => None
126+
102127
/** The LabelUsage data associated with `lbl` in the current context */
103128
private def labelUsage(lbl: Symbol)(using Context): Option[LabelUsage] =
104129
for
@@ -117,18 +142,34 @@ class DropBreaks extends MiniPhase:
117142
case _ =>
118143
ctx
119144

120-
/** If `tree` is not a LabeledTry, include all enclosing labels in the
121-
* `LabelsShadowedByTry` context property. This means that breaks to these
122-
* labels will not be translated to labeled returns in the body of the try.
145+
/** Include all enclosing labels in the `ShadowedLabels` context property.
146+
* This means that breaks to these labels will not be translated to labeled
147+
* returns while this context is valid.
123148
*/
124-
override def prepareForTry(tree: Try)(using Context): Context = tree match
125-
case LabelTry(_, _) => ctx
126-
case _ => ctx.property(LabelUsages) match
149+
private def shadowLabels(using Context): Context =
150+
ctx.property(LabelUsages) match
127151
case Some(usesMap) =>
128-
val setSoFar = ctx.property(LabelsShadowedByTry).getOrElse(Set.empty)
129-
ctx.fresh.setProperty(LabelsShadowedByTry, setSoFar ++ usesMap.keysIterator)
152+
val setSoFar = ctx.property(ShadowedLabels).getOrElse(Set.empty)
153+
ctx.fresh.setProperty(ShadowedLabels, setSoFar ++ usesMap.keysIterator)
130154
case _ => ctx
131155

156+
/** Need to suppress labeled returns if the stack can change between
157+
* source and target of the jump
158+
*/
159+
protected def stackChange(using Context) = shadowLabels
160+
161+
/** Need to suppress labeled returns if there is an intervening try
162+
*/
163+
override def prepareForTry(tree: Try)(using Context): Context = tree match
164+
case LabelTry(_, _) => ctx
165+
case _ => shadowLabels
166+
167+
override def prepareForApply(tree: Apply)(using Context): Context = tree match
168+
case Break(_, _) => ctx
169+
case _ => stackChange
170+
171+
// other stack changing operations are handled in RecordStackChange
172+
132173
/** If `tree` is a BreakBoundary, transform it as follows:
133174
* - Wrap it in a labeled block if its label has local uses
134175
* - Drop the try/catch if its label has no other uses
@@ -147,29 +188,9 @@ class DropBreaks extends MiniPhase:
147188
case _ =>
148189
tree
149190

150-
private def isBreak(sym: Symbol)(using Context): Boolean =
151-
sym.name == nme.apply && sym.owner == defn.breakModule.moduleClass
152-
153-
private def transformBreak(tree: Tree, arg: Tree, lbl: Symbol)(using Context): Tree =
154-
report.log(i"transform break $tree/$arg/$lbl")
155-
labelUsage(lbl) match
156-
case Some(uses: LabelUsage)
157-
if uses.enclMeth == ctx.owner.enclosingMethod
158-
&& !ctx.property(LabelsShadowedByTry).getOrElse(Set.empty).contains(lbl)
159-
=>
160-
uses.otherRefs -= 1
161-
uses.returnRefs += 1
162-
Return(arg, ref(uses.goto)).withSpan(arg.span)
163-
case _ =>
164-
tree
165-
166-
167-
/** Rewrite a break call
168-
*
169-
* break.apply[...](value)(using lbl)
170-
*
191+
/** Rewrite a break with argument `arg` and label `lbl`
171192
* where `lbl` is a label defined in the current method and is not included in
172-
* LabelsShadowedByTry to
193+
* ShadowedLabels to
173194
*
174195
* return[target] arg
175196
*
@@ -179,22 +200,29 @@ class DropBreaks extends MiniPhase:
179200
* binding containing `local` is dropped.
180201
*/
181202
override def transformApply(tree: Apply)(using Context): Tree = tree match
182-
case Apply(Apply(fn, args), (id: Ident) :: Nil) if isBreak(fn.symbol) =>
183-
val arg = (args: @unchecked) match
184-
case arg :: Nil => arg
185-
case Nil => Literal(Constant(())).withSpan(tree.span)
186-
transformBreak(tree, arg, id.symbol)
187-
case _ =>
188-
tree
203+
case Break(lbl, arg) =>
204+
labelUsage(lbl) match
205+
case Some(uses: LabelUsage)
206+
if uses.enclMeth == ctx.owner.enclosingMethod
207+
&& !ctx.property(ShadowedLabels).getOrElse(Set.empty).contains(lbl)
208+
=>
209+
uses.otherRefs -= 1
210+
uses.returnRefs += 1
211+
Return(arg, ref(uses.goto)).withSpan(arg.span)
212+
case _ => tree
213+
case _ => tree
189214

190215
/** If `tree` refers to an enclosing label, increase its non local recount.
191216
* This increase is corrected in `transformInlined` if the reference turns
192217
* out to be part of a BreakThrow to a local, non-shadowed label.
193218
*/
194219
override def transformIdent(tree: Ident)(using Context): Tree =
195-
if tree.symbol.name == nme.local then
196-
for uses <- labelUsage(tree.symbol) do
197-
uses.otherRefs += 1
220+
for uses <- labelUsage(tree.symbol) do
221+
uses.otherRefs += 1
198222
tree
199223

224+
//override def transformReturn(tree: Return)(using Context): Tree =
225+
// if !tree.from.isEmpty && tree.expr.tpe.isExactlyNothing then tree.expr
226+
// else tree
227+
200228
end DropBreaks

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

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import core.DenotTransformers._
66
import core.Symbols._
77
import core.Contexts._
88
import core.Types._
9-
import core.Flags._
109
import core.Decorators._
10+
import core.Flags._
1111
import core.NameKinds.LiftedTreeName
1212
import NonLocalReturns._
1313
import util.Store
@@ -27,7 +27,7 @@ import util.Store
2727
* after an exception, so the fact that values on the stack are 'lost' does not matter
2828
* (copied from https://github.com/scala/scala/pull/922).
2929
*/
30-
class LiftTry extends MiniPhase with IdentityDenotTransformer { thisPhase =>
30+
class LiftTry extends MiniPhase, IdentityDenotTransformer, RecordStackChange { thisPhase =>
3131
import ast.tpd._
3232

3333
override def phaseName: String = LiftTry.name
@@ -40,35 +40,14 @@ class LiftTry extends MiniPhase with IdentityDenotTransformer { thisPhase =>
4040
override def initContext(ctx: FreshContext): Unit =
4141
NeedLift = ctx.addLocation(false)
4242

43-
private def liftingCtx(p: Boolean)(using Context) =
43+
private def liftingCtx(p: Boolean)(using Context): Context =
4444
if (needLift == p) ctx else ctx.fresh.updateStore(NeedLift, p)
4545

46-
override def prepareForApply(tree: Apply)(using Context): Context =
47-
liftingCtx(true)
46+
protected def stackChange(using Context): Context = liftingCtx(true)
4847

4948
override def prepareForDefDef(tree: DefDef)(using Context): Context =
5049
liftingCtx(false)
5150

52-
override def prepareForValDef(tree: ValDef)(using Context): Context =
53-
if !tree.symbol.exists
54-
|| tree.symbol.isSelfSym
55-
|| tree.symbol.owner == ctx.owner.enclosingMethod
56-
&& !tree.symbol.is(Lazy)
57-
// The current implementation wraps initializers of lazy vals in
58-
// calls to an initialize method, which means that a `try` in the
59-
// initializer needs to be lifted. Note that the new scheme proposed
60-
// in #6979 would avoid this.
61-
then ctx
62-
else liftingCtx(true)
63-
64-
override def prepareForAssign(tree: Assign)(using Context): Context =
65-
if (tree.lhs.symbol.maybeOwner == ctx.owner.enclosingMethod) ctx
66-
else liftingCtx(true)
67-
68-
override def prepareForReturn(tree: Return)(using Context): Context =
69-
if (!isNonLocalReturn(tree)) ctx
70-
else liftingCtx(true)
71-
7251
override def prepareForTemplate(tree: Template)(using Context): Context =
7352
liftingCtx(false)
7453

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package dotty.tools.dotc
2+
package transform
3+
4+
import MegaPhase.MiniPhase
5+
import core.*
6+
import Contexts.*
7+
import Flags.Lazy
8+
import NonLocalReturns.isNonLocalReturn
9+
10+
/** A trait shared between LiftTry and DropBreaks.
11+
* Overrides prepare methods for trees that push to the stack before some of their elements
12+
* are evaluated.
13+
*/
14+
trait RecordStackChange extends MiniPhase:
15+
import ast.tpd.*
16+
17+
/** The method to call to record a stack change */
18+
protected def stackChange(using Context): Context
19+
20+
override def prepareForApply(tree: Apply)(using Context): Context =
21+
stackChange
22+
23+
override def prepareForValDef(tree: ValDef)(using Context): Context =
24+
if !tree.symbol.exists
25+
|| tree.symbol.isSelfSym
26+
|| tree.symbol.owner == ctx.owner.enclosingMethod
27+
&& !tree.symbol.is(Lazy)
28+
// The current implementation wraps initializers of lazy vals in
29+
// calls to an initialize method, which means that a `try` in the
30+
// initializer needs to be lifted. Note that the new scheme proposed
31+
// in #6979 would avoid this.
32+
then ctx
33+
else stackChange
34+
35+
override def prepareForAssign(tree: Assign)(using Context): Context =
36+
if (tree.lhs.symbol.maybeOwner == ctx.owner.enclosingMethod) ctx
37+
else stackChange
38+
39+
override def prepareForReturn(tree: Return)(using Context): Context =
40+
if (!isNonLocalReturn(tree)) ctx
41+
else stackChange

tests/run/break-opt.scala

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,21 @@ object breakOpt:
5757
if x == 0 then break()
5858
y
5959

60+
def test7(x0: Int): Option[Int] =
61+
boundary:
62+
Some(
63+
if x0 < 0 then break(None) // no jump possible, since stacksize changes
64+
else x0 + 1
65+
)
66+
67+
68+
def test8(x0: Int): Option[Int] =
69+
boundary:
70+
lazy val x =
71+
if x0 < 0 then break(None) // no jump possible, since stacksize changes
72+
else x0 + 1
73+
Some(x)
74+
6075
@main def Test =
6176
import breakOpt.*
6277
assert(test1(0) == 0)
@@ -68,5 +83,7 @@ object breakOpt:
6883
test4(-1)
6984
assert(test5(2) == 1)
7085
assert(test6(3) == 18)
86+
assert(test7(3) == Some(4))
87+
assert(test7(-3) == None)
7188

7289

0 commit comments

Comments
 (0)