Skip to content

Commit fa7053e

Browse files
committed
made compile errors for forking more specific
1 parent 992bbbd commit fa7053e

File tree

3 files changed

+87
-39
lines changed

3 files changed

+87
-39
lines changed

core/src/main/scala/ox/either.scala

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
package ox
22

3-
import scala.annotation.implicitNotFound
43
import scala.compiletime.{error, summonFrom}
54
import scala.util.boundary
65
import scala.util.boundary.{Label, break}
76
import scala.util.control.NonFatal
87
import scala.util.NotGiven
98

109
object either:
11-
private type NotInForkedScope = NotGiven[Forked]
10+
private type WithoutScopeMarkers[A] = (Supervised, Supervised) ?=> (Forked, Forked) ?=> A
11+
12+
private inline def availableInScope[A]: Boolean =
13+
summonFrom {
14+
case _: NotGiven[A] => false
15+
case _: A => true
16+
}
1217

1318
/** Within an [[either]] block, allows unwrapping [[Either]] and [[Option]] values using [[ok()]]. The result is the right-value of an
1419
* `Either`, or the defined-value of the `Option`. In case a failure is encountered (a left-value of an `Either`, or a `None`), the
@@ -39,19 +44,21 @@ object either:
3944
* v1.ok() ++ v2.ok()
4045
* }}}
4146
*/
42-
inline def apply[E, A](inline body: (Forked, Forked) ?=> Label[Either[E, A]] ?=> A): Either[E, A] =
47+
inline def apply[E, A](inline body: WithoutScopeMarkers[Label[Either[E, A]] ?=> A]): Either[E, A] =
4348
given Forked = ForkedEvidence // just to satisfy the context function
49+
given Supervised = SupervisedEvidence // just to satisfy the context function
4450
boundary(Right(body))
4551

4652
extension [E, A](inline t: Either[E, A])
4753
/** Unwrap the value of the `Either`, short-circuiting the computation to the enclosing [[either]], in case this is a left-value. Can't
4854
* be used in forked blocks without an either block in fork to prevent escaped Breaks that crash forked threads.
4955
*/
50-
transparent inline def ok()(using
51-
@implicitNotFound(
56+
transparent inline def ok(): A =
57+
inline if availableInScope[Forked] && !availableInScope[Supervised] then
58+
error(
5259
"This use of .ok() belongs to either block outside of the fork and is therefore illegal. Use either block inside of the forked block."
53-
) notForked: NotInForkedScope
54-
): A =
60+
)
61+
5562
summonFrom {
5663
case given boundary.Label[Either[E, Nothing]] =>
5764
t match
@@ -66,11 +73,12 @@ object either:
6673
/** Unwrap the value of the `Option`, short-circuiting the computation to the enclosing [[either]], in case this is a `None`. Can't be
6774
* used in forked blocks without an either block in fork to prevent escaped Breaks that crash forked threads.
6875
*/
69-
transparent inline def ok()(using
70-
@implicitNotFound(
76+
transparent inline def ok(): A =
77+
inline if availableInScope[Forked] then
78+
error(
7179
"This use of .ok() belongs to either block outside of the fork and is therefore illegal. Use either block inside of the forked block."
72-
) notForked: NotInForkedScope
73-
): A =
80+
)
81+
7482
summonFrom {
7583
case given boundary.Label[Either[Unit, Nothing]] =>
7684
t match
@@ -87,11 +95,12 @@ object either:
8795
/** Fail the computation by short-circuiting the enclosing [[either]] block with en error of type `E`. Can't be used in forked blocks
8896
* without an either block in fork to prevent escaped Breaks that crash forked threads.
8997
*/
90-
transparent inline def fail()(using
91-
@implicitNotFound(
92-
"This use of .fail() belongs to either block outside of the fork and is therefore illegal. Use either block inside of the forked block."
93-
) notForked: NotInForkedScope
94-
): Nothing =
98+
transparent inline def fail(): Nothing =
99+
inline if availableInScope[Forked] then
100+
error(
101+
"This use of .ok() belongs to either block outside of the fork and is therefore illegal. Use either block inside of the forked block."
102+
)
103+
95104
summonFrom {
96105
case given boundary.Label[Either[E, Nothing]] => break(Left(e))
97106
case given boundary.Label[Either[Nothing, Nothing]] =>

core/src/main/scala/ox/supervised.scala

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import java.util.concurrent.atomic.AtomicInteger
44
import java.util.concurrent.{CompletableFuture, ConcurrentHashMap}
55
import scala.reflect.ClassTag
66

7+
sealed abstract class Supervised
8+
object SupervisedEvidence extends Supervised
9+
710
/** Starts a new concurrency scope, which allows starting forks in the given code block `f`. Forks can be started using [[fork]],
811
* [[forkUser]], [[forkCancellable]] and [[forkUnsupervised]]. All forks are guaranteed to complete before this scope completes.
912
*
@@ -12,19 +15,19 @@ import scala.reflect.ClassTag
1215
* [[fork]] (daemon) don't have to complete successfully for the scope to end.
1316
* - the scope also ends once the first supervised fork (including the `f` main body) fails with an exception
1417
* - when the scope **ends**, all running forks are cancelled
15-
* - the scope **completes** (that is, this method returns) only once all forks started by `f` have completed (either successfully, or with
16-
* an exception)
18+
* - the scope **completes** (that is, this method returns) only once all forks started by `f` have completed (either successfully, or
19+
* with an exception)
1720
*
1821
* Upon successful completion, returns the result of evaluating `f`. Upon failure, the exception that caused the scope to end is re-thrown
1922
* (regardless if the exception was thrown from the main body, or from a fork). Any other exceptions that occur when completing the scope
2023
* are added as suppressed.
2124
*
2225
* @see
23-
* [[unsupervised]] Starts a scope in unsupervised mode
26+
* [[unsupervised]] Starts a scope in unsupervised mode
2427
* @see
25-
* [[supervisedError]] Starts a scope in supervised mode, with the additional ability to report application errors
28+
* [[supervisedError]] Starts a scope in supervised mode, with the additional ability to report application errors
2629
*/
27-
def supervised[T](f: Ox ?=> T): T = supervisedError(NoErrorMode)(f)
30+
def supervised[T](f: Supervised ?=> Ox ?=> T): T = supervisedError(NoErrorMode)(f)
2831

2932
/** Starts a new concurrency scope, which allows starting forks in the given code block `f`. Forks can be started using [[fork]],
3033
* [[forkError]], [[forkUser]], [[forkUserError]], [[forkCancellable]] and [[forkUnsupervised]]. All forks are guaranteed to complete
@@ -36,12 +39,12 @@ def supervised[T](f: Ox ?=> T): T = supervisedError(NoErrorMode)(f)
3639
* @see
3740
* [[forkError]] On details how to use application errors.
3841
*/
39-
def supervisedError[E, F[_], T](em: ErrorMode[E, F])(f: OxError[E, F] ?=> F[T]): F[T] =
42+
def supervisedError[E, F[_], T](em: ErrorMode[E, F])(f: Supervised ?=> OxError[E, F] ?=> F[T]): F[T] =
4043
val s = DefaultSupervisor[E]
4144
val capability = OxError(s, em)
4245
try
4346
val scopeResult = scopedWithCapability(capability) {
44-
val mainBodyFork = forkUserError(using capability)(f(using capability))
47+
val mainBodyFork = forkUserError(using capability)(f(using SupervisedEvidence)(using capability))
4548
val supervisorResult = s.join() // might throw if any supervised fork threw
4649
if supervisorResult == ErrorModeSupervisorResult.Success then
4750
// if no exceptions, the main f-fork must be done by now

core/src/test/scala/ox/ForkEitherInteropTest.scala

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ class ForkEitherInteropTest extends AnyFlatSpec with Matchers {
1212
import ox.either.*
1313
1414
val e1: Either[Throwable, Unit] = Left(Exception("uh oh"))
15-
either {
16-
unsupervised {
15+
unsupervised {
16+
either {
1717
forkUnsupervised {
1818
e1.ok()
1919
}
@@ -28,8 +28,8 @@ class ForkEitherInteropTest extends AnyFlatSpec with Matchers {
2828
import ox.either.*
2929
3030
val e1: Either[Throwable, Unit] = Left(Exception("uh oh"))
31-
either {
32-
unsupervised {
31+
unsupervised {
32+
either {
3333
forkCancellable {
3434
e1.ok()
3535
}
@@ -44,8 +44,8 @@ class ForkEitherInteropTest extends AnyFlatSpec with Matchers {
4444
import ox.either.*
4545
4646
val e1: Either[Throwable, Unit] = Left(Exception("uh oh"))
47-
either {
48-
supervised {
47+
supervised {
48+
either {
4949
fork {
5050
e1.ok()
5151
}
@@ -61,8 +61,8 @@ class ForkEitherInteropTest extends AnyFlatSpec with Matchers {
6161
import ox.either.*
6262
6363
val e1: Either[Throwable, Unit] = Left(Exception("uh oh"))
64-
either {
65-
supervised {
64+
supervised {
65+
either {
6666
forkUser {
6767
e1.ok()
6868
}
@@ -78,8 +78,8 @@ class ForkEitherInteropTest extends AnyFlatSpec with Matchers {
7878
import ox.either.*
7979
8080
val e1: Either[Throwable, Unit] = Left(Exception("uh oh"))
81-
either {
82-
supervisedError(EitherMode[Throwable]) {
81+
supervisedError(EitherMode[Throwable]) {
82+
either {
8383
val f = forkError {
8484
e1.ok()
8585
Right(23)
@@ -98,12 +98,15 @@ class ForkEitherInteropTest extends AnyFlatSpec with Matchers {
9898
import ox.either.*
9999
100100
val e1: Either[Throwable, Unit] = Left(Exception("uh oh"))
101-
either {
102-
supervisedError(EitherMode[String]) {
103-
forkUserError {
101+
supervisedError(EitherMode[String]) {
102+
either {
103+
val f = forkUserError {
104104
e1.ok()
105105
Right(23)
106106
}
107+
108+
f.join()
109+
Left("oops")
107110
}
108111
}"""
109112
}
@@ -115,14 +118,30 @@ class ForkEitherInteropTest extends AnyFlatSpec with Matchers {
115118
import ox.either.*
116119
117120
val e1: Either[Throwable, Unit] = Left(Exception("uh oh"))
118-
either {
119-
supervised {
121+
supervised {
122+
either {
120123
forkAll(Vector(() => e1.ok()))
121124
}
122125
}"""
123126
}
124127
}
125128

129+
"fork/either interop" should "disallow usage of fork in either blocks" in {
130+
assertReceivesForkedScopeError {
131+
"""import ox.*
132+
import ox.either.*
133+
134+
val e1: Either[Throwable, Unit] = Left(Exception("uh oh"))
135+
supervised {
136+
either {
137+
fork {
138+
e1.ok()
139+
}
140+
}
141+
}"""
142+
}
143+
}
144+
126145
"either in fork" should "allow usage of either.ok() combinator" in {
127146
"""
128147
import ox.*
@@ -141,7 +160,7 @@ class ForkEitherInteropTest extends AnyFlatSpec with Matchers {
141160
""" should compile
142161
}
143162

144-
"fork/either interop" should "pass logical tests" in {
163+
"fork/either interop" should "work with either scopes nested in forks" in {
145164
import ox.*
146165
import ox.either.*
147166

@@ -163,6 +182,23 @@ class ForkEitherInteropTest extends AnyFlatSpec with Matchers {
163182
outer.shouldEqual(Right("outer"))
164183
}
165184

185+
"fork/either interop" should "allow ok() combinators if supervised block guarantees either block correctness" in {
186+
import ox.*
187+
import ox.either.*
188+
189+
val sth: Either[Throwable, String] = Right("it works!")
190+
191+
val res = either {
192+
supervised {
193+
fork {
194+
sth.ok()
195+
}.join()
196+
}
197+
}
198+
199+
res.shouldEqual(Right("it works!"))
200+
}
201+
166202
private transparent inline def assertReceivesForkedScopeError(inline code: String): Unit =
167203
val errs = typeCheckErrors(code)
168204
if !errs

0 commit comments

Comments
 (0)