Skip to content

Commit c33cf4d

Browse files
committed
Introduce boundary/break control abstraction.
The abstractions are intended to replace the `scala.util.control.Breaks` and `scala.uitl.control.NonLocalReturns`. They are simpler, safer, and more performant, since there is a new MiniPhase `DropBreaks` that rewrites local breaks to labeled returns, i.e. jumps. The abstractions are not experimental. This break from usual procedure is because we need to roll them out fast. Non local returns were just deprecated in 3.2, and we proposed `NonLocalReturns.{returning,throwReturn}` as an alternative. But these APIs were a mistake and should be deprecated. So rolling out boundary/break now counts as a bugfix.
1 parent 6f5bb34 commit c33cf4d

File tree

13 files changed

+495
-1
lines changed

13 files changed

+495
-1
lines changed

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ class Compiler {
8787
new sjs.ExplicitJSClasses, // Make all JS classes explicit (Scala.js only)
8888
new ExplicitOuter, // Add accessors to outer classes from nested ones.
8989
new ExplicitSelf, // Make references to non-trivial self types explicit as casts
90-
new StringInterpolatorOpt) :: // Optimizes raw and s and f string interpolators by rewriting them to string concatenations or formats
90+
new StringInterpolatorOpt, // Optimizes raw and s and f string interpolators by rewriting them to string concatenations or formats
91+
new DropBreaks) :: // Optimize local Break throws by rewriting them
9192
List(new PruneErasedDefs, // Drop erased definitions from scopes and simplify erased expressions
9293
new UninitializedDefs, // Replaces `compiletime.uninitialized` by `_`
9394
new InlinePatterns, // Remove placeholders of inlined patterns

compiler/src/dotty/tools/dotc/ast/TreeInfo.scala

+6
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,12 @@ trait TreeInfo[T <: Untyped] { self: Trees.Instance[T] =>
102102
case _ => tree
103103
}
104104

105+
def stripTyped(tree: Tree): Tree = unsplice(tree) match
106+
case Typed(expr, _) =>
107+
stripTyped(expr)
108+
case _ =>
109+
tree
110+
105111
/** The number of arguments in an application */
106112
def numArgs(tree: Tree): Int = unsplice(tree) match {
107113
case Apply(fn, args) => numArgs(fn) + args.length

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

+3
Original file line numberDiff line numberDiff line change
@@ -968,6 +968,9 @@ class Definitions {
968968
def TupledFunctionClass(using Context): ClassSymbol = TupledFunctionTypeRef.symbol.asClass
969969
def RuntimeTupleFunctionsModule(using Context): Symbol = requiredModule("scala.runtime.TupledFunctions")
970970

971+
@tu lazy val LabelClass: Symbol = requiredClass("scala.util.boundary.Label")
972+
@tu lazy val BreakClass: Symbol = requiredClass("scala.util.boundary.Break")
973+
971974
@tu lazy val CapsModule: Symbol = requiredModule("scala.caps")
972975
@tu lazy val captureRoot: TermSymbol = CapsModule.requiredValue("*")
973976
@tu lazy val CapsUnsafeModule: Symbol = requiredModule("scala.caps.unsafe")

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

+2
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,8 @@ object NameKinds {
325325

326326
val LocalOptInlineLocalObj: UniqueNameKind = new UniqueNameKind("ilo")
327327

328+
val BoundaryName: UniqueNameKind = new UniqueNameKind("boundary")
329+
328330
/** The kind of names of default argument getters */
329331
val DefaultGetterName: NumberedNameKind = new NumberedNameKind(DEFAULTGETTER, "DefaultGetter") {
330332
def mkString(underlying: TermName, info: ThisInfo) = {

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

+3
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,7 @@ object StdNames {
357357
val Flag : N = "Flag"
358358
val Ident: N = "Ident"
359359
val Import: N = "Import"
360+
val Label_this: N = "Label_this"
360361
val Literal: N = "Literal"
361362
val LiteralAnnotArg: N = "LiteralAnnotArg"
362363
val Matchable: N = "Matchable"
@@ -511,10 +512,12 @@ object StdNames {
511512
val isInstanceOfPM: N = "$isInstanceOf$"
512513
val java: N = "java"
513514
val key: N = "key"
515+
val label: N = "label"
514516
val lang: N = "lang"
515517
val language: N = "language"
516518
val length: N = "length"
517519
val lengthCompare: N = "lengthCompare"
520+
val local: N = "local"
518521
val longHash: N = "longHash"
519522
val macroThis : N = "_this"
520523
val macroContext : N = "c"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
package dotty.tools
2+
package dotc
3+
package transform
4+
5+
import ast.{Trees, tpd}
6+
import core.*
7+
import Decorators.*
8+
import NameKinds.BoundaryName
9+
import MegaPhase._
10+
import Types._, Contexts._, Flags._, DenotTransformers._
11+
import Symbols._, StdNames._, Trees._
12+
import util.Property
13+
import Flags.MethodOrLazy
14+
15+
object DropBreaks:
16+
val name: String = "dropBreaks"
17+
val description: String = "replace local Break throws by labeled returns"
18+
19+
/** Usage data and other info associated with a Label symbol.
20+
* @param goto the return-label to use for a labeled return.
21+
* @param enclMeth the enclosing method
22+
*/
23+
class LabelUsage(val goto: TermSymbol, val enclMeth: Symbol):
24+
/** The number of references to associated label that come from labeled returns */
25+
var returnRefs: Int = 0
26+
/** The number of other references to assocated label */
27+
var otherRefs: Int = 0
28+
29+
private val LabelUsages = new Property.Key[Map[Symbol, LabelUsage]]
30+
private val LabelsShadowedByTry = new Property.Key[Set[Symbol]]
31+
32+
/** Rewrites local Break throws to labeled returns.
33+
* Drops `try` statements on breaks if no other uses of its label remain.
34+
* A Break throw with a `Label` created by some enclosing boundary is replaced
35+
* with a labeled return if
36+
*
37+
* - the throw and the boundary are in the same method, and
38+
* - there is no try expression inside the boundary that encloses the throw.
39+
*/
40+
class DropBreaks extends MiniPhase:
41+
import DropBreaks.*
42+
43+
import tpd._
44+
45+
override def phaseName: String = DropBreaks.name
46+
47+
override def description: String = DropBreaks.description
48+
49+
override def runsAfterGroupsOf: Set[String] = Set(ElimByName.name)
50+
// we want by-name parameters to be converted to closures
51+
52+
private object LabelTry:
53+
54+
object GuardedThrow:
55+
56+
/** `(ex, local)` provided `expr` matches
57+
*
58+
* if ex.label.eq(local) then ex.value else throw ex
59+
*/
60+
def unapply(expr: Tree)(using Context): Option[(Symbol, Symbol)] = stripTyped(expr) match
61+
case If(
62+
Apply(Select(Select(ex: Ident, label), eq), (lbl @ Ident(local)) :: Nil),
63+
Select(ex2: Ident, value),
64+
Apply(throww, (ex3: Ident) :: Nil))
65+
if label == nme.label && eq == nme.eq && local == nme.local && value == nme.value
66+
&& throww.symbol == defn.throwMethod
67+
&& ex.symbol == ex2.symbol && ex.symbol == ex3.symbol =>
68+
Some((ex.symbol, lbl.symbol))
69+
case _ =>
70+
None
71+
end GuardedThrow
72+
73+
/** `(local, body)` provided `tree` matches
74+
*
75+
* try body
76+
* catch case ex: Break =>
77+
* if ex.label.eq(local) then ex.value else throw ex
78+
*/
79+
def unapply(tree: Tree)(using Context): Option[(Symbol, Tree)] = stripTyped(tree) match
80+
case Try(body, CaseDef(pat @ Bind(_, Typed(_, tpt)), EmptyTree, GuardedThrow(exc, local)) :: Nil, EmptyTree)
81+
if tpt.tpe.isRef(defn.BreakClass) && exc == pat.symbol =>
82+
Some((local, body))
83+
case _ =>
84+
None
85+
end LabelTry
86+
87+
private object BreakBoundary:
88+
89+
/** `(local, body)` provided `tree` matches
90+
*
91+
* { val local: Label[...] = ...; <LabelTry(local, body)> }
92+
*/
93+
def unapply(tree: Tree)(using Context): Option[(Symbol, Tree)] = tree match
94+
case Block((vd @ ValDef(nme.local, _, _)) :: Nil, LabelTry(caughtAndRhs))
95+
if vd.symbol.info.isRef(defn.LabelClass) && vd.symbol == caughtAndRhs._1 =>
96+
Some(caughtAndRhs)
97+
case _ =>
98+
None
99+
end BreakBoundary
100+
101+
private object BreakThrow:
102+
103+
/** `(local, arg)` provided `tree` matches inlined
104+
*
105+
* val Label_this: ... = local
106+
* throw new Break[...](Label_this, arg)
107+
*/
108+
def unapply(tree: Tree)(using Context): Option[(Symbol, Tree)] = tree match
109+
case Inlined(_,
110+
(vd @ ValDef(label_this1, _, id: Ident)):: Nil,
111+
Apply(throww, Apply(constr, Inlined(_, _, Ident(label_this2)) :: arg :: Nil) :: Nil))
112+
if throww.symbol == defn.throwMethod
113+
&& label_this1 == nme.Label_this && label_this2 == nme.Label_this
114+
&& id.symbol.name == nme.local
115+
&& constr.symbol.isClassConstructor && constr.symbol.owner == defn.BreakClass =>
116+
Some((id.symbol, arg))
117+
case _ =>
118+
None
119+
end BreakThrow
120+
121+
/** The LabelUsage data associated with `lbl` in the current context */
122+
private def labelUsage(lbl: Symbol)(using Context): Option[LabelUsage] =
123+
for
124+
usesMap <- ctx.property(LabelUsages)
125+
uses <- usesMap.get(lbl)
126+
yield
127+
uses
128+
129+
/** If `tree` is a BreakBoundary, associate a fresh `LabelUsage` with its label. */
130+
override def prepareForBlock(tree: Block)(using Context): Context = tree match
131+
case BreakBoundary(label, _) =>
132+
val mapSoFar = ctx.property(LabelUsages).getOrElse(Map.empty)
133+
val goto = newSymbol(ctx.owner, BoundaryName.fresh(), Synthetic | Label, tree.tpe)
134+
ctx.fresh.setProperty(LabelUsages,
135+
mapSoFar.updated(label, LabelUsage(goto, ctx.owner.enclosingMethod)))
136+
case _ =>
137+
ctx
138+
139+
/** If `tree` is not a LabeledTry, include all enclosing labels in the
140+
* `LabelsShadowedByTry` context property. This means that breaks to these
141+
* labels will not be translated to labeled returns in the body of the try.
142+
*/
143+
override def prepareForTry(tree: Try)(using Context): Context = tree match
144+
case LabelTry(_, _) => ctx
145+
case _ => ctx.property(LabelUsages) match
146+
case Some(usesMap) =>
147+
val setSoFar = ctx.property(LabelsShadowedByTry).getOrElse(Set.empty)
148+
ctx.fresh.setProperty(LabelsShadowedByTry, setSoFar ++ usesMap.keysIterator)
149+
case _ => ctx
150+
151+
/** If `tree` is a BreakBoundary, transform it as follows:
152+
* - Wrap it in a labeled block if its label has local uses
153+
* - Drop the try/catch if its label has no other uses
154+
*/
155+
override def transformBlock(tree: Block)(using Context): Tree = tree match
156+
case BreakBoundary(label, expr) =>
157+
val uses = ctx.property(LabelUsages).get(label)
158+
val tree1 =
159+
if uses.otherRefs > 1 then
160+
// one non-local ref is always in the catch clause; this one does not count
161+
tree
162+
else
163+
expr
164+
report.log(i"trans boundary block $label // ${uses.returnRefs}, ${uses.otherRefs}")
165+
if uses.returnRefs > 0 then Labeled(uses.goto, tree1) else tree1
166+
case _ =>
167+
tree
168+
169+
/** Rewrite a BreakThrow
170+
*
171+
* val Label_this: ... = local
172+
* throw new Break[...](Label_this, arg)
173+
*
174+
* where `local` is defined in the current method and is not included in
175+
* LabeldShowedByTry to
176+
*
177+
* return[target] arg
178+
*
179+
* where `target` is the `goto` return label associated with `local`.
180+
* Adjust associated ref counts accordingly. The local refcount is increased
181+
* and the non-local refcount is decreased, since `local` the `Label_this`
182+
* binding containing `local` is dropped.
183+
*/
184+
override def transformInlined(tree: Inlined)(using Context): Tree = tree match
185+
case BreakThrow(lbl, arg) =>
186+
report.log(i"trans inlined $arg, ${arg.source}, ${ctx.outer.source}, ${tree.source}")
187+
labelUsage(lbl) match
188+
case Some(uses: LabelUsage)
189+
if uses.enclMeth == ctx.owner.enclosingMethod
190+
&& !ctx.property(LabelsShadowedByTry).getOrElse(Set.empty).contains(lbl)
191+
=>
192+
uses.otherRefs -= 1
193+
uses.returnRefs += 1
194+
cpy.Inlined(tree)(tree.call, Nil,
195+
inContext(ctx.withSource(tree.expansion.source)) {
196+
Return(arg, ref(uses.goto)).withSpan(arg.span)
197+
})
198+
case _ =>
199+
tree
200+
case _ =>
201+
tree
202+
203+
/** If `tree` refers to an enclosing label, increase its non local recount.
204+
* This increase is corrected in `transformInlined` if the reference turns
205+
* out to be part of a BreakThrow to a local, non-shadowed label.
206+
*/
207+
override def transformIdent(tree: Ident)(using Context): Tree =
208+
if tree.symbol.name == nme.local then
209+
for uses <- labelUsage(tree.symbol) do
210+
uses.otherRefs += 1
211+
tree
212+
213+
end DropBreaks

library/src/scala/util/boundary.scala

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package scala.util
2+
import control.ControlException
3+
4+
/** A boundary that can be exited by `break` calls.
5+
* `boundary` and `break` represent a unified and superior alternative for the
6+
* `scala.util.control.NonLocalReturns` and `scala.util.control.Breaks` APIs.
7+
* The main differences are:
8+
*
9+
* - Unified names: `boundary` to establish a scope, `break` to leave it.
10+
* `break` can optionally return a value.
11+
* - Integration with exceptions. `break`s are logically non-fatal exceptions.
12+
* The `Break` exception class extends `ControlException` which is a regular
13+
* `RuntimeException`, optimized so that stack trace generation is suppressed.
14+
* - Better performance: breaks to enclosing scopes in the same method can
15+
* be rwritten to jumps.
16+
*/
17+
object boundary:
18+
19+
/** User code should call `break.apply` instead of throwing this exception
20+
* directly.
21+
*/
22+
class Break[T](val label: Label[T], val value: T) extends ControlException
23+
24+
/** Labels are targets indicating which boundary will be exited by a `break`.
25+
*/
26+
class Label[T]:
27+
transparent inline def break(value: T): Nothing = throw Break(this, value)
28+
29+
/** Run `body` with freshly generated label as implicit argument. Catch any
30+
* breaks associated with that label and return their results instead of
31+
* `body`'s result.
32+
*/
33+
transparent inline def apply[T <: R, R](inline body: Label[T] ?=> R): R =
34+
val local = Label[T]()
35+
try body(using local)
36+
catch case ex: Break[T] @unchecked =>
37+
if ex.label eq local then ex.value
38+
else throw ex
39+
40+
end boundary

library/src/scala/util/break.scala

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package scala.util
2+
3+
/** This object has two apply methods that abort the current computation
4+
* up to an enclosing `boundary` call.
5+
*/
6+
object break:
7+
8+
/** Abort current computation and instead return `value` as the value of
9+
* the enclosing `boundary` call that created `label`.
10+
*/
11+
transparent inline def apply[T](value: T)(using l: boundary.Label[T]): Nothing =
12+
l.break(value)
13+
14+
/** Abort current computation and instead continue after the `boundary` call that
15+
* created `label`.
16+
*/
17+
transparent inline def apply()(using l: boundary.Label[Unit]): Nothing =
18+
apply(())
19+
20+
end break
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package scala.util.control
2+
3+
/** A parent class for throwable objects intended for flow control.
4+
*
5+
* Instances of ControlException don't record a stacktrace and are therefore
6+
* much more efficient to throw than normal exceptions.
7+
*
8+
* Unlike `ControlThrowable`, `ControlException` is a regular `RuntimeException`
9+
* that is supposed to be handled like any other exception.
10+
*
11+
* Instances of `ControlException` should not normally have a cause.
12+
* Legacy subclasses may set a cause using `initCause`.
13+
*/
14+
abstract class ControlException(message: String | Null) extends Throwable(
15+
message, /*cause*/ null, /*enableSuppression=*/ false, /*writableStackTrace*/ false):
16+
17+
def this() = this(message = null)
18+

tests/run/break-opt.check

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
done
2+
done

0 commit comments

Comments
 (0)