From 5c75b22abc64d60e1943dd6e79d3c7f042c28953 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Tue, 21 Jan 2025 16:52:33 +0100 Subject: [PATCH 1/5] Add original version of gears repo --- .../captures/gears/Async.scala | 435 +++++++++++++ .../captures/gears/AsyncOperations.scala | 53 ++ .../captures/gears/AsyncSupport.scala | 46 ++ .../captures/gears/Cancellable.scala | 48 ++ .../captures/gears/CompletionGroup.scala | 59 ++ .../captures/gears/DefaultSupport.scala | 7 + .../captures/gears/JvmAsyncOperations.scala | 19 + .../captures/gears/Listener.scala | 113 ++++ .../pos-custom-args/captures/gears/PIO.scala | 170 +++++ .../captures/gears/ScalaConverters.scala | 29 + .../captures/gears/Timer.scala | 80 +++ .../captures/gears/VThreadSupport.scala | 136 ++++ .../captures/gears/channels.scala | 457 ++++++++++++++ .../captures/gears/clientAndServerUDP.scala | 35 ++ .../captures/gears/futures.scala | 580 ++++++++++++++++++ .../captures/gears/locking.scala | 48 ++ .../captures/gears/measureTimes.scala | 512 ++++++++++++++++ .../captures/gears/package.scala | 14 + .../captures/gears/readAndWriteFile.scala | 20 + .../captures/gears/readWholeFile.scala | 25 + .../captures/gears/retry.scala | 156 +++++ 21 files changed, 3042 insertions(+) create mode 100644 tests/pos-custom-args/captures/gears/Async.scala create mode 100644 tests/pos-custom-args/captures/gears/AsyncOperations.scala create mode 100644 tests/pos-custom-args/captures/gears/AsyncSupport.scala create mode 100644 tests/pos-custom-args/captures/gears/Cancellable.scala create mode 100644 tests/pos-custom-args/captures/gears/CompletionGroup.scala create mode 100644 tests/pos-custom-args/captures/gears/DefaultSupport.scala create mode 100644 tests/pos-custom-args/captures/gears/JvmAsyncOperations.scala create mode 100644 tests/pos-custom-args/captures/gears/Listener.scala create mode 100644 tests/pos-custom-args/captures/gears/PIO.scala create mode 100644 tests/pos-custom-args/captures/gears/ScalaConverters.scala create mode 100644 tests/pos-custom-args/captures/gears/Timer.scala create mode 100644 tests/pos-custom-args/captures/gears/VThreadSupport.scala create mode 100644 tests/pos-custom-args/captures/gears/channels.scala create mode 100644 tests/pos-custom-args/captures/gears/clientAndServerUDP.scala create mode 100644 tests/pos-custom-args/captures/gears/futures.scala create mode 100644 tests/pos-custom-args/captures/gears/locking.scala create mode 100644 tests/pos-custom-args/captures/gears/measureTimes.scala create mode 100644 tests/pos-custom-args/captures/gears/package.scala create mode 100644 tests/pos-custom-args/captures/gears/readAndWriteFile.scala create mode 100644 tests/pos-custom-args/captures/gears/readWholeFile.scala create mode 100644 tests/pos-custom-args/captures/gears/retry.scala diff --git a/tests/pos-custom-args/captures/gears/Async.scala b/tests/pos-custom-args/captures/gears/Async.scala new file mode 100644 index 000000000000..d0a866f2df00 --- /dev/null +++ b/tests/pos-custom-args/captures/gears/Async.scala @@ -0,0 +1,435 @@ +package gears.async + +import language.experimental.captureChecking + +import gears.async.Listener.NumberedLock +import gears.async.Listener.withLock + +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicLong +import java.util.concurrent.locks.ReentrantLock +import scala.collection.mutable +import scala.util.boundary + +/** The async context: provides the capability to asynchronously [[Async.await await]] for [[Async.Source Source]]s, and + * defines a scope for structured concurrency through a [[CompletionGroup]]. + * + * As both a context and a capability, the idiomatic way of using [[Async]] is to be implicitly passed around + * functions, as an `using` parameter: + * {{{ + * def function()(using Async): T = ??? + * }}} + * + * It is not recommended to store [[Async]] in a class field, since it complicates scoping rules. + * + * @param support + * An implementation of the underlying asynchronous operations (suspend and resume). See [[AsyncSupport]]. + * @param scheduler + * An implementation of a scheduler, for scheduling computation as they are spawned or resumed. See [[Scheduler]]. + * + * @see + * [[Async$.blocking Async.blocking]] for a way to construct an [[Async]] instance. + * @see + * [[Async$.group Async.group]] and [[Future$.apply Future.apply]] for [[Async]]-subscoping operations. + */ +trait Async(using val support: AsyncSupport, val scheduler: support.Scheduler) extends caps.Capability: + /** Waits for completion of source `src` and returns the result. Suspends the computation. + * + * @see + * [[Async.Source.awaitResult]] and [[Async$.await]] for extension methods calling [[Async!.await]] from the source + * itself. + */ + def await[T](src: Async.Source[T]^): T + + /** Returns the cancellation group for this [[Async]] context. */ + def group: CompletionGroup + + /** Returns an [[Async]] context of the same kind as this one, with a new cancellation group. */ + def withGroup(group: CompletionGroup): Async + +object Async: + private class Blocking(val group: CompletionGroup)(using support: AsyncSupport, scheduler: support.Scheduler) + extends Async(using support, scheduler): + private val lock = ReentrantLock() + private val condVar = lock.newCondition() + + /** Wait for completion of async source `src` and return the result */ + override def await[T](src: Async.Source[T]^): T = + src + .poll() + .getOrElse: + var result: Option[T] = None + src.onComplete: + Listener.acceptingListener: (t, _) => + lock.lock() + try + result = Some(t) + condVar.signalAll() + finally lock.unlock() + + lock.lock() + try + while result.isEmpty do condVar.await() + result.get + finally lock.unlock() + + /** An Async of the same kind as this one, with a new cancellation group */ + override def withGroup(group: CompletionGroup): Async = Blocking(group) + + /** Execute asynchronous computation `body` on currently running thread. The thread will suspend when the computation + * waits. + */ + def blocking[T](body: Async.Spawn ?=> T)(using support: AsyncSupport, scheduler: support.Scheduler): T = + group(body)(using Blocking(CompletionGroup.Unlinked)) + + /** Returns the currently executing Async context. Equivalent to `summon[Async]`. */ + inline def current(using async: Async): async.type = async + + /** [[Async.Spawn]] is a special subtype of [[Async]], also capable of spawning runnable [[Future]]s. + * + * Most functions should not take [[Spawn]] as a parameter, unless the function explicitly wants to spawn "dangling" + * runnable [[Future]]s. Instead, functions should take [[Async]] and spawn scoped futures within [[Async.group]]. + */ + opaque type Spawn <: Async = Async + + /** Runs `body` inside a spawnable context where it is allowed to spawn concurrently runnable [[Future]]s. When the + * body returns, all spawned futures are cancelled and waited for. + */ + def group[T](body: Async.Spawn ?=> T)(using Async): T = + withNewCompletionGroup(CompletionGroup().link())(body) + + /** Runs a body within another completion group. When the body returns, the group is cancelled and its completion + * awaited with the `Unlinked` group. + */ + private[async] def withNewCompletionGroup[T](group: CompletionGroup)(body: Async.Spawn ?=> T)(using + async: Async + ): T = + val completionAsync = + if CompletionGroup.Unlinked == async.group + then async + else async.withGroup(CompletionGroup.Unlinked) + + try body(using async.withGroup(group)) + finally + group.cancel() + group.waitCompletion()(using completionAsync) + + /** An asynchronous data source. Sources can be persistent or ephemeral. A persistent source will always pass same + * data to calls of [[Source!.poll]] and [[Source!.onComplete]]. An ephemeral source can pass new data in every call. + * + * @see + * An example of a persistent source is [[gears.async.Future]]. + * @see + * An example of an ephemeral source is [[gears.async.Channel]]. + */ + trait Source[+T]: + /** The unique symbol representing the current source. */ + val symbol: SourceSymbol[T] = SourceSymbol.next + /** Checks whether data is available at present and pass it to `k` if so. Calls to `poll` are always synchronous and + * non-blocking. + * + * The process is as follows: + * - If no data is immediately available, return `false` immediately. + * - If there is data available, attempt to lock `k`. + * - If `k` is no longer available, `true` is returned to signal this source's general availability. + * - If locking `k` succeeds: + * - If data is still available, complete `k` and return true. + * - Otherwise, unlock `k` and return false. + * + * Note that in all cases, a return value of `false` indicates that `k` should be put into `onComplete` to receive + * data in a later point in time. + * + * @return + * Whether poll was able to pass data to `k`. Note that this is regardless of `k` being available to receive the + * data. In most cases, one should pass `k` into [[Source!.onComplete]] if `poll` returns `false`. + */ + def poll(k: Listener[T]^): Boolean + + /** Once data is available, pass it to the listener `k`. `onComplete` is always non-blocking. + * + * Note that `k`'s methods will be executed on the same thread as the [[Source]], usually in sequence. It is hence + * important that the listener itself does not perform expensive operations. + */ + def onComplete(k: Listener[T]^): Unit + + /** Signal that listener `k` is dead (i.e. will always fail to acquire locks from now on), and should be removed + * from `onComplete` queues. + * + * This permits original, (i.e. non-derived) sources like futures or channels to drop the listener from their + * waiting sets. + */ + def dropListener(k: Listener[T]^): Unit + + /** Similar to [[Async.Source!.poll(k:Listener[T])* poll]], but instead of passing in a listener, directly return + * the value `T` if it is available. + */ + def poll(): Option[T] = + var resultOpt: Option[T] = None + poll(Listener.acceptingListener { (x, _) => resultOpt = Some(x) }) + resultOpt + + /** Waits for an item to arrive from the source. Suspends until an item returns. + * + * This is an utility method for direct waiting with `Async`, instead of going through listeners. + */ + final def awaitResult(using ac: Async) = ac.await(this) + end Source + + // an opaque identity for symbols + opaque type SourceSymbol[+T] = Long + private [Async] object SourceSymbol: + private val index = AtomicLong() + inline def next: SourceSymbol[Any] = + index.incrementAndGet() + // ... it can be quickly obtained from any Source + given[T]: scala.Conversion[Source[T], SourceSymbol[T]] = _.symbol + + extension [T](src: Source[scala.util.Try[T]]^) + /** Waits for an item to arrive from the source, then automatically unwraps it. Suspends until an item returns. + * @see + * [[Source!.awaitResult awaitResult]] for non-unwrapping await. + */ + def await(using Async): T = src.awaitResult.get + extension [E, T](src: Source[Either[E, T]]^) + /** Waits for an item to arrive from the source, then automatically unwraps it. Suspends until an item returns. + * @see + * [[Source!.awaitResult awaitResult]] for non-unwrapping await. + */ + inline def await(using inline async: Async) = src.awaitResult.right.get + + /** An original source has a standard definition of [[Source.onComplete onComplete]] in terms of [[Source.poll poll]] + * and [[OriginalSource.addListener addListener]]. + * + * Implementations should be the resource owner to handle listener queue and completion using an object monitor on + * the instance. + */ + abstract class OriginalSource[+T] extends Source[T]: + /** Add `k` to the listener set of this source. */ + protected def addListener(k: Listener[T]^): Unit + + def onComplete(k: Listener[T]^): Unit = synchronized: + if !poll(k) then addListener(k) + + end OriginalSource + + object Source: + /** Create a [[Source]] containing the given values, resolved once for each. + * + * @return + * an ephemeral source of values arriving to listeners in a queue. Once all values are received, attaching a + * listener with [[Source!.onComplete onComplete]] will be a no-op (i.e. the listener will never be called). + */ + def values[T](values: T*) = + import scala.collection.JavaConverters._ + val q = java.util.concurrent.ConcurrentLinkedQueue[T]() + q.addAll(values.asJavaCollection) + new Source[T]: + override def poll(k: Listener[T]^): Boolean = + if q.isEmpty() then false + else if !k.acquireLock() then true + else + val item = q.poll() + if item == null then + k.releaseLock() + false + else + k.complete(item, this) + true + + override def onComplete(k: Listener[T]^): Unit = poll(k) + override def dropListener(k: Listener[T]^): Unit = () + end values + + extension [T](src: Source[T]^) + /** Create a new source that requires the original source to run the given transformation function on every value + * received. + * + * Note that `f` is **always** run on the computation that produces the values from the original source, so this is + * very likely to run **sequentially** and be a performance bottleneck. + * + * @param f + * the transformation function to be run on every value. `f` is run *before* the item is passed to the + * [[Listener]]. + */ + def transformValuesWith[U](f: T => U): Source[U]^{f, src} = + new Source[U]: + val selfSrc = this + def transform(k: Listener[U]^): Listener.ForwardingListener[T]^{k, f} = + new Listener.ForwardingListener[T](selfSrc, k): + val lock = k.lock + def complete(data: T, source: SourceSymbol[T]) = + k.complete(f(data), selfSrc) + + def poll(k: Listener[U]^): Boolean = + src.poll(transform(k)) + def onComplete(k: Listener[U]^): Unit = + src.onComplete(transform(k)) + def dropListener(k: Listener[U]^): Unit = + src.dropListener(transform(k)) + + /** Creates a source that "races" a list of sources. + * + * Listeners attached to this source is resolved with the first item arriving from one of the sources. If multiple + * sources are available at the same time, one of the items will be returned with no priority. Items that are not + * returned are '''not''' consumed from the upstream sources. + * + * @see + * [[raceWithOrigin]] for a race source that also returns the upstream origin of the item. + * @see + * [[Async$.select Async.select]] for a convenient syntax to race sources and awaiting them with [[Async]]. + */ + def race[T](@caps.use sources: Seq[Source[T]^]): Source[T]^{sources*} = raceImpl((v: T, _: SourceSymbol[T]) => v)(sources) + def race[T](s1: Source[T]^): Source[T]^{s1} = race(Seq(s1)) + def race[T](s1: Source[T]^, s2: Source[T]^): Source[T]^{s1, s2} = race(Seq(s1, s2)) + def race[T](s1: Source[T]^, s2: Source[T]^, s3: Source[T]^): Source[T]^{s1, s2, s3} = race(Seq(s1, s2, s3)) + + /** Like [[race]], but the returned value includes a reference to the upstream source that the item came from. + * @see + * [[Async$.select Async.select]] for a convenient syntax to race sources and awaiting them with [[Async]]. + */ + def raceWithOrigin[T](@caps.use sources: (Source[T]^)*): Source[(T, SourceSymbol[T])]^{sources*} = + raceImpl((v: T, src: SourceSymbol[T]) => (v, src))(sources) + + /** Pass first result from any of `sources` to the continuation */ + private def raceImpl[T, U](map: (U, SourceSymbol[U]) -> T)(@caps.use sources: Seq[Source[U]^]): Source[T]^{sources*} = + new Source[T]: + val selfSrc = this + def poll(k: Listener[T]^): Boolean = + val it = sources.iterator + var found = false + + val listener: Listener[U]^{k} = new Listener.ForwardingListener[U](selfSrc, k): + val lock = k.lock + def complete(data: U, source: SourceSymbol[U]) = + k.complete(map(data, source), selfSrc) + end listener + + while it.hasNext && !found do found = it.next.poll(listener) + + found + + def dropAll(l: Listener[U]^) = sources.foreach(_.dropListener(l)) + + def onComplete(k: Listener[T]^): Unit = + val listener: Listener[U]^{k, sources*} = new Listener.ForwardingListener[U](this, k) { + val self = this + inline def lockIsOurs = k.lock == null + val lock = + if k.lock != null then + // if the upstream listener holds a lock already, we can utilize it. + new Listener.ListenerLock: + val selfNumber = k.lock.selfNumber + override def acquire() = + if found then false // already completed + else if !k.lock.acquire() then + if !found && !synchronized { // getAndSet alternative, avoid racing only with self here. + val old = found + found = true + old + } + then dropAll(self) // same as dropListener(k), but avoids an allocation + false + else if found then + k.lock.release() + false + else true + override def release() = k.lock.release() + else + new Listener.ListenerLock with NumberedLock: + val selfNumber: Long = number + def acquire() = + if found then false + else + acquireLock() + if found then + releaseLock() + // no cleanup needed here, since we have done this by an earlier `complete` or `lockNext` + false + else true + def release() = + releaseLock() + + var found = false + + def complete(item: U, src: SourceSymbol[U]) = + found = true + if lockIsOurs then lock.release() + sources.foreach(s => if s.symbol != src then s.dropListener(self)) + k.complete(map(item, src), selfSrc) + } // end listener + + sources.foreach(_.onComplete(listener)) + + def dropListener(k: Listener[T]^): Unit = + val listener = Listener.ForwardingListener.empty(this, k) + sources.foreach(_.dropListener(listener)) + + + /** Cases for handling async sources in a [[select]]. [[SelectCase]] can be constructed by extension methods `handle` + * of [[Source]]. + * + * @see + * [[handle Source.handle]] (and its operator alias [[~~> ~~>]]) + * @see + * [[Async$.select Async.select]] where [[SelectCase]] is used. + */ + trait SelectCase[+T]: + type Src + val src: Source[Src]^ + val f: Src => T + inline final def apply(input: Src) = f(input) + + extension [T](_src: Source[T]^) + /** Attach a handler to `src`, creating a [[SelectCase]]. + * @see + * [[Async$.select Async.select]] where [[SelectCase]] is used. + */ + def handle[U](_f: T => U): SelectCase[U]^{_src, _f} = new SelectCase: + type Src = T + val src = _src + val f = _f + + /** Alias for [[handle]] + * @see + * [[Async$.select Async.select]] where [[SelectCase]] is used. + */ + inline def ~~>[U](_f: T => U): SelectCase[U]^{_src, _f} = _src.handle(_f) + + /** Race a list of sources with the corresponding handler functions, once an item has come back. Like [[race]], + * [[select]] guarantees exactly one of the sources are polled. Unlike [[transformValuesWith]], the handler in + * [[select]] is run in the same async context as the calling context of [[select]]. + * + * @see + * [[handle Source.handle]] (and its operator alias [[~~> ~~>]]) for methods to create [[SelectCase]]s. + * @example + * {{{ + * // Race a channel read with a timeout + * val ch = SyncChannel[Int]() + * // ... + * val timeout = Future(sleep(1500.millis)) + * + * Async.select( + * ch.readSrc.handle: item => + * Some(item * 2), + * timeout ~~> _ => None + * ) + * }}} + */ + def select[T](@caps.use cases: (SelectCase[T]^)*)(using Async) = + val (input, which) = raceWithOrigin(cases.map(_.src)*).awaitResult + val sc = cases.find(_.src.symbol == which).get + sc(input.asInstanceOf[sc.Src]) + + /** Race two sources, wrapping them respectively in [[Left]] and [[Right]] cases. + * @return + * a new [[Source]] that resolves with [[Left]] if `src1` returns an item, [[Right]] if `src2` returns an item, + * whichever comes first. + * @see + * [[race]] and [[select]] for racing more than two sources. + */ + def either[T1, T2](src1: Source[T1]^, src2: Source[T2]^): Source[Either[T1, T2]]^{src1, src2} = + val left = src1.transformValuesWith(Left(_)) + val right = src2.transformValuesWith(Right(_)) + race(left, right) +end Async + diff --git a/tests/pos-custom-args/captures/gears/AsyncOperations.scala b/tests/pos-custom-args/captures/gears/AsyncOperations.scala new file mode 100644 index 000000000000..ab7228d505d0 --- /dev/null +++ b/tests/pos-custom-args/captures/gears/AsyncOperations.scala @@ -0,0 +1,53 @@ +package gears.async + +import language.experimental.captureChecking + +import gears.async.AsyncOperations.sleep + +import java.util.concurrent.TimeoutException +import scala.concurrent.duration.FiniteDuration + +/** Defines fundamental operations that require the support of the scheduler. This is commonly provided alongside with + * the given implementation of [[Scheduler]]. + * @see + * [[Scheduler]] for the definition of the scheduler itself. + */ +trait AsyncOperations: + /** Suspends the current [[Async]] context for at least `millis` milliseconds. */ + def sleep(millis: Long)(using Async): Unit + +object AsyncOperations: + /** Suspends the current [[Async]] context for at least `millis` milliseconds. + * @param millis + * The duration to suspend, in milliseconds. Must be a positive integer. + */ + inline def sleep(millis: Long)(using AsyncOperations, Async): Unit = + summon[AsyncOperations].sleep(millis) + + /** Suspends the current [[Async]] context for `duration`. + * @param duration + * The duration to suspend. Must be positive. + */ + inline def sleep(duration: FiniteDuration)(using AsyncOperations, Async): Unit = + sleep(duration.toMillis) + +/** Runs `op` with a timeout. When the timeout occurs, `op` is cancelled through the given [[Async]] context, and + * [[java.util.concurrent.TimeoutException]] is thrown. + */ +def withTimeout[T](timeout: FiniteDuration)(op: Async ?=> T)(using AsyncOperations, Async): T = + Async.group: spawn ?=> + Async.select( + Future(op).handle(_.get), + Future(sleep(timeout)).handle: _ => + throw TimeoutException() + ) + +/** Runs `op` with a timeout. When the timeout occurs, `op` is cancelled through the given [[Async]] context, and + * [[None]] is returned. + */ +def withTimeoutOption[T](timeout: FiniteDuration)(op: Async ?=> T)(using AsyncOperations, Async): Option[T] = + Async.group: + Async.select( + Future(op).handle(v => Some(v.get)), + Future(sleep(timeout)).handle(_ => None) + ) diff --git a/tests/pos-custom-args/captures/gears/AsyncSupport.scala b/tests/pos-custom-args/captures/gears/AsyncSupport.scala new file mode 100644 index 000000000000..3005f512c401 --- /dev/null +++ b/tests/pos-custom-args/captures/gears/AsyncSupport.scala @@ -0,0 +1,46 @@ +package gears.async + +import language.experimental.captureChecking + +import scala.concurrent.duration._ +import scala.annotation.capability + +/** The delimited continuation suspension interface. Represents a suspended computation asking for a value of type `T` + * to continue (and eventually returning a value of type `R`). + */ +trait Suspension[-T, +R]: + def resume(arg: T): R + +/** Support for suspension capabilities through a delimited continuation interface. */ +trait SuspendSupport: + /** A marker for the "limit" of "delimited continuation". */ + type Label[R, Cap^] <: caps.Capability + + /** The provided suspension type. */ + type Suspension[-T, +R] <: gears.async.Suspension[T, R] + + /** Set the suspension marker as the body's caller, and execute `body`. */ + def boundary[R, Cap^](body: Label[R, Cap] ?->{Cap^} R): R^{Cap^} + + /** Should return immediately if resume is called from within body */ + def suspend[T, R, Cap^](body: Suspension[T, R]^{Cap^} => R^{Cap^})(using Label[R, Cap]): T + +/** Extends [[SuspendSupport]] with "asynchronous" boundary/resume functions, in the presence of a [[Scheduler]] */ +trait AsyncSupport extends SuspendSupport: + type Scheduler <: gears.async.Scheduler + + /** Resume a [[Suspension]] at some point in the future, scheduled by the scheduler. */ + private[async] def resumeAsync[T, R](suspension: Suspension[T, R])(arg: T)(using s: Scheduler): Unit = + s.execute(() => suspension.resume(arg)) + + /** Schedule a computation with the suspension boundary already created. */ + private[async] def scheduleBoundary[Cap^](body: Label[Unit, Cap] ?-> Unit)(using s: Scheduler): Unit = + s.execute(() => boundary[Unit, Cap](body)) + +/** A scheduler implementation, with the ability to execute a computation immediately or after a delay. */ +trait Scheduler: + def execute(body: Runnable): Unit + def schedule(delay: FiniteDuration, body: Runnable): Cancellable + +object AsyncSupport: + inline def apply()(using ac: AsyncSupport) = ac diff --git a/tests/pos-custom-args/captures/gears/Cancellable.scala b/tests/pos-custom-args/captures/gears/Cancellable.scala new file mode 100644 index 000000000000..aa7d70e07aa5 --- /dev/null +++ b/tests/pos-custom-args/captures/gears/Cancellable.scala @@ -0,0 +1,48 @@ +package gears.async + +import language.experimental.captureChecking + +/** A trait for cancellable entities that can be grouped. */ +trait Cancellable: + private var group: CompletionGroup = CompletionGroup.Unlinked + + /** Issue a cancel request */ + def cancel(): Unit + + /** Add this cancellable to the given group after removing it from the previous group in which it was. + */ + def link(group: CompletionGroup): this.type = synchronized: + this.group.drop(this.unsafeAssumePure) + this.group = group + this.group.add(this.unsafeAssumePure) + this + + /** Link this cancellable to the cancellable group of the current async context. + */ + def link()(using async: Async): this.type = + link(async.group) + + /** Unlink this cancellable from its group. */ + def unlink(): this.type = + link(CompletionGroup.Unlinked) + + /** Assume that the [[Cancellable]] is pure, in the case that cancellation does *not* refer to captured resources. + */ + inline def unsafeAssumePure: Cancellable = caps.unsafe.unsafeAssumePure(this) + +end Cancellable + +object Cancellable: + /** A special [[Cancellable]] object that just tracks whether its linked group was cancelled. */ + trait Tracking extends Cancellable: + def isCancelled: Boolean + + object Tracking: + def apply() = new Tracking: + private var cancelled: Boolean = false + + def cancel(): Unit = + cancelled = true + + def isCancelled = cancelled +end Cancellable diff --git a/tests/pos-custom-args/captures/gears/CompletionGroup.scala b/tests/pos-custom-args/captures/gears/CompletionGroup.scala new file mode 100644 index 000000000000..8a2b01daaf6a --- /dev/null +++ b/tests/pos-custom-args/captures/gears/CompletionGroup.scala @@ -0,0 +1,59 @@ +package gears.async +import language.experimental.captureChecking + +import scala.collection.mutable +import scala.util.Success + +import Future.Promise + +/** A group of cancellable objects that are completed together. Cancelling the group means cancelling all its + * uncompleted members. + */ +class CompletionGroup extends Cancellable.Tracking: + private val members: mutable.Set[Cancellable] = mutable.Set() + private var canceled: Boolean = false + private var cancelWait: Option[Promise[Unit]] = None + + /** Cancel all members */ + def cancel(): Unit = + synchronized: + if canceled then Seq.empty + else + canceled = true + members.toSeq + .foreach(_.cancel()) + + /** Wait for all members of the group to complete and unlink themselves. */ + private[async] def waitCompletion()(using Async): Unit = + synchronized: + if members.nonEmpty && cancelWait.isEmpty then cancelWait = Some(Promise()) + cancelWait.foreach(cWait => cWait.await) + unlink() + + /** Add given member to the members set. If the group has already been cancelled, cancels that member immediately. */ + def add(member: Cancellable): Unit = + val alreadyCancelled = synchronized: + members += member // Add this member no matter what since we'll wait for it still + canceled + if alreadyCancelled then member.cancel() + + /** Remove given member from the members set if it is an element */ + def drop(member: Cancellable): Unit = synchronized: + members -= member + if members.isEmpty && cancelWait.isDefined then cancelWait.get.complete(Success(())) + + def isCancelled = canceled + +object CompletionGroup: + + /** A sentinel group of cancellables that are in fact not linked to any real group. `cancel`, `add`, and `drop` do + * nothing when called on this group. + */ + object Unlinked extends CompletionGroup: + override def cancel(): Unit = () + override def waitCompletion()(using Async): Unit = () + override def add(member: Cancellable): Unit = () + override def drop(member: Cancellable): Unit = () + end Unlinked + +end CompletionGroup diff --git a/tests/pos-custom-args/captures/gears/DefaultSupport.scala b/tests/pos-custom-args/captures/gears/DefaultSupport.scala new file mode 100644 index 000000000000..53dbe01eb15f --- /dev/null +++ b/tests/pos-custom-args/captures/gears/DefaultSupport.scala @@ -0,0 +1,7 @@ +package gears.async.default + +import gears.async._ + +given AsyncOperations = JvmAsyncOperations +given VThreadSupport.type = VThreadSupport +given VThreadScheduler.type = VThreadScheduler diff --git a/tests/pos-custom-args/captures/gears/JvmAsyncOperations.scala b/tests/pos-custom-args/captures/gears/JvmAsyncOperations.scala new file mode 100644 index 000000000000..b33ad94e944f --- /dev/null +++ b/tests/pos-custom-args/captures/gears/JvmAsyncOperations.scala @@ -0,0 +1,19 @@ +package gears.async + +import language.experimental.captureChecking + +object JvmAsyncOperations extends AsyncOperations: + override def sleep(millis: Long)(using Async): Unit = + jvmInterruptible(Thread.sleep(millis)) + + /** Runs `fn` in a [[cancellationScope]] where it will be interrupted (as a Java thread) upon cancellation. + * + * Note that `fn` will need to handle both [[java.util.concurrent.CancellationException]] (when performing Gears + * operations such as `.await`) *and* [[java.lang.InterruptedException]], so the intended use case is usually to wrap + * interruptible Java operations, containing `fn` to a narrow scope. + */ + def jvmInterruptible[T](fn: => T)(using Async): T = + val th = Thread.currentThread() + cancellationScope(() => th.interrupt()): + try fn + catch case _: InterruptedException => throw new CancellationException() diff --git a/tests/pos-custom-args/captures/gears/Listener.scala b/tests/pos-custom-args/captures/gears/Listener.scala new file mode 100644 index 000000000000..56edd38728b7 --- /dev/null +++ b/tests/pos-custom-args/captures/gears/Listener.scala @@ -0,0 +1,113 @@ +package gears.async + +import language.experimental.captureChecking + +import gears.async.Async.Source +import gears.async.Async.SourceSymbol + +import java.util.concurrent.locks.ReentrantLock +import scala.annotation.tailrec + +/** A listener, representing an one-time value receiver of an [[Async.Source]]. + * + * Most of the time listeners should involve only calling a receiver function, and can be created by [[Listener.apply]] + * or [[Listener.acceptingListener]]. + * + * However, should the listener want to attempt synchronization, it has to expose some locking-related interfaces. See + * [[Listener.lock]]. + */ +trait Listener[-T]: + import Listener._ + + /** Complete the listener with the given item, from the given source. **If the listener exposes a + * [[Listener.ListenerLock]]**, it is required to acquire this lock before calling [[complete]]. This can also be + * done conveniently with [[completeNow]]. For performance reasons, this condition is usually not checked and will + * end up causing unexpected behavior if not satisfied. + * + * The listener must automatically release its own lock upon completion. + */ + def complete(data: T, source: Async.SourceSymbol[T]): Unit + + /** Represents the exposed API for synchronization on listeners at receiving time. If the listener does not have any + * form of synchronization, [[lock]] should be `null`. + */ + val lock: (Listener.ListenerLock^) | Null + + /** Attempts to acquire locks and then calling [[complete]] with the given item and source. If locking fails, + * [[releaseLock]] is automatically called. + */ + def completeNow(data: T, source: Async.SourceSymbol[T]): Boolean = + if acquireLock() then + this.complete(data, source) + true + else false + + /** Release the listener's lock if it exists. */ + inline final def releaseLock(): Unit = if lock != null then lock.release() + + /** Attempts to lock the listener, if such a lock exists. Succeeds with `true` immediately if [[lock]] is `null`. + */ + inline final def acquireLock(): Boolean = + if lock != null then lock.acquire() else true + +object Listener: + /** A simple [[Listener]] that always accepts the item and sends it to the consumer. */ + /* inline bug */ def acceptingListener[T](consumer: (T, SourceSymbol[T]) => Unit): Listener[T]^{consumer} = + new Listener[T]: + val lock = null + def complete(data: T, source: SourceSymbol[T]) = consumer(data, source) + + /** Returns a simple [[Listener]] that always accepts the item and sends it to the consumer. */ + def apply[T](consumer: (T, SourceSymbol[T]) => Unit): Listener[T]^{consumer} = acceptingListener(consumer) + + /** A special class of listener that forwards the inner listener through the given source. For purposes of + * [[Async.Source.dropListener]] these listeners are compared for equality by the hash of the source and the inner + * listener. + */ + abstract case class ForwardingListener[-T](src: Async.Source[?]^, inner: Listener[?]^) extends Listener[T] + + object ForwardingListener: + /** Creates an empty [[ForwardingListener]] for equality comparison. */ + def empty(src: Async.Source[?]^, inner: Listener[?]^): ForwardingListener[Any]^{src, inner} = new ForwardingListener[Any](src, inner): + val lock = null + override def complete(data: Any, source: SourceSymbol[Any]) = ??? + + /** A lock required by a listener to be acquired before accepting values. Should there be multiple listeners that + * needs to be locked at the same time, they should be locked by larger-number-first. + * + * Some implementations are provided for ease of implementations: + * - For custom listener implementations involving locks: [[NumberedLock]] provides uniquely numbered locks. + * - For source transformation implementations: [[withLock]] is a convenient `.map` for `[[ListenerLock]] | Null`. + */ + trait ListenerLock: + /** The assigned number of the lock. It is required that listeners that can be locked together to have different + * [[selfNumber numbers]]. This requirement can be simply done by using a lock created using [[NumberedLock]]. + */ + val selfNumber: Long + + /** Attempt to lock the current [[ListenerLock]]. Locks are guaranteed to be held as short as possible. + */ + def acquire(): Boolean + + /** Release the current lock. */ + def release(): Unit + end ListenerLock + + /** Maps the lock of a listener, if it exists. */ + inline def withLock[T](listener: Listener[?])(inline body: ListenerLock => T): T | Null = + listener.lock match + case null => null + case l: ListenerLock => body(l) + + /** A helper instance that provides an uniquely numbered mutex. */ + trait NumberedLock: + import NumberedLock._ + + val number = listenerNumber.getAndIncrement() + private val lock0 = ReentrantLock() + + protected def acquireLock() = lock0.lock() + protected def releaseLock() = lock0.unlock() + + object NumberedLock: + private val listenerNumber = java.util.concurrent.atomic.AtomicLong() diff --git a/tests/pos-custom-args/captures/gears/PIO.scala b/tests/pos-custom-args/captures/gears/PIO.scala new file mode 100644 index 000000000000..8f3b8770d165 --- /dev/null +++ b/tests/pos-custom-args/captures/gears/PIO.scala @@ -0,0 +1,170 @@ +package PosixLikeIO + +import language.experimental.captureChecking +import caps.CapSet + +import gears.async.Scheduler +import gears.async.default.given +import gears.async.{Async, Future} + +import java.net.{DatagramPacket, DatagramSocket, InetAddress, InetSocketAddress, ServerSocket, Socket} +import java.nio.ByteBuffer +import java.nio.channels.{AsynchronousFileChannel, CompletionHandler, SocketChannel} +import java.nio.charset.{Charset, StandardCharsets} +import java.nio.file.{Path, StandardOpenOption} +import java.util.concurrent.CancellationException +import scala.Tuple.Union +import scala.concurrent.ExecutionContext +import scala.util.{Failure, Success, Try} + +import Future.Promise + +object File: + extension[Cap^] (resolver: Future.Resolver[Int, Cap]) + private[File] def toCompletionHandler = new CompletionHandler[Integer, ByteBuffer] { + override def completed(result: Integer, attachment: ByteBuffer): Unit = resolver.resolve(result) + override def failed(e: Throwable, attachment: ByteBuffer): Unit = resolver.reject(e) + } + +class File(val path: String) { + import File._ + + private var channel: Option[AsynchronousFileChannel] = None + + def isOpened: Boolean = channel.isDefined && channel.get.isOpen + + def open(options: StandardOpenOption*): File = + assert(channel.isEmpty) + val options1 = if (options.isEmpty) Seq(StandardOpenOption.READ) else options + channel = Some(AsynchronousFileChannel.open(Path.of(path), options1*)) + this + + def close(): Unit = + if (channel.isDefined) + channel.get.close() + channel = None + + def read(buffer: ByteBuffer): Future[Int] = + assert(channel.isDefined) + + Future.withResolver[Int, CapSet]: resolver => + channel.get.read( + buffer, + 0, + buffer, + resolver.toCompletionHandler + ) + + def readString(size: Int, charset: Charset = StandardCharsets.UTF_8): Future[String] = + assert(channel.isDefined) + assert(size >= 0) + + val buffer = ByteBuffer.allocate(size) + Future.withResolver[String, CapSet]: resolver => + channel.get.read( + buffer, + 0, + buffer, + new CompletionHandler[Integer, ByteBuffer] { + override def completed(result: Integer, attachment: ByteBuffer): Unit = + resolver.resolve(charset.decode(attachment.slice(0, result)).toString()) + override def failed(e: Throwable, attachment: ByteBuffer): Unit = resolver.reject(e) + } + ) + + def write(buffer: ByteBuffer): Future[Int] = + assert(channel.isDefined) + + Future.withResolver[Int, CapSet]: resolver => + channel.get.write( + buffer, + 0, + buffer, + resolver.toCompletionHandler + ) + + def writeString(s: String, charset: Charset = StandardCharsets.UTF_8): Future[Int] = + write(ByteBuffer.wrap(s.getBytes(charset))) + + override def finalize(): Unit = { + super.finalize() + if (channel.isDefined) + channel.get.close() + } +} + +class SocketUDP() { + import SocketUDP._ + private var socket: Option[DatagramSocket] = None + + def isOpened: Boolean = socket.isDefined && !socket.get.isClosed + + def bindAndOpen(port: Int): SocketUDP = + assert(socket.isEmpty) + socket = Some(DatagramSocket(port)) + this + + def open(): SocketUDP = + assert(socket.isEmpty) + socket = Some(DatagramSocket()) + this + + def close(): Unit = + if (socket.isDefined) + socket.get.close() + socket = None + + def send(data: ByteBuffer, address: String, port: Int): Future[Unit] = + assert(socket.isDefined) + + Future.withResolver[Unit, CapSet]: resolver => + resolver.spawn: + val packet: DatagramPacket = + new DatagramPacket(data.array(), data.limit(), InetAddress.getByName(address), port) + socket.get.send(packet) + + def receive(): Future[DatagramPacket] = + assert(socket.isDefined) + + Future.withResolver[DatagramPacket, CapSet]: resolver => + resolver.spawn: + val buffer = Array.fill[Byte](10 * 1024)(0) + val packet: DatagramPacket = DatagramPacket(buffer, 10 * 1024) + socket.get.receive(packet) + packet + + override def finalize(): Unit = { + super.finalize() + if (socket.isDefined) + socket.get.close() + } +} + +object SocketUDP: + extension [T, Cap^](resolver: Future.Resolver[T, Cap]) + private[SocketUDP] inline def spawn(body: => T)(using s: Scheduler) = + s.execute(() => + resolver.complete(Try(body).recover { case _: InterruptedException => + throw CancellationException() + }) + ) + +object PIOHelper { + def withFile[T](path: String, options: StandardOpenOption*)(f: File => T): T = + val file = File(path).open(options*) + val ret = f(file) + file.close() + ret + + def withSocketUDP[T]()(f: SocketUDP => T): T = + val s = SocketUDP().open() + val ret = f(s) + s.close() + ret + + def withSocketUDP[T](port: Int)(f: SocketUDP => T): T = + val s = SocketUDP().bindAndOpen(port) + val ret = f(s) + s.close() + ret +} diff --git a/tests/pos-custom-args/captures/gears/ScalaConverters.scala b/tests/pos-custom-args/captures/gears/ScalaConverters.scala new file mode 100644 index 000000000000..fadbf5dde17f --- /dev/null +++ b/tests/pos-custom-args/captures/gears/ScalaConverters.scala @@ -0,0 +1,29 @@ +package gears.async + +import language.experimental.captureChecking + +import scala.concurrent.ExecutionContext +import scala.concurrent.{Future as StdFuture, Promise as StdPromise} +import scala.util.Try + +/** Converters from Gears types to Scala API types and back. */ +object ScalaConverters: + extension [T](fut: StdFuture[T]^) + /** Converts a [[scala.concurrent.Future Scala Future]] into a gears [[Future]]. Requires an + * [[scala.concurrent.ExecutionContext ExecutionContext]], as the job of completing the returned [[Future]] will be + * done through this context. Since [[scala.concurrent.Future Scala Future]] cannot be cancelled, the returned + * [[Future]] will *not* clean up the pending job when cancelled. + */ + def asGears(using ExecutionContext): Future[T]^{fut} = + Future.withResolver[T, caps.CapSet]: resolver => + fut.andThen(result => resolver.complete(result)) + + extension [T](fut: Future[T]^) + /** Converts a gears [[Future]] into a Scala [[scala.concurrent.Future Scala Future]]. Note that if `fut` is + * cancelled, the returned [[scala.concurrent.Future Scala Future]] will also be completed with + * `Failure(CancellationException)`. + */ + def asScala: StdFuture[T]^{fut} = + val p = StdPromise[T]() + fut.onComplete(Listener((res, _) => p.complete(res))) + p.future diff --git a/tests/pos-custom-args/captures/gears/Timer.scala b/tests/pos-custom-args/captures/gears/Timer.scala new file mode 100644 index 000000000000..1f00b0e852a8 --- /dev/null +++ b/tests/pos-custom-args/captures/gears/Timer.scala @@ -0,0 +1,80 @@ +package gears.async + +import language.experimental.captureChecking + +import gears.async.Listener + +import java.util.concurrent.CancellationException +import java.util.concurrent.TimeoutException +import scala.annotation.tailrec +import scala.collection.mutable +import scala.concurrent.TimeoutException +import scala.concurrent.duration._ +import scala.util.{Failure, Success, Try} +import scala.annotation.unchecked.uncheckedCaptures + +import AsyncOperations.sleep +import Future.Promise + + +/** Timer exposes a steady [[Async.Source]] of ticks that happens every `tickDuration` milliseconds. Note that the timer + * does not start ticking until `start` is called (which is a blocking operation, until the timer is cancelled). + * + * You might want to manually `cancel` the timer, so that it gets garbage collected (before the enclosing [[Async]] + * scope ends). + */ +class Timer(tickDuration: Duration) extends Cancellable { + enum TimerEvent: + case Tick + case Cancelled + + private var isCancelled = false + + private object Source extends Async.OriginalSource[this.TimerEvent] { + private val listeners : mutable.Set[(Listener[TimerEvent]^) @uncheckedCaptures] = + mutable.Set[(Listener[TimerEvent]^) @uncheckedCaptures]() + + def tick(): Unit = synchronized { + listeners.filterInPlace(l => + l.completeNow(TimerEvent.Tick, src) + false + ) + } + override def poll(k: Listener[TimerEvent]^): Boolean = + if isCancelled then k.completeNow(TimerEvent.Cancelled, this) + else false // subscribing to a timer always takes you to the next tick + override def dropListener(k: Listener[TimerEvent]^): Unit = listeners -= k + override protected def addListener(k: Listener[TimerEvent]^): Unit = + if isCancelled then k.completeNow(TimerEvent.Cancelled, this) + else + Timer.this.synchronized: + if isCancelled then k.completeNow(TimerEvent.Cancelled, this) + else listeners += k + + def cancel(): Unit = + synchronized { isCancelled = true } + src.synchronized { + Source.listeners.foreach(_.completeNow(TimerEvent.Cancelled, src)) + Source.listeners.clear() + } + } + + /** Ticks of the timer are delivered through this source. Note that ticks are ephemeral. */ + inline final def src: Async.Source[this.TimerEvent] = Source + + /** Starts the timer. Suspends until the timer is cancelled. */ + def run()(using Async, AsyncOperations): Unit = + cancellationScope(this): + loop() + + @tailrec private def loop()(using Async, AsyncOperations): Unit = + if !isCancelled then + try sleep(tickDuration.toMillis) + catch case _: CancellationException => cancel() + if !isCancelled then + Source.tick() + loop() + + override def cancel(): Unit = Source.cancel() +} + diff --git a/tests/pos-custom-args/captures/gears/VThreadSupport.scala b/tests/pos-custom-args/captures/gears/VThreadSupport.scala new file mode 100644 index 000000000000..a287d7627c73 --- /dev/null +++ b/tests/pos-custom-args/captures/gears/VThreadSupport.scala @@ -0,0 +1,136 @@ +package gears.async + +import language.experimental.captureChecking + +import java.lang.invoke.{MethodHandles, VarHandle} +import java.util.concurrent.locks.ReentrantLock +import scala.annotation.unchecked.uncheckedVariance +import scala.concurrent.duration.FiniteDuration +import scala.annotation.constructorOnly +import scala.collection.mutable + +object VThreadScheduler extends Scheduler: + private val VTFactory = Thread + .ofVirtual() + .name("gears.async.VThread-", 0L) + .factory() + + override def execute(body: Runnable): Unit = + val th = VTFactory.newThread(body) + th.start() + () + + private[gears] inline def unsafeExecute(body: Runnable^): Unit = execute(caps.unsafe.unsafeAssumePure(body)) + + override def schedule(delay: FiniteDuration, body: Runnable): Cancellable = + import caps.unsafe.unsafeAssumePure + + val sr = ScheduledRunnable(delay, body) + // SAFETY: should not be able to access body, only for cancellation + sr.unsafeAssumePure: Cancellable + + private final class ScheduledRunnable(delay: FiniteDuration, body: Runnable) extends Cancellable: + @volatile var interruptGuard = true // to avoid interrupting the body + + val th = VTFactory.newThread: () => + try Thread.sleep(delay.toMillis) + catch case e: InterruptedException => () /* we got cancelled, don't propagate */ + if ScheduledRunnable.interruptGuardVar.getAndSet(this, false) then body.run() + th.start() + + final override def cancel(): Unit = + if ScheduledRunnable.interruptGuardVar.getAndSet(this, false) then th.interrupt() + end ScheduledRunnable + + private object ScheduledRunnable: + val interruptGuardVar = + MethodHandles + .lookup() + .in(classOf[ScheduledRunnable]) + .findVarHandle(classOf[ScheduledRunnable], "interruptGuard", classOf[Boolean]) + +object VThreadSupport extends AsyncSupport: + type Scheduler = VThreadScheduler.type + + private final class VThreadLabel[R]() extends caps.Capability: + private var result: Option[R] = None + private val lock = ReentrantLock() + private val cond = lock.newCondition() + + private[VThreadSupport] def clearResult() = + lock.lock() + result = None + lock.unlock() + + private[VThreadSupport] def setResult(data: R) = + lock.lock() + try + result = Some(data) + cond.signalAll() + finally lock.unlock() + + private[VThreadSupport] def waitResult(): R = + lock.lock() + try + while result.isEmpty do cond.await() + result.get + finally lock.unlock() + + override opaque type Label[R, Cap^] <: caps.Capability = VThreadLabel[R] + + // outside boundary: waiting on label + // inside boundary: waiting on suspension + private final class VThreadSuspension[-T, +R](using private[VThreadSupport] val l: VThreadLabel[R] @uncheckedVariance) + extends gears.async.Suspension[T, R]: + private var nextInput: Option[T] = None + private val lock = ReentrantLock() + private val cond = lock.newCondition() + + private[VThreadSupport] def setInput(data: T) = + lock.lock() + try + nextInput = Some(data) + cond.signalAll() + finally lock.unlock() + + // variance is safe because the only caller created the object + private[VThreadSupport] def waitInput(): T @uncheckedVariance = + lock.lock() + try + while nextInput.isEmpty do cond.await() + nextInput.get + finally lock.unlock() + + // normal resume only tells other thread to run again -> resumeAsync may redirect here + override def resume(arg: T): R = + l.clearResult() + setInput(arg) + l.waitResult() + + override opaque type Suspension[-T, +R] <: gears.async.Suspension[T, R] = VThreadSuspension[T, R] + + override def boundary[R, Cap^](body: Label[R, Cap]^ ?->{Cap^} R): R = + val label = VThreadLabel[R]() + VThreadScheduler.unsafeExecute: () => + val result = body(using label) + label.setResult(result) + + label.waitResult() + + override private[async] def resumeAsync[T, R](suspension: Suspension[T, R])(arg: T)(using Scheduler): Unit = + suspension.l.clearResult() + suspension.setInput(arg) + + override def scheduleBoundary[Cap^](body: Label[Unit, Cap] ?-> Unit)(using Scheduler): Unit = + VThreadScheduler.execute: () => + val label = VThreadLabel[Unit]() + body(using label) + + override def suspend[T, R, Cap^](body: Suspension[T, R]^{Cap^} => R^{Cap^})(using l: Label[R, Cap]^): T = + val sus = new VThreadSuspension[T, R](using caps.unsafe.unsafeAssumePure(l)) + val res = body(sus) + l.setResult( + // SAFETY: will only be stored and returned by the Suspension resumption mechanism + caps.unsafe.unsafeAssumePure(res) + ) + sus.waitInput() diff --git a/tests/pos-custom-args/captures/gears/channels.scala b/tests/pos-custom-args/captures/gears/channels.scala new file mode 100644 index 000000000000..adb2d69377c3 --- /dev/null +++ b/tests/pos-custom-args/captures/gears/channels.scala @@ -0,0 +1,457 @@ +package gears.async + +import language.experimental.captureChecking + +import gears.async.Async.Source +import gears.async.Listener.acceptingListener +import gears.async.listeners.lockBoth + +import scala.annotation.unchecked.uncheckedCaptures +import scala.collection.mutable +import scala.util.control.Breaks.{break, breakable} +import scala.util.{Failure, Success, Try} + +import Channel.{Closed, Res} +import mutable.{ArrayBuffer, ListBuffer} + +/** The part of a channel one can send values to. Blocking behavior depends on the implementation. + */ +trait SendableChannel[-T]: + /** Create an [[Async.Source]] representing the send action of value `x`. + * + * Note that *each* listener attached to and accepting an [[Unit]] value corresponds to `x` being sent once. + * + * To create an [[Async.Source]] that sends the item exactly once regardless of listeners attached, wrap the [[send]] + * operation inside a [[gears.async.Future]]: + * {{{ + * val sendOnce = Future(ch.send(x)) + * }}} + * + * @return + * an [[Async.Source]] that resolves with `Right(())` when `x` is sent to the channel, or `Left(Closed)` if the + * channel is already closed. This source will perform a send operation every time a listener is attached to it, or + * every time it is [[Async$.await]]ed on. + */ + def sendSource(x: T): Async.Source[Res[Unit]] + + /** Send `x` over the channel, suspending until the item has been sent or, if the channel is buffered, queued. + * @throws ChannelClosedException + * if the channel was closed. + */ + def send(x: T)(using Async): Unit = sendSource(x).awaitResult match + case Right(_) => () + case Left(_) => throw ChannelClosedException() +end SendableChannel + +/** The part of a channel one can read values from. Blocking behavior depends on the implementation. + */ +trait ReadableChannel[+T]: + /** An [[Async.Source]] corresponding to items being sent over the channel. Note that *each* listener attached to and + * accepting a [[Right]] value corresponds to one value received over the channel. + * + * To create an [[Async.Source]] that reads *exactly one* item regardless of listeners attached, wrap the [[read]] + * operation inside a [[gears.async.Future]]. + * {{{ + * val readOnce = Future(ch.read(x)) + * }}} + */ + val readSource: Async.Source[Res[T]] + + /** Read an item from the channel, suspending until the item has been received. Returns + * `Failure(ChannelClosedException)` if the channel was closed. + */ + def read()(using Async): Res[T] = readSource.awaitResult +end ReadableChannel + +/** A generic channel that can be sent to, received from and closed. + * @example + * {{{ + * // send from one Future, read from multiple + * val ch = SyncChannel[Int]() + * val sender = Future: + * for i <- 1 to 20 do + * ch.send(i) + * ch.close() + * val receivers = (1 to 5).map: n => + * Future: + * boundary: + * while true: + * ch.read() match + * case Right(k) => println(s"Receiver $n got: $k") + * case Left(_) => boundary.break() + * + * receivers.awaitAll + * }}} + * @see + * [[SyncChannel]], [[BufferedChannel]] and [[UnboundedChannel]] for channel implementations. + */ +trait Channel[T] extends SendableChannel[T], ReadableChannel[T], java.io.Closeable: + /** Restrict this channel to send-only. */ + inline final def asSendable: SendableChannel[T] = this + + /** Restrict this channel to read-only. */ + inline final def asReadable: ReadableChannel[T] = this + + /** Restrict this channel to close-only. */ + inline final def asCloseable: java.io.Closeable = this + + protected type Reader = Listener[Res[T]] + protected type Sender = Listener[Res[Unit]] +end Channel + +/** Synchronous channels, sometimes called rendez-vous channels, has the following semantics: + * - [[Channel.send send]] to an unclosed channel blocks until a [[Channel.read read]] listener commits to receiving + * the value (via successfully locking). + * + * See [[SyncChannel$.apply]] for creation of synchronous channels. + */ +trait SyncChannel[T] extends Channel[T] + +/** Buffered channels are channels with an internal value buffer (represented internally as an array with positive + * size). They have the following semantics: + * - [[Channel.send send]], when the buffer is not full, appends the value to the buffer and success immediately. + * - [[Channel.send send]], when the buffer is full, suspends until some buffer slot is freed and assigned to this + * sender. + * + * See [[BufferedChannel$.apply]] for creation of buffered channels. + */ +trait BufferedChannel[T] extends Channel[T] + +/** Unbounded channels are buffered channels that do not have an upper bound on the number of items in the channel. In + * other words, the buffer is treated as never being full and will expand as needed. + * + * See [[UnboundedChannel$.apply]] for creation of unbounded channels. + */ +trait UnboundedChannel[T] extends BufferedChannel[T]: + /** Sends the item immediately. + * + * @throws ChannelClosedException + * if the channel is closed. + */ + def sendImmediately(x: T): Unit + +/** The exception raised by [[Channel.send send]] (or [[UnboundedChannel.sendImmediately]]) on a closed [[Channel]]. + * + * It is also returned wrapped in `Failure` when reading form a closed channel. [[ChannelMultiplexer]] sends + * `Failure(ChannelClosedException)` to all subscribers when it receives a `close()` signal. + */ +class ChannelClosedException extends Exception + +object SyncChannel: + /** Creates a new [[SyncChannel]]. */ + def apply[T](): SyncChannel[T] = Impl() + + private class Impl[T] extends Channel.Impl[T] with SyncChannel[T]: + override def pollRead(r: Reader^): Boolean = synchronized: + // match reader with buffer of senders + checkClosed(readSource, r) || cells.matchReader(r) + + override def pollSend(src: CanSend, s: Sender^): Boolean = synchronized: + // match reader with buffer of senders + checkClosed(src, s) || cells.matchSender(src, s) + end Impl +end SyncChannel + +object BufferedChannel: + /** Create a new buffered channel with the given buffer size. */ + def apply[T](size: Int = 10): BufferedChannel[T] = Impl(size) + + private class Impl[T](size: Int) extends Channel.Impl[T] with BufferedChannel[T]: + require(size > 0, "Buffered channels must have a buffer size greater than 0") + val buf = new mutable.Queue[T](size) + + // Match a reader -> check space in buf -> fail + override def pollSend(src: CanSend, s: Sender^): Boolean = synchronized: + checkClosed(src, s) || cells.matchSender(src, s) || senderToBuf(src, s) + + // Check space in buf -> fail + // If we can pop from buf -> try to feed a sender + override def pollRead(r: Reader^): Boolean = synchronized: + if checkClosed(readSource, r) then true + else if !buf.isEmpty then + if r.completeNow(Right(buf.head), readSource) then + buf.dequeue() + if cells.hasSender then + val (src, s) = cells.nextSender + cells.dequeue() // buf always has space available after dequeue + senderToBuf(src, s) + true + else false + + // Try to add a sender to the buffer + def senderToBuf(src: CanSend, s: Sender^): Boolean = + if buf.size < size then + if s.completeNow(Right(()), src) then buf += src.item + true + else false + end Impl +end BufferedChannel + +object UnboundedChannel: + /** Creates a new [[UnboundedChannel]]. */ + def apply[T](): UnboundedChannel[T] = Impl[T]() + + private final class Impl[T]() extends Channel.Impl[T] with UnboundedChannel[T] { + val buf = new mutable.Queue[T]() + + override def sendImmediately(x: T): Unit = + var result: SendResult = Left(Closed) + pollSend(CanSend(x), acceptingListener((r, _) => result = r)) + if result.isLeft then throw ChannelClosedException() + + override def pollRead(r: Reader^): Boolean = synchronized: + if checkClosed(readSource, r) then true + else if !buf.isEmpty then + if r.completeNow(Right(buf.head), readSource) then + // there are never senders in the cells + buf.dequeue() + true + else false + + override def pollSend(src: CanSend, s: Sender^): Boolean = synchronized: + if checkClosed(src, s) || cells.matchSender(src, s) then true + else if s.completeNow(Right(()), src) then + buf += src.item + true + else false + } +end UnboundedChannel + +object Channel: + /** Signals that the channel is closed. */ + case object Closed + + type Closed = Closed.type + + private[async] type Res[T] = Either[Closed, T] + + private[async] abstract class Impl[T] extends Channel[T]: + protected type ReadResult = Res[T] + protected type SendResult = Res[Unit] + + var isClosed = false + val cells = CellBuf() + // Poll a reader, returning false if it should be put into queue + def pollRead(r: Reader^): Boolean + // Poll a reader, returning false if it should be put into queue + def pollSend(src: CanSend, s: Sender^): Boolean + + protected final def checkClosed[T](src: Async.Source[Res[T]], l: Listener[Res[T]]^): Boolean = + if isClosed then + l.completeNow(Left(Closed), src) + true + else false + + override val readSource: Source[ReadResult] = new Source { + override def poll(k: Reader^): Boolean = pollRead(k) + override def onComplete(k: Reader^): Unit = Impl.this.synchronized: + if !pollRead(k) then cells.addReader(k) + override def dropListener(k: Reader^): Unit = Impl.this.synchronized: + if !isClosed then cells.dropReader(k) + } + override final def sendSource(x: T): Source[SendResult] = CanSend(x) + override final def close(): Unit = + synchronized: + if !isClosed then + isClosed = true + cells.cancel() + + /** Complete a pair of locked sender and reader. */ + protected final def complete(src: CanSend, reader: Listener[ReadResult]^, sender: Listener[SendResult]^) = + reader.complete(Right(src.item), readSource) + sender.complete(Right(()), src) + + // Not a case class because equality should be referential, as otherwise + // dependent on a (possibly odd) equality of T. Users do not expect that + // cancelling a send of a given item might in fact cancel that of an equal one. + protected final class CanSend(val item: T) extends Source[SendResult] { + override def poll(k: Listener[SendResult]^): Boolean = pollSend(this, k) + override def onComplete(k: Listener[SendResult]^): Unit = Impl.this.synchronized: + if !pollSend(this, k) then cells.addSender(this, k) + override def dropListener(k: Listener[SendResult]^): Unit = Impl.this.synchronized: + if !isClosed then cells.dropSender(this, k) + } + + /** CellBuf is a queue of cells, which consists of a sleeping sender or reader. The queue always guarantees that + * there are *only* all readers or all senders. It must be externally synchronized. + */ + private[async] class CellBuf(): + import caps.unsafe.unsafeAssumePure // very unsafe WIP + + type Cell = Reader | (CanSend, Sender) + // reader == 0 || sender == 0 always + private var reader = 0 + private var sender = 0 + + private val pending = mutable.Queue[Cell]() + + /* Boring push/pop methods */ + + def hasReader = reader > 0 + def hasSender = sender > 0 + def nextReader = + require(reader > 0) + pending.head.asInstanceOf[Reader] + def nextSender = + require(sender > 0) + pending.head.asInstanceOf[(CanSend, Sender)] + def dequeue() = + pending.dequeue() + if reader > 0 then reader -= 1 else sender -= 1 + def addReader(r: Reader^): this.type = + require(sender == 0) + reader += 1 + pending.enqueue(r.unsafeAssumePure) + this + def addSender(src: CanSend, s: Sender^): this.type = + require(reader == 0) + sender += 1 + pending.enqueue((src, s.unsafeAssumePure)) + this + def dropReader(r: Reader^): this.type = + if reader > 0 then if pending.removeFirst(_ == r).isDefined then reader -= 1 + this + def dropSender(src: CanSend, s: Sender^): this.type = + if sender > 0 then if pending.removeFirst(_ == (src, s)).isDefined then sender -= 1 + this + + /** Match a possible reader to a queue of senders: try to go through the queue with lock pairing, stopping when + * finding a good pair. + */ + def matchReader(r: Reader^): Boolean = + while hasSender do + val (src, s) = nextSender + tryComplete(src, s)(r) match + case () => return true + case listener if listener == r => return true + case _ => dequeue() // drop gone sender from queue + false + + /** Match a possible sender to a queue of readers: try to go through the queue with lock pairing, stopping when + * finding a good pair. + */ + def matchSender(src: CanSend, s: Sender^): Boolean = + while hasReader do + val r = nextReader + tryComplete(src, s)(r) match + case () => return true + case listener if listener == s => return true + case _ => dequeue() // drop gone reader from queue + false + + private inline def tryComplete(src: CanSend, s: Sender^)(r: Reader^): s.type | r.type | Unit = + lockBoth(r, s) match + case true => + Impl.this.complete(src, r, s) + dequeue() // drop completed reader/sender from queue + () + case listener: (r.type | s.type) => listener + + def cancel() = + pending.foreach { + case (src, s) => s.completeNow(Left(Closed), src) + case r: Reader => r.completeNow(Left(Closed), readSource) + } + pending.clear() + reader = 0 + sender = 0 + end CellBuf + end Impl +end Channel + +/** Channel multiplexer is an object where one can register publisher and subscriber channels. When it is run, it + * continuously races the set of publishers and once it reads a value, it sends a copy to each subscriber. + * + * When a publisher or subscriber channel is closed, it will be removed from the multiplexer's set. + * + * For an unchanging set of publishers and subscribers and assuming that the multiplexer is the only reader of the + * publisher channels, every subscriber will receive the same set of messages, in the same order and it will be exactly + * all messages sent by the publishers. The only guarantee on the order of the values the subscribers see is that + * values from the same publisher will arrive in order. + * + * Channel multiplexer can also be closed, in that case all subscribers will receive `Failure(ChannelClosedException)` + * but no attempt at closing either publishers or subscribers will be made. + */ +trait ChannelMultiplexer[T] extends java.io.Closeable: + /** Run the multiplexer. Returns after this multiplexer has been cancelled. */ + def run()(using Async): Unit + + def addPublisher(c: ReadableChannel[T]): Unit + def removePublisher(c: ReadableChannel[T]): Unit + + def addSubscriber(c: SendableChannel[Try[T]]): Unit + def removeSubscriber(c: SendableChannel[Try[T]]): Unit +end ChannelMultiplexer + +object ChannelMultiplexer: + private enum Message: + case Quit, Refresh + + def apply[T](): ChannelMultiplexer[T] = Impl[T]() + + private class Impl[T] extends ChannelMultiplexer[T]: + private var isClosed = false + private val publishers = ArrayBuffer[ReadableChannel[T]]() + private val subscribers = ArrayBuffer[SendableChannel[Try[T]]]() + private val infoChannel = UnboundedChannel[Message]() + + def run()(using Async) = { + var shouldTerminate = false + while (!shouldTerminate) { + val publishersCopy = synchronized(publishers.toSeq) + + val pubCases = + publishersCopy.map: pub => + pub.readSource.handle: + case Right(v) => + val subscribersCopy = synchronized(subscribers.toList) + var c = 0 + for (s <- subscribersCopy) { + c += 1 + try s.send(Success(v)) + catch + case closedEx: ChannelClosedException => + removeSubscriber(s) + } + case Left(_) => removePublisher(pub) + + val infoCase = infoChannel.readSource.handle: + case Left(_) | Right(Message.Quit) => + val subscribersCopy = synchronized(subscribers.toList) + for (s <- subscribersCopy) s.send(Failure(ChannelClosedException())) + shouldTerminate = true + case Right(Message.Refresh) => () + + Async.select((infoCase +: pubCases)*) + } + } + + override def close(): Unit = + val shouldStop = synchronized: + if !isClosed then + isClosed = true + true + else false + if shouldStop then infoChannel.sendImmediately(Message.Quit) + + override def removePublisher(c: ReadableChannel[T]): Unit = + synchronized: + if isClosed then throw ChannelClosedException() + publishers -= c + infoChannel.sendImmediately(Message.Refresh) + + override def removeSubscriber(c: SendableChannel[Try[T]]): Unit = synchronized: + if isClosed then throw ChannelClosedException() + subscribers -= c + + override def addPublisher(c: ReadableChannel[T]): Unit = + synchronized: + if isClosed then throw ChannelClosedException() + publishers += c + infoChannel.sendImmediately(Message.Refresh) + + override def addSubscriber(c: SendableChannel[Try[T]]): Unit = synchronized: + if isClosed then throw ChannelClosedException() + subscribers += c + +end ChannelMultiplexer diff --git a/tests/pos-custom-args/captures/gears/clientAndServerUDP.scala b/tests/pos-custom-args/captures/gears/clientAndServerUDP.scala new file mode 100644 index 000000000000..a1c30b41fe52 --- /dev/null +++ b/tests/pos-custom-args/captures/gears/clientAndServerUDP.scala @@ -0,0 +1,35 @@ +package PosixLikeIO.examples + +import gears.async.AsyncOperations.* +import gears.async.default.given +import gears.async.{Async, Future} + +import java.net.DatagramPacket +import java.nio.ByteBuffer +import java.nio.file.StandardOpenOption +import scala.concurrent.ExecutionContext + +import PosixLikeIO.{PIOHelper, SocketUDP} + +@main def clientAndServerUDP(): Unit = + given ExecutionContext = ExecutionContext.global + Async.blocking: + val server = Future: + PIOHelper.withSocketUDP(8134): serverSocket => + val got: DatagramPacket = serverSocket.receive().awaitResult.get + val messageReceived = String(got.getData.slice(0, got.getLength), "UTF-8") + val responseMessage = (messageReceived.toInt + 1).toString.getBytes + serverSocket.send(ByteBuffer.wrap(responseMessage), got.getAddress.toString.substring(1), got.getPort) + sleep(50) + + def client(value: Int): Future[Unit] = + Future: + PIOHelper.withSocketUDP(): clientSocket => + val data: Array[Byte] = value.toString.getBytes + clientSocket.send(ByteBuffer.wrap(data), "localhost", 8134).awaitResult.get + val responseDatagram = clientSocket.receive().awaitResult.get + val messageReceived = String(responseDatagram.getData.slice(0, responseDatagram.getLength), "UTF-8").toInt + println("Sent " + value.toString + " and got " + messageReceived.toString + " in return.") + + client(100).await + server.await diff --git a/tests/pos-custom-args/captures/gears/futures.scala b/tests/pos-custom-args/captures/gears/futures.scala new file mode 100644 index 000000000000..67315bf01f6c --- /dev/null +++ b/tests/pos-custom-args/captures/gears/futures.scala @@ -0,0 +1,580 @@ +package gears.async + +import language.experimental.captureChecking + +import java.util.concurrent.CancellationException +import java.util.concurrent.atomic.AtomicBoolean +import scala.annotation.tailrec +import scala.annotation.unchecked.uncheckedVariance +import scala.collection.mutable +import scala.compiletime.uninitialized +import scala.util +import scala.util.control.NonFatal +import scala.util.{Failure, Success, Try} +import gears.async.Async.SourceSymbol + +/** Futures are [[Async.Source Source]]s that has the following properties: + * - They represent a single value: Once resolved, [[Async.await await]]-ing on a [[Future]] should always return the + * same value. + * - They can potentially be cancelled, via [[Cancellable.cancel the cancel method]]. + * + * There are two kinds of futures, active and passive. + * - '''Active''' futures are ones that are spawned with [[Future.apply]] and [[Task.start]]. They require the + * [[Async.Spawn]] context, and run on their own (as long as the [[Async.Spawn]] scope has not ended). Active + * futures represent concurrent computations within Gear's structured concurrency tree. Idiomatic Gears code should + * ''never'' return active futures. Should a function be async (i.e. takes an [[Async]] context parameter), they + * should return values or throw exceptions directly. + * - '''Passive''' futures are ones that are created by [[Future.Promise]] (through + * [[Future.Promise.asFuture asFuture]]) and [[Future.withResolver]]. They represent yet-arrived values coming from + * ''outside'' of Gear's structured concurrency tree (for example, from network or the file system, or even from + * another concurrency system like [[scala.concurrent.Future Scala standard library futures]]). Idiomatic Gears + * libraries should return this kind of [[Future]] if deemed neccessary, but functions returning passive futures + * should ''not'' take an [[Async]] context. + * + * @see + * [[Future.apply]] and [[Task.start]] for creating active futures. + * @see + * [[Future.Promise]] and [[Future.withResolver]] for creating passive futures. + * @see + * [[Future.awaitAll]], [[Future.awaitFirst]] and [[Future.Collector]] for tools to work with multiple futures. + * @see + * [[ScalaConverters.asGears]] and [[ScalaConverters.asScala]] for converting between Scala futures and Gears + * futures. + */ +trait Future[+T] extends Async.OriginalSource[Try[T]], Cancellable + +object Future: + /** A future that is completed explicitly by calling its `complete` method. There are three public implementations + * + * - RunnableFuture: Completion is done by running a block of code + * - Promise.apply: Completion is done by external request. + * - withResolver: Completion is done by external request set up from a block of code. + */ + private class CoreFuture[+T] extends Future[T]: + + @volatile protected var hasCompleted: Boolean = false + protected var cancelRequest = AtomicBoolean(false) + private var result: Try[T] = uninitialized // guaranteed to be set if hasCompleted = true + private val waiting: mutable.Set[Listener[Try[T]]] = mutable.Set() + + // Async.Source method implementations + + import caps.unsafe.unsafeAssumePure + + def poll(k: Listener[Try[T]]^): Boolean = + if hasCompleted then + k.completeNow(result, this) + true + else false + + def addListener(k: Listener[Try[T]]^): Unit = synchronized: + waiting += k.unsafeAssumePure + + def dropListener(k: Listener[Try[T]]^): Unit = synchronized: + waiting -= k.unsafeAssumePure + + // Cancellable method implementations + + def cancel(): Unit = + setCancelled() + + override def link(group: CompletionGroup): this.type = + // though hasCompleted is accessible without "synchronized", + // we want it not to be run while the future was trying to complete. + synchronized: + if !hasCompleted || group == CompletionGroup.Unlinked then super.link(group) + else this + + /** Sets the cancellation state and returns `true` if the future has not been completed and cancelled before. */ + protected final def setCancelled(): Boolean = + !hasCompleted && cancelRequest.compareAndSet(false, true) + + /** Complete future with result. If future was cancelled in the meantime, return a CancellationException failure + * instead. Note: @uncheckedVariance is safe here since `complete` is called from only two places: + * - from the initializer of RunnableFuture, where we are sure that `T` is exactly the type with which the future + * was created, and + * - from Promise.complete, where we are sure the type `T` is exactly the type with which the future was created + * since `Promise` is invariant. + */ + private[Future] def complete(result: Try[T] @uncheckedVariance): Unit = + val toNotify = synchronized: + if hasCompleted then Nil + else + this.result = result + hasCompleted = true + val ws = waiting.toList + waiting.clear() + unlink() + ws + for listener <- toNotify do listener.completeNow(result, this) + + end CoreFuture + + /** A future that is completed by evaluating `body` as a separate asynchronous operation in the given `scheduler` + */ + private class RunnableFuture[+T](body: Async.Spawn ?-> T)(using ac: Async) extends CoreFuture[T]: + private given acSupport: ac.support.type = ac.support + private given acScheduler: ac.support.Scheduler = ac.scheduler + /** RunnableFuture maintains its own inner [[CompletionGroup]], that is separated from the provided Async + * instance's. When the future is cancelled, we only cancel this CompletionGroup. This effectively means any + * `.await` operations within the future is cancelled *only if they link into this group*. The future body run with + * this inner group by default, but it can always opt-out (e.g. with [[uninterruptible]]). + */ + private var innerGroup: CompletionGroup = CompletionGroup() + + private def checkCancellation(): Unit = + if cancelRequest.get() then throw new CancellationException() + + private class FutureAsync[Cap^](val group: CompletionGroup)(using label: acSupport.Label[Unit, Cap]) + extends Async(using acSupport, acScheduler): + /** Await a source first by polling it, and, if that fails, by suspending in a onComplete call. + */ + override def await[U](src: Async.Source[U]^): U = + class CancelSuspension extends Cancellable: + var suspension: acSupport.Suspension[Try[U], Unit]^{Cap^} = uninitialized + var listener: Listener[U]^{this, Cap^} = uninitialized + var completed = false + + def complete() = synchronized: + val completedBefore = completed + completed = true + completedBefore + + override def cancel() = + val completedBefore = complete() + if !completedBefore then + src.dropListener(listener) + // SAFETY: we always await for this suspension to end + val pureSusp = caps.unsafe.unsafeAssumePure(suspension) + acSupport.resumeAsync(pureSusp)(Failure(new CancellationException())) + + if group.isCancelled then throw new CancellationException() + + src + .poll() + .getOrElse: + val cancellable = CancelSuspension() + val res = acSupport.suspend[Try[U], Unit, Cap](k => + val listener = Listener.acceptingListener[U]: (x, _) => + val completedBefore = cancellable.complete() + // SAFETY: Future should already capture Cap^ + val purek = caps.unsafe.unsafeAssumePure(k) + if !completedBefore then acSupport.resumeAsync(purek)(Success(x)) + cancellable.suspension = k + cancellable.listener = listener + cancellable.link(group) // may resume + remove listener immediately + src.onComplete(listener) + ) + cancellable.unlink() + res.get + + override def withGroup(group: CompletionGroup): Async = FutureAsync[Cap](group) + + override def cancel(): Unit = if setCancelled() then this.innerGroup.cancel() + + link() + ac.support.scheduleBoundary: + val result = Async.withNewCompletionGroup(innerGroup)(Try({ + val r = body + checkCancellation() + r + }).recoverWith { case _: InterruptedException | _: CancellationException => + Failure(new CancellationException()) + })(using FutureAsync(CompletionGroup.Unlinked)) + complete(result) + + end RunnableFuture + + + /** Create a future that asynchronously executes `body` that wraps its execution in a [[scala.util.Try]]. The returned + * future is linked to the given [[Async.Spawn]] scope by default, i.e. it is cancelled when this scope ends. + */ + def apply[T](body: Async.Spawn ?=> T)(using async: Async, spawnable: Async.Spawn)( + using async.type =:= spawnable.type + ): Future[T]^{body, spawnable} = + val f = (async: Async.Spawn) => body(using async) + val puref = caps.unsafe.unsafeAssumePure(f) + // SAFETY: body is recorded in the capture set of Future, which should be cancelled when gone out of scope. + RunnableFuture(async ?=> puref(async))(using spawnable) + + /** A future that is immediately completed with the given result. */ + def now[T](result: Try[T]): Future[T] = + val f = CoreFuture[T]() + f.complete(result) + f + + /** An alias to [[now]]. */ + inline def completed[T](result: Try[T]) = now(result) + + /** A future that immediately resolves with the given result. Similar to `Future.now(Success(result))`. */ + inline def resolved[T](result: T): Future[T] = now(Success(result)) + + /** A future that immediately rejects with the given exception. Similar to `Future.now(Failure(exception))`. */ + inline def rejected(exception: Throwable): Future[Nothing] = now(Failure(exception)) + + extension [T](f1: Future[T]^) + /** Parallel composition of two futures. If both futures succeed, succeed with their values in a pair. Otherwise, + * fail with the failure that was returned first. + */ + def zip[U](f2: Future[U]^): Future[(T, U)]^{f1, f2} = + Future.withResolver[(T, U), caps.CapSet^{f1, f2}]: r => + Async + .either(f1, f2) + .onComplete(Listener { (v, _) => + v match + case Left(Success(x1)) => + f2.onComplete(Listener { (x2, _) => r.complete(x2.map((x1, _))) }) + case Right(Success(x2)) => + f1.onComplete(Listener { (x1, _) => r.complete(x1.map((_, x2))) }) + case Left(Failure(ex)) => r.reject(ex) + case Right(Failure(ex)) => r.reject(ex) + }) + + // /** Parallel composition of tuples of futures. Disabled since scaladoc is crashing with it. (https://github.com/scala/scala3/issues/19925) */ + // def *:[U <: Tuple](f2: Future[U]): Future[T *: U] = Future.withResolver: r => + // Async + // .either(f1, f2) + // .onComplete(Listener { (v, _) => + // v match + // case Left(Success(x1)) => + // f2.onComplete(Listener { (x2, _) => r.complete(x2.map(x1 *: _)) }) + // case Right(Success(x2)) => + // f1.onComplete(Listener { (x1, _) => r.complete(x1.map(_ *: x2)) }) + // case Left(Failure(ex)) => r.reject(ex) + // case Right(Failure(ex)) => r.reject(ex) + // }) + + /** Alternative parallel composition of this task with `other` task. If either task succeeds, succeed with the + * success that was returned first. Otherwise, fail with the failure that was returned last. + * @see + * [[orWithCancel]] for an alternative version where the slower future is cancelled. + */ + def or(f2: Future[T]^): Future[T]^{f1, f2} = orImpl(false)(f2) + + /** Like `or` but the slower future is cancelled. If either task succeeds, succeed with the success that was + * returned first and the other is cancelled. Otherwise, fail with the failure that was returned last. + */ + def orWithCancel(f2: Future[T]^): Future[T]^{f1, f2} = orImpl(true)(f2) + + inline def orImpl(inline withCancel: Boolean)(f2: Future[T]^): Future[T]^{f1, f2} = Future.withResolver[T, caps.CapSet^{f1, f2}]: r => + Async + .raceWithOrigin(f1, f2) + .onComplete(Listener { case ((v, which), _) => + v match + case Success(value) => + inline if withCancel then (if which == f1 then f2 else f1).cancel() + r.resolve(value) + case Failure(_) => + (if which == f1.symbol then f2 else f1).onComplete(Listener((v, _) => r.complete(v))) + }) + + end extension + + /** A promise is a [[Future]] that is be completed manually via the `complete` method. + * @see + * [[Promise$.apply]] to create a new, empty promise. + * @see + * [[Future.withResolver]] to create a passive [[Future]] from callback-style asynchronous calls. + */ + trait Promise[T] extends Future[T]: + inline def asFuture: Future[T] = this + + /** Define the result value of `future`. */ + def complete(result: Try[T]): Unit + + object Promise: + /** Create a new, unresolved [[Promise]]. */ + def apply[T](): Promise[T] = + new CoreFuture[T] with Promise[T]: + override def cancel(): Unit = + if setCancelled() then complete(Failure(new CancellationException())) + + /** Define the result value of `future`. However, if `future` was cancelled in the meantime complete with a + * `CancellationException` failure instead. + */ + override def complete(result: Try[T]): Unit = super[CoreFuture].complete(result) + end Promise + + /** The group of handlers to be used in [[withResolver]]. As a Future is completed only once, only one of + * resolve/reject/complete may be used and only once. + */ + trait Resolver[-T, Cap^]: + /** Complete the future with a data item successfully */ + def resolve(item: T): Unit = complete(Success(item)) + + /** Complete the future with a failure */ + def reject(exc: Throwable): Unit = complete(Failure(exc)) + + /** Complete the future with a [[CancellationException]] */ + def rejectAsCancelled(): Unit = complete(Failure(new CancellationException())) + + /** Complete the future with the result, be it Success or Failure */ + def complete(result: Try[T]): Unit + + /** Register a cancellation handler to be called when the created future is cancelled. Note that only one handler + * may be used. The handler should eventually complete the Future using one of complete/resolve/reject*. The + * default handler is set up to [[rejectAsCancelled]] immediately. + */ + def onCancel(handler: (() -> Unit)^{Cap^}): Unit + end Resolver + + /** Create a promise that may be completed asynchronously using external means. + * + * The body is run synchronously on the callers thread to setup an external asynchronous operation whose + * success/failure it communicates using the [[Resolver]] to complete the future. + * + * If the external operation supports cancellation, the body can register one handler using [[Resolver.onCancel]]. + */ + def withResolver[T, Cap^](body: Resolver[T, Cap]^{Cap^} => Unit): Future[T]^{Cap^} = + val future: (CoreFuture[T] & Resolver[T, Cap] & Promise[T])^{Cap^} = new CoreFuture[T] with Resolver[T, Cap] with Promise[T]: + // TODO: undo this once bug is fixed + @volatile var cancelHandle: (() -> Unit) = () => rejectAsCancelled() + override def onCancel(handler: (() -> Unit)^{Cap^}): Unit = + cancelHandle = /* TODO remove */ caps.unsafe.unsafeAssumePure(handler) + override def complete(result: Try[T]): Unit = super.complete(result) + + override def cancel(): Unit = + if setCancelled() then cancelHandle() + end future + body(future) + future + end withResolver + + sealed abstract class BaseCollector[T, Cap^](): + private val ch = UnboundedChannel[Future[T]^{Cap^}]() + + private val futMap = mutable.Map[SourceSymbol[Try[T]], Future[T]^{Cap^}]() + + /** Output channels of all finished futures. */ + final def results: ReadableChannel[Future[T]^{Cap^}] = ch.asReadable + + private val listener = Listener((_, fut) => + // safe, as we only attach this listener to Future[T] + val future = futMap.synchronized: + futMap.remove(fut.asInstanceOf[SourceSymbol[Try[T]]]).get + ch.sendImmediately(future) + ) + + protected final def addFuture(future: Future[T]^{Cap^}) = + futMap.synchronized { futMap += (future.symbol -> future) } + future.onComplete(listener) + end BaseCollector + + + /** Collects a list of futures into a channel of futures, arriving as they finish. + * @example + * {{{ + * // Sleep sort + * val futs = numbers.map(i => Future(sleep(i.millis))) + * val collector = Collector(futs*) + * + * val output = mutable.ArrayBuffer[Int]() + * for i <- 1 to futs.size: + * output += collector.results.read().await + * }}} + * @see + * [[Future.awaitAll]] and [[Future.awaitFirst]] for simple usage of the collectors to get all results or the first + * succeeding one. + */ + class Collector[T](futures: (Future[T]^)*) extends BaseCollector[T, caps.CapSet^{futures*}]: + futures.foreach(addFuture) + end Collector + + /** Like [[Collector]], but exposes the ability to add futures after creation. */ + class MutableCollector[T, Cap^](futures: (Future[T]^{Cap^})*) extends BaseCollector[T, Cap]: + futures.foreach(addFuture) + /** Add a new [[Future]] into the collector. */ + inline def add(future: Future[T]^{Cap^}) = addFuture(future) + inline def +=(future: Future[T]^{Cap^}) = add(future) + + extension [T](@caps.use fs: Seq[Future[T]^]) + /** `.await` for all futures in the sequence, returns the results in a sequence, or throws if any futures fail. */ + def awaitAll(using Async) = + val collector = Collector(fs*) + for _ <- fs do collector.results.read().right.get.await + fs.map(_.await) + + /** Like [[awaitAll]], but cancels all futures as soon as one of them fails. */ + def awaitAllOrCancel(using Async) = + val collector = Collector(fs*) + try + for _ <- fs do collector.results.read().right.get.await + fs.map(_.await) + catch + case NonFatal(e) => + fs.foreach(_.cancel()) + throw e + + /** Race all futures, returning the first successful value. Throws the last exception received, if everything fails. + */ + def awaitFirst(using Async): T = awaitFirstImpl(false) + + /** Like [[awaitFirst]], but cancels all other futures as soon as the first future succeeds. */ + def awaitFirstWithCancel(using Async): T = awaitFirstImpl(true) + + private inline def awaitFirstImpl(withCancel: Boolean)(using Async): T = + val collector = Collector(fs*) + @scala.annotation.tailrec + def loop(attempt: Int): T = + collector.results.read().right.get.awaitResult match + case Failure(exception) => + if attempt == fs.length then /* everything failed */ throw exception else loop(attempt + 1) + case Success(value) => + inline if withCancel then fs.foreach(_.cancel()) + value + loop(1) +end Future + +/** TaskSchedule describes the way in which a task should be repeated. Tasks can be set to run for example every 100 + * milliseconds or repeated as long as they fail. `maxRepetitions` describes the maximum amount of repetitions allowed, + * after that regardless of TaskSchedule chosen, the task is not repeated anymore and the last returned value is + * returned. `maxRepetitions` equal to zero means that repetitions can go on potentially forever. + */ +enum TaskSchedule: + case Every(val millis: Long, val maxRepetitions: Long = 0) + case ExponentialBackoff(val millis: Long, val exponentialBase: Int = 2, val maxRepetitions: Long = 0) + case FibonacciBackoff(val millis: Long, val maxRepetitions: Long = 0) + case RepeatUntilFailure(val millis: Long = 0, val maxRepetitions: Long = 0) + case RepeatUntilSuccess(val millis: Long = 0, val maxRepetitions: Long = 0) + +/** A task is a template that can be turned into a runnable future Composing tasks can be referentially transparent. + * Tasks can be also ran on a specified schedule. + */ +class Task[+T](val body: (Async, AsyncOperations) ?=> T): + + /** Run the current task and returns the result. */ + def run()(using Async, AsyncOperations): T = body + + /** Start a future computed from the `body` of this task */ + def start()(using async: Async, spawn: Async.Spawn)(using asyncOps: AsyncOperations)(using async.type =:= spawn.type): Future[T]^{body, spawn} = + Future(body)(using async, spawn) + + def schedule(s: TaskSchedule): Task[T]^{body} = + s match { + case TaskSchedule.Every(millis, maxRepetitions) => + assert(millis >= 1) + assert(maxRepetitions >= 0) + Task { + var repetitions = 0 + var ret: T = body + repetitions += 1 + if (maxRepetitions == 1) ret + else { + while (maxRepetitions == 0 || repetitions < maxRepetitions) { + AsyncOperations.sleep(millis) + ret = body + repetitions += 1 + } + ret + } + } + case TaskSchedule.ExponentialBackoff(millis, exponentialBase, maxRepetitions) => + assert(millis >= 1) + assert(exponentialBase >= 2) + assert(maxRepetitions >= 0) + Task { + var repetitions = 0 + var ret: T = body + repetitions += 1 + if (maxRepetitions == 1) ret + else { + var timeToSleep = millis + while (maxRepetitions == 0 || repetitions < maxRepetitions) { + AsyncOperations.sleep(timeToSleep) + timeToSleep *= exponentialBase + ret = body + repetitions += 1 + } + ret + } + } + case TaskSchedule.FibonacciBackoff(millis, maxRepetitions) => + assert(millis >= 1) + assert(maxRepetitions >= 0) + Task { + var repetitions = 0 + var a: Long = 0 + var b: Long = 1 + var ret: T = body + repetitions += 1 + if (maxRepetitions == 1) ret + else { + AsyncOperations.sleep(millis) + ret = body + repetitions += 1 + if (maxRepetitions == 2) ret + else { + while (maxRepetitions == 0 || repetitions < maxRepetitions) { + val aOld = a + a = b + b = aOld + b + AsyncOperations.sleep(b * millis) + ret = body + repetitions += 1 + } + ret + } + } + } + case TaskSchedule.RepeatUntilFailure(millis, maxRepetitions) => + assert(millis >= 0) + assert(maxRepetitions >= 0) + Task { + @tailrec + def helper(repetitions: Long = 0): T = + if (repetitions > 0 && millis > 0) + AsyncOperations.sleep(millis) + val ret: T = body + ret match { + case Failure(_) => ret + case _ if (repetitions + 1) == maxRepetitions && maxRepetitions != 0 => ret + case _ => helper(repetitions + 2) + } + helper() + } + case TaskSchedule.RepeatUntilSuccess(millis, maxRepetitions) => + assert(millis >= 0) + assert(maxRepetitions >= 0) + Task { + @tailrec + def helper(repetitions: Long = 0): T = + if (repetitions > 0 && millis > 0) + AsyncOperations.sleep(millis) + val ret: T = body + ret match { + case Success(_) => ret + case _ if (repetitions + 1) == maxRepetitions && maxRepetitions != 0 => ret + case _ => helper(repetitions + 2) + } + helper() + } + } + +end Task + +/** Runs the `body` inside in an [[Async]] context that does *not* propagate cancellation until the end. + * + * In other words, `body` is never notified of the cancellation of the `ac` context; but `uninterruptible` would still + * throw a [[CancellationException]] ''after `body` finishes running'' if `ac` was cancelled. + */ +def uninterruptible[T](body: Async ?=> T)(using ac: Async): T = + val tracker = Cancellable.Tracking().link() + + val r = + try + val group = CompletionGroup() + Async.withNewCompletionGroup(group)(body) + finally tracker.unlink() + + if tracker.isCancelled then throw new CancellationException() + r + +/** Link `cancellable` to the completion group of the current [[Async]] context during `fn`. + * + * If the [[Async]] context is cancelled during the execution of `fn`, `cancellable` will also be immediately + * cancelled. + */ +def cancellationScope[T](cancellable: Cancellable)(fn: => T)(using a: Async): T = + cancellable.link() + try fn + finally cancellable.unlink() diff --git a/tests/pos-custom-args/captures/gears/locking.scala b/tests/pos-custom-args/captures/gears/locking.scala new file mode 100644 index 000000000000..e07ec390aa60 --- /dev/null +++ b/tests/pos-custom-args/captures/gears/locking.scala @@ -0,0 +1,48 @@ +/** Package listeners provide some auxilliary methods to work with listeners. */ +package gears.async.listeners + +import language.experimental.captureChecking + +import gears.async._ + +import scala.annotation.tailrec + +import Listener.ListenerLock + +/** Two listeners being locked at the same time, while having the same [[Listener.ListenerLock.selfNumber lock number]]. + */ +case class ConflictingLocksException( + listeners: (Listener[?]^, Listener[?]^) +) extends Exception + +/** Attempt to lock both listeners belonging to possibly different sources at the same time. Lock orders are respected + * by comparing numbers on every step. + * + * Returns `true` on success, or the listener that fails first. + * + * @throws ConflictingLocksException + * In the case that two locks sharing the same number is encountered, this exception is thrown with the conflicting + * listeners. + */ +def lockBoth[T, U]( + lt: Listener[T]^, + lu: Listener[U]^ +): (lt.type | lu.type | true) = + val lockT = if lt.lock == null then return (if lu.acquireLock() then true else lu) else lt.lock + val lockU = if lu.lock == null then return (if lt.acquireLock() then true else lt) else lu.lock + + def doLock[T, U](lt: Listener[T]^, lu: Listener[U]^)( + lockT: ListenerLock^{lt}, + lockU: ListenerLock^{lu} + ): (lt.type | lu.type | true) = + // assert(lockT.number > lockU.number) + if !lockT.acquire() then lt + else if !lockU.acquire() then + lockT.release() + lu + else true + + if lockT.selfNumber == lockU.selfNumber then throw ConflictingLocksException((lt, lu)) + else if lockT.selfNumber > lockU.selfNumber then doLock(lt, lu)(lockT, lockU) + else doLock(lu, lt)(lockU, lockT) +end lockBoth diff --git a/tests/pos-custom-args/captures/gears/measureTimes.scala b/tests/pos-custom-args/captures/gears/measureTimes.scala new file mode 100644 index 000000000000..5be5c96a79e8 --- /dev/null +++ b/tests/pos-custom-args/captures/gears/measureTimes.scala @@ -0,0 +1,512 @@ +package measurements + +import language.experimental.captureChecking + +import gears.async.default.given +import gears.async.{Async, BufferedChannel, ChannelMultiplexer, Future, SyncChannel} + +import java.io.{FileReader, FileWriter} +import java.nio.file.{Files, NoSuchFileException, Paths, StandardOpenOption} +import java.util.concurrent.atomic.AtomicInteger +import scala.collection.mutable +import scala.collection.mutable.{ArrayBuffer, HashMap} +import scala.concurrent.ExecutionContext +import scala.util.CommandLineParser.FromString.given_FromString_Int +import scala.util.Try + +import PosixLikeIO.PIOHelper + +case class TimeMeasurementResult(millisecondsPerOperation: Double, standardDeviation: Double) + +def measureIterations[T](action: () => T): Int = + val counter = AtomicInteger(0) + + val t1 = Thread.startVirtualThread: () => + try { + while (!Thread.interrupted()) { + action() + val r = counter.getAndIncrement() + } + } catch { + case (_: InterruptedException) => () + } + + Thread.sleep(10 * 1000) + counter.set(0) + Thread.sleep(60 * 1000) + t1.interrupt() + counter.get() + +@main def measureFutureOverhead(): Unit = + given ExecutionContext = ExecutionContext.global + + val threadJoins = measureIterations: () => + val t = Thread.startVirtualThread: () => + var z = 1 + t.join() + + val futureJoins = measureIterations: () => + Async.blocking: + val f = Future: + var z = 1 + f.awaitResult + + println("Thread joins per second: " + (threadJoins / 60)) + println("Future joins per second: " + (futureJoins / 60)) + println("Overhead: " + ((threadJoins + 0.0) / (futureJoins + 0.0))) + + /* + Linux: + Thread joins per second: 292647 + Future joins per second: 86032 + Overhead: 3.401577460379452 + */ + +@main def measureRaceOverhead(): Unit = + given ExecutionContext = ExecutionContext.global + + val c1: Double = measureIterations: () => + Async.blocking: + Async.race(Future { Thread.sleep(10) }, Future { Thread.sleep(100) }, Future { Thread.sleep(50) }).await + Async.race(Future { Thread.sleep(50) }, Future { Thread.sleep(10) }, Future { Thread.sleep(100) }).await + Async.race(Future { Thread.sleep(100) }, Future { Thread.sleep(50) }, Future { Thread.sleep(10) }).await + + val c2: Double = measureIterations: () => + Async.blocking: + val f11 = Future { Thread.sleep(10) } + val f12 = Future { Thread.sleep(50) } + val f13 = Future { Thread.sleep(100) } + f11.awaitResult + + val f21 = Future { Thread.sleep(100) } + val f22 = Future { Thread.sleep(10) } + val f23 = Future { Thread.sleep(50) } + f22.awaitResult + + val f31 = Future { Thread.sleep(50) } + val f32 = Future { Thread.sleep(100) } + val f33 = Future { Thread.sleep(10) } + f33.awaitResult + + val c1_seconds_wasted_for_waits = c1 * 0.01 + val c1_per_second_adjusted = c1 / 3 / (60 - c1_seconds_wasted_for_waits) + val c2_seconds_wasted_for_waits = c2 * 0.01 + val c2_per_second_adjusted = c1 / 3 / (60 - c2_seconds_wasted_for_waits) + + println("Raced futures awaited per second: " + c1_per_second_adjusted) + println("Non-raced futures per second: " + c2_per_second_adjusted) + println("Overhead: " + (c2_per_second_adjusted / c1_per_second_adjusted)) + + /* Linux + Raced futures awaited per second: 15.590345727332032 + Non-raced futures per second: 15.597976831457009 + Overhead: 1.0004894762604013 + */ + +@main def measureRaceOverheadVsJava(): Unit = + given ExecutionContext = ExecutionContext.global + + val c1: Double = measureIterations: () => + Async.blocking: + Async.race(Future { Thread.sleep(10) }, Future { Thread.sleep(100) }, Future { Thread.sleep(50) }).await + Async.race(Future { Thread.sleep(50) }, Future { Thread.sleep(10) }, Future { Thread.sleep(100) }).await + Async.race(Future { Thread.sleep(100) }, Future { Thread.sleep(50) }, Future { Thread.sleep(10) }).await + + val c2: Double = measureIterations: () => + @volatile var i1 = true + val f11 = Thread.startVirtualThread(() => { Thread.sleep(10); i1 = false }) + val f12 = Thread.startVirtualThread(() => { Thread.sleep(50); i1 = false }) + val f13 = Thread.startVirtualThread(() => { Thread.sleep(100); i1 = false }) + while (i1) () + + @volatile var i2 = true + val f21 = Thread.startVirtualThread(() => { Thread.sleep(100); i2 = false }) + val f22 = Thread.startVirtualThread(() => { Thread.sleep(10); i2 = false }) + val f23 = Thread.startVirtualThread(() => { Thread.sleep(50); i2 = false }) + while (i2) () + + @volatile var i3 = true + val f31 = Thread.startVirtualThread(() => { Thread.sleep(50); i3 = false }) + val f32 = Thread.startVirtualThread(() => { Thread.sleep(100); i3 = false }) + val f33 = Thread.startVirtualThread(() => { Thread.sleep(10); i3 = false }) + while (i3) () + + f11.interrupt() + f12.interrupt() + f13.interrupt() + f21.interrupt() + f22.interrupt() + f23.interrupt() + f31.interrupt() + f32.interrupt() + f33.interrupt() + + val c1_seconds_wasted_for_waits = c1 * 0.01 + val c1_per_second_adjusted = c1 / 3 / (60 - c1_seconds_wasted_for_waits) + val c2_seconds_wasted_for_waits = c2 * 0.01 + val c2_per_second_adjusted = c1 / 3 / (60 - c2_seconds_wasted_for_waits) + + println("Raced futures awaited per second: " + c1_per_second_adjusted) + println("Java threads awaited per second: " + c2_per_second_adjusted) + println("Overhead: " + (c2_per_second_adjusted / c1_per_second_adjusted)) + + /* Linux + Raced futures awaited per second: 15.411487529449996 + Java threads awaited per second: 15.671210243700953 + Overhead: 1.0168525402726147 + */ + +@main def channelsVsJava(): Unit = + given ExecutionContext = ExecutionContext.global + + val sec = 60 + + // java + @volatile var shared: Long = 0 + @volatile var timeForWriting = true + val t1 = Thread.startVirtualThread: () => + var i: Long = 0 + while (true) { + while (!timeForWriting) () + shared = i + timeForWriting = false + i += 1 + } + + val t2 = Thread.startVirtualThread: () => + while (true) { + while (timeForWriting) () + var z = shared + timeForWriting = true + } + + Thread.sleep(sec * 1000) + t1.interrupt() + t2.interrupt() + val javaSendsPerSecond: Long = shared / sec + println("Java \"channel\" sends per second: " + javaSendsPerSecond) + + var syncChannelSendsPerSecond = 0.0 + var bufferedChannelSendsPerSecond = 0.0 + var cmOverSyncSendsPerSecond = 0.0 + var cmOverBufferedSendsPerSecond = 0.0 + + Async.blocking: + val c = SyncChannel[Long]() + val f1 = Future: + var i: Long = 0 + while (true) { + try { + c.send(i) + } catch { + case (e: InterruptedException) => { + syncChannelSendsPerSecond = i / sec + throw e + } + } + i += 1 + } + val f2 = Future: + while (true) { + c.read() + } + + Thread.sleep(sec * 1000) + f1.cancel() + f2.cancel() + Thread.sleep(500) + println("SyncChannel sends per second: " + syncChannelSendsPerSecond) + + Async.blocking: + val c = BufferedChannel[Long](1) + val f1 = Future: + var i: Long = 0 + while (true) { + try { + c.send(i) + } catch { + case (e: InterruptedException) => { + bufferedChannelSendsPerSecond = i / sec + throw e + } + } + i += 1 + } + val f2 = Future: + while (true) { + c.read() + } + + Thread.sleep(sec * 1000) + f1.cancel() + f2.cancel() + Thread.sleep(500) + println("BufferedChannel sends per second: " + bufferedChannelSendsPerSecond) + + Async.blocking: + val m = ChannelMultiplexer[Long]() + val c = SyncChannel[Long]() + val cr = SyncChannel[Try[Long]]() + m.addPublisher(c) + m.addSubscriber(cr) + Thread.sleep(50) + + val f1 = Future: + var i: Long = 0 + while (true) { + try { + c.send(i) + } catch { + case (e: InterruptedException) => { + cmOverSyncSendsPerSecond = i / sec + throw e + } + } + i += 1 + } + val f2 = Future: + while (true) { + cr.read() + } + + Thread.sleep(sec * 1000) + f1.cancel() + f2.cancel() + Thread.sleep(500) + println("ChannelMultiplexer over SyncChannels sends per second: " + cmOverSyncSendsPerSecond) + + Async.blocking: + val m = ChannelMultiplexer[Long]() + val c = BufferedChannel[Long](1) + val cr = BufferedChannel[Try[Long]](1) + m.addPublisher(c) + m.addSubscriber(cr) + Thread.sleep(50) + + val f1 = Future: + var i: Long = 0 + while (true) { + try { + c.send(i) + } catch { + case (e: InterruptedException) => { + cmOverBufferedSendsPerSecond = i / sec + throw e + } + } + i += 1 + } + val f2 = Future: + while (true) { + cr.read() + } + + Thread.sleep(sec * 1000) + f1.cancel() + f2.cancel() + Thread.sleep(500) + println("ChannelMultiplexer over BufferedChannels sends per second: " + cmOverBufferedSendsPerSecond) + + /* Linux + Java "channel" sends per second: 8691652 + SyncChannel sends per second: 319371.0 + BufferedChannel sends per second: 308286.0 + ChannelMultiplexer over SyncChannels sends per second: 155737.0 + ChannelMultiplexer over BufferedChannels sends per second: 151995.0 + */ + +/** Warmup for 10 seconds and benchmark for 60 seconds. + */ +def measureRunTimes[T](action: () => T): TimeMeasurementResult = + + var timesIn25Milliseconds: Long = 0 + { + val minibenchmarkStart = System.nanoTime() + while (System.nanoTime() - minibenchmarkStart < 25L * 1000 * 1000) { + action() + timesIn25Milliseconds += 1 + } + assert(timesIn25Milliseconds >= 1) + } + + val times = ArrayBuffer[Double]() + + { + val warmupStart = System.currentTimeMillis() + while (System.currentTimeMillis() - warmupStart < 10L * 1000) + action() + } + + System.err.println("Warming up completed.") + + val benchmarkingStart = System.nanoTime() + var benchmarkingTimeStillNotPassed = true + while (benchmarkingTimeStillNotPassed) { + + val start = System.nanoTime() + for (_ <- 1L to timesIn25Milliseconds) + action() + val end = System.nanoTime() + var nanoTimePerOperation: Double = (end - start + (0.0).toDouble) / timesIn25Milliseconds.toDouble + times.append(nanoTimePerOperation) + + if (end - benchmarkingStart >= 60L * 1000 * 1000 * 1000) + benchmarkingTimeStillNotPassed = false + } + + var avg: Double = 0.0 + times.foreach(avg += _) + avg /= times.length + + var stdev: Double = 0.0 + for (x <- times) + stdev += (x - avg) * (x - avg) + assert(times.length >= 2) + stdev /= (times.length - 1) + stdev = Math.sqrt(stdev) + + TimeMeasurementResult(avg / 1000 / 1000, stdev / 1000 / 1000) + +@main def measureSomething(): Unit = + + val g = measureRunTimes: () => + var t = 100100 + t *= 321984834 + t /= 1238433 + t /= 1222 + Thread.sleep(11) + println(g) + +@main def measureTimesNew: Unit = + + // mkdir -p /tmp/FIO && sudo mount -t tmpfs -o size=8g tmpfs /tmp/FIO + + given ExecutionContext = ExecutionContext.global + + val dataAlmostJson = StringBuffer() // TEST:String -> PARAMETER:String -> METHOD:String -> TIMES:List[Double] + dataAlmostJson.append("{") + + def measure[T](methodName: String, timesInner: Int = 100, timesOuter: Int = 100)(action: () => T): String = + val times = ArrayBuffer[Double]() + for (_ <- 1 to timesOuter) + val timeStart = System.nanoTime() + for (_ <- 1 to timesInner) + action() + val timeEnd = System.nanoTime() + times += ((timeEnd - timeStart + 0.0) / 1000 / 1000 / timesInner) + + var avg: Double = 0.0 + times.foreach(avg += _) + avg /= times.length + + var stdev: Double = 0.0 + for (x <- times) + stdev += (x - avg) * (x - avg) + assert(times.length >= 2) + stdev /= (times.length - 1) + stdev = Math.sqrt(stdev) + + val ret = StringBuffer() + ret.append("\"") + ret.append(methodName) + ret.append("\": [") + ret.append(avg) + ret.append(", ") + ret.append(stdev) + ret.append("],\n") + ret.toString + + val bigStringBuilder = new StringBuilder() + for (_ <- 1 to 10 * 1024 * 1024) bigStringBuilder.append("abcd") + val bigString = bigStringBuilder.toString() + + def deleteFiles(): Unit = + for (p <- Array("x", "y", "z")) + try Files.delete(Paths.get("/tmp/FIO/" + p + ".txt")) + catch case e: NoSuchFileException => () + + deleteFiles() + + dataAlmostJson.append("\n\t\"File writing\": {\n") + { + for (size <- Seq(4, 40 * 1024 * 1024)) + println("size " + size.toString) + dataAlmostJson.append("\n\t\t\"Size " + size.toString + "\": {\n") + { + dataAlmostJson.append(measure("PosixLikeIO", timesInner = if size < 100 then 100 else 10): () => + Async.blocking: + PIOHelper.withFile("/tmp/FIO/x.txt", StandardOpenOption.CREATE, StandardOpenOption.WRITE): f => + f.writeString(bigString.substring(0, size)).awaitResult + ) + println("done 1") + + dataAlmostJson.append(measure("Java FileWriter", timesInner = if size < 100 then 100 else 10): () => + val writer = new FileWriter("/tmp/FIO/y.txt") + writer.write(bigString.substring(0, size), 0, size) + writer.close() + ) + println("done 2") + + dataAlmostJson.append(measure("Java Files.writeString", timesInner = if size < 100 then 100 else 10): () => + Files.writeString(Paths.get("/tmp/FIO/z.txt"), bigString.substring(0, size))) + println("done 3") + } + dataAlmostJson.append("},\n") + } + dataAlmostJson.append("},\n") + + dataAlmostJson.append("\n\t\"File reading\": {\n") + { + for (size <- Seq(4, 40 * 1024 * 1024)) + println("size " + size.toString) + deleteFiles() + Files.writeString(Paths.get("/tmp/FIO/x.txt"), bigString.substring(0, size)) + Files.writeString(Paths.get("/tmp/FIO/y.txt"), bigString.substring(0, size)) + Files.writeString(Paths.get("/tmp/FIO/z.txt"), bigString.substring(0, size)) + + dataAlmostJson.append("\n\t\t\"Size " + size.toString + "\": {\n") + { + dataAlmostJson.append(measure("PosixLikeIO", timesInner = if size < 100 then 100 else 10): () => + Async.blocking: + PIOHelper.withFile("/tmp/FIO/x.txt", StandardOpenOption.READ): f => + f.readString(size).awaitResult + ) + println("done 1") + + val buffer = new Array[Char](size) + dataAlmostJson.append(measure("Java FileReeader", timesInner = if size < 100 then 100 else 10): () => + val reader = new FileReader("/tmp/FIO/y.txt") + reader.read(buffer) + reader.close() + ) + println("done 2") + + dataAlmostJson.append(measure("Java Files.readString", timesInner = if size < 100 then 100 else 10): () => + Files.readString(Paths.get("/tmp/FIO/z.txt"))) + println("done 3") + } + dataAlmostJson.append("},\n") + } + dataAlmostJson.append("},\n") + + dataAlmostJson.append("}") + println(dataAlmostJson.toString) + + /* Linux + { + "File writing": { + + "Size 4": { + "PosixLikeIO": [0.0397784622, 0.08412340604573831], + "Java FileWriter": [0.010826620499999997, 0.00979259772337624], + "Java Files.write": [0.007529464599999997, 0.0028499973824777695], + }, + + "Size 41943040": { + "PosixLikeIO": [16.846597593, 0.889024137544089], + "Java FileWriter": [29.068105414999977, 3.766062167872921], + "Java Files.write": [18.96376850600001, 0.20493288428568684], + }, + }, + } + */ diff --git a/tests/pos-custom-args/captures/gears/package.scala b/tests/pos-custom-args/captures/gears/package.scala new file mode 100644 index 000000000000..13066a181bb9 --- /dev/null +++ b/tests/pos-custom-args/captures/gears/package.scala @@ -0,0 +1,14 @@ +package gears + +import language.experimental.captureChecking + +/** Asynchronous programming support with direct-style Scala. + * @see + * [[gears.async.Async]] for an introduction to the [[Async]] context and how to create them. + * @see + * [[gears.async.Future]] for a simple interface to spawn concurrent computations. + * @see + * [[gears.async.Channel]] for a simple inter-future communication primitive. + */ +package object async: + type CancellationException = java.util.concurrent.CancellationException diff --git a/tests/pos-custom-args/captures/gears/readAndWriteFile.scala b/tests/pos-custom-args/captures/gears/readAndWriteFile.scala new file mode 100644 index 000000000000..a2bf39a96176 --- /dev/null +++ b/tests/pos-custom-args/captures/gears/readAndWriteFile.scala @@ -0,0 +1,20 @@ +package PosixLikeIO.examples + +import gears.async.Async +import gears.async.default.given + +import java.nio.ByteBuffer +import java.nio.charset.StandardCharsets +import java.nio.file.StandardOpenOption +import scala.concurrent.ExecutionContext + +import PosixLikeIO.PIOHelper + +@main def readAndWriteFile(): Unit = + given ExecutionContext = ExecutionContext.global + Async.blocking: + PIOHelper.withFile("/home/julian/Desktop/x.txt", StandardOpenOption.READ, StandardOpenOption.WRITE): f => + f.writeString("Hello world! (1)").await + println(f.readString(1024).await) + f.writeString("Hello world! (2)").await + println(f.readString(1024).await) diff --git a/tests/pos-custom-args/captures/gears/readWholeFile.scala b/tests/pos-custom-args/captures/gears/readWholeFile.scala new file mode 100644 index 000000000000..7811122c8d79 --- /dev/null +++ b/tests/pos-custom-args/captures/gears/readWholeFile.scala @@ -0,0 +1,25 @@ +package PosixLikeIO.examples + +import gears.async.Async +import gears.async.default.given + +import java.nio.ByteBuffer +import java.nio.charset.StandardCharsets +import java.nio.file.StandardOpenOption +import scala.concurrent.ExecutionContext + +import PosixLikeIO.PIOHelper + +@main def readWholeFile(): Unit = + given ExecutionContext = ExecutionContext.global + Async.blocking: + PIOHelper.withFile("/home/julian/Desktop/x.txt", StandardOpenOption.READ): f => + val b = ByteBuffer.allocate(1024) + val retCode = f.read(b).awaitResult.get + assert(retCode >= 0) + val s = StandardCharsets.UTF_8.decode(b.slice(0, retCode)).toString() + println("Read size with read(): " + retCode.toString()) + println("Data: " + s) + + println("Read with readString():") + println(f.readString(1000).awaitResult) diff --git a/tests/pos-custom-args/captures/gears/retry.scala b/tests/pos-custom-args/captures/gears/retry.scala new file mode 100644 index 000000000000..d06507f5cbb3 --- /dev/null +++ b/tests/pos-custom-args/captures/gears/retry.scala @@ -0,0 +1,156 @@ +package gears.async + +import language.experimental.captureChecking + +import gears.async.Async +import gears.async.AsyncOperations.sleep +import gears.async.Retry.Delay + +import scala.concurrent.duration._ +import scala.util.Random +import scala.util.boundary +import scala.util.control.NonFatal +import scala.util.{Failure, Success, Try} + +/** Utility class to perform asynchronous actions with retrying policies on exceptions. + * + * See [[Retry]] companion object for common policies as a starting point. + */ +case class Retry( + retryOnSuccess: Boolean = false, + maximumFailures: Option[Int] = None, + delay: Delay = Delay.none +): + /** Runs `body` with the current policy in its own scope, returning the result or the last failure as an exception. + */ + def apply[T](op: => T)(using Async, AsyncOperations): T = + var failures = 0 + var lastDelay: FiniteDuration = 0.second + boundary: + while true do + try + val value = op + if retryOnSuccess then + failures = 0 + lastDelay = delay.delayFor(failures, lastDelay) + sleep(lastDelay) + else boundary.break(value) + catch + case b: boundary.Break[?] => throw b // handle this manually as it will be otherwise caught by NonFatal + case NonFatal(exception) => + if maximumFailures.exists(_ == failures) then // maximum failure count reached + throw exception + else + failures = failures + 1 + lastDelay = delay.delayFor(failures, lastDelay) + sleep(lastDelay) + end while + ??? + + /** Set the maximum failure count. */ + def withMaximumFailures(max: Int) = + assert(max >= 0) + this.copy(maximumFailures = Some(max)) + + /** Set the delay policy between runs. See [[Retry.Delay]]. */ + def withDelay(delay: Delay) = this.copy(delay = delay) + +object Retry: + /** Ignores the result and attempt the action in an infinite loop. [[Retry.withMaximumFailures]] can be useful for + * bailing on multiple failures. [[scala.util.boundary]] can be used for manually breaking. + */ + val forever = Retry(retryOnSuccess = true) + + /** Returns the result, or attempt to retry if an exception is raised. */ + val untilSuccess = Retry(retryOnSuccess = false) + + /** Attempt to retry the operation *until* an exception is raised. In this mode, [[Retry]] always throws an exception + * on return. + */ + val untilFailure = Retry(retryOnSuccess = true).withMaximumFailures(0) + + /** Defines a delay policy based on the number of successive failures and the duration of the last delay. See + * [[Delay]] companion object for some provided delay policies. + */ + trait Delay: + /** Return the expected duration to delay the next attempt from the current attempt. + * + * @param failuresCount + * The number of successive failures until the current attempt. Note that if the last attempt was a success, + * `failuresCount` is `0`. + * @param lastDelay + * The duration of the last delay. + */ + def delayFor(failuresCount: Int, lastDelay: FiniteDuration): FiniteDuration + + object Delay: + /** No delays. */ + val none = constant(0.second) + + /** A fixed amount of delays, whether the last attempt was a success or failure. */ + def constant(duration: FiniteDuration) = new Delay: + def delayFor(failuresCount: Int, lastDelay: FiniteDuration): FiniteDuration = duration + + /** Returns a delay policy for exponential backoff. + * @param maximum + * The maximum duration possible for a delay. + * @param starting + * The delay duration between successful attempts, and after the first failures. + * @param multiplier + * Scale the delay duration by this multiplier for each successive failure. Defaults to `2`. + * @param jitter + * An additional jitter to randomize the delay duration. Defaults to none. See [[Jitter]]. + */ + def backoff(maximum: Duration, starting: FiniteDuration, multiplier: Double = 2, jitter: Jitter = Jitter.none) = + new Delay: + def delayFor(failuresCount: Int, lastDelay: FiniteDuration): FiniteDuration = + val sleep = jitter + .jitterDelay( + lastDelay, + if failuresCount <= 1 then starting + else (starting.toMillis * scala.math.pow(multiplier, failuresCount - 1)).millis + ) + maximum match + case max: FiniteDuration => sleep.min(max) + case _ => sleep /* infinite maximum */ + + /** Decorrelated exponential backoff: randomize between the last delay duration and a multiple of that duration. */ + def deccorelated(maximum: Duration, starting: Duration, multiplier: Double = 3) = + new Delay: + def delayFor(failuresCount: Int, lastDelay: FiniteDuration): FiniteDuration = + val lowerBound = + if failuresCount <= 1 then 0.second else lastDelay + val upperBound = + (if failuresCount <= 1 then starting + else multiplier * lastDelay).min(maximum) + Random.between(lowerBound.toMillis, upperBound.toMillis + 1).millis + + /** A randomizer for the delay duration, to avoid accidental coordinated DoS on failures. See [[Jitter]] companion + * objects for some provided jitter implementations. + */ + trait Jitter: + /** Returns the *actual* duration to delay between attempts, given the theoretical given delay and actual last delay + * duration, possibly with some randomization. + * @param last + * The last delay duration performed, with jitter applied. + * @param maximum + * The theoretical amount of delay governed by the [[Delay]] policy, serving as an upper bound. + */ + def jitterDelay(last: FiniteDuration, maximum: FiniteDuration): FiniteDuration + + object Jitter: + import FiniteDuration as Duration + + /** No jitter, always return the exact duration suggested by the policy. */ + val none = new Jitter: + def jitterDelay(last: Duration, maximum: Duration): Duration = maximum + + /** Full jitter: randomize between 0 and the suggested delay duration. */ + val full = new Jitter: + def jitterDelay(last: Duration, maximum: Duration): Duration = Random.between(0, maximum.toMillis + 1).millis + + /** Equal jitter: randomize between the last delay duration and the suggested delay duration. */ + val equal = new Jitter: + def jitterDelay(last: Duration, maximum: Duration): Duration = + val base = maximum.toMillis / 2 + (base + Random.between(0, maximum.toMillis - base + 1)).millis From 283308f13c9b76a93bb26951ffd2557fefc6c22f Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Tue, 21 Jan 2025 16:53:10 +0100 Subject: [PATCH 2/5] Patch the files to make them compile without JDK21 --- .../captures/gears/VThreadSupport.scala | 7 ++--- .../captures/gears/clientAndServerUDP.scala | 17 +++++----- .../captures/gears/measureTimes.scala | 31 +++++++++++-------- 3 files changed, 29 insertions(+), 26 deletions(-) diff --git a/tests/pos-custom-args/captures/gears/VThreadSupport.scala b/tests/pos-custom-args/captures/gears/VThreadSupport.scala index a287d7627c73..7ea2d44e21bf 100644 --- a/tests/pos-custom-args/captures/gears/VThreadSupport.scala +++ b/tests/pos-custom-args/captures/gears/VThreadSupport.scala @@ -10,10 +10,9 @@ import scala.annotation.constructorOnly import scala.collection.mutable object VThreadScheduler extends Scheduler: - private val VTFactory = Thread - .ofVirtual() - .name("gears.async.VThread-", 0L) - .factory() + private val VTFactory = new java.util.concurrent.ThreadFactory: + def newThread(r: Runnable): Thread = + new Thread(null, r, "gears.async.VThread-", 0L) override def execute(body: Runnable): Unit = val th = VTFactory.newThread(body) diff --git a/tests/pos-custom-args/captures/gears/clientAndServerUDP.scala b/tests/pos-custom-args/captures/gears/clientAndServerUDP.scala index a1c30b41fe52..d0ec49b6aa2e 100644 --- a/tests/pos-custom-args/captures/gears/clientAndServerUDP.scala +++ b/tests/pos-custom-args/captures/gears/clientAndServerUDP.scala @@ -22,14 +22,13 @@ import PosixLikeIO.{PIOHelper, SocketUDP} serverSocket.send(ByteBuffer.wrap(responseMessage), got.getAddress.toString.substring(1), got.getPort) sleep(50) - def client(value: Int): Future[Unit] = - Future: - PIOHelper.withSocketUDP(): clientSocket => - val data: Array[Byte] = value.toString.getBytes - clientSocket.send(ByteBuffer.wrap(data), "localhost", 8134).awaitResult.get - val responseDatagram = clientSocket.receive().awaitResult.get - val messageReceived = String(responseDatagram.getData.slice(0, responseDatagram.getLength), "UTF-8").toInt - println("Sent " + value.toString + " and got " + messageReceived.toString + " in return.") + def client(value: Int)(using Async) = + PIOHelper.withSocketUDP(): clientSocket => + val data: Array[Byte] = value.toString.getBytes + clientSocket.send(ByteBuffer.wrap(data), "localhost", 8134).awaitResult.get + val responseDatagram = clientSocket.receive().awaitResult.get + val messageReceived = String(responseDatagram.getData.slice(0, responseDatagram.getLength), "UTF-8").toInt + println("Sent " + value.toString + " and got " + messageReceived.toString + " in return.") - client(100).await + client(100) server.await diff --git a/tests/pos-custom-args/captures/gears/measureTimes.scala b/tests/pos-custom-args/captures/gears/measureTimes.scala index 5be5c96a79e8..bf44d4280d84 100644 --- a/tests/pos-custom-args/captures/gears/measureTimes.scala +++ b/tests/pos-custom-args/captures/gears/measureTimes.scala @@ -16,12 +16,17 @@ import scala.util.Try import PosixLikeIO.PIOHelper +private val VTFactory = new java.util.concurrent.ThreadFactory: + def newThread(r: Runnable): Thread = + new Thread(null, r, "gears.async.VThread-", 0L) + + case class TimeMeasurementResult(millisecondsPerOperation: Double, standardDeviation: Double) def measureIterations[T](action: () => T): Int = val counter = AtomicInteger(0) - val t1 = Thread.startVirtualThread: () => + val t1 = VTFactory.newThread: () => try { while (!Thread.interrupted()) { action() @@ -41,7 +46,7 @@ def measureIterations[T](action: () => T): Int = given ExecutionContext = ExecutionContext.global val threadJoins = measureIterations: () => - val t = Thread.startVirtualThread: () => + val t = VTFactory.newThread: () => var z = 1 t.join() @@ -114,21 +119,21 @@ def measureIterations[T](action: () => T): Int = val c2: Double = measureIterations: () => @volatile var i1 = true - val f11 = Thread.startVirtualThread(() => { Thread.sleep(10); i1 = false }) - val f12 = Thread.startVirtualThread(() => { Thread.sleep(50); i1 = false }) - val f13 = Thread.startVirtualThread(() => { Thread.sleep(100); i1 = false }) + val f11 = VTFactory.newThread(() => { Thread.sleep(10); i1 = false }) + val f12 = VTFactory.newThread(() => { Thread.sleep(50); i1 = false }) + val f13 = VTFactory.newThread(() => { Thread.sleep(100); i1 = false }) while (i1) () @volatile var i2 = true - val f21 = Thread.startVirtualThread(() => { Thread.sleep(100); i2 = false }) - val f22 = Thread.startVirtualThread(() => { Thread.sleep(10); i2 = false }) - val f23 = Thread.startVirtualThread(() => { Thread.sleep(50); i2 = false }) + val f21 = VTFactory.newThread(() => { Thread.sleep(100); i2 = false }) + val f22 = VTFactory.newThread(() => { Thread.sleep(10); i2 = false }) + val f23 = VTFactory.newThread(() => { Thread.sleep(50); i2 = false }) while (i2) () @volatile var i3 = true - val f31 = Thread.startVirtualThread(() => { Thread.sleep(50); i3 = false }) - val f32 = Thread.startVirtualThread(() => { Thread.sleep(100); i3 = false }) - val f33 = Thread.startVirtualThread(() => { Thread.sleep(10); i3 = false }) + val f31 = VTFactory.newThread(() => { Thread.sleep(50); i3 = false }) + val f32 = VTFactory.newThread(() => { Thread.sleep(100); i3 = false }) + val f33 = VTFactory.newThread(() => { Thread.sleep(10); i3 = false }) while (i3) () f11.interrupt() @@ -164,7 +169,7 @@ def measureIterations[T](action: () => T): Int = // java @volatile var shared: Long = 0 @volatile var timeForWriting = true - val t1 = Thread.startVirtualThread: () => + val t1 = VTFactory.newThread: () => var i: Long = 0 while (true) { while (!timeForWriting) () @@ -173,7 +178,7 @@ def measureIterations[T](action: () => T): Int = i += 1 } - val t2 = Thread.startVirtualThread: () => + val t2 = VTFactory.newThread: () => while (true) { while (timeForWriting) () var z = shared From 766f84feccbcaf9e7449e1f364eb091b92492725 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Tue, 21 Jan 2025 16:55:40 +0100 Subject: [PATCH 3/5] Add original version of gears repo for neg tests --- .../captures/gears/Async.scala | 435 +++++++++++++ .../captures/gears/AsyncOperations.scala | 53 ++ .../captures/gears/AsyncSupport.scala | 46 ++ .../captures/gears/CCBehavior.scala | 117 ++++ .../captures/gears/Cancellable.scala | 48 ++ .../captures/gears/CompletionGroup.scala | 59 ++ .../captures/gears/DefaultSupport.scala | 7 + .../captures/gears/JvmAsyncOperations.scala | 19 + .../captures/gears/Listener.scala | 113 ++++ .../neg-custom-args/captures/gears/PIO.scala | 170 +++++ .../captures/gears/ScalaConverters.scala | 29 + .../captures/gears/Timer.scala | 80 +++ .../captures/gears/VThreadSupport.scala | 136 ++++ .../captures/gears/channels.scala | 457 ++++++++++++++ .../captures/gears/clientAndServerUDP.scala | 35 ++ .../captures/gears/futures.scala | 580 ++++++++++++++++++ .../captures/gears/locking.scala | 48 ++ .../captures/gears/measureTimes.scala | 512 ++++++++++++++++ .../captures/gears/package.scala | 14 + .../captures/gears/readAndWriteFile.scala | 20 + .../captures/gears/readWholeFile.scala | 25 + .../captures/gears/retry.scala | 156 +++++ 22 files changed, 3159 insertions(+) create mode 100644 tests/neg-custom-args/captures/gears/Async.scala create mode 100644 tests/neg-custom-args/captures/gears/AsyncOperations.scala create mode 100644 tests/neg-custom-args/captures/gears/AsyncSupport.scala create mode 100644 tests/neg-custom-args/captures/gears/CCBehavior.scala create mode 100644 tests/neg-custom-args/captures/gears/Cancellable.scala create mode 100644 tests/neg-custom-args/captures/gears/CompletionGroup.scala create mode 100644 tests/neg-custom-args/captures/gears/DefaultSupport.scala create mode 100644 tests/neg-custom-args/captures/gears/JvmAsyncOperations.scala create mode 100644 tests/neg-custom-args/captures/gears/Listener.scala create mode 100644 tests/neg-custom-args/captures/gears/PIO.scala create mode 100644 tests/neg-custom-args/captures/gears/ScalaConverters.scala create mode 100644 tests/neg-custom-args/captures/gears/Timer.scala create mode 100644 tests/neg-custom-args/captures/gears/VThreadSupport.scala create mode 100644 tests/neg-custom-args/captures/gears/channels.scala create mode 100644 tests/neg-custom-args/captures/gears/clientAndServerUDP.scala create mode 100644 tests/neg-custom-args/captures/gears/futures.scala create mode 100644 tests/neg-custom-args/captures/gears/locking.scala create mode 100644 tests/neg-custom-args/captures/gears/measureTimes.scala create mode 100644 tests/neg-custom-args/captures/gears/package.scala create mode 100644 tests/neg-custom-args/captures/gears/readAndWriteFile.scala create mode 100644 tests/neg-custom-args/captures/gears/readWholeFile.scala create mode 100644 tests/neg-custom-args/captures/gears/retry.scala diff --git a/tests/neg-custom-args/captures/gears/Async.scala b/tests/neg-custom-args/captures/gears/Async.scala new file mode 100644 index 000000000000..d0a866f2df00 --- /dev/null +++ b/tests/neg-custom-args/captures/gears/Async.scala @@ -0,0 +1,435 @@ +package gears.async + +import language.experimental.captureChecking + +import gears.async.Listener.NumberedLock +import gears.async.Listener.withLock + +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicLong +import java.util.concurrent.locks.ReentrantLock +import scala.collection.mutable +import scala.util.boundary + +/** The async context: provides the capability to asynchronously [[Async.await await]] for [[Async.Source Source]]s, and + * defines a scope for structured concurrency through a [[CompletionGroup]]. + * + * As both a context and a capability, the idiomatic way of using [[Async]] is to be implicitly passed around + * functions, as an `using` parameter: + * {{{ + * def function()(using Async): T = ??? + * }}} + * + * It is not recommended to store [[Async]] in a class field, since it complicates scoping rules. + * + * @param support + * An implementation of the underlying asynchronous operations (suspend and resume). See [[AsyncSupport]]. + * @param scheduler + * An implementation of a scheduler, for scheduling computation as they are spawned or resumed. See [[Scheduler]]. + * + * @see + * [[Async$.blocking Async.blocking]] for a way to construct an [[Async]] instance. + * @see + * [[Async$.group Async.group]] and [[Future$.apply Future.apply]] for [[Async]]-subscoping operations. + */ +trait Async(using val support: AsyncSupport, val scheduler: support.Scheduler) extends caps.Capability: + /** Waits for completion of source `src` and returns the result. Suspends the computation. + * + * @see + * [[Async.Source.awaitResult]] and [[Async$.await]] for extension methods calling [[Async!.await]] from the source + * itself. + */ + def await[T](src: Async.Source[T]^): T + + /** Returns the cancellation group for this [[Async]] context. */ + def group: CompletionGroup + + /** Returns an [[Async]] context of the same kind as this one, with a new cancellation group. */ + def withGroup(group: CompletionGroup): Async + +object Async: + private class Blocking(val group: CompletionGroup)(using support: AsyncSupport, scheduler: support.Scheduler) + extends Async(using support, scheduler): + private val lock = ReentrantLock() + private val condVar = lock.newCondition() + + /** Wait for completion of async source `src` and return the result */ + override def await[T](src: Async.Source[T]^): T = + src + .poll() + .getOrElse: + var result: Option[T] = None + src.onComplete: + Listener.acceptingListener: (t, _) => + lock.lock() + try + result = Some(t) + condVar.signalAll() + finally lock.unlock() + + lock.lock() + try + while result.isEmpty do condVar.await() + result.get + finally lock.unlock() + + /** An Async of the same kind as this one, with a new cancellation group */ + override def withGroup(group: CompletionGroup): Async = Blocking(group) + + /** Execute asynchronous computation `body` on currently running thread. The thread will suspend when the computation + * waits. + */ + def blocking[T](body: Async.Spawn ?=> T)(using support: AsyncSupport, scheduler: support.Scheduler): T = + group(body)(using Blocking(CompletionGroup.Unlinked)) + + /** Returns the currently executing Async context. Equivalent to `summon[Async]`. */ + inline def current(using async: Async): async.type = async + + /** [[Async.Spawn]] is a special subtype of [[Async]], also capable of spawning runnable [[Future]]s. + * + * Most functions should not take [[Spawn]] as a parameter, unless the function explicitly wants to spawn "dangling" + * runnable [[Future]]s. Instead, functions should take [[Async]] and spawn scoped futures within [[Async.group]]. + */ + opaque type Spawn <: Async = Async + + /** Runs `body` inside a spawnable context where it is allowed to spawn concurrently runnable [[Future]]s. When the + * body returns, all spawned futures are cancelled and waited for. + */ + def group[T](body: Async.Spawn ?=> T)(using Async): T = + withNewCompletionGroup(CompletionGroup().link())(body) + + /** Runs a body within another completion group. When the body returns, the group is cancelled and its completion + * awaited with the `Unlinked` group. + */ + private[async] def withNewCompletionGroup[T](group: CompletionGroup)(body: Async.Spawn ?=> T)(using + async: Async + ): T = + val completionAsync = + if CompletionGroup.Unlinked == async.group + then async + else async.withGroup(CompletionGroup.Unlinked) + + try body(using async.withGroup(group)) + finally + group.cancel() + group.waitCompletion()(using completionAsync) + + /** An asynchronous data source. Sources can be persistent or ephemeral. A persistent source will always pass same + * data to calls of [[Source!.poll]] and [[Source!.onComplete]]. An ephemeral source can pass new data in every call. + * + * @see + * An example of a persistent source is [[gears.async.Future]]. + * @see + * An example of an ephemeral source is [[gears.async.Channel]]. + */ + trait Source[+T]: + /** The unique symbol representing the current source. */ + val symbol: SourceSymbol[T] = SourceSymbol.next + /** Checks whether data is available at present and pass it to `k` if so. Calls to `poll` are always synchronous and + * non-blocking. + * + * The process is as follows: + * - If no data is immediately available, return `false` immediately. + * - If there is data available, attempt to lock `k`. + * - If `k` is no longer available, `true` is returned to signal this source's general availability. + * - If locking `k` succeeds: + * - If data is still available, complete `k` and return true. + * - Otherwise, unlock `k` and return false. + * + * Note that in all cases, a return value of `false` indicates that `k` should be put into `onComplete` to receive + * data in a later point in time. + * + * @return + * Whether poll was able to pass data to `k`. Note that this is regardless of `k` being available to receive the + * data. In most cases, one should pass `k` into [[Source!.onComplete]] if `poll` returns `false`. + */ + def poll(k: Listener[T]^): Boolean + + /** Once data is available, pass it to the listener `k`. `onComplete` is always non-blocking. + * + * Note that `k`'s methods will be executed on the same thread as the [[Source]], usually in sequence. It is hence + * important that the listener itself does not perform expensive operations. + */ + def onComplete(k: Listener[T]^): Unit + + /** Signal that listener `k` is dead (i.e. will always fail to acquire locks from now on), and should be removed + * from `onComplete` queues. + * + * This permits original, (i.e. non-derived) sources like futures or channels to drop the listener from their + * waiting sets. + */ + def dropListener(k: Listener[T]^): Unit + + /** Similar to [[Async.Source!.poll(k:Listener[T])* poll]], but instead of passing in a listener, directly return + * the value `T` if it is available. + */ + def poll(): Option[T] = + var resultOpt: Option[T] = None + poll(Listener.acceptingListener { (x, _) => resultOpt = Some(x) }) + resultOpt + + /** Waits for an item to arrive from the source. Suspends until an item returns. + * + * This is an utility method for direct waiting with `Async`, instead of going through listeners. + */ + final def awaitResult(using ac: Async) = ac.await(this) + end Source + + // an opaque identity for symbols + opaque type SourceSymbol[+T] = Long + private [Async] object SourceSymbol: + private val index = AtomicLong() + inline def next: SourceSymbol[Any] = + index.incrementAndGet() + // ... it can be quickly obtained from any Source + given[T]: scala.Conversion[Source[T], SourceSymbol[T]] = _.symbol + + extension [T](src: Source[scala.util.Try[T]]^) + /** Waits for an item to arrive from the source, then automatically unwraps it. Suspends until an item returns. + * @see + * [[Source!.awaitResult awaitResult]] for non-unwrapping await. + */ + def await(using Async): T = src.awaitResult.get + extension [E, T](src: Source[Either[E, T]]^) + /** Waits for an item to arrive from the source, then automatically unwraps it. Suspends until an item returns. + * @see + * [[Source!.awaitResult awaitResult]] for non-unwrapping await. + */ + inline def await(using inline async: Async) = src.awaitResult.right.get + + /** An original source has a standard definition of [[Source.onComplete onComplete]] in terms of [[Source.poll poll]] + * and [[OriginalSource.addListener addListener]]. + * + * Implementations should be the resource owner to handle listener queue and completion using an object monitor on + * the instance. + */ + abstract class OriginalSource[+T] extends Source[T]: + /** Add `k` to the listener set of this source. */ + protected def addListener(k: Listener[T]^): Unit + + def onComplete(k: Listener[T]^): Unit = synchronized: + if !poll(k) then addListener(k) + + end OriginalSource + + object Source: + /** Create a [[Source]] containing the given values, resolved once for each. + * + * @return + * an ephemeral source of values arriving to listeners in a queue. Once all values are received, attaching a + * listener with [[Source!.onComplete onComplete]] will be a no-op (i.e. the listener will never be called). + */ + def values[T](values: T*) = + import scala.collection.JavaConverters._ + val q = java.util.concurrent.ConcurrentLinkedQueue[T]() + q.addAll(values.asJavaCollection) + new Source[T]: + override def poll(k: Listener[T]^): Boolean = + if q.isEmpty() then false + else if !k.acquireLock() then true + else + val item = q.poll() + if item == null then + k.releaseLock() + false + else + k.complete(item, this) + true + + override def onComplete(k: Listener[T]^): Unit = poll(k) + override def dropListener(k: Listener[T]^): Unit = () + end values + + extension [T](src: Source[T]^) + /** Create a new source that requires the original source to run the given transformation function on every value + * received. + * + * Note that `f` is **always** run on the computation that produces the values from the original source, so this is + * very likely to run **sequentially** and be a performance bottleneck. + * + * @param f + * the transformation function to be run on every value. `f` is run *before* the item is passed to the + * [[Listener]]. + */ + def transformValuesWith[U](f: T => U): Source[U]^{f, src} = + new Source[U]: + val selfSrc = this + def transform(k: Listener[U]^): Listener.ForwardingListener[T]^{k, f} = + new Listener.ForwardingListener[T](selfSrc, k): + val lock = k.lock + def complete(data: T, source: SourceSymbol[T]) = + k.complete(f(data), selfSrc) + + def poll(k: Listener[U]^): Boolean = + src.poll(transform(k)) + def onComplete(k: Listener[U]^): Unit = + src.onComplete(transform(k)) + def dropListener(k: Listener[U]^): Unit = + src.dropListener(transform(k)) + + /** Creates a source that "races" a list of sources. + * + * Listeners attached to this source is resolved with the first item arriving from one of the sources. If multiple + * sources are available at the same time, one of the items will be returned with no priority. Items that are not + * returned are '''not''' consumed from the upstream sources. + * + * @see + * [[raceWithOrigin]] for a race source that also returns the upstream origin of the item. + * @see + * [[Async$.select Async.select]] for a convenient syntax to race sources and awaiting them with [[Async]]. + */ + def race[T](@caps.use sources: Seq[Source[T]^]): Source[T]^{sources*} = raceImpl((v: T, _: SourceSymbol[T]) => v)(sources) + def race[T](s1: Source[T]^): Source[T]^{s1} = race(Seq(s1)) + def race[T](s1: Source[T]^, s2: Source[T]^): Source[T]^{s1, s2} = race(Seq(s1, s2)) + def race[T](s1: Source[T]^, s2: Source[T]^, s3: Source[T]^): Source[T]^{s1, s2, s3} = race(Seq(s1, s2, s3)) + + /** Like [[race]], but the returned value includes a reference to the upstream source that the item came from. + * @see + * [[Async$.select Async.select]] for a convenient syntax to race sources and awaiting them with [[Async]]. + */ + def raceWithOrigin[T](@caps.use sources: (Source[T]^)*): Source[(T, SourceSymbol[T])]^{sources*} = + raceImpl((v: T, src: SourceSymbol[T]) => (v, src))(sources) + + /** Pass first result from any of `sources` to the continuation */ + private def raceImpl[T, U](map: (U, SourceSymbol[U]) -> T)(@caps.use sources: Seq[Source[U]^]): Source[T]^{sources*} = + new Source[T]: + val selfSrc = this + def poll(k: Listener[T]^): Boolean = + val it = sources.iterator + var found = false + + val listener: Listener[U]^{k} = new Listener.ForwardingListener[U](selfSrc, k): + val lock = k.lock + def complete(data: U, source: SourceSymbol[U]) = + k.complete(map(data, source), selfSrc) + end listener + + while it.hasNext && !found do found = it.next.poll(listener) + + found + + def dropAll(l: Listener[U]^) = sources.foreach(_.dropListener(l)) + + def onComplete(k: Listener[T]^): Unit = + val listener: Listener[U]^{k, sources*} = new Listener.ForwardingListener[U](this, k) { + val self = this + inline def lockIsOurs = k.lock == null + val lock = + if k.lock != null then + // if the upstream listener holds a lock already, we can utilize it. + new Listener.ListenerLock: + val selfNumber = k.lock.selfNumber + override def acquire() = + if found then false // already completed + else if !k.lock.acquire() then + if !found && !synchronized { // getAndSet alternative, avoid racing only with self here. + val old = found + found = true + old + } + then dropAll(self) // same as dropListener(k), but avoids an allocation + false + else if found then + k.lock.release() + false + else true + override def release() = k.lock.release() + else + new Listener.ListenerLock with NumberedLock: + val selfNumber: Long = number + def acquire() = + if found then false + else + acquireLock() + if found then + releaseLock() + // no cleanup needed here, since we have done this by an earlier `complete` or `lockNext` + false + else true + def release() = + releaseLock() + + var found = false + + def complete(item: U, src: SourceSymbol[U]) = + found = true + if lockIsOurs then lock.release() + sources.foreach(s => if s.symbol != src then s.dropListener(self)) + k.complete(map(item, src), selfSrc) + } // end listener + + sources.foreach(_.onComplete(listener)) + + def dropListener(k: Listener[T]^): Unit = + val listener = Listener.ForwardingListener.empty(this, k) + sources.foreach(_.dropListener(listener)) + + + /** Cases for handling async sources in a [[select]]. [[SelectCase]] can be constructed by extension methods `handle` + * of [[Source]]. + * + * @see + * [[handle Source.handle]] (and its operator alias [[~~> ~~>]]) + * @see + * [[Async$.select Async.select]] where [[SelectCase]] is used. + */ + trait SelectCase[+T]: + type Src + val src: Source[Src]^ + val f: Src => T + inline final def apply(input: Src) = f(input) + + extension [T](_src: Source[T]^) + /** Attach a handler to `src`, creating a [[SelectCase]]. + * @see + * [[Async$.select Async.select]] where [[SelectCase]] is used. + */ + def handle[U](_f: T => U): SelectCase[U]^{_src, _f} = new SelectCase: + type Src = T + val src = _src + val f = _f + + /** Alias for [[handle]] + * @see + * [[Async$.select Async.select]] where [[SelectCase]] is used. + */ + inline def ~~>[U](_f: T => U): SelectCase[U]^{_src, _f} = _src.handle(_f) + + /** Race a list of sources with the corresponding handler functions, once an item has come back. Like [[race]], + * [[select]] guarantees exactly one of the sources are polled. Unlike [[transformValuesWith]], the handler in + * [[select]] is run in the same async context as the calling context of [[select]]. + * + * @see + * [[handle Source.handle]] (and its operator alias [[~~> ~~>]]) for methods to create [[SelectCase]]s. + * @example + * {{{ + * // Race a channel read with a timeout + * val ch = SyncChannel[Int]() + * // ... + * val timeout = Future(sleep(1500.millis)) + * + * Async.select( + * ch.readSrc.handle: item => + * Some(item * 2), + * timeout ~~> _ => None + * ) + * }}} + */ + def select[T](@caps.use cases: (SelectCase[T]^)*)(using Async) = + val (input, which) = raceWithOrigin(cases.map(_.src)*).awaitResult + val sc = cases.find(_.src.symbol == which).get + sc(input.asInstanceOf[sc.Src]) + + /** Race two sources, wrapping them respectively in [[Left]] and [[Right]] cases. + * @return + * a new [[Source]] that resolves with [[Left]] if `src1` returns an item, [[Right]] if `src2` returns an item, + * whichever comes first. + * @see + * [[race]] and [[select]] for racing more than two sources. + */ + def either[T1, T2](src1: Source[T1]^, src2: Source[T2]^): Source[Either[T1, T2]]^{src1, src2} = + val left = src1.transformValuesWith(Left(_)) + val right = src2.transformValuesWith(Right(_)) + race(left, right) +end Async + diff --git a/tests/neg-custom-args/captures/gears/AsyncOperations.scala b/tests/neg-custom-args/captures/gears/AsyncOperations.scala new file mode 100644 index 000000000000..ab7228d505d0 --- /dev/null +++ b/tests/neg-custom-args/captures/gears/AsyncOperations.scala @@ -0,0 +1,53 @@ +package gears.async + +import language.experimental.captureChecking + +import gears.async.AsyncOperations.sleep + +import java.util.concurrent.TimeoutException +import scala.concurrent.duration.FiniteDuration + +/** Defines fundamental operations that require the support of the scheduler. This is commonly provided alongside with + * the given implementation of [[Scheduler]]. + * @see + * [[Scheduler]] for the definition of the scheduler itself. + */ +trait AsyncOperations: + /** Suspends the current [[Async]] context for at least `millis` milliseconds. */ + def sleep(millis: Long)(using Async): Unit + +object AsyncOperations: + /** Suspends the current [[Async]] context for at least `millis` milliseconds. + * @param millis + * The duration to suspend, in milliseconds. Must be a positive integer. + */ + inline def sleep(millis: Long)(using AsyncOperations, Async): Unit = + summon[AsyncOperations].sleep(millis) + + /** Suspends the current [[Async]] context for `duration`. + * @param duration + * The duration to suspend. Must be positive. + */ + inline def sleep(duration: FiniteDuration)(using AsyncOperations, Async): Unit = + sleep(duration.toMillis) + +/** Runs `op` with a timeout. When the timeout occurs, `op` is cancelled through the given [[Async]] context, and + * [[java.util.concurrent.TimeoutException]] is thrown. + */ +def withTimeout[T](timeout: FiniteDuration)(op: Async ?=> T)(using AsyncOperations, Async): T = + Async.group: spawn ?=> + Async.select( + Future(op).handle(_.get), + Future(sleep(timeout)).handle: _ => + throw TimeoutException() + ) + +/** Runs `op` with a timeout. When the timeout occurs, `op` is cancelled through the given [[Async]] context, and + * [[None]] is returned. + */ +def withTimeoutOption[T](timeout: FiniteDuration)(op: Async ?=> T)(using AsyncOperations, Async): Option[T] = + Async.group: + Async.select( + Future(op).handle(v => Some(v.get)), + Future(sleep(timeout)).handle(_ => None) + ) diff --git a/tests/neg-custom-args/captures/gears/AsyncSupport.scala b/tests/neg-custom-args/captures/gears/AsyncSupport.scala new file mode 100644 index 000000000000..3005f512c401 --- /dev/null +++ b/tests/neg-custom-args/captures/gears/AsyncSupport.scala @@ -0,0 +1,46 @@ +package gears.async + +import language.experimental.captureChecking + +import scala.concurrent.duration._ +import scala.annotation.capability + +/** The delimited continuation suspension interface. Represents a suspended computation asking for a value of type `T` + * to continue (and eventually returning a value of type `R`). + */ +trait Suspension[-T, +R]: + def resume(arg: T): R + +/** Support for suspension capabilities through a delimited continuation interface. */ +trait SuspendSupport: + /** A marker for the "limit" of "delimited continuation". */ + type Label[R, Cap^] <: caps.Capability + + /** The provided suspension type. */ + type Suspension[-T, +R] <: gears.async.Suspension[T, R] + + /** Set the suspension marker as the body's caller, and execute `body`. */ + def boundary[R, Cap^](body: Label[R, Cap] ?->{Cap^} R): R^{Cap^} + + /** Should return immediately if resume is called from within body */ + def suspend[T, R, Cap^](body: Suspension[T, R]^{Cap^} => R^{Cap^})(using Label[R, Cap]): T + +/** Extends [[SuspendSupport]] with "asynchronous" boundary/resume functions, in the presence of a [[Scheduler]] */ +trait AsyncSupport extends SuspendSupport: + type Scheduler <: gears.async.Scheduler + + /** Resume a [[Suspension]] at some point in the future, scheduled by the scheduler. */ + private[async] def resumeAsync[T, R](suspension: Suspension[T, R])(arg: T)(using s: Scheduler): Unit = + s.execute(() => suspension.resume(arg)) + + /** Schedule a computation with the suspension boundary already created. */ + private[async] def scheduleBoundary[Cap^](body: Label[Unit, Cap] ?-> Unit)(using s: Scheduler): Unit = + s.execute(() => boundary[Unit, Cap](body)) + +/** A scheduler implementation, with the ability to execute a computation immediately or after a delay. */ +trait Scheduler: + def execute(body: Runnable): Unit + def schedule(delay: FiniteDuration, body: Runnable): Cancellable + +object AsyncSupport: + inline def apply()(using ac: AsyncSupport) = ac diff --git a/tests/neg-custom-args/captures/gears/CCBehavior.scala b/tests/neg-custom-args/captures/gears/CCBehavior.scala new file mode 100644 index 000000000000..a1e207696433 --- /dev/null +++ b/tests/neg-custom-args/captures/gears/CCBehavior.scala @@ -0,0 +1,117 @@ +import language.experimental.captureChecking + +import gears.async.AsyncOperations.* +import gears.async.default.given +import gears.async.{Async, AsyncSupport, Future, uninterruptible} + +import java.util.concurrent.CancellationException +import scala.annotation.capability +import scala.concurrent.duration.{Duration, DurationInt} +import scala.util.Success +import scala.util.boundary +import gears.async.Channel +import gears.async.SyncChannel + +type Result[+T, +E] = Either[E, T] +object Result: + opaque type Label[-T, -E] = boundary.Label[Result[T, E]] + // ^ doesn't work? + + def apply[T, E](body: Label[T, E]^ ?=> T): Result[T, E] = + boundary(Right(body)) + + extension [U, E](r: Result[U, E])(using Label[Nothing, E]^) + def ok: U = r match + case Left(value) => boundary.break(Left(value)) + case Right(value) => value + +class CaptureCheckingBehavior extends munit.FunSuite: + import Result.* + import caps.use + import scala.collection.mutable + + test("good") { + // don't do this in real code! capturing Async.blocking's Async context across functions is hard to track + Async.blocking: async ?=> + def good1[T, E](@use frs: List[Future[Result[T, E]]^]): Future[Result[List[T], E]]^{frs*, async} = + Future: fut ?=> + Result: ret ?=> + frs.map(_.await.ok) + + def good2[T, E](@use rf: Result[Future[T]^, E]): Future[Result[T, E]]^{rf*, async} = + Future: + Result: + rf.ok.await // OK, Future argument has type Result[T] + + def useless4[T, E](fr: Future[Result[T, E]]^) = + fr.await.map(Future(_)) + } + + // test("bad - collectors") { + // val futs: Seq[Future[Int]^] = Async.blocking: async ?=> + // val fs: Seq[Future[Int]^{async}] = (0 to 10).map(i => Future { i }) + // fs + // Async.blocking: + // futs.awaitAll // should not compile + // } + + test("future withResolver capturing") { + class File() extends caps.Capability: + def close() = () + def read(callback: Int => Unit) = () + val f = File() + val read = Future.withResolver[Int, caps.CapSet^{f}]: r => + f.read(r.resolve) + r.onCancel(f.close) + } + + test("awaitAll/awaitFirst") { + trait File extends caps.Capability: + def readFut(): Future[Int]^{this} + object File: + def open[T](filename: String)(body: File => T)(using Async): T = ??? + + def readAll(@caps.use files: (File^)*) = files.map(_.readFut()) + + Async.blocking: + File.open("a.txt"): a => + File.open("b.txt"): b => + val futs = readAll(a, b) + val allFut = Future(futs.awaitAll) + allFut + .await // uncomment to leak + } + + // test("channel") { + // trait File extends caps.Capability: + // def read(): Int = ??? + // Async.blocking: + // val ch = SyncChannel[File]() + // // Sender + // val sender = Future: + // val f = new File {} + // ch.send(f) + // val recv = Future: + // val f = ch.read().right.get + // f.read() + // } + + test("very bad") { + Async.blocking: async ?=> + def fail3[T, E](fr: Future[Result[T, E]]^): Result[Any, Any] = + Result: label ?=> + Future: fut ?=> + fr.await.ok // error, escaping label from Result + + // val fut = Future(Left(5)) + // val res = fail3(fut) + // println(res.right.get.asInstanceOf[Future[Any]].awaitResult) + } + + // test("bad") { + // Async.blocking: async ?=> + // def fail3[T, E](fr: Future[Result[T, E]]^): Result[Future[T]^{async}, E] = + // Result: label ?=> + // Future: fut ?=> + // fr.await.ok // error, escaping label from Result + // } diff --git a/tests/neg-custom-args/captures/gears/Cancellable.scala b/tests/neg-custom-args/captures/gears/Cancellable.scala new file mode 100644 index 000000000000..aa7d70e07aa5 --- /dev/null +++ b/tests/neg-custom-args/captures/gears/Cancellable.scala @@ -0,0 +1,48 @@ +package gears.async + +import language.experimental.captureChecking + +/** A trait for cancellable entities that can be grouped. */ +trait Cancellable: + private var group: CompletionGroup = CompletionGroup.Unlinked + + /** Issue a cancel request */ + def cancel(): Unit + + /** Add this cancellable to the given group after removing it from the previous group in which it was. + */ + def link(group: CompletionGroup): this.type = synchronized: + this.group.drop(this.unsafeAssumePure) + this.group = group + this.group.add(this.unsafeAssumePure) + this + + /** Link this cancellable to the cancellable group of the current async context. + */ + def link()(using async: Async): this.type = + link(async.group) + + /** Unlink this cancellable from its group. */ + def unlink(): this.type = + link(CompletionGroup.Unlinked) + + /** Assume that the [[Cancellable]] is pure, in the case that cancellation does *not* refer to captured resources. + */ + inline def unsafeAssumePure: Cancellable = caps.unsafe.unsafeAssumePure(this) + +end Cancellable + +object Cancellable: + /** A special [[Cancellable]] object that just tracks whether its linked group was cancelled. */ + trait Tracking extends Cancellable: + def isCancelled: Boolean + + object Tracking: + def apply() = new Tracking: + private var cancelled: Boolean = false + + def cancel(): Unit = + cancelled = true + + def isCancelled = cancelled +end Cancellable diff --git a/tests/neg-custom-args/captures/gears/CompletionGroup.scala b/tests/neg-custom-args/captures/gears/CompletionGroup.scala new file mode 100644 index 000000000000..8a2b01daaf6a --- /dev/null +++ b/tests/neg-custom-args/captures/gears/CompletionGroup.scala @@ -0,0 +1,59 @@ +package gears.async +import language.experimental.captureChecking + +import scala.collection.mutable +import scala.util.Success + +import Future.Promise + +/** A group of cancellable objects that are completed together. Cancelling the group means cancelling all its + * uncompleted members. + */ +class CompletionGroup extends Cancellable.Tracking: + private val members: mutable.Set[Cancellable] = mutable.Set() + private var canceled: Boolean = false + private var cancelWait: Option[Promise[Unit]] = None + + /** Cancel all members */ + def cancel(): Unit = + synchronized: + if canceled then Seq.empty + else + canceled = true + members.toSeq + .foreach(_.cancel()) + + /** Wait for all members of the group to complete and unlink themselves. */ + private[async] def waitCompletion()(using Async): Unit = + synchronized: + if members.nonEmpty && cancelWait.isEmpty then cancelWait = Some(Promise()) + cancelWait.foreach(cWait => cWait.await) + unlink() + + /** Add given member to the members set. If the group has already been cancelled, cancels that member immediately. */ + def add(member: Cancellable): Unit = + val alreadyCancelled = synchronized: + members += member // Add this member no matter what since we'll wait for it still + canceled + if alreadyCancelled then member.cancel() + + /** Remove given member from the members set if it is an element */ + def drop(member: Cancellable): Unit = synchronized: + members -= member + if members.isEmpty && cancelWait.isDefined then cancelWait.get.complete(Success(())) + + def isCancelled = canceled + +object CompletionGroup: + + /** A sentinel group of cancellables that are in fact not linked to any real group. `cancel`, `add`, and `drop` do + * nothing when called on this group. + */ + object Unlinked extends CompletionGroup: + override def cancel(): Unit = () + override def waitCompletion()(using Async): Unit = () + override def add(member: Cancellable): Unit = () + override def drop(member: Cancellable): Unit = () + end Unlinked + +end CompletionGroup diff --git a/tests/neg-custom-args/captures/gears/DefaultSupport.scala b/tests/neg-custom-args/captures/gears/DefaultSupport.scala new file mode 100644 index 000000000000..53dbe01eb15f --- /dev/null +++ b/tests/neg-custom-args/captures/gears/DefaultSupport.scala @@ -0,0 +1,7 @@ +package gears.async.default + +import gears.async._ + +given AsyncOperations = JvmAsyncOperations +given VThreadSupport.type = VThreadSupport +given VThreadScheduler.type = VThreadScheduler diff --git a/tests/neg-custom-args/captures/gears/JvmAsyncOperations.scala b/tests/neg-custom-args/captures/gears/JvmAsyncOperations.scala new file mode 100644 index 000000000000..b33ad94e944f --- /dev/null +++ b/tests/neg-custom-args/captures/gears/JvmAsyncOperations.scala @@ -0,0 +1,19 @@ +package gears.async + +import language.experimental.captureChecking + +object JvmAsyncOperations extends AsyncOperations: + override def sleep(millis: Long)(using Async): Unit = + jvmInterruptible(Thread.sleep(millis)) + + /** Runs `fn` in a [[cancellationScope]] where it will be interrupted (as a Java thread) upon cancellation. + * + * Note that `fn` will need to handle both [[java.util.concurrent.CancellationException]] (when performing Gears + * operations such as `.await`) *and* [[java.lang.InterruptedException]], so the intended use case is usually to wrap + * interruptible Java operations, containing `fn` to a narrow scope. + */ + def jvmInterruptible[T](fn: => T)(using Async): T = + val th = Thread.currentThread() + cancellationScope(() => th.interrupt()): + try fn + catch case _: InterruptedException => throw new CancellationException() diff --git a/tests/neg-custom-args/captures/gears/Listener.scala b/tests/neg-custom-args/captures/gears/Listener.scala new file mode 100644 index 000000000000..56edd38728b7 --- /dev/null +++ b/tests/neg-custom-args/captures/gears/Listener.scala @@ -0,0 +1,113 @@ +package gears.async + +import language.experimental.captureChecking + +import gears.async.Async.Source +import gears.async.Async.SourceSymbol + +import java.util.concurrent.locks.ReentrantLock +import scala.annotation.tailrec + +/** A listener, representing an one-time value receiver of an [[Async.Source]]. + * + * Most of the time listeners should involve only calling a receiver function, and can be created by [[Listener.apply]] + * or [[Listener.acceptingListener]]. + * + * However, should the listener want to attempt synchronization, it has to expose some locking-related interfaces. See + * [[Listener.lock]]. + */ +trait Listener[-T]: + import Listener._ + + /** Complete the listener with the given item, from the given source. **If the listener exposes a + * [[Listener.ListenerLock]]**, it is required to acquire this lock before calling [[complete]]. This can also be + * done conveniently with [[completeNow]]. For performance reasons, this condition is usually not checked and will + * end up causing unexpected behavior if not satisfied. + * + * The listener must automatically release its own lock upon completion. + */ + def complete(data: T, source: Async.SourceSymbol[T]): Unit + + /** Represents the exposed API for synchronization on listeners at receiving time. If the listener does not have any + * form of synchronization, [[lock]] should be `null`. + */ + val lock: (Listener.ListenerLock^) | Null + + /** Attempts to acquire locks and then calling [[complete]] with the given item and source. If locking fails, + * [[releaseLock]] is automatically called. + */ + def completeNow(data: T, source: Async.SourceSymbol[T]): Boolean = + if acquireLock() then + this.complete(data, source) + true + else false + + /** Release the listener's lock if it exists. */ + inline final def releaseLock(): Unit = if lock != null then lock.release() + + /** Attempts to lock the listener, if such a lock exists. Succeeds with `true` immediately if [[lock]] is `null`. + */ + inline final def acquireLock(): Boolean = + if lock != null then lock.acquire() else true + +object Listener: + /** A simple [[Listener]] that always accepts the item and sends it to the consumer. */ + /* inline bug */ def acceptingListener[T](consumer: (T, SourceSymbol[T]) => Unit): Listener[T]^{consumer} = + new Listener[T]: + val lock = null + def complete(data: T, source: SourceSymbol[T]) = consumer(data, source) + + /** Returns a simple [[Listener]] that always accepts the item and sends it to the consumer. */ + def apply[T](consumer: (T, SourceSymbol[T]) => Unit): Listener[T]^{consumer} = acceptingListener(consumer) + + /** A special class of listener that forwards the inner listener through the given source. For purposes of + * [[Async.Source.dropListener]] these listeners are compared for equality by the hash of the source and the inner + * listener. + */ + abstract case class ForwardingListener[-T](src: Async.Source[?]^, inner: Listener[?]^) extends Listener[T] + + object ForwardingListener: + /** Creates an empty [[ForwardingListener]] for equality comparison. */ + def empty(src: Async.Source[?]^, inner: Listener[?]^): ForwardingListener[Any]^{src, inner} = new ForwardingListener[Any](src, inner): + val lock = null + override def complete(data: Any, source: SourceSymbol[Any]) = ??? + + /** A lock required by a listener to be acquired before accepting values. Should there be multiple listeners that + * needs to be locked at the same time, they should be locked by larger-number-first. + * + * Some implementations are provided for ease of implementations: + * - For custom listener implementations involving locks: [[NumberedLock]] provides uniquely numbered locks. + * - For source transformation implementations: [[withLock]] is a convenient `.map` for `[[ListenerLock]] | Null`. + */ + trait ListenerLock: + /** The assigned number of the lock. It is required that listeners that can be locked together to have different + * [[selfNumber numbers]]. This requirement can be simply done by using a lock created using [[NumberedLock]]. + */ + val selfNumber: Long + + /** Attempt to lock the current [[ListenerLock]]. Locks are guaranteed to be held as short as possible. + */ + def acquire(): Boolean + + /** Release the current lock. */ + def release(): Unit + end ListenerLock + + /** Maps the lock of a listener, if it exists. */ + inline def withLock[T](listener: Listener[?])(inline body: ListenerLock => T): T | Null = + listener.lock match + case null => null + case l: ListenerLock => body(l) + + /** A helper instance that provides an uniquely numbered mutex. */ + trait NumberedLock: + import NumberedLock._ + + val number = listenerNumber.getAndIncrement() + private val lock0 = ReentrantLock() + + protected def acquireLock() = lock0.lock() + protected def releaseLock() = lock0.unlock() + + object NumberedLock: + private val listenerNumber = java.util.concurrent.atomic.AtomicLong() diff --git a/tests/neg-custom-args/captures/gears/PIO.scala b/tests/neg-custom-args/captures/gears/PIO.scala new file mode 100644 index 000000000000..8f3b8770d165 --- /dev/null +++ b/tests/neg-custom-args/captures/gears/PIO.scala @@ -0,0 +1,170 @@ +package PosixLikeIO + +import language.experimental.captureChecking +import caps.CapSet + +import gears.async.Scheduler +import gears.async.default.given +import gears.async.{Async, Future} + +import java.net.{DatagramPacket, DatagramSocket, InetAddress, InetSocketAddress, ServerSocket, Socket} +import java.nio.ByteBuffer +import java.nio.channels.{AsynchronousFileChannel, CompletionHandler, SocketChannel} +import java.nio.charset.{Charset, StandardCharsets} +import java.nio.file.{Path, StandardOpenOption} +import java.util.concurrent.CancellationException +import scala.Tuple.Union +import scala.concurrent.ExecutionContext +import scala.util.{Failure, Success, Try} + +import Future.Promise + +object File: + extension[Cap^] (resolver: Future.Resolver[Int, Cap]) + private[File] def toCompletionHandler = new CompletionHandler[Integer, ByteBuffer] { + override def completed(result: Integer, attachment: ByteBuffer): Unit = resolver.resolve(result) + override def failed(e: Throwable, attachment: ByteBuffer): Unit = resolver.reject(e) + } + +class File(val path: String) { + import File._ + + private var channel: Option[AsynchronousFileChannel] = None + + def isOpened: Boolean = channel.isDefined && channel.get.isOpen + + def open(options: StandardOpenOption*): File = + assert(channel.isEmpty) + val options1 = if (options.isEmpty) Seq(StandardOpenOption.READ) else options + channel = Some(AsynchronousFileChannel.open(Path.of(path), options1*)) + this + + def close(): Unit = + if (channel.isDefined) + channel.get.close() + channel = None + + def read(buffer: ByteBuffer): Future[Int] = + assert(channel.isDefined) + + Future.withResolver[Int, CapSet]: resolver => + channel.get.read( + buffer, + 0, + buffer, + resolver.toCompletionHandler + ) + + def readString(size: Int, charset: Charset = StandardCharsets.UTF_8): Future[String] = + assert(channel.isDefined) + assert(size >= 0) + + val buffer = ByteBuffer.allocate(size) + Future.withResolver[String, CapSet]: resolver => + channel.get.read( + buffer, + 0, + buffer, + new CompletionHandler[Integer, ByteBuffer] { + override def completed(result: Integer, attachment: ByteBuffer): Unit = + resolver.resolve(charset.decode(attachment.slice(0, result)).toString()) + override def failed(e: Throwable, attachment: ByteBuffer): Unit = resolver.reject(e) + } + ) + + def write(buffer: ByteBuffer): Future[Int] = + assert(channel.isDefined) + + Future.withResolver[Int, CapSet]: resolver => + channel.get.write( + buffer, + 0, + buffer, + resolver.toCompletionHandler + ) + + def writeString(s: String, charset: Charset = StandardCharsets.UTF_8): Future[Int] = + write(ByteBuffer.wrap(s.getBytes(charset))) + + override def finalize(): Unit = { + super.finalize() + if (channel.isDefined) + channel.get.close() + } +} + +class SocketUDP() { + import SocketUDP._ + private var socket: Option[DatagramSocket] = None + + def isOpened: Boolean = socket.isDefined && !socket.get.isClosed + + def bindAndOpen(port: Int): SocketUDP = + assert(socket.isEmpty) + socket = Some(DatagramSocket(port)) + this + + def open(): SocketUDP = + assert(socket.isEmpty) + socket = Some(DatagramSocket()) + this + + def close(): Unit = + if (socket.isDefined) + socket.get.close() + socket = None + + def send(data: ByteBuffer, address: String, port: Int): Future[Unit] = + assert(socket.isDefined) + + Future.withResolver[Unit, CapSet]: resolver => + resolver.spawn: + val packet: DatagramPacket = + new DatagramPacket(data.array(), data.limit(), InetAddress.getByName(address), port) + socket.get.send(packet) + + def receive(): Future[DatagramPacket] = + assert(socket.isDefined) + + Future.withResolver[DatagramPacket, CapSet]: resolver => + resolver.spawn: + val buffer = Array.fill[Byte](10 * 1024)(0) + val packet: DatagramPacket = DatagramPacket(buffer, 10 * 1024) + socket.get.receive(packet) + packet + + override def finalize(): Unit = { + super.finalize() + if (socket.isDefined) + socket.get.close() + } +} + +object SocketUDP: + extension [T, Cap^](resolver: Future.Resolver[T, Cap]) + private[SocketUDP] inline def spawn(body: => T)(using s: Scheduler) = + s.execute(() => + resolver.complete(Try(body).recover { case _: InterruptedException => + throw CancellationException() + }) + ) + +object PIOHelper { + def withFile[T](path: String, options: StandardOpenOption*)(f: File => T): T = + val file = File(path).open(options*) + val ret = f(file) + file.close() + ret + + def withSocketUDP[T]()(f: SocketUDP => T): T = + val s = SocketUDP().open() + val ret = f(s) + s.close() + ret + + def withSocketUDP[T](port: Int)(f: SocketUDP => T): T = + val s = SocketUDP().bindAndOpen(port) + val ret = f(s) + s.close() + ret +} diff --git a/tests/neg-custom-args/captures/gears/ScalaConverters.scala b/tests/neg-custom-args/captures/gears/ScalaConverters.scala new file mode 100644 index 000000000000..fadbf5dde17f --- /dev/null +++ b/tests/neg-custom-args/captures/gears/ScalaConverters.scala @@ -0,0 +1,29 @@ +package gears.async + +import language.experimental.captureChecking + +import scala.concurrent.ExecutionContext +import scala.concurrent.{Future as StdFuture, Promise as StdPromise} +import scala.util.Try + +/** Converters from Gears types to Scala API types and back. */ +object ScalaConverters: + extension [T](fut: StdFuture[T]^) + /** Converts a [[scala.concurrent.Future Scala Future]] into a gears [[Future]]. Requires an + * [[scala.concurrent.ExecutionContext ExecutionContext]], as the job of completing the returned [[Future]] will be + * done through this context. Since [[scala.concurrent.Future Scala Future]] cannot be cancelled, the returned + * [[Future]] will *not* clean up the pending job when cancelled. + */ + def asGears(using ExecutionContext): Future[T]^{fut} = + Future.withResolver[T, caps.CapSet]: resolver => + fut.andThen(result => resolver.complete(result)) + + extension [T](fut: Future[T]^) + /** Converts a gears [[Future]] into a Scala [[scala.concurrent.Future Scala Future]]. Note that if `fut` is + * cancelled, the returned [[scala.concurrent.Future Scala Future]] will also be completed with + * `Failure(CancellationException)`. + */ + def asScala: StdFuture[T]^{fut} = + val p = StdPromise[T]() + fut.onComplete(Listener((res, _) => p.complete(res))) + p.future diff --git a/tests/neg-custom-args/captures/gears/Timer.scala b/tests/neg-custom-args/captures/gears/Timer.scala new file mode 100644 index 000000000000..1f00b0e852a8 --- /dev/null +++ b/tests/neg-custom-args/captures/gears/Timer.scala @@ -0,0 +1,80 @@ +package gears.async + +import language.experimental.captureChecking + +import gears.async.Listener + +import java.util.concurrent.CancellationException +import java.util.concurrent.TimeoutException +import scala.annotation.tailrec +import scala.collection.mutable +import scala.concurrent.TimeoutException +import scala.concurrent.duration._ +import scala.util.{Failure, Success, Try} +import scala.annotation.unchecked.uncheckedCaptures + +import AsyncOperations.sleep +import Future.Promise + + +/** Timer exposes a steady [[Async.Source]] of ticks that happens every `tickDuration` milliseconds. Note that the timer + * does not start ticking until `start` is called (which is a blocking operation, until the timer is cancelled). + * + * You might want to manually `cancel` the timer, so that it gets garbage collected (before the enclosing [[Async]] + * scope ends). + */ +class Timer(tickDuration: Duration) extends Cancellable { + enum TimerEvent: + case Tick + case Cancelled + + private var isCancelled = false + + private object Source extends Async.OriginalSource[this.TimerEvent] { + private val listeners : mutable.Set[(Listener[TimerEvent]^) @uncheckedCaptures] = + mutable.Set[(Listener[TimerEvent]^) @uncheckedCaptures]() + + def tick(): Unit = synchronized { + listeners.filterInPlace(l => + l.completeNow(TimerEvent.Tick, src) + false + ) + } + override def poll(k: Listener[TimerEvent]^): Boolean = + if isCancelled then k.completeNow(TimerEvent.Cancelled, this) + else false // subscribing to a timer always takes you to the next tick + override def dropListener(k: Listener[TimerEvent]^): Unit = listeners -= k + override protected def addListener(k: Listener[TimerEvent]^): Unit = + if isCancelled then k.completeNow(TimerEvent.Cancelled, this) + else + Timer.this.synchronized: + if isCancelled then k.completeNow(TimerEvent.Cancelled, this) + else listeners += k + + def cancel(): Unit = + synchronized { isCancelled = true } + src.synchronized { + Source.listeners.foreach(_.completeNow(TimerEvent.Cancelled, src)) + Source.listeners.clear() + } + } + + /** Ticks of the timer are delivered through this source. Note that ticks are ephemeral. */ + inline final def src: Async.Source[this.TimerEvent] = Source + + /** Starts the timer. Suspends until the timer is cancelled. */ + def run()(using Async, AsyncOperations): Unit = + cancellationScope(this): + loop() + + @tailrec private def loop()(using Async, AsyncOperations): Unit = + if !isCancelled then + try sleep(tickDuration.toMillis) + catch case _: CancellationException => cancel() + if !isCancelled then + Source.tick() + loop() + + override def cancel(): Unit = Source.cancel() +} + diff --git a/tests/neg-custom-args/captures/gears/VThreadSupport.scala b/tests/neg-custom-args/captures/gears/VThreadSupport.scala new file mode 100644 index 000000000000..a287d7627c73 --- /dev/null +++ b/tests/neg-custom-args/captures/gears/VThreadSupport.scala @@ -0,0 +1,136 @@ +package gears.async + +import language.experimental.captureChecking + +import java.lang.invoke.{MethodHandles, VarHandle} +import java.util.concurrent.locks.ReentrantLock +import scala.annotation.unchecked.uncheckedVariance +import scala.concurrent.duration.FiniteDuration +import scala.annotation.constructorOnly +import scala.collection.mutable + +object VThreadScheduler extends Scheduler: + private val VTFactory = Thread + .ofVirtual() + .name("gears.async.VThread-", 0L) + .factory() + + override def execute(body: Runnable): Unit = + val th = VTFactory.newThread(body) + th.start() + () + + private[gears] inline def unsafeExecute(body: Runnable^): Unit = execute(caps.unsafe.unsafeAssumePure(body)) + + override def schedule(delay: FiniteDuration, body: Runnable): Cancellable = + import caps.unsafe.unsafeAssumePure + + val sr = ScheduledRunnable(delay, body) + // SAFETY: should not be able to access body, only for cancellation + sr.unsafeAssumePure: Cancellable + + private final class ScheduledRunnable(delay: FiniteDuration, body: Runnable) extends Cancellable: + @volatile var interruptGuard = true // to avoid interrupting the body + + val th = VTFactory.newThread: () => + try Thread.sleep(delay.toMillis) + catch case e: InterruptedException => () /* we got cancelled, don't propagate */ + if ScheduledRunnable.interruptGuardVar.getAndSet(this, false) then body.run() + th.start() + + final override def cancel(): Unit = + if ScheduledRunnable.interruptGuardVar.getAndSet(this, false) then th.interrupt() + end ScheduledRunnable + + private object ScheduledRunnable: + val interruptGuardVar = + MethodHandles + .lookup() + .in(classOf[ScheduledRunnable]) + .findVarHandle(classOf[ScheduledRunnable], "interruptGuard", classOf[Boolean]) + +object VThreadSupport extends AsyncSupport: + type Scheduler = VThreadScheduler.type + + private final class VThreadLabel[R]() extends caps.Capability: + private var result: Option[R] = None + private val lock = ReentrantLock() + private val cond = lock.newCondition() + + private[VThreadSupport] def clearResult() = + lock.lock() + result = None + lock.unlock() + + private[VThreadSupport] def setResult(data: R) = + lock.lock() + try + result = Some(data) + cond.signalAll() + finally lock.unlock() + + private[VThreadSupport] def waitResult(): R = + lock.lock() + try + while result.isEmpty do cond.await() + result.get + finally lock.unlock() + + override opaque type Label[R, Cap^] <: caps.Capability = VThreadLabel[R] + + // outside boundary: waiting on label + // inside boundary: waiting on suspension + private final class VThreadSuspension[-T, +R](using private[VThreadSupport] val l: VThreadLabel[R] @uncheckedVariance) + extends gears.async.Suspension[T, R]: + private var nextInput: Option[T] = None + private val lock = ReentrantLock() + private val cond = lock.newCondition() + + private[VThreadSupport] def setInput(data: T) = + lock.lock() + try + nextInput = Some(data) + cond.signalAll() + finally lock.unlock() + + // variance is safe because the only caller created the object + private[VThreadSupport] def waitInput(): T @uncheckedVariance = + lock.lock() + try + while nextInput.isEmpty do cond.await() + nextInput.get + finally lock.unlock() + + // normal resume only tells other thread to run again -> resumeAsync may redirect here + override def resume(arg: T): R = + l.clearResult() + setInput(arg) + l.waitResult() + + override opaque type Suspension[-T, +R] <: gears.async.Suspension[T, R] = VThreadSuspension[T, R] + + override def boundary[R, Cap^](body: Label[R, Cap]^ ?->{Cap^} R): R = + val label = VThreadLabel[R]() + VThreadScheduler.unsafeExecute: () => + val result = body(using label) + label.setResult(result) + + label.waitResult() + + override private[async] def resumeAsync[T, R](suspension: Suspension[T, R])(arg: T)(using Scheduler): Unit = + suspension.l.clearResult() + suspension.setInput(arg) + + override def scheduleBoundary[Cap^](body: Label[Unit, Cap] ?-> Unit)(using Scheduler): Unit = + VThreadScheduler.execute: () => + val label = VThreadLabel[Unit]() + body(using label) + + override def suspend[T, R, Cap^](body: Suspension[T, R]^{Cap^} => R^{Cap^})(using l: Label[R, Cap]^): T = + val sus = new VThreadSuspension[T, R](using caps.unsafe.unsafeAssumePure(l)) + val res = body(sus) + l.setResult( + // SAFETY: will only be stored and returned by the Suspension resumption mechanism + caps.unsafe.unsafeAssumePure(res) + ) + sus.waitInput() diff --git a/tests/neg-custom-args/captures/gears/channels.scala b/tests/neg-custom-args/captures/gears/channels.scala new file mode 100644 index 000000000000..adb2d69377c3 --- /dev/null +++ b/tests/neg-custom-args/captures/gears/channels.scala @@ -0,0 +1,457 @@ +package gears.async + +import language.experimental.captureChecking + +import gears.async.Async.Source +import gears.async.Listener.acceptingListener +import gears.async.listeners.lockBoth + +import scala.annotation.unchecked.uncheckedCaptures +import scala.collection.mutable +import scala.util.control.Breaks.{break, breakable} +import scala.util.{Failure, Success, Try} + +import Channel.{Closed, Res} +import mutable.{ArrayBuffer, ListBuffer} + +/** The part of a channel one can send values to. Blocking behavior depends on the implementation. + */ +trait SendableChannel[-T]: + /** Create an [[Async.Source]] representing the send action of value `x`. + * + * Note that *each* listener attached to and accepting an [[Unit]] value corresponds to `x` being sent once. + * + * To create an [[Async.Source]] that sends the item exactly once regardless of listeners attached, wrap the [[send]] + * operation inside a [[gears.async.Future]]: + * {{{ + * val sendOnce = Future(ch.send(x)) + * }}} + * + * @return + * an [[Async.Source]] that resolves with `Right(())` when `x` is sent to the channel, or `Left(Closed)` if the + * channel is already closed. This source will perform a send operation every time a listener is attached to it, or + * every time it is [[Async$.await]]ed on. + */ + def sendSource(x: T): Async.Source[Res[Unit]] + + /** Send `x` over the channel, suspending until the item has been sent or, if the channel is buffered, queued. + * @throws ChannelClosedException + * if the channel was closed. + */ + def send(x: T)(using Async): Unit = sendSource(x).awaitResult match + case Right(_) => () + case Left(_) => throw ChannelClosedException() +end SendableChannel + +/** The part of a channel one can read values from. Blocking behavior depends on the implementation. + */ +trait ReadableChannel[+T]: + /** An [[Async.Source]] corresponding to items being sent over the channel. Note that *each* listener attached to and + * accepting a [[Right]] value corresponds to one value received over the channel. + * + * To create an [[Async.Source]] that reads *exactly one* item regardless of listeners attached, wrap the [[read]] + * operation inside a [[gears.async.Future]]. + * {{{ + * val readOnce = Future(ch.read(x)) + * }}} + */ + val readSource: Async.Source[Res[T]] + + /** Read an item from the channel, suspending until the item has been received. Returns + * `Failure(ChannelClosedException)` if the channel was closed. + */ + def read()(using Async): Res[T] = readSource.awaitResult +end ReadableChannel + +/** A generic channel that can be sent to, received from and closed. + * @example + * {{{ + * // send from one Future, read from multiple + * val ch = SyncChannel[Int]() + * val sender = Future: + * for i <- 1 to 20 do + * ch.send(i) + * ch.close() + * val receivers = (1 to 5).map: n => + * Future: + * boundary: + * while true: + * ch.read() match + * case Right(k) => println(s"Receiver $n got: $k") + * case Left(_) => boundary.break() + * + * receivers.awaitAll + * }}} + * @see + * [[SyncChannel]], [[BufferedChannel]] and [[UnboundedChannel]] for channel implementations. + */ +trait Channel[T] extends SendableChannel[T], ReadableChannel[T], java.io.Closeable: + /** Restrict this channel to send-only. */ + inline final def asSendable: SendableChannel[T] = this + + /** Restrict this channel to read-only. */ + inline final def asReadable: ReadableChannel[T] = this + + /** Restrict this channel to close-only. */ + inline final def asCloseable: java.io.Closeable = this + + protected type Reader = Listener[Res[T]] + protected type Sender = Listener[Res[Unit]] +end Channel + +/** Synchronous channels, sometimes called rendez-vous channels, has the following semantics: + * - [[Channel.send send]] to an unclosed channel blocks until a [[Channel.read read]] listener commits to receiving + * the value (via successfully locking). + * + * See [[SyncChannel$.apply]] for creation of synchronous channels. + */ +trait SyncChannel[T] extends Channel[T] + +/** Buffered channels are channels with an internal value buffer (represented internally as an array with positive + * size). They have the following semantics: + * - [[Channel.send send]], when the buffer is not full, appends the value to the buffer and success immediately. + * - [[Channel.send send]], when the buffer is full, suspends until some buffer slot is freed and assigned to this + * sender. + * + * See [[BufferedChannel$.apply]] for creation of buffered channels. + */ +trait BufferedChannel[T] extends Channel[T] + +/** Unbounded channels are buffered channels that do not have an upper bound on the number of items in the channel. In + * other words, the buffer is treated as never being full and will expand as needed. + * + * See [[UnboundedChannel$.apply]] for creation of unbounded channels. + */ +trait UnboundedChannel[T] extends BufferedChannel[T]: + /** Sends the item immediately. + * + * @throws ChannelClosedException + * if the channel is closed. + */ + def sendImmediately(x: T): Unit + +/** The exception raised by [[Channel.send send]] (or [[UnboundedChannel.sendImmediately]]) on a closed [[Channel]]. + * + * It is also returned wrapped in `Failure` when reading form a closed channel. [[ChannelMultiplexer]] sends + * `Failure(ChannelClosedException)` to all subscribers when it receives a `close()` signal. + */ +class ChannelClosedException extends Exception + +object SyncChannel: + /** Creates a new [[SyncChannel]]. */ + def apply[T](): SyncChannel[T] = Impl() + + private class Impl[T] extends Channel.Impl[T] with SyncChannel[T]: + override def pollRead(r: Reader^): Boolean = synchronized: + // match reader with buffer of senders + checkClosed(readSource, r) || cells.matchReader(r) + + override def pollSend(src: CanSend, s: Sender^): Boolean = synchronized: + // match reader with buffer of senders + checkClosed(src, s) || cells.matchSender(src, s) + end Impl +end SyncChannel + +object BufferedChannel: + /** Create a new buffered channel with the given buffer size. */ + def apply[T](size: Int = 10): BufferedChannel[T] = Impl(size) + + private class Impl[T](size: Int) extends Channel.Impl[T] with BufferedChannel[T]: + require(size > 0, "Buffered channels must have a buffer size greater than 0") + val buf = new mutable.Queue[T](size) + + // Match a reader -> check space in buf -> fail + override def pollSend(src: CanSend, s: Sender^): Boolean = synchronized: + checkClosed(src, s) || cells.matchSender(src, s) || senderToBuf(src, s) + + // Check space in buf -> fail + // If we can pop from buf -> try to feed a sender + override def pollRead(r: Reader^): Boolean = synchronized: + if checkClosed(readSource, r) then true + else if !buf.isEmpty then + if r.completeNow(Right(buf.head), readSource) then + buf.dequeue() + if cells.hasSender then + val (src, s) = cells.nextSender + cells.dequeue() // buf always has space available after dequeue + senderToBuf(src, s) + true + else false + + // Try to add a sender to the buffer + def senderToBuf(src: CanSend, s: Sender^): Boolean = + if buf.size < size then + if s.completeNow(Right(()), src) then buf += src.item + true + else false + end Impl +end BufferedChannel + +object UnboundedChannel: + /** Creates a new [[UnboundedChannel]]. */ + def apply[T](): UnboundedChannel[T] = Impl[T]() + + private final class Impl[T]() extends Channel.Impl[T] with UnboundedChannel[T] { + val buf = new mutable.Queue[T]() + + override def sendImmediately(x: T): Unit = + var result: SendResult = Left(Closed) + pollSend(CanSend(x), acceptingListener((r, _) => result = r)) + if result.isLeft then throw ChannelClosedException() + + override def pollRead(r: Reader^): Boolean = synchronized: + if checkClosed(readSource, r) then true + else if !buf.isEmpty then + if r.completeNow(Right(buf.head), readSource) then + // there are never senders in the cells + buf.dequeue() + true + else false + + override def pollSend(src: CanSend, s: Sender^): Boolean = synchronized: + if checkClosed(src, s) || cells.matchSender(src, s) then true + else if s.completeNow(Right(()), src) then + buf += src.item + true + else false + } +end UnboundedChannel + +object Channel: + /** Signals that the channel is closed. */ + case object Closed + + type Closed = Closed.type + + private[async] type Res[T] = Either[Closed, T] + + private[async] abstract class Impl[T] extends Channel[T]: + protected type ReadResult = Res[T] + protected type SendResult = Res[Unit] + + var isClosed = false + val cells = CellBuf() + // Poll a reader, returning false if it should be put into queue + def pollRead(r: Reader^): Boolean + // Poll a reader, returning false if it should be put into queue + def pollSend(src: CanSend, s: Sender^): Boolean + + protected final def checkClosed[T](src: Async.Source[Res[T]], l: Listener[Res[T]]^): Boolean = + if isClosed then + l.completeNow(Left(Closed), src) + true + else false + + override val readSource: Source[ReadResult] = new Source { + override def poll(k: Reader^): Boolean = pollRead(k) + override def onComplete(k: Reader^): Unit = Impl.this.synchronized: + if !pollRead(k) then cells.addReader(k) + override def dropListener(k: Reader^): Unit = Impl.this.synchronized: + if !isClosed then cells.dropReader(k) + } + override final def sendSource(x: T): Source[SendResult] = CanSend(x) + override final def close(): Unit = + synchronized: + if !isClosed then + isClosed = true + cells.cancel() + + /** Complete a pair of locked sender and reader. */ + protected final def complete(src: CanSend, reader: Listener[ReadResult]^, sender: Listener[SendResult]^) = + reader.complete(Right(src.item), readSource) + sender.complete(Right(()), src) + + // Not a case class because equality should be referential, as otherwise + // dependent on a (possibly odd) equality of T. Users do not expect that + // cancelling a send of a given item might in fact cancel that of an equal one. + protected final class CanSend(val item: T) extends Source[SendResult] { + override def poll(k: Listener[SendResult]^): Boolean = pollSend(this, k) + override def onComplete(k: Listener[SendResult]^): Unit = Impl.this.synchronized: + if !pollSend(this, k) then cells.addSender(this, k) + override def dropListener(k: Listener[SendResult]^): Unit = Impl.this.synchronized: + if !isClosed then cells.dropSender(this, k) + } + + /** CellBuf is a queue of cells, which consists of a sleeping sender or reader. The queue always guarantees that + * there are *only* all readers or all senders. It must be externally synchronized. + */ + private[async] class CellBuf(): + import caps.unsafe.unsafeAssumePure // very unsafe WIP + + type Cell = Reader | (CanSend, Sender) + // reader == 0 || sender == 0 always + private var reader = 0 + private var sender = 0 + + private val pending = mutable.Queue[Cell]() + + /* Boring push/pop methods */ + + def hasReader = reader > 0 + def hasSender = sender > 0 + def nextReader = + require(reader > 0) + pending.head.asInstanceOf[Reader] + def nextSender = + require(sender > 0) + pending.head.asInstanceOf[(CanSend, Sender)] + def dequeue() = + pending.dequeue() + if reader > 0 then reader -= 1 else sender -= 1 + def addReader(r: Reader^): this.type = + require(sender == 0) + reader += 1 + pending.enqueue(r.unsafeAssumePure) + this + def addSender(src: CanSend, s: Sender^): this.type = + require(reader == 0) + sender += 1 + pending.enqueue((src, s.unsafeAssumePure)) + this + def dropReader(r: Reader^): this.type = + if reader > 0 then if pending.removeFirst(_ == r).isDefined then reader -= 1 + this + def dropSender(src: CanSend, s: Sender^): this.type = + if sender > 0 then if pending.removeFirst(_ == (src, s)).isDefined then sender -= 1 + this + + /** Match a possible reader to a queue of senders: try to go through the queue with lock pairing, stopping when + * finding a good pair. + */ + def matchReader(r: Reader^): Boolean = + while hasSender do + val (src, s) = nextSender + tryComplete(src, s)(r) match + case () => return true + case listener if listener == r => return true + case _ => dequeue() // drop gone sender from queue + false + + /** Match a possible sender to a queue of readers: try to go through the queue with lock pairing, stopping when + * finding a good pair. + */ + def matchSender(src: CanSend, s: Sender^): Boolean = + while hasReader do + val r = nextReader + tryComplete(src, s)(r) match + case () => return true + case listener if listener == s => return true + case _ => dequeue() // drop gone reader from queue + false + + private inline def tryComplete(src: CanSend, s: Sender^)(r: Reader^): s.type | r.type | Unit = + lockBoth(r, s) match + case true => + Impl.this.complete(src, r, s) + dequeue() // drop completed reader/sender from queue + () + case listener: (r.type | s.type) => listener + + def cancel() = + pending.foreach { + case (src, s) => s.completeNow(Left(Closed), src) + case r: Reader => r.completeNow(Left(Closed), readSource) + } + pending.clear() + reader = 0 + sender = 0 + end CellBuf + end Impl +end Channel + +/** Channel multiplexer is an object where one can register publisher and subscriber channels. When it is run, it + * continuously races the set of publishers and once it reads a value, it sends a copy to each subscriber. + * + * When a publisher or subscriber channel is closed, it will be removed from the multiplexer's set. + * + * For an unchanging set of publishers and subscribers and assuming that the multiplexer is the only reader of the + * publisher channels, every subscriber will receive the same set of messages, in the same order and it will be exactly + * all messages sent by the publishers. The only guarantee on the order of the values the subscribers see is that + * values from the same publisher will arrive in order. + * + * Channel multiplexer can also be closed, in that case all subscribers will receive `Failure(ChannelClosedException)` + * but no attempt at closing either publishers or subscribers will be made. + */ +trait ChannelMultiplexer[T] extends java.io.Closeable: + /** Run the multiplexer. Returns after this multiplexer has been cancelled. */ + def run()(using Async): Unit + + def addPublisher(c: ReadableChannel[T]): Unit + def removePublisher(c: ReadableChannel[T]): Unit + + def addSubscriber(c: SendableChannel[Try[T]]): Unit + def removeSubscriber(c: SendableChannel[Try[T]]): Unit +end ChannelMultiplexer + +object ChannelMultiplexer: + private enum Message: + case Quit, Refresh + + def apply[T](): ChannelMultiplexer[T] = Impl[T]() + + private class Impl[T] extends ChannelMultiplexer[T]: + private var isClosed = false + private val publishers = ArrayBuffer[ReadableChannel[T]]() + private val subscribers = ArrayBuffer[SendableChannel[Try[T]]]() + private val infoChannel = UnboundedChannel[Message]() + + def run()(using Async) = { + var shouldTerminate = false + while (!shouldTerminate) { + val publishersCopy = synchronized(publishers.toSeq) + + val pubCases = + publishersCopy.map: pub => + pub.readSource.handle: + case Right(v) => + val subscribersCopy = synchronized(subscribers.toList) + var c = 0 + for (s <- subscribersCopy) { + c += 1 + try s.send(Success(v)) + catch + case closedEx: ChannelClosedException => + removeSubscriber(s) + } + case Left(_) => removePublisher(pub) + + val infoCase = infoChannel.readSource.handle: + case Left(_) | Right(Message.Quit) => + val subscribersCopy = synchronized(subscribers.toList) + for (s <- subscribersCopy) s.send(Failure(ChannelClosedException())) + shouldTerminate = true + case Right(Message.Refresh) => () + + Async.select((infoCase +: pubCases)*) + } + } + + override def close(): Unit = + val shouldStop = synchronized: + if !isClosed then + isClosed = true + true + else false + if shouldStop then infoChannel.sendImmediately(Message.Quit) + + override def removePublisher(c: ReadableChannel[T]): Unit = + synchronized: + if isClosed then throw ChannelClosedException() + publishers -= c + infoChannel.sendImmediately(Message.Refresh) + + override def removeSubscriber(c: SendableChannel[Try[T]]): Unit = synchronized: + if isClosed then throw ChannelClosedException() + subscribers -= c + + override def addPublisher(c: ReadableChannel[T]): Unit = + synchronized: + if isClosed then throw ChannelClosedException() + publishers += c + infoChannel.sendImmediately(Message.Refresh) + + override def addSubscriber(c: SendableChannel[Try[T]]): Unit = synchronized: + if isClosed then throw ChannelClosedException() + subscribers += c + +end ChannelMultiplexer diff --git a/tests/neg-custom-args/captures/gears/clientAndServerUDP.scala b/tests/neg-custom-args/captures/gears/clientAndServerUDP.scala new file mode 100644 index 000000000000..a1c30b41fe52 --- /dev/null +++ b/tests/neg-custom-args/captures/gears/clientAndServerUDP.scala @@ -0,0 +1,35 @@ +package PosixLikeIO.examples + +import gears.async.AsyncOperations.* +import gears.async.default.given +import gears.async.{Async, Future} + +import java.net.DatagramPacket +import java.nio.ByteBuffer +import java.nio.file.StandardOpenOption +import scala.concurrent.ExecutionContext + +import PosixLikeIO.{PIOHelper, SocketUDP} + +@main def clientAndServerUDP(): Unit = + given ExecutionContext = ExecutionContext.global + Async.blocking: + val server = Future: + PIOHelper.withSocketUDP(8134): serverSocket => + val got: DatagramPacket = serverSocket.receive().awaitResult.get + val messageReceived = String(got.getData.slice(0, got.getLength), "UTF-8") + val responseMessage = (messageReceived.toInt + 1).toString.getBytes + serverSocket.send(ByteBuffer.wrap(responseMessage), got.getAddress.toString.substring(1), got.getPort) + sleep(50) + + def client(value: Int): Future[Unit] = + Future: + PIOHelper.withSocketUDP(): clientSocket => + val data: Array[Byte] = value.toString.getBytes + clientSocket.send(ByteBuffer.wrap(data), "localhost", 8134).awaitResult.get + val responseDatagram = clientSocket.receive().awaitResult.get + val messageReceived = String(responseDatagram.getData.slice(0, responseDatagram.getLength), "UTF-8").toInt + println("Sent " + value.toString + " and got " + messageReceived.toString + " in return.") + + client(100).await + server.await diff --git a/tests/neg-custom-args/captures/gears/futures.scala b/tests/neg-custom-args/captures/gears/futures.scala new file mode 100644 index 000000000000..67315bf01f6c --- /dev/null +++ b/tests/neg-custom-args/captures/gears/futures.scala @@ -0,0 +1,580 @@ +package gears.async + +import language.experimental.captureChecking + +import java.util.concurrent.CancellationException +import java.util.concurrent.atomic.AtomicBoolean +import scala.annotation.tailrec +import scala.annotation.unchecked.uncheckedVariance +import scala.collection.mutable +import scala.compiletime.uninitialized +import scala.util +import scala.util.control.NonFatal +import scala.util.{Failure, Success, Try} +import gears.async.Async.SourceSymbol + +/** Futures are [[Async.Source Source]]s that has the following properties: + * - They represent a single value: Once resolved, [[Async.await await]]-ing on a [[Future]] should always return the + * same value. + * - They can potentially be cancelled, via [[Cancellable.cancel the cancel method]]. + * + * There are two kinds of futures, active and passive. + * - '''Active''' futures are ones that are spawned with [[Future.apply]] and [[Task.start]]. They require the + * [[Async.Spawn]] context, and run on their own (as long as the [[Async.Spawn]] scope has not ended). Active + * futures represent concurrent computations within Gear's structured concurrency tree. Idiomatic Gears code should + * ''never'' return active futures. Should a function be async (i.e. takes an [[Async]] context parameter), they + * should return values or throw exceptions directly. + * - '''Passive''' futures are ones that are created by [[Future.Promise]] (through + * [[Future.Promise.asFuture asFuture]]) and [[Future.withResolver]]. They represent yet-arrived values coming from + * ''outside'' of Gear's structured concurrency tree (for example, from network or the file system, or even from + * another concurrency system like [[scala.concurrent.Future Scala standard library futures]]). Idiomatic Gears + * libraries should return this kind of [[Future]] if deemed neccessary, but functions returning passive futures + * should ''not'' take an [[Async]] context. + * + * @see + * [[Future.apply]] and [[Task.start]] for creating active futures. + * @see + * [[Future.Promise]] and [[Future.withResolver]] for creating passive futures. + * @see + * [[Future.awaitAll]], [[Future.awaitFirst]] and [[Future.Collector]] for tools to work with multiple futures. + * @see + * [[ScalaConverters.asGears]] and [[ScalaConverters.asScala]] for converting between Scala futures and Gears + * futures. + */ +trait Future[+T] extends Async.OriginalSource[Try[T]], Cancellable + +object Future: + /** A future that is completed explicitly by calling its `complete` method. There are three public implementations + * + * - RunnableFuture: Completion is done by running a block of code + * - Promise.apply: Completion is done by external request. + * - withResolver: Completion is done by external request set up from a block of code. + */ + private class CoreFuture[+T] extends Future[T]: + + @volatile protected var hasCompleted: Boolean = false + protected var cancelRequest = AtomicBoolean(false) + private var result: Try[T] = uninitialized // guaranteed to be set if hasCompleted = true + private val waiting: mutable.Set[Listener[Try[T]]] = mutable.Set() + + // Async.Source method implementations + + import caps.unsafe.unsafeAssumePure + + def poll(k: Listener[Try[T]]^): Boolean = + if hasCompleted then + k.completeNow(result, this) + true + else false + + def addListener(k: Listener[Try[T]]^): Unit = synchronized: + waiting += k.unsafeAssumePure + + def dropListener(k: Listener[Try[T]]^): Unit = synchronized: + waiting -= k.unsafeAssumePure + + // Cancellable method implementations + + def cancel(): Unit = + setCancelled() + + override def link(group: CompletionGroup): this.type = + // though hasCompleted is accessible without "synchronized", + // we want it not to be run while the future was trying to complete. + synchronized: + if !hasCompleted || group == CompletionGroup.Unlinked then super.link(group) + else this + + /** Sets the cancellation state and returns `true` if the future has not been completed and cancelled before. */ + protected final def setCancelled(): Boolean = + !hasCompleted && cancelRequest.compareAndSet(false, true) + + /** Complete future with result. If future was cancelled in the meantime, return a CancellationException failure + * instead. Note: @uncheckedVariance is safe here since `complete` is called from only two places: + * - from the initializer of RunnableFuture, where we are sure that `T` is exactly the type with which the future + * was created, and + * - from Promise.complete, where we are sure the type `T` is exactly the type with which the future was created + * since `Promise` is invariant. + */ + private[Future] def complete(result: Try[T] @uncheckedVariance): Unit = + val toNotify = synchronized: + if hasCompleted then Nil + else + this.result = result + hasCompleted = true + val ws = waiting.toList + waiting.clear() + unlink() + ws + for listener <- toNotify do listener.completeNow(result, this) + + end CoreFuture + + /** A future that is completed by evaluating `body` as a separate asynchronous operation in the given `scheduler` + */ + private class RunnableFuture[+T](body: Async.Spawn ?-> T)(using ac: Async) extends CoreFuture[T]: + private given acSupport: ac.support.type = ac.support + private given acScheduler: ac.support.Scheduler = ac.scheduler + /** RunnableFuture maintains its own inner [[CompletionGroup]], that is separated from the provided Async + * instance's. When the future is cancelled, we only cancel this CompletionGroup. This effectively means any + * `.await` operations within the future is cancelled *only if they link into this group*. The future body run with + * this inner group by default, but it can always opt-out (e.g. with [[uninterruptible]]). + */ + private var innerGroup: CompletionGroup = CompletionGroup() + + private def checkCancellation(): Unit = + if cancelRequest.get() then throw new CancellationException() + + private class FutureAsync[Cap^](val group: CompletionGroup)(using label: acSupport.Label[Unit, Cap]) + extends Async(using acSupport, acScheduler): + /** Await a source first by polling it, and, if that fails, by suspending in a onComplete call. + */ + override def await[U](src: Async.Source[U]^): U = + class CancelSuspension extends Cancellable: + var suspension: acSupport.Suspension[Try[U], Unit]^{Cap^} = uninitialized + var listener: Listener[U]^{this, Cap^} = uninitialized + var completed = false + + def complete() = synchronized: + val completedBefore = completed + completed = true + completedBefore + + override def cancel() = + val completedBefore = complete() + if !completedBefore then + src.dropListener(listener) + // SAFETY: we always await for this suspension to end + val pureSusp = caps.unsafe.unsafeAssumePure(suspension) + acSupport.resumeAsync(pureSusp)(Failure(new CancellationException())) + + if group.isCancelled then throw new CancellationException() + + src + .poll() + .getOrElse: + val cancellable = CancelSuspension() + val res = acSupport.suspend[Try[U], Unit, Cap](k => + val listener = Listener.acceptingListener[U]: (x, _) => + val completedBefore = cancellable.complete() + // SAFETY: Future should already capture Cap^ + val purek = caps.unsafe.unsafeAssumePure(k) + if !completedBefore then acSupport.resumeAsync(purek)(Success(x)) + cancellable.suspension = k + cancellable.listener = listener + cancellable.link(group) // may resume + remove listener immediately + src.onComplete(listener) + ) + cancellable.unlink() + res.get + + override def withGroup(group: CompletionGroup): Async = FutureAsync[Cap](group) + + override def cancel(): Unit = if setCancelled() then this.innerGroup.cancel() + + link() + ac.support.scheduleBoundary: + val result = Async.withNewCompletionGroup(innerGroup)(Try({ + val r = body + checkCancellation() + r + }).recoverWith { case _: InterruptedException | _: CancellationException => + Failure(new CancellationException()) + })(using FutureAsync(CompletionGroup.Unlinked)) + complete(result) + + end RunnableFuture + + + /** Create a future that asynchronously executes `body` that wraps its execution in a [[scala.util.Try]]. The returned + * future is linked to the given [[Async.Spawn]] scope by default, i.e. it is cancelled when this scope ends. + */ + def apply[T](body: Async.Spawn ?=> T)(using async: Async, spawnable: Async.Spawn)( + using async.type =:= spawnable.type + ): Future[T]^{body, spawnable} = + val f = (async: Async.Spawn) => body(using async) + val puref = caps.unsafe.unsafeAssumePure(f) + // SAFETY: body is recorded in the capture set of Future, which should be cancelled when gone out of scope. + RunnableFuture(async ?=> puref(async))(using spawnable) + + /** A future that is immediately completed with the given result. */ + def now[T](result: Try[T]): Future[T] = + val f = CoreFuture[T]() + f.complete(result) + f + + /** An alias to [[now]]. */ + inline def completed[T](result: Try[T]) = now(result) + + /** A future that immediately resolves with the given result. Similar to `Future.now(Success(result))`. */ + inline def resolved[T](result: T): Future[T] = now(Success(result)) + + /** A future that immediately rejects with the given exception. Similar to `Future.now(Failure(exception))`. */ + inline def rejected(exception: Throwable): Future[Nothing] = now(Failure(exception)) + + extension [T](f1: Future[T]^) + /** Parallel composition of two futures. If both futures succeed, succeed with their values in a pair. Otherwise, + * fail with the failure that was returned first. + */ + def zip[U](f2: Future[U]^): Future[(T, U)]^{f1, f2} = + Future.withResolver[(T, U), caps.CapSet^{f1, f2}]: r => + Async + .either(f1, f2) + .onComplete(Listener { (v, _) => + v match + case Left(Success(x1)) => + f2.onComplete(Listener { (x2, _) => r.complete(x2.map((x1, _))) }) + case Right(Success(x2)) => + f1.onComplete(Listener { (x1, _) => r.complete(x1.map((_, x2))) }) + case Left(Failure(ex)) => r.reject(ex) + case Right(Failure(ex)) => r.reject(ex) + }) + + // /** Parallel composition of tuples of futures. Disabled since scaladoc is crashing with it. (https://github.com/scala/scala3/issues/19925) */ + // def *:[U <: Tuple](f2: Future[U]): Future[T *: U] = Future.withResolver: r => + // Async + // .either(f1, f2) + // .onComplete(Listener { (v, _) => + // v match + // case Left(Success(x1)) => + // f2.onComplete(Listener { (x2, _) => r.complete(x2.map(x1 *: _)) }) + // case Right(Success(x2)) => + // f1.onComplete(Listener { (x1, _) => r.complete(x1.map(_ *: x2)) }) + // case Left(Failure(ex)) => r.reject(ex) + // case Right(Failure(ex)) => r.reject(ex) + // }) + + /** Alternative parallel composition of this task with `other` task. If either task succeeds, succeed with the + * success that was returned first. Otherwise, fail with the failure that was returned last. + * @see + * [[orWithCancel]] for an alternative version where the slower future is cancelled. + */ + def or(f2: Future[T]^): Future[T]^{f1, f2} = orImpl(false)(f2) + + /** Like `or` but the slower future is cancelled. If either task succeeds, succeed with the success that was + * returned first and the other is cancelled. Otherwise, fail with the failure that was returned last. + */ + def orWithCancel(f2: Future[T]^): Future[T]^{f1, f2} = orImpl(true)(f2) + + inline def orImpl(inline withCancel: Boolean)(f2: Future[T]^): Future[T]^{f1, f2} = Future.withResolver[T, caps.CapSet^{f1, f2}]: r => + Async + .raceWithOrigin(f1, f2) + .onComplete(Listener { case ((v, which), _) => + v match + case Success(value) => + inline if withCancel then (if which == f1 then f2 else f1).cancel() + r.resolve(value) + case Failure(_) => + (if which == f1.symbol then f2 else f1).onComplete(Listener((v, _) => r.complete(v))) + }) + + end extension + + /** A promise is a [[Future]] that is be completed manually via the `complete` method. + * @see + * [[Promise$.apply]] to create a new, empty promise. + * @see + * [[Future.withResolver]] to create a passive [[Future]] from callback-style asynchronous calls. + */ + trait Promise[T] extends Future[T]: + inline def asFuture: Future[T] = this + + /** Define the result value of `future`. */ + def complete(result: Try[T]): Unit + + object Promise: + /** Create a new, unresolved [[Promise]]. */ + def apply[T](): Promise[T] = + new CoreFuture[T] with Promise[T]: + override def cancel(): Unit = + if setCancelled() then complete(Failure(new CancellationException())) + + /** Define the result value of `future`. However, if `future` was cancelled in the meantime complete with a + * `CancellationException` failure instead. + */ + override def complete(result: Try[T]): Unit = super[CoreFuture].complete(result) + end Promise + + /** The group of handlers to be used in [[withResolver]]. As a Future is completed only once, only one of + * resolve/reject/complete may be used and only once. + */ + trait Resolver[-T, Cap^]: + /** Complete the future with a data item successfully */ + def resolve(item: T): Unit = complete(Success(item)) + + /** Complete the future with a failure */ + def reject(exc: Throwable): Unit = complete(Failure(exc)) + + /** Complete the future with a [[CancellationException]] */ + def rejectAsCancelled(): Unit = complete(Failure(new CancellationException())) + + /** Complete the future with the result, be it Success or Failure */ + def complete(result: Try[T]): Unit + + /** Register a cancellation handler to be called when the created future is cancelled. Note that only one handler + * may be used. The handler should eventually complete the Future using one of complete/resolve/reject*. The + * default handler is set up to [[rejectAsCancelled]] immediately. + */ + def onCancel(handler: (() -> Unit)^{Cap^}): Unit + end Resolver + + /** Create a promise that may be completed asynchronously using external means. + * + * The body is run synchronously on the callers thread to setup an external asynchronous operation whose + * success/failure it communicates using the [[Resolver]] to complete the future. + * + * If the external operation supports cancellation, the body can register one handler using [[Resolver.onCancel]]. + */ + def withResolver[T, Cap^](body: Resolver[T, Cap]^{Cap^} => Unit): Future[T]^{Cap^} = + val future: (CoreFuture[T] & Resolver[T, Cap] & Promise[T])^{Cap^} = new CoreFuture[T] with Resolver[T, Cap] with Promise[T]: + // TODO: undo this once bug is fixed + @volatile var cancelHandle: (() -> Unit) = () => rejectAsCancelled() + override def onCancel(handler: (() -> Unit)^{Cap^}): Unit = + cancelHandle = /* TODO remove */ caps.unsafe.unsafeAssumePure(handler) + override def complete(result: Try[T]): Unit = super.complete(result) + + override def cancel(): Unit = + if setCancelled() then cancelHandle() + end future + body(future) + future + end withResolver + + sealed abstract class BaseCollector[T, Cap^](): + private val ch = UnboundedChannel[Future[T]^{Cap^}]() + + private val futMap = mutable.Map[SourceSymbol[Try[T]], Future[T]^{Cap^}]() + + /** Output channels of all finished futures. */ + final def results: ReadableChannel[Future[T]^{Cap^}] = ch.asReadable + + private val listener = Listener((_, fut) => + // safe, as we only attach this listener to Future[T] + val future = futMap.synchronized: + futMap.remove(fut.asInstanceOf[SourceSymbol[Try[T]]]).get + ch.sendImmediately(future) + ) + + protected final def addFuture(future: Future[T]^{Cap^}) = + futMap.synchronized { futMap += (future.symbol -> future) } + future.onComplete(listener) + end BaseCollector + + + /** Collects a list of futures into a channel of futures, arriving as they finish. + * @example + * {{{ + * // Sleep sort + * val futs = numbers.map(i => Future(sleep(i.millis))) + * val collector = Collector(futs*) + * + * val output = mutable.ArrayBuffer[Int]() + * for i <- 1 to futs.size: + * output += collector.results.read().await + * }}} + * @see + * [[Future.awaitAll]] and [[Future.awaitFirst]] for simple usage of the collectors to get all results or the first + * succeeding one. + */ + class Collector[T](futures: (Future[T]^)*) extends BaseCollector[T, caps.CapSet^{futures*}]: + futures.foreach(addFuture) + end Collector + + /** Like [[Collector]], but exposes the ability to add futures after creation. */ + class MutableCollector[T, Cap^](futures: (Future[T]^{Cap^})*) extends BaseCollector[T, Cap]: + futures.foreach(addFuture) + /** Add a new [[Future]] into the collector. */ + inline def add(future: Future[T]^{Cap^}) = addFuture(future) + inline def +=(future: Future[T]^{Cap^}) = add(future) + + extension [T](@caps.use fs: Seq[Future[T]^]) + /** `.await` for all futures in the sequence, returns the results in a sequence, or throws if any futures fail. */ + def awaitAll(using Async) = + val collector = Collector(fs*) + for _ <- fs do collector.results.read().right.get.await + fs.map(_.await) + + /** Like [[awaitAll]], but cancels all futures as soon as one of them fails. */ + def awaitAllOrCancel(using Async) = + val collector = Collector(fs*) + try + for _ <- fs do collector.results.read().right.get.await + fs.map(_.await) + catch + case NonFatal(e) => + fs.foreach(_.cancel()) + throw e + + /** Race all futures, returning the first successful value. Throws the last exception received, if everything fails. + */ + def awaitFirst(using Async): T = awaitFirstImpl(false) + + /** Like [[awaitFirst]], but cancels all other futures as soon as the first future succeeds. */ + def awaitFirstWithCancel(using Async): T = awaitFirstImpl(true) + + private inline def awaitFirstImpl(withCancel: Boolean)(using Async): T = + val collector = Collector(fs*) + @scala.annotation.tailrec + def loop(attempt: Int): T = + collector.results.read().right.get.awaitResult match + case Failure(exception) => + if attempt == fs.length then /* everything failed */ throw exception else loop(attempt + 1) + case Success(value) => + inline if withCancel then fs.foreach(_.cancel()) + value + loop(1) +end Future + +/** TaskSchedule describes the way in which a task should be repeated. Tasks can be set to run for example every 100 + * milliseconds or repeated as long as they fail. `maxRepetitions` describes the maximum amount of repetitions allowed, + * after that regardless of TaskSchedule chosen, the task is not repeated anymore and the last returned value is + * returned. `maxRepetitions` equal to zero means that repetitions can go on potentially forever. + */ +enum TaskSchedule: + case Every(val millis: Long, val maxRepetitions: Long = 0) + case ExponentialBackoff(val millis: Long, val exponentialBase: Int = 2, val maxRepetitions: Long = 0) + case FibonacciBackoff(val millis: Long, val maxRepetitions: Long = 0) + case RepeatUntilFailure(val millis: Long = 0, val maxRepetitions: Long = 0) + case RepeatUntilSuccess(val millis: Long = 0, val maxRepetitions: Long = 0) + +/** A task is a template that can be turned into a runnable future Composing tasks can be referentially transparent. + * Tasks can be also ran on a specified schedule. + */ +class Task[+T](val body: (Async, AsyncOperations) ?=> T): + + /** Run the current task and returns the result. */ + def run()(using Async, AsyncOperations): T = body + + /** Start a future computed from the `body` of this task */ + def start()(using async: Async, spawn: Async.Spawn)(using asyncOps: AsyncOperations)(using async.type =:= spawn.type): Future[T]^{body, spawn} = + Future(body)(using async, spawn) + + def schedule(s: TaskSchedule): Task[T]^{body} = + s match { + case TaskSchedule.Every(millis, maxRepetitions) => + assert(millis >= 1) + assert(maxRepetitions >= 0) + Task { + var repetitions = 0 + var ret: T = body + repetitions += 1 + if (maxRepetitions == 1) ret + else { + while (maxRepetitions == 0 || repetitions < maxRepetitions) { + AsyncOperations.sleep(millis) + ret = body + repetitions += 1 + } + ret + } + } + case TaskSchedule.ExponentialBackoff(millis, exponentialBase, maxRepetitions) => + assert(millis >= 1) + assert(exponentialBase >= 2) + assert(maxRepetitions >= 0) + Task { + var repetitions = 0 + var ret: T = body + repetitions += 1 + if (maxRepetitions == 1) ret + else { + var timeToSleep = millis + while (maxRepetitions == 0 || repetitions < maxRepetitions) { + AsyncOperations.sleep(timeToSleep) + timeToSleep *= exponentialBase + ret = body + repetitions += 1 + } + ret + } + } + case TaskSchedule.FibonacciBackoff(millis, maxRepetitions) => + assert(millis >= 1) + assert(maxRepetitions >= 0) + Task { + var repetitions = 0 + var a: Long = 0 + var b: Long = 1 + var ret: T = body + repetitions += 1 + if (maxRepetitions == 1) ret + else { + AsyncOperations.sleep(millis) + ret = body + repetitions += 1 + if (maxRepetitions == 2) ret + else { + while (maxRepetitions == 0 || repetitions < maxRepetitions) { + val aOld = a + a = b + b = aOld + b + AsyncOperations.sleep(b * millis) + ret = body + repetitions += 1 + } + ret + } + } + } + case TaskSchedule.RepeatUntilFailure(millis, maxRepetitions) => + assert(millis >= 0) + assert(maxRepetitions >= 0) + Task { + @tailrec + def helper(repetitions: Long = 0): T = + if (repetitions > 0 && millis > 0) + AsyncOperations.sleep(millis) + val ret: T = body + ret match { + case Failure(_) => ret + case _ if (repetitions + 1) == maxRepetitions && maxRepetitions != 0 => ret + case _ => helper(repetitions + 2) + } + helper() + } + case TaskSchedule.RepeatUntilSuccess(millis, maxRepetitions) => + assert(millis >= 0) + assert(maxRepetitions >= 0) + Task { + @tailrec + def helper(repetitions: Long = 0): T = + if (repetitions > 0 && millis > 0) + AsyncOperations.sleep(millis) + val ret: T = body + ret match { + case Success(_) => ret + case _ if (repetitions + 1) == maxRepetitions && maxRepetitions != 0 => ret + case _ => helper(repetitions + 2) + } + helper() + } + } + +end Task + +/** Runs the `body` inside in an [[Async]] context that does *not* propagate cancellation until the end. + * + * In other words, `body` is never notified of the cancellation of the `ac` context; but `uninterruptible` would still + * throw a [[CancellationException]] ''after `body` finishes running'' if `ac` was cancelled. + */ +def uninterruptible[T](body: Async ?=> T)(using ac: Async): T = + val tracker = Cancellable.Tracking().link() + + val r = + try + val group = CompletionGroup() + Async.withNewCompletionGroup(group)(body) + finally tracker.unlink() + + if tracker.isCancelled then throw new CancellationException() + r + +/** Link `cancellable` to the completion group of the current [[Async]] context during `fn`. + * + * If the [[Async]] context is cancelled during the execution of `fn`, `cancellable` will also be immediately + * cancelled. + */ +def cancellationScope[T](cancellable: Cancellable)(fn: => T)(using a: Async): T = + cancellable.link() + try fn + finally cancellable.unlink() diff --git a/tests/neg-custom-args/captures/gears/locking.scala b/tests/neg-custom-args/captures/gears/locking.scala new file mode 100644 index 000000000000..e07ec390aa60 --- /dev/null +++ b/tests/neg-custom-args/captures/gears/locking.scala @@ -0,0 +1,48 @@ +/** Package listeners provide some auxilliary methods to work with listeners. */ +package gears.async.listeners + +import language.experimental.captureChecking + +import gears.async._ + +import scala.annotation.tailrec + +import Listener.ListenerLock + +/** Two listeners being locked at the same time, while having the same [[Listener.ListenerLock.selfNumber lock number]]. + */ +case class ConflictingLocksException( + listeners: (Listener[?]^, Listener[?]^) +) extends Exception + +/** Attempt to lock both listeners belonging to possibly different sources at the same time. Lock orders are respected + * by comparing numbers on every step. + * + * Returns `true` on success, or the listener that fails first. + * + * @throws ConflictingLocksException + * In the case that two locks sharing the same number is encountered, this exception is thrown with the conflicting + * listeners. + */ +def lockBoth[T, U]( + lt: Listener[T]^, + lu: Listener[U]^ +): (lt.type | lu.type | true) = + val lockT = if lt.lock == null then return (if lu.acquireLock() then true else lu) else lt.lock + val lockU = if lu.lock == null then return (if lt.acquireLock() then true else lt) else lu.lock + + def doLock[T, U](lt: Listener[T]^, lu: Listener[U]^)( + lockT: ListenerLock^{lt}, + lockU: ListenerLock^{lu} + ): (lt.type | lu.type | true) = + // assert(lockT.number > lockU.number) + if !lockT.acquire() then lt + else if !lockU.acquire() then + lockT.release() + lu + else true + + if lockT.selfNumber == lockU.selfNumber then throw ConflictingLocksException((lt, lu)) + else if lockT.selfNumber > lockU.selfNumber then doLock(lt, lu)(lockT, lockU) + else doLock(lu, lt)(lockU, lockT) +end lockBoth diff --git a/tests/neg-custom-args/captures/gears/measureTimes.scala b/tests/neg-custom-args/captures/gears/measureTimes.scala new file mode 100644 index 000000000000..5be5c96a79e8 --- /dev/null +++ b/tests/neg-custom-args/captures/gears/measureTimes.scala @@ -0,0 +1,512 @@ +package measurements + +import language.experimental.captureChecking + +import gears.async.default.given +import gears.async.{Async, BufferedChannel, ChannelMultiplexer, Future, SyncChannel} + +import java.io.{FileReader, FileWriter} +import java.nio.file.{Files, NoSuchFileException, Paths, StandardOpenOption} +import java.util.concurrent.atomic.AtomicInteger +import scala.collection.mutable +import scala.collection.mutable.{ArrayBuffer, HashMap} +import scala.concurrent.ExecutionContext +import scala.util.CommandLineParser.FromString.given_FromString_Int +import scala.util.Try + +import PosixLikeIO.PIOHelper + +case class TimeMeasurementResult(millisecondsPerOperation: Double, standardDeviation: Double) + +def measureIterations[T](action: () => T): Int = + val counter = AtomicInteger(0) + + val t1 = Thread.startVirtualThread: () => + try { + while (!Thread.interrupted()) { + action() + val r = counter.getAndIncrement() + } + } catch { + case (_: InterruptedException) => () + } + + Thread.sleep(10 * 1000) + counter.set(0) + Thread.sleep(60 * 1000) + t1.interrupt() + counter.get() + +@main def measureFutureOverhead(): Unit = + given ExecutionContext = ExecutionContext.global + + val threadJoins = measureIterations: () => + val t = Thread.startVirtualThread: () => + var z = 1 + t.join() + + val futureJoins = measureIterations: () => + Async.blocking: + val f = Future: + var z = 1 + f.awaitResult + + println("Thread joins per second: " + (threadJoins / 60)) + println("Future joins per second: " + (futureJoins / 60)) + println("Overhead: " + ((threadJoins + 0.0) / (futureJoins + 0.0))) + + /* + Linux: + Thread joins per second: 292647 + Future joins per second: 86032 + Overhead: 3.401577460379452 + */ + +@main def measureRaceOverhead(): Unit = + given ExecutionContext = ExecutionContext.global + + val c1: Double = measureIterations: () => + Async.blocking: + Async.race(Future { Thread.sleep(10) }, Future { Thread.sleep(100) }, Future { Thread.sleep(50) }).await + Async.race(Future { Thread.sleep(50) }, Future { Thread.sleep(10) }, Future { Thread.sleep(100) }).await + Async.race(Future { Thread.sleep(100) }, Future { Thread.sleep(50) }, Future { Thread.sleep(10) }).await + + val c2: Double = measureIterations: () => + Async.blocking: + val f11 = Future { Thread.sleep(10) } + val f12 = Future { Thread.sleep(50) } + val f13 = Future { Thread.sleep(100) } + f11.awaitResult + + val f21 = Future { Thread.sleep(100) } + val f22 = Future { Thread.sleep(10) } + val f23 = Future { Thread.sleep(50) } + f22.awaitResult + + val f31 = Future { Thread.sleep(50) } + val f32 = Future { Thread.sleep(100) } + val f33 = Future { Thread.sleep(10) } + f33.awaitResult + + val c1_seconds_wasted_for_waits = c1 * 0.01 + val c1_per_second_adjusted = c1 / 3 / (60 - c1_seconds_wasted_for_waits) + val c2_seconds_wasted_for_waits = c2 * 0.01 + val c2_per_second_adjusted = c1 / 3 / (60 - c2_seconds_wasted_for_waits) + + println("Raced futures awaited per second: " + c1_per_second_adjusted) + println("Non-raced futures per second: " + c2_per_second_adjusted) + println("Overhead: " + (c2_per_second_adjusted / c1_per_second_adjusted)) + + /* Linux + Raced futures awaited per second: 15.590345727332032 + Non-raced futures per second: 15.597976831457009 + Overhead: 1.0004894762604013 + */ + +@main def measureRaceOverheadVsJava(): Unit = + given ExecutionContext = ExecutionContext.global + + val c1: Double = measureIterations: () => + Async.blocking: + Async.race(Future { Thread.sleep(10) }, Future { Thread.sleep(100) }, Future { Thread.sleep(50) }).await + Async.race(Future { Thread.sleep(50) }, Future { Thread.sleep(10) }, Future { Thread.sleep(100) }).await + Async.race(Future { Thread.sleep(100) }, Future { Thread.sleep(50) }, Future { Thread.sleep(10) }).await + + val c2: Double = measureIterations: () => + @volatile var i1 = true + val f11 = Thread.startVirtualThread(() => { Thread.sleep(10); i1 = false }) + val f12 = Thread.startVirtualThread(() => { Thread.sleep(50); i1 = false }) + val f13 = Thread.startVirtualThread(() => { Thread.sleep(100); i1 = false }) + while (i1) () + + @volatile var i2 = true + val f21 = Thread.startVirtualThread(() => { Thread.sleep(100); i2 = false }) + val f22 = Thread.startVirtualThread(() => { Thread.sleep(10); i2 = false }) + val f23 = Thread.startVirtualThread(() => { Thread.sleep(50); i2 = false }) + while (i2) () + + @volatile var i3 = true + val f31 = Thread.startVirtualThread(() => { Thread.sleep(50); i3 = false }) + val f32 = Thread.startVirtualThread(() => { Thread.sleep(100); i3 = false }) + val f33 = Thread.startVirtualThread(() => { Thread.sleep(10); i3 = false }) + while (i3) () + + f11.interrupt() + f12.interrupt() + f13.interrupt() + f21.interrupt() + f22.interrupt() + f23.interrupt() + f31.interrupt() + f32.interrupt() + f33.interrupt() + + val c1_seconds_wasted_for_waits = c1 * 0.01 + val c1_per_second_adjusted = c1 / 3 / (60 - c1_seconds_wasted_for_waits) + val c2_seconds_wasted_for_waits = c2 * 0.01 + val c2_per_second_adjusted = c1 / 3 / (60 - c2_seconds_wasted_for_waits) + + println("Raced futures awaited per second: " + c1_per_second_adjusted) + println("Java threads awaited per second: " + c2_per_second_adjusted) + println("Overhead: " + (c2_per_second_adjusted / c1_per_second_adjusted)) + + /* Linux + Raced futures awaited per second: 15.411487529449996 + Java threads awaited per second: 15.671210243700953 + Overhead: 1.0168525402726147 + */ + +@main def channelsVsJava(): Unit = + given ExecutionContext = ExecutionContext.global + + val sec = 60 + + // java + @volatile var shared: Long = 0 + @volatile var timeForWriting = true + val t1 = Thread.startVirtualThread: () => + var i: Long = 0 + while (true) { + while (!timeForWriting) () + shared = i + timeForWriting = false + i += 1 + } + + val t2 = Thread.startVirtualThread: () => + while (true) { + while (timeForWriting) () + var z = shared + timeForWriting = true + } + + Thread.sleep(sec * 1000) + t1.interrupt() + t2.interrupt() + val javaSendsPerSecond: Long = shared / sec + println("Java \"channel\" sends per second: " + javaSendsPerSecond) + + var syncChannelSendsPerSecond = 0.0 + var bufferedChannelSendsPerSecond = 0.0 + var cmOverSyncSendsPerSecond = 0.0 + var cmOverBufferedSendsPerSecond = 0.0 + + Async.blocking: + val c = SyncChannel[Long]() + val f1 = Future: + var i: Long = 0 + while (true) { + try { + c.send(i) + } catch { + case (e: InterruptedException) => { + syncChannelSendsPerSecond = i / sec + throw e + } + } + i += 1 + } + val f2 = Future: + while (true) { + c.read() + } + + Thread.sleep(sec * 1000) + f1.cancel() + f2.cancel() + Thread.sleep(500) + println("SyncChannel sends per second: " + syncChannelSendsPerSecond) + + Async.blocking: + val c = BufferedChannel[Long](1) + val f1 = Future: + var i: Long = 0 + while (true) { + try { + c.send(i) + } catch { + case (e: InterruptedException) => { + bufferedChannelSendsPerSecond = i / sec + throw e + } + } + i += 1 + } + val f2 = Future: + while (true) { + c.read() + } + + Thread.sleep(sec * 1000) + f1.cancel() + f2.cancel() + Thread.sleep(500) + println("BufferedChannel sends per second: " + bufferedChannelSendsPerSecond) + + Async.blocking: + val m = ChannelMultiplexer[Long]() + val c = SyncChannel[Long]() + val cr = SyncChannel[Try[Long]]() + m.addPublisher(c) + m.addSubscriber(cr) + Thread.sleep(50) + + val f1 = Future: + var i: Long = 0 + while (true) { + try { + c.send(i) + } catch { + case (e: InterruptedException) => { + cmOverSyncSendsPerSecond = i / sec + throw e + } + } + i += 1 + } + val f2 = Future: + while (true) { + cr.read() + } + + Thread.sleep(sec * 1000) + f1.cancel() + f2.cancel() + Thread.sleep(500) + println("ChannelMultiplexer over SyncChannels sends per second: " + cmOverSyncSendsPerSecond) + + Async.blocking: + val m = ChannelMultiplexer[Long]() + val c = BufferedChannel[Long](1) + val cr = BufferedChannel[Try[Long]](1) + m.addPublisher(c) + m.addSubscriber(cr) + Thread.sleep(50) + + val f1 = Future: + var i: Long = 0 + while (true) { + try { + c.send(i) + } catch { + case (e: InterruptedException) => { + cmOverBufferedSendsPerSecond = i / sec + throw e + } + } + i += 1 + } + val f2 = Future: + while (true) { + cr.read() + } + + Thread.sleep(sec * 1000) + f1.cancel() + f2.cancel() + Thread.sleep(500) + println("ChannelMultiplexer over BufferedChannels sends per second: " + cmOverBufferedSendsPerSecond) + + /* Linux + Java "channel" sends per second: 8691652 + SyncChannel sends per second: 319371.0 + BufferedChannel sends per second: 308286.0 + ChannelMultiplexer over SyncChannels sends per second: 155737.0 + ChannelMultiplexer over BufferedChannels sends per second: 151995.0 + */ + +/** Warmup for 10 seconds and benchmark for 60 seconds. + */ +def measureRunTimes[T](action: () => T): TimeMeasurementResult = + + var timesIn25Milliseconds: Long = 0 + { + val minibenchmarkStart = System.nanoTime() + while (System.nanoTime() - minibenchmarkStart < 25L * 1000 * 1000) { + action() + timesIn25Milliseconds += 1 + } + assert(timesIn25Milliseconds >= 1) + } + + val times = ArrayBuffer[Double]() + + { + val warmupStart = System.currentTimeMillis() + while (System.currentTimeMillis() - warmupStart < 10L * 1000) + action() + } + + System.err.println("Warming up completed.") + + val benchmarkingStart = System.nanoTime() + var benchmarkingTimeStillNotPassed = true + while (benchmarkingTimeStillNotPassed) { + + val start = System.nanoTime() + for (_ <- 1L to timesIn25Milliseconds) + action() + val end = System.nanoTime() + var nanoTimePerOperation: Double = (end - start + (0.0).toDouble) / timesIn25Milliseconds.toDouble + times.append(nanoTimePerOperation) + + if (end - benchmarkingStart >= 60L * 1000 * 1000 * 1000) + benchmarkingTimeStillNotPassed = false + } + + var avg: Double = 0.0 + times.foreach(avg += _) + avg /= times.length + + var stdev: Double = 0.0 + for (x <- times) + stdev += (x - avg) * (x - avg) + assert(times.length >= 2) + stdev /= (times.length - 1) + stdev = Math.sqrt(stdev) + + TimeMeasurementResult(avg / 1000 / 1000, stdev / 1000 / 1000) + +@main def measureSomething(): Unit = + + val g = measureRunTimes: () => + var t = 100100 + t *= 321984834 + t /= 1238433 + t /= 1222 + Thread.sleep(11) + println(g) + +@main def measureTimesNew: Unit = + + // mkdir -p /tmp/FIO && sudo mount -t tmpfs -o size=8g tmpfs /tmp/FIO + + given ExecutionContext = ExecutionContext.global + + val dataAlmostJson = StringBuffer() // TEST:String -> PARAMETER:String -> METHOD:String -> TIMES:List[Double] + dataAlmostJson.append("{") + + def measure[T](methodName: String, timesInner: Int = 100, timesOuter: Int = 100)(action: () => T): String = + val times = ArrayBuffer[Double]() + for (_ <- 1 to timesOuter) + val timeStart = System.nanoTime() + for (_ <- 1 to timesInner) + action() + val timeEnd = System.nanoTime() + times += ((timeEnd - timeStart + 0.0) / 1000 / 1000 / timesInner) + + var avg: Double = 0.0 + times.foreach(avg += _) + avg /= times.length + + var stdev: Double = 0.0 + for (x <- times) + stdev += (x - avg) * (x - avg) + assert(times.length >= 2) + stdev /= (times.length - 1) + stdev = Math.sqrt(stdev) + + val ret = StringBuffer() + ret.append("\"") + ret.append(methodName) + ret.append("\": [") + ret.append(avg) + ret.append(", ") + ret.append(stdev) + ret.append("],\n") + ret.toString + + val bigStringBuilder = new StringBuilder() + for (_ <- 1 to 10 * 1024 * 1024) bigStringBuilder.append("abcd") + val bigString = bigStringBuilder.toString() + + def deleteFiles(): Unit = + for (p <- Array("x", "y", "z")) + try Files.delete(Paths.get("/tmp/FIO/" + p + ".txt")) + catch case e: NoSuchFileException => () + + deleteFiles() + + dataAlmostJson.append("\n\t\"File writing\": {\n") + { + for (size <- Seq(4, 40 * 1024 * 1024)) + println("size " + size.toString) + dataAlmostJson.append("\n\t\t\"Size " + size.toString + "\": {\n") + { + dataAlmostJson.append(measure("PosixLikeIO", timesInner = if size < 100 then 100 else 10): () => + Async.blocking: + PIOHelper.withFile("/tmp/FIO/x.txt", StandardOpenOption.CREATE, StandardOpenOption.WRITE): f => + f.writeString(bigString.substring(0, size)).awaitResult + ) + println("done 1") + + dataAlmostJson.append(measure("Java FileWriter", timesInner = if size < 100 then 100 else 10): () => + val writer = new FileWriter("/tmp/FIO/y.txt") + writer.write(bigString.substring(0, size), 0, size) + writer.close() + ) + println("done 2") + + dataAlmostJson.append(measure("Java Files.writeString", timesInner = if size < 100 then 100 else 10): () => + Files.writeString(Paths.get("/tmp/FIO/z.txt"), bigString.substring(0, size))) + println("done 3") + } + dataAlmostJson.append("},\n") + } + dataAlmostJson.append("},\n") + + dataAlmostJson.append("\n\t\"File reading\": {\n") + { + for (size <- Seq(4, 40 * 1024 * 1024)) + println("size " + size.toString) + deleteFiles() + Files.writeString(Paths.get("/tmp/FIO/x.txt"), bigString.substring(0, size)) + Files.writeString(Paths.get("/tmp/FIO/y.txt"), bigString.substring(0, size)) + Files.writeString(Paths.get("/tmp/FIO/z.txt"), bigString.substring(0, size)) + + dataAlmostJson.append("\n\t\t\"Size " + size.toString + "\": {\n") + { + dataAlmostJson.append(measure("PosixLikeIO", timesInner = if size < 100 then 100 else 10): () => + Async.blocking: + PIOHelper.withFile("/tmp/FIO/x.txt", StandardOpenOption.READ): f => + f.readString(size).awaitResult + ) + println("done 1") + + val buffer = new Array[Char](size) + dataAlmostJson.append(measure("Java FileReeader", timesInner = if size < 100 then 100 else 10): () => + val reader = new FileReader("/tmp/FIO/y.txt") + reader.read(buffer) + reader.close() + ) + println("done 2") + + dataAlmostJson.append(measure("Java Files.readString", timesInner = if size < 100 then 100 else 10): () => + Files.readString(Paths.get("/tmp/FIO/z.txt"))) + println("done 3") + } + dataAlmostJson.append("},\n") + } + dataAlmostJson.append("},\n") + + dataAlmostJson.append("}") + println(dataAlmostJson.toString) + + /* Linux + { + "File writing": { + + "Size 4": { + "PosixLikeIO": [0.0397784622, 0.08412340604573831], + "Java FileWriter": [0.010826620499999997, 0.00979259772337624], + "Java Files.write": [0.007529464599999997, 0.0028499973824777695], + }, + + "Size 41943040": { + "PosixLikeIO": [16.846597593, 0.889024137544089], + "Java FileWriter": [29.068105414999977, 3.766062167872921], + "Java Files.write": [18.96376850600001, 0.20493288428568684], + }, + }, + } + */ diff --git a/tests/neg-custom-args/captures/gears/package.scala b/tests/neg-custom-args/captures/gears/package.scala new file mode 100644 index 000000000000..13066a181bb9 --- /dev/null +++ b/tests/neg-custom-args/captures/gears/package.scala @@ -0,0 +1,14 @@ +package gears + +import language.experimental.captureChecking + +/** Asynchronous programming support with direct-style Scala. + * @see + * [[gears.async.Async]] for an introduction to the [[Async]] context and how to create them. + * @see + * [[gears.async.Future]] for a simple interface to spawn concurrent computations. + * @see + * [[gears.async.Channel]] for a simple inter-future communication primitive. + */ +package object async: + type CancellationException = java.util.concurrent.CancellationException diff --git a/tests/neg-custom-args/captures/gears/readAndWriteFile.scala b/tests/neg-custom-args/captures/gears/readAndWriteFile.scala new file mode 100644 index 000000000000..a2bf39a96176 --- /dev/null +++ b/tests/neg-custom-args/captures/gears/readAndWriteFile.scala @@ -0,0 +1,20 @@ +package PosixLikeIO.examples + +import gears.async.Async +import gears.async.default.given + +import java.nio.ByteBuffer +import java.nio.charset.StandardCharsets +import java.nio.file.StandardOpenOption +import scala.concurrent.ExecutionContext + +import PosixLikeIO.PIOHelper + +@main def readAndWriteFile(): Unit = + given ExecutionContext = ExecutionContext.global + Async.blocking: + PIOHelper.withFile("/home/julian/Desktop/x.txt", StandardOpenOption.READ, StandardOpenOption.WRITE): f => + f.writeString("Hello world! (1)").await + println(f.readString(1024).await) + f.writeString("Hello world! (2)").await + println(f.readString(1024).await) diff --git a/tests/neg-custom-args/captures/gears/readWholeFile.scala b/tests/neg-custom-args/captures/gears/readWholeFile.scala new file mode 100644 index 000000000000..7811122c8d79 --- /dev/null +++ b/tests/neg-custom-args/captures/gears/readWholeFile.scala @@ -0,0 +1,25 @@ +package PosixLikeIO.examples + +import gears.async.Async +import gears.async.default.given + +import java.nio.ByteBuffer +import java.nio.charset.StandardCharsets +import java.nio.file.StandardOpenOption +import scala.concurrent.ExecutionContext + +import PosixLikeIO.PIOHelper + +@main def readWholeFile(): Unit = + given ExecutionContext = ExecutionContext.global + Async.blocking: + PIOHelper.withFile("/home/julian/Desktop/x.txt", StandardOpenOption.READ): f => + val b = ByteBuffer.allocate(1024) + val retCode = f.read(b).awaitResult.get + assert(retCode >= 0) + val s = StandardCharsets.UTF_8.decode(b.slice(0, retCode)).toString() + println("Read size with read(): " + retCode.toString()) + println("Data: " + s) + + println("Read with readString():") + println(f.readString(1000).awaitResult) diff --git a/tests/neg-custom-args/captures/gears/retry.scala b/tests/neg-custom-args/captures/gears/retry.scala new file mode 100644 index 000000000000..d06507f5cbb3 --- /dev/null +++ b/tests/neg-custom-args/captures/gears/retry.scala @@ -0,0 +1,156 @@ +package gears.async + +import language.experimental.captureChecking + +import gears.async.Async +import gears.async.AsyncOperations.sleep +import gears.async.Retry.Delay + +import scala.concurrent.duration._ +import scala.util.Random +import scala.util.boundary +import scala.util.control.NonFatal +import scala.util.{Failure, Success, Try} + +/** Utility class to perform asynchronous actions with retrying policies on exceptions. + * + * See [[Retry]] companion object for common policies as a starting point. + */ +case class Retry( + retryOnSuccess: Boolean = false, + maximumFailures: Option[Int] = None, + delay: Delay = Delay.none +): + /** Runs `body` with the current policy in its own scope, returning the result or the last failure as an exception. + */ + def apply[T](op: => T)(using Async, AsyncOperations): T = + var failures = 0 + var lastDelay: FiniteDuration = 0.second + boundary: + while true do + try + val value = op + if retryOnSuccess then + failures = 0 + lastDelay = delay.delayFor(failures, lastDelay) + sleep(lastDelay) + else boundary.break(value) + catch + case b: boundary.Break[?] => throw b // handle this manually as it will be otherwise caught by NonFatal + case NonFatal(exception) => + if maximumFailures.exists(_ == failures) then // maximum failure count reached + throw exception + else + failures = failures + 1 + lastDelay = delay.delayFor(failures, lastDelay) + sleep(lastDelay) + end while + ??? + + /** Set the maximum failure count. */ + def withMaximumFailures(max: Int) = + assert(max >= 0) + this.copy(maximumFailures = Some(max)) + + /** Set the delay policy between runs. See [[Retry.Delay]]. */ + def withDelay(delay: Delay) = this.copy(delay = delay) + +object Retry: + /** Ignores the result and attempt the action in an infinite loop. [[Retry.withMaximumFailures]] can be useful for + * bailing on multiple failures. [[scala.util.boundary]] can be used for manually breaking. + */ + val forever = Retry(retryOnSuccess = true) + + /** Returns the result, or attempt to retry if an exception is raised. */ + val untilSuccess = Retry(retryOnSuccess = false) + + /** Attempt to retry the operation *until* an exception is raised. In this mode, [[Retry]] always throws an exception + * on return. + */ + val untilFailure = Retry(retryOnSuccess = true).withMaximumFailures(0) + + /** Defines a delay policy based on the number of successive failures and the duration of the last delay. See + * [[Delay]] companion object for some provided delay policies. + */ + trait Delay: + /** Return the expected duration to delay the next attempt from the current attempt. + * + * @param failuresCount + * The number of successive failures until the current attempt. Note that if the last attempt was a success, + * `failuresCount` is `0`. + * @param lastDelay + * The duration of the last delay. + */ + def delayFor(failuresCount: Int, lastDelay: FiniteDuration): FiniteDuration + + object Delay: + /** No delays. */ + val none = constant(0.second) + + /** A fixed amount of delays, whether the last attempt was a success or failure. */ + def constant(duration: FiniteDuration) = new Delay: + def delayFor(failuresCount: Int, lastDelay: FiniteDuration): FiniteDuration = duration + + /** Returns a delay policy for exponential backoff. + * @param maximum + * The maximum duration possible for a delay. + * @param starting + * The delay duration between successful attempts, and after the first failures. + * @param multiplier + * Scale the delay duration by this multiplier for each successive failure. Defaults to `2`. + * @param jitter + * An additional jitter to randomize the delay duration. Defaults to none. See [[Jitter]]. + */ + def backoff(maximum: Duration, starting: FiniteDuration, multiplier: Double = 2, jitter: Jitter = Jitter.none) = + new Delay: + def delayFor(failuresCount: Int, lastDelay: FiniteDuration): FiniteDuration = + val sleep = jitter + .jitterDelay( + lastDelay, + if failuresCount <= 1 then starting + else (starting.toMillis * scala.math.pow(multiplier, failuresCount - 1)).millis + ) + maximum match + case max: FiniteDuration => sleep.min(max) + case _ => sleep /* infinite maximum */ + + /** Decorrelated exponential backoff: randomize between the last delay duration and a multiple of that duration. */ + def deccorelated(maximum: Duration, starting: Duration, multiplier: Double = 3) = + new Delay: + def delayFor(failuresCount: Int, lastDelay: FiniteDuration): FiniteDuration = + val lowerBound = + if failuresCount <= 1 then 0.second else lastDelay + val upperBound = + (if failuresCount <= 1 then starting + else multiplier * lastDelay).min(maximum) + Random.between(lowerBound.toMillis, upperBound.toMillis + 1).millis + + /** A randomizer for the delay duration, to avoid accidental coordinated DoS on failures. See [[Jitter]] companion + * objects for some provided jitter implementations. + */ + trait Jitter: + /** Returns the *actual* duration to delay between attempts, given the theoretical given delay and actual last delay + * duration, possibly with some randomization. + * @param last + * The last delay duration performed, with jitter applied. + * @param maximum + * The theoretical amount of delay governed by the [[Delay]] policy, serving as an upper bound. + */ + def jitterDelay(last: FiniteDuration, maximum: FiniteDuration): FiniteDuration + + object Jitter: + import FiniteDuration as Duration + + /** No jitter, always return the exact duration suggested by the policy. */ + val none = new Jitter: + def jitterDelay(last: Duration, maximum: Duration): Duration = maximum + + /** Full jitter: randomize between 0 and the suggested delay duration. */ + val full = new Jitter: + def jitterDelay(last: Duration, maximum: Duration): Duration = Random.between(0, maximum.toMillis + 1).millis + + /** Equal jitter: randomize between the last delay duration and the suggested delay duration. */ + val equal = new Jitter: + def jitterDelay(last: Duration, maximum: Duration): Duration = + val base = maximum.toMillis / 2 + (base + Random.between(0, maximum.toMillis - base + 1)).millis From 75c60f470170fed1d92b382230b629b322d544b5 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Tue, 21 Jan 2025 16:57:00 +0100 Subject: [PATCH 4/5] Patch the files to make them compile without JDK21 --- .../captures/gears/CCBehavior.scala | 175 +++++++++--------- .../captures/gears/VThreadSupport.scala | 7 +- .../captures/gears/clientAndServerUDP.scala | 17 +- .../captures/gears/measureTimes.scala | 30 +-- 4 files changed, 114 insertions(+), 115 deletions(-) diff --git a/tests/neg-custom-args/captures/gears/CCBehavior.scala b/tests/neg-custom-args/captures/gears/CCBehavior.scala index a1e207696433..f75c5924bf5b 100644 --- a/tests/neg-custom-args/captures/gears/CCBehavior.scala +++ b/tests/neg-custom-args/captures/gears/CCBehavior.scala @@ -25,93 +25,90 @@ object Result: case Left(value) => boundary.break(Left(value)) case Right(value) => value -class CaptureCheckingBehavior extends munit.FunSuite: - import Result.* - import caps.use - import scala.collection.mutable - - test("good") { - // don't do this in real code! capturing Async.blocking's Async context across functions is hard to track - Async.blocking: async ?=> - def good1[T, E](@use frs: List[Future[Result[T, E]]^]): Future[Result[List[T], E]]^{frs*, async} = +import Result.* + +def good = + // don't do this in real code! capturing Async.blocking's Async context across functions is hard to track + Async.blocking: async ?=> + def good1[T, E](@caps.use frs: List[Future[Result[T, E]]^]): Future[Result[List[T], E]]^{frs*, async} = + Future: fut ?=> + Result: ret ?=> + frs.map(_.await.ok) + + def good2[T, E](@caps.use rf: Result[Future[T]^, E]): Future[Result[T, E]]^{rf*, async} = + Future: + Result: + rf.ok.await // OK, Future argument has type Result[T] + + def useless4[T, E](fr: Future[Result[T, E]]^) = + fr.await.map(Future(_)) + + +def `bad - collectors` = + val futs: Seq[Future[Int]^] = Async.blocking: async ?=> // error + val fs: Seq[Future[Int]^{async}] = (0 to 10).map(i => Future { i }) + fs + Async.blocking: + futs.awaitAll // error + + +def `future withResolver capturing` = { + class File() extends caps.Capability: + def close() = () + def read(callback: Int => Unit) = () + val f = File() + val read = Future.withResolver[Int, caps.CapSet^{f}]: r => + f.read(r.resolve) + r.onCancel(f.close) +} + +def `awaitAll/awaitFirst` = { + trait File extends caps.Capability: + def readFut(): Future[Int]^{this} + object File: + def open[T](filename: String)(body: File => T)(using Async): T = ??? + + def readAll(@caps.use files: (File^)*) = files.map(_.readFut()) + + Async.blocking: // error + File.open("a.txt"): a => // error + File.open("b.txt"): b => // error + val futs = readAll(a, b) + val allFut = Future(futs.awaitAll) + allFut + // .await // uncomment to leak +} + +def `channel` = { + trait File extends caps.Capability: + def read(): Int = ??? + Async.blocking: + val ch = SyncChannel[File]() // error + // Sender + val sender = Future: + val f = new File {} + ch.send(f) + val recv = Future: + val f = ch.read().right.get + f.read() +} + +def `upcasting to any` = { + Async.blocking: async ?=> + def fail3[T, E](fr: Future[Result[T, E]]^): Result[Any, Any] = + Result: label ?=> // escaping label from Result, not yet err Future: fut ?=> - Result: ret ?=> - frs.map(_.await.ok) - - def good2[T, E](@use rf: Result[Future[T]^, E]): Future[Result[T, E]]^{rf*, async} = - Future: - Result: - rf.ok.await // OK, Future argument has type Result[T] - - def useless4[T, E](fr: Future[Result[T, E]]^) = - fr.await.map(Future(_)) - } - - // test("bad - collectors") { - // val futs: Seq[Future[Int]^] = Async.blocking: async ?=> - // val fs: Seq[Future[Int]^{async}] = (0 to 10).map(i => Future { i }) - // fs - // Async.blocking: - // futs.awaitAll // should not compile - // } - - test("future withResolver capturing") { - class File() extends caps.Capability: - def close() = () - def read(callback: Int => Unit) = () - val f = File() - val read = Future.withResolver[Int, caps.CapSet^{f}]: r => - f.read(r.resolve) - r.onCancel(f.close) - } - - test("awaitAll/awaitFirst") { - trait File extends caps.Capability: - def readFut(): Future[Int]^{this} - object File: - def open[T](filename: String)(body: File => T)(using Async): T = ??? - - def readAll(@caps.use files: (File^)*) = files.map(_.readFut()) - - Async.blocking: - File.open("a.txt"): a => - File.open("b.txt"): b => - val futs = readAll(a, b) - val allFut = Future(futs.awaitAll) - allFut - .await // uncomment to leak - } - - // test("channel") { - // trait File extends caps.Capability: - // def read(): Int = ??? - // Async.blocking: - // val ch = SyncChannel[File]() - // // Sender - // val sender = Future: - // val f = new File {} - // ch.send(f) - // val recv = Future: - // val f = ch.read().right.get - // f.read() - // } - - test("very bad") { - Async.blocking: async ?=> - def fail3[T, E](fr: Future[Result[T, E]]^): Result[Any, Any] = - Result: label ?=> - Future: fut ?=> - fr.await.ok // error, escaping label from Result - - // val fut = Future(Left(5)) - // val res = fail3(fut) - // println(res.right.get.asInstanceOf[Future[Any]].awaitResult) - } - - // test("bad") { - // Async.blocking: async ?=> - // def fail3[T, E](fr: Future[Result[T, E]]^): Result[Future[T]^{async}, E] = - // Result: label ?=> - // Future: fut ?=> - // fr.await.ok // error, escaping label from Result - // } + fr.await.ok + + // val fut = Future(Left(5)) + // val res = fail3(fut) + // println(res.right.get.asInstanceOf[Future[Any]].awaitResult) +} + +def `bad` = { + Async.blocking: async ?=> + def fail3[T, E](fr: Future[Result[T, E]]^): Result[Future[T]^{async}, E] = + Result: label ?=> // error, escaping label from Result + Future: fut ?=> + fr.await.ok +} diff --git a/tests/neg-custom-args/captures/gears/VThreadSupport.scala b/tests/neg-custom-args/captures/gears/VThreadSupport.scala index a287d7627c73..7ea2d44e21bf 100644 --- a/tests/neg-custom-args/captures/gears/VThreadSupport.scala +++ b/tests/neg-custom-args/captures/gears/VThreadSupport.scala @@ -10,10 +10,9 @@ import scala.annotation.constructorOnly import scala.collection.mutable object VThreadScheduler extends Scheduler: - private val VTFactory = Thread - .ofVirtual() - .name("gears.async.VThread-", 0L) - .factory() + private val VTFactory = new java.util.concurrent.ThreadFactory: + def newThread(r: Runnable): Thread = + new Thread(null, r, "gears.async.VThread-", 0L) override def execute(body: Runnable): Unit = val th = VTFactory.newThread(body) diff --git a/tests/neg-custom-args/captures/gears/clientAndServerUDP.scala b/tests/neg-custom-args/captures/gears/clientAndServerUDP.scala index a1c30b41fe52..d0ec49b6aa2e 100644 --- a/tests/neg-custom-args/captures/gears/clientAndServerUDP.scala +++ b/tests/neg-custom-args/captures/gears/clientAndServerUDP.scala @@ -22,14 +22,13 @@ import PosixLikeIO.{PIOHelper, SocketUDP} serverSocket.send(ByteBuffer.wrap(responseMessage), got.getAddress.toString.substring(1), got.getPort) sleep(50) - def client(value: Int): Future[Unit] = - Future: - PIOHelper.withSocketUDP(): clientSocket => - val data: Array[Byte] = value.toString.getBytes - clientSocket.send(ByteBuffer.wrap(data), "localhost", 8134).awaitResult.get - val responseDatagram = clientSocket.receive().awaitResult.get - val messageReceived = String(responseDatagram.getData.slice(0, responseDatagram.getLength), "UTF-8").toInt - println("Sent " + value.toString + " and got " + messageReceived.toString + " in return.") + def client(value: Int)(using Async) = + PIOHelper.withSocketUDP(): clientSocket => + val data: Array[Byte] = value.toString.getBytes + clientSocket.send(ByteBuffer.wrap(data), "localhost", 8134).awaitResult.get + val responseDatagram = clientSocket.receive().awaitResult.get + val messageReceived = String(responseDatagram.getData.slice(0, responseDatagram.getLength), "UTF-8").toInt + println("Sent " + value.toString + " and got " + messageReceived.toString + " in return.") - client(100).await + client(100) server.await diff --git a/tests/neg-custom-args/captures/gears/measureTimes.scala b/tests/neg-custom-args/captures/gears/measureTimes.scala index 5be5c96a79e8..ec74bf7b1b87 100644 --- a/tests/neg-custom-args/captures/gears/measureTimes.scala +++ b/tests/neg-custom-args/captures/gears/measureTimes.scala @@ -16,12 +16,16 @@ import scala.util.Try import PosixLikeIO.PIOHelper +private val VTFactory = new java.util.concurrent.ThreadFactory: + def newThread(r: Runnable): Thread = + new Thread(null, r, "gears.async.VThread-", 0L) + case class TimeMeasurementResult(millisecondsPerOperation: Double, standardDeviation: Double) def measureIterations[T](action: () => T): Int = val counter = AtomicInteger(0) - val t1 = Thread.startVirtualThread: () => + val t1 = VTFactory.newThread: () => try { while (!Thread.interrupted()) { action() @@ -41,7 +45,7 @@ def measureIterations[T](action: () => T): Int = given ExecutionContext = ExecutionContext.global val threadJoins = measureIterations: () => - val t = Thread.startVirtualThread: () => + val t = VTFactory.newThread: () => var z = 1 t.join() @@ -114,21 +118,21 @@ def measureIterations[T](action: () => T): Int = val c2: Double = measureIterations: () => @volatile var i1 = true - val f11 = Thread.startVirtualThread(() => { Thread.sleep(10); i1 = false }) - val f12 = Thread.startVirtualThread(() => { Thread.sleep(50); i1 = false }) - val f13 = Thread.startVirtualThread(() => { Thread.sleep(100); i1 = false }) + val f11 = VTFactory.newThread(() => { Thread.sleep(10); i1 = false }) + val f12 = VTFactory.newThread(() => { Thread.sleep(50); i1 = false }) + val f13 = VTFactory.newThread(() => { Thread.sleep(100); i1 = false }) while (i1) () @volatile var i2 = true - val f21 = Thread.startVirtualThread(() => { Thread.sleep(100); i2 = false }) - val f22 = Thread.startVirtualThread(() => { Thread.sleep(10); i2 = false }) - val f23 = Thread.startVirtualThread(() => { Thread.sleep(50); i2 = false }) + val f21 = VTFactory.newThread(() => { Thread.sleep(100); i2 = false }) + val f22 = VTFactory.newThread(() => { Thread.sleep(10); i2 = false }) + val f23 = VTFactory.newThread(() => { Thread.sleep(50); i2 = false }) while (i2) () @volatile var i3 = true - val f31 = Thread.startVirtualThread(() => { Thread.sleep(50); i3 = false }) - val f32 = Thread.startVirtualThread(() => { Thread.sleep(100); i3 = false }) - val f33 = Thread.startVirtualThread(() => { Thread.sleep(10); i3 = false }) + val f31 = VTFactory.newThread(() => { Thread.sleep(50); i3 = false }) + val f32 = VTFactory.newThread(() => { Thread.sleep(100); i3 = false }) + val f33 = VTFactory.newThread(() => { Thread.sleep(10); i3 = false }) while (i3) () f11.interrupt() @@ -164,7 +168,7 @@ def measureIterations[T](action: () => T): Int = // java @volatile var shared: Long = 0 @volatile var timeForWriting = true - val t1 = Thread.startVirtualThread: () => + val t1 = VTFactory.newThread: () => var i: Long = 0 while (true) { while (!timeForWriting) () @@ -173,7 +177,7 @@ def measureIterations[T](action: () => T): Int = i += 1 } - val t2 = Thread.startVirtualThread: () => + val t2 = VTFactory.newThread: () => while (true) { while (timeForWriting) () var z = shared From 465d025683950ac5c96914d86c4b82f24f55c265 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Tue, 21 Jan 2025 16:57:22 +0100 Subject: [PATCH 5/5] Add .check error messages --- tests/neg-custom-args/captures/gears.check | 41 ++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 tests/neg-custom-args/captures/gears.check diff --git a/tests/neg-custom-args/captures/gears.check b/tests/neg-custom-args/captures/gears.check new file mode 100644 index 000000000000..7ac3252822ff --- /dev/null +++ b/tests/neg-custom-args/captures/gears.check @@ -0,0 +1,41 @@ +-- Error: tests/neg-custom-args/captures/gears/CCBehavior.scala:52:4 --------------------------------------------------- +52 | futs.awaitAll // error + | ^^^^ + | Local reach capability futs* leaks into capture scope of method bad - collectors +-- Error: tests/neg-custom-args/captures/gears/CCBehavior.scala:86:25 -------------------------------------------------- +86 | val ch = SyncChannel[File]() // error + | ^^^^ + | Type variable T of object SyncChannel cannot be instantiated to box File^ since + | that type captures the root capability `cap`. +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/gears/CCBehavior.scala:111:12 ---------------------------- +111 | Result: label ?=> // error, escaping label from Result + | ^ + | Found: Result[box gears.async².Future[box T^?]^{fr, label, async, label²}, box E^?] + | Required: Either[E, box gears.async².Future[T]^{async}] + | + | where: async is a parameter in an anonymous function in method bad + | async² is a package in package gears + | label is a reference to a value parameter + | label² is a reference to a value parameter +112 | Future: fut ?=> +113 | fr.await.ok + | + | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/gears/CCBehavior.scala:48:38 -------------------------------------------------- +48 | val futs: Seq[Future[Int]^] = Async.blocking: async ?=> // error + | ^^^^^^^^^^^^^^ + | local reference async leaks into outer capture set of type parameter T of method blocking in object Async +-- Error: tests/neg-custom-args/captures/gears/CCBehavior.scala:73:8 --------------------------------------------------- +73 | Async.blocking: // error + | ^^^^^^^^^^^^^^ + |local reference _$3 from (_$3: box File^{files*}): box gears.async.Future[Int]^{_$3, files*} leaks into outer capture set of type parameter T of method blocking in object Async +-- Error: tests/neg-custom-args/captures/gears/CCBehavior.scala:74:9 --------------------------------------------------- +74 | File.open("a.txt"): a => // error + | ^^^^^^^^^ + |local reference _$3 from (_$3: box File^{files*}): box gears.async.Future[Int]^{_$3, files*} leaks into outer capture set of type parameter T of method open in object File +-- Error: tests/neg-custom-args/captures/gears/CCBehavior.scala:75:11 -------------------------------------------------- +75 | File.open("b.txt"): b => // error + | ^^^^^^^^^ + |local reference _$3 from (_$3: box File^{files*}): box gears.async.Future[Int]^{_$3, files*} leaks into outer capture set of type parameter T of method open in object File +there were 20 feature warnings; re-run with -feature for details +there were 19 deprecation warnings; re-run with -deprecation for details