From 315e47fe899f5daaeb221fbee86c7b832351ea56 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Thu, 6 Mar 2025 14:48:37 +0100 Subject: [PATCH 1/3] Add capture checking to `util.boundary` --- library/src/scala/util/boundary.scala | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/library/src/scala/util/boundary.scala b/library/src/scala/util/boundary.scala index 3039fc70be90..83902dd62bc0 100644 --- a/library/src/scala/util/boundary.scala +++ b/library/src/scala/util/boundary.scala @@ -1,4 +1,6 @@ package scala.util + +import language.experimental.captureChecking import scala.annotation.implicitNotFound /** A boundary that can be exited by `break` calls. @@ -27,18 +29,27 @@ import scala.annotation.implicitNotFound * ``` */ object boundary: + import caps.unsafe.unsafeAssumePure /** User code should call `break.apply` instead of throwing this exception * directly. */ - final class Break[T] private[boundary](val label: Label[T], val value: T) + final class Break[T] private[boundary] (private[boundary] val label: Label[T]^{}, val value: T) extends RuntimeException( - /*message*/ null, /*cause*/ null, /*enableSuppression=*/ false, /*writableStackTrace*/ false) + /*message*/ null, /*cause*/ null, /*enableSuppression=*/ false, /*writableStackTrace*/ false): + + /** Compare the given [[Label]] to the one this [[Break]] was constructed with. */ + inline def isSameLabelAs(other: Label[T]) = label eq other + + object Break: + def apply[T](label: Label[T], value: T) = + // SAFETY: labels cannot leak from [[Break]], and is only used for equality comparison. + new Break(label.unsafeAssumePure, value) /** Labels are targets indicating which boundary will be exited by a `break`. */ @implicitNotFound("explain=A Label is generated from an enclosing `scala.util.boundary` call.\nMaybe that boundary is missing?") - final class Label[-T] + final class Label[-T] extends caps.Capability /** Abort current computation and instead return `value` as the value of * the enclosing `boundary` call that created `label`. @@ -60,7 +71,7 @@ object boundary: val local = Label[T]() try body(using local) catch case ex: Break[T] @unchecked => - if ex.label eq local then ex.value + if ex.isSameLabelAs(local) then ex.value else throw ex end boundary From 28c21ee1b7e61c6335bc0bff42a554b1f514b30e Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Tue, 11 Mar 2025 16:48:21 +0100 Subject: [PATCH 2/3] Add capture-checking test for boundary --- tests/neg-custom-args/captures/boundary.check | 27 +++++++++++++++++++ tests/neg-custom-args/captures/boundary.scala | 9 +++++++ 2 files changed, 36 insertions(+) create mode 100644 tests/neg-custom-args/captures/boundary.check create mode 100644 tests/neg-custom-args/captures/boundary.scala diff --git a/tests/neg-custom-args/captures/boundary.check b/tests/neg-custom-args/captures/boundary.check new file mode 100644 index 000000000000..4b51012191ed --- /dev/null +++ b/tests/neg-custom-args/captures/boundary.check @@ -0,0 +1,27 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/boundary.scala:8:31 -------------------------------------- +8 | boundary.break(l2)(using l1) // error + | ^^ + | Found: (local : scala.util.boundary.Label[scala.util.boundary.Label[Unit]]^) + | Required: scala.util.boundary.Label[box scala.util.boundary.Label[Unit]^{local²}]^ + | + | where: local is a value locally defined in object test + | local² is a value locally defined in object test + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/boundary.scala:6:32 -------------------------------------- + 6 | boundary[boundary.Label[Unit]]: l1 ?=> // error + | ^ + | Found: scala.util.boundary.Break[scala.util.boundary.Label[Unit]] @unchecked + | Required: scala.util.boundary.Break[box scala.util.boundary.Label[Unit]^] @unchecked + 7 | boundary[Unit]: l2 ?=> + 8 | boundary.break(l2)(using l1) // error + 9 | ??? + |-------------------------------------------------------------------------------------------------------------------- + |Inline stack trace + |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + |This location contains code that was inlined from boundary.scala:73 +73 | catch case ex: Break[T] @unchecked => + | ^ + -------------------------------------------------------------------------------------------------------------------- + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/boundary.scala b/tests/neg-custom-args/captures/boundary.scala new file mode 100644 index 000000000000..4f2a994d392e --- /dev/null +++ b/tests/neg-custom-args/captures/boundary.scala @@ -0,0 +1,9 @@ +import language.experimental.captureChecking + +import scala.util.boundary + +object test: + boundary[boundary.Label[Unit]]: l1 ?=> // error + boundary[Unit]: l2 ?=> + boundary.break(l2)(using l1) // error + ??? From 0e5040692443ad15dee6a820942755180ce14dca Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Wed, 12 Mar 2025 13:16:54 +0100 Subject: [PATCH 3/3] Patch DropBreaks to correctly detect boundary blocks --- compiler/src/dotty/tools/dotc/core/StdNames.scala | 1 + compiler/src/dotty/tools/dotc/transform/DropBreaks.scala | 4 ++-- library/src/scala/util/boundary.scala | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 90e5544f19af..f05155b941fb 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -532,6 +532,7 @@ object StdNames { val isEmpty: N = "isEmpty" val isInstanceOf_ : N = "isInstanceOf" val isInstanceOfPM: N = "$isInstanceOf$" + val isSameLabelAs : N = "isSameLabelAs" val java: N = "java" val key: N = "key" val label: N = "label" diff --git a/compiler/src/dotty/tools/dotc/transform/DropBreaks.scala b/compiler/src/dotty/tools/dotc/transform/DropBreaks.scala index 5f26a6af6c3c..b1c0080705ad 100644 --- a/compiler/src/dotty/tools/dotc/transform/DropBreaks.scala +++ b/compiler/src/dotty/tools/dotc/transform/DropBreaks.scala @@ -63,10 +63,10 @@ class DropBreaks extends MiniPhase: */ def unapply(expr: Tree)(using Context): Option[(Symbol, Symbol)] = stripTyped(expr) match case If( - Apply(Select(Select(ex: Ident, label), eq), (lbl @ Ident(local)) :: Nil), + Apply(Select(ex: Ident, isSameLabelAs), (lbl @ Ident(local)) :: Nil), Select(ex2: Ident, value), Apply(throww, (ex3: Ident) :: Nil)) - if label == nme.label && eq == nme.eq && local == nme.local && value == nme.value + if isSameLabelAs == nme.isSameLabelAs && local == nme.local && value == nme.value && throww.symbol == defn.throwMethod && ex.symbol == ex2.symbol && ex.symbol == ex3.symbol => Some((ex.symbol, lbl.symbol)) diff --git a/library/src/scala/util/boundary.scala b/library/src/scala/util/boundary.scala index 83902dd62bc0..e72a1a142661 100644 --- a/library/src/scala/util/boundary.scala +++ b/library/src/scala/util/boundary.scala @@ -39,7 +39,7 @@ object boundary: /*message*/ null, /*cause*/ null, /*enableSuppression=*/ false, /*writableStackTrace*/ false): /** Compare the given [[Label]] to the one this [[Break]] was constructed with. */ - inline def isSameLabelAs(other: Label[T]) = label eq other + def isSameLabelAs(other: Label[T]) = label eq other object Break: def apply[T](label: Label[T], value: T) =