diff --git a/kyo-core/shared/src/main/scala/kyo/Admission.scala b/kyo-core/shared/src/main/scala/kyo/Admission.scala index 8db25ef48..1c147abb7 100644 --- a/kyo-core/shared/src/main/scala/kyo/Admission.scala +++ b/kyo-core/shared/src/main/scala/kyo/Admission.scala @@ -10,7 +10,7 @@ import kyo.scheduler.Scheduler * @param frame * The execution frame where the task was rejected */ -case class Rejected(frame: Frame) extends Exception(frame.position.show) +class Rejected()(using Frame) extends KyoException("Admisssion control has rejected execution to mitigate overloading.") /** Admission control mechanism that helps prevent system overload by selectively rejecting tasks at the boundary of incoming workload. * @@ -47,7 +47,7 @@ object Admission: */ def run[A, S](key: String)(v: A < S)(using frame: Frame): A < (IO & S & Abort[Rejected]) = IO { - if Scheduler.get.reject(key) then Abort.fail(Rejected(frame)) + if Scheduler.get.reject(key) then Abort.fail(Rejected()) else v } @@ -72,7 +72,7 @@ object Admission: */ def run[A, S](key: Int)(v: A < S)(using frame: Frame): A < (IO & S & Abort[Rejected]) = IO { - if Scheduler.get.reject(key) then Abort.fail(Rejected(frame)) + if Scheduler.get.reject(key) then Abort.fail(Rejected()) else v } @@ -95,7 +95,7 @@ object Admission: */ def run[A, S](v: A < S)(using frame: Frame): A < (IO & S & Abort[Rejected]) = IO { - if Scheduler.get.reject() then Abort.fail(Rejected(frame)) + if Scheduler.get.reject() then Abort.fail(Rejected()) else v } diff --git a/kyo-core/shared/src/main/scala/kyo/Async.scala b/kyo-core/shared/src/main/scala/kyo/Async.scala index 54e183456..e9222affa 100644 --- a/kyo-core/shared/src/main/scala/kyo/Async.scala +++ b/kyo-core/shared/src/main/scala/kyo/Async.scala @@ -166,7 +166,7 @@ object Async: IO.Unsafe { val sleepFiber = clock.unsafe.sleep(after) val task = IOTask[Ctx, E | Timeout, A](v, trace, context) - sleepFiber.onComplete(_ => discard(task.interrupt(Result.Fail(Timeout(frame))))) + sleepFiber.onComplete(_ => discard(task.interrupt(Result.Fail(Timeout())))) task.onComplete(_ => discard(sleepFiber.interrupt())) Async.get(task) } diff --git a/kyo-core/shared/src/main/scala/kyo/Closed.scala b/kyo-core/shared/src/main/scala/kyo/Closed.scala index 43b5cdfc9..64cd2d96f 100644 --- a/kyo-core/shared/src/main/scala/kyo/Closed.scala +++ b/kyo-core/shared/src/main/scala/kyo/Closed.scala @@ -2,6 +2,6 @@ package kyo import scala.util.control.NoStackTrace -case class Closed(message: String, createdAt: Frame, failedAt: Frame) - extends Exception(s"Resource created at ${createdAt.position.show} is closed. Failure at ${failedAt.position.show}: $message") +class Closed(resource: Text, createdAt: Frame, details: Text = "")(using Frame) + extends KyoException(t"$resource created at ${createdAt.position.show} is closed.", details) with NoStackTrace diff --git a/kyo-core/shared/src/main/scala/kyo/Hub.scala b/kyo-core/shared/src/main/scala/kyo/Hub.scala index 74edb143f..77fa708df 100644 --- a/kyo-core/shared/src/main/scala/kyo/Hub.scala +++ b/kyo-core/shared/src/main/scala/kyo/Hub.scala @@ -80,7 +80,7 @@ class Hub[A] private[kyo] ( * a Maybe containing any remaining elements in the Hub */ def close(using frame: Frame): Maybe[Seq[A]] < IO = - fiber.interruptDiscard(Result.Panic(Closed("Hub", initFrame, frame))).andThen { + fiber.interruptDiscard(Result.Panic(Closed("Hub", initFrame))).andThen { ch.close.map { r => IO { val array = listeners.toArray() @@ -111,7 +111,7 @@ class Hub[A] private[kyo] ( * a new Listener */ def listen(bufferSize: Int)(using frame: Frame): Listener[A] < (IO & Abort[Closed]) = - def fail = Abort.fail(Closed("Hub", initFrame, frame)) + def fail = Abort.fail(Closed("Hub", initFrame)) closed.map { case true => fail case false => diff --git a/kyo-core/shared/src/main/scala/kyo/Interrupt.scala b/kyo-core/shared/src/main/scala/kyo/Interrupt.scala new file mode 100644 index 000000000..921707893 --- /dev/null +++ b/kyo-core/shared/src/main/scala/kyo/Interrupt.scala @@ -0,0 +1,3 @@ +package kyo + +class Interrupt()(using Frame) extends KyoException("Fiber has been interrupted.") diff --git a/kyo-core/shared/src/main/scala/kyo/Meter.scala b/kyo-core/shared/src/main/scala/kyo/Meter.scala index 4c602ff8c..fe16ae1e1 100644 --- a/kyo-core/shared/src/main/scala/kyo/Meter.scala +++ b/kyo-core/shared/src/main/scala/kyo/Meter.scala @@ -341,7 +341,7 @@ object Meter: val st = state.getAndSet(Int.MinValue) val ok = st != Int.MinValue // The meter wasn't already closed if ok then - val fail = Result.fail(Closed("Semaphore is closed", initFrame, frame)) + val fail = Result.fail(Closed("Meter", initFrame)) // Complete the closed promise to fail new operations closed.completeDiscard(fail) // Drain the pending waiters diff --git a/kyo-core/shared/src/main/scala/kyo/Queue.scala b/kyo-core/shared/src/main/scala/kyo/Queue.scala index 63158b20a..124f36b7f 100644 --- a/kyo-core/shared/src/main/scala/kyo/Queue.scala +++ b/kyo-core/shared/src/main/scala/kyo/Queue.scala @@ -296,7 +296,7 @@ object Queue: final protected val _closed = AtomicRef.Unsafe.init(Maybe.empty[Result.Error[Closed]]) final def close()(using frame: Frame, allow: AllowUnsafe) = - val fail = Result.Fail(Closed("Queue", initFrame, frame)) + val fail = Result.Fail(Closed("Queue", initFrame)) Maybe.when(_closed.compareAndSet(Maybe.empty, Maybe(fail)))(_drain()) end close diff --git a/kyo-core/shared/src/main/scala/kyo/Resource.scala b/kyo-core/shared/src/main/scala/kyo/Resource.scala index d05f9aa01..7568f0ea3 100644 --- a/kyo-core/shared/src/main/scala/kyo/Resource.scala +++ b/kyo-core/shared/src/main/scala/kyo/Resource.scala @@ -32,10 +32,10 @@ object Resource: case Result.Success(_) => () case _ => throw new Closed( - "Resource finalizer queue already closed. This may happen if " + - "a background fiber escapes the scope of a 'Resource.run' call.", + "Finalizer", finalizer.createdAt, - frame + "The finalizer queue is already closed. This may happen if " + + "a background fiber escapes the scope of a 'Resource.run' call." ) } } diff --git a/kyo-core/shared/src/main/scala/kyo/System.scala b/kyo-core/shared/src/main/scala/kyo/System.scala index 15b0fc007..01c6b2af7 100644 --- a/kyo-core/shared/src/main/scala/kyo/System.scala +++ b/kyo-core/shared/src/main/scala/kyo/System.scala @@ -200,46 +200,51 @@ object System: * @return * A Result containing either the parsed value or an error. */ - def apply(s: String): Result[E, A] + def apply(s: String)(using Frame): Result[E, A] end Parser /** Companion object for Parser, containing default implementations. */ object Parser: - given Parser[Nothing, String] = v => Result.success(v) - given Parser[NumberFormatException, Int] = v => Result.catching[NumberFormatException](v.toInt) - given Parser[NumberFormatException, Long] = v => Result.catching[NumberFormatException](v.toLong) - given Parser[NumberFormatException, Float] = v => Result.catching[NumberFormatException](v.toFloat) - given Parser[NumberFormatException, Double] = v => Result.catching[NumberFormatException](v.toDouble) - given Parser[IllegalArgumentException, Boolean] = v => Result.catching[IllegalArgumentException](v.toBoolean) - given Parser[NumberFormatException, Byte] = v => Result.catching[NumberFormatException](v.toByte) - given Parser[NumberFormatException, Short] = v => Result.catching[NumberFormatException](v.toShort) - given Parser[Duration.InvalidDuration, Duration] = v => Duration.parse(v) + def apply[E, A](f: Frame ?=> String => Result[E, A]) = + new Parser[E, A]: + def apply(s: String)(using Frame) = f(s) + + given Parser[Nothing, String] = Parser(v => Result.success(v)) + given Parser[NumberFormatException, Int] = Parser(v => Result.catching[NumberFormatException](v.toInt)) + given Parser[NumberFormatException, Long] = Parser(v => Result.catching[NumberFormatException](v.toLong)) + given Parser[NumberFormatException, Float] = Parser(v => Result.catching[NumberFormatException](v.toFloat)) + given Parser[NumberFormatException, Double] = Parser(v => Result.catching[NumberFormatException](v.toDouble)) + given Parser[IllegalArgumentException, Boolean] = Parser(v => Result.catching[IllegalArgumentException](v.toBoolean)) + given Parser[NumberFormatException, Byte] = Parser(v => Result.catching[NumberFormatException](v.toByte)) + given Parser[NumberFormatException, Short] = Parser(v => Result.catching[NumberFormatException](v.toShort)) + given Parser[Duration.InvalidDuration, Duration] = Parser(v => Duration.parse(v)) given Parser[IllegalArgumentException, java.util.UUID] = - v => Result.catching[IllegalArgumentException](java.util.UUID.fromString(v)) + Parser(v => Result.catching[IllegalArgumentException](java.util.UUID.fromString(v))) given Parser[DateTimeParseException, java.time.LocalDate] = - v => Result.catching[DateTimeParseException](java.time.LocalDate.parse(v)) + Parser(v => Result.catching[DateTimeParseException](java.time.LocalDate.parse(v))) given Parser[DateTimeParseException, java.time.LocalTime] = - v => Result.catching[DateTimeParseException](java.time.LocalTime.parse(v)) + Parser(v => Result.catching[DateTimeParseException](java.time.LocalTime.parse(v))) given Parser[DateTimeParseException, java.time.LocalDateTime] = - v => Result.catching[DateTimeParseException](java.time.LocalDateTime.parse(v)) + Parser(v => Result.catching[DateTimeParseException](java.time.LocalDateTime.parse(v))) given Parser[URISyntaxException, java.net.URI] = - v => Result.catching[URISyntaxException](new java.net.URI(v)) + Parser(v => Result.catching[URISyntaxException](new java.net.URI(v))) given Parser[MalformedURLException, java.net.URL] = - v => Result.catching[MalformedURLException](new java.net.URL(v)) + Parser(v => Result.catching[MalformedURLException](new java.net.URL(v))) given [E, A](using p: Parser[E, A], frame: Frame): Parser[E, Seq[A]] = - v => Result.collect(Chunk.from(v.split(",")).map(v => p(v.trim()))) + Parser(v => Result.collect(Chunk.from(v.split(",")).map(v => p(v.trim())))) given Parser[IllegalArgumentException, Char] = - v => + Parser { v => if v.length() == 1 then Result.success(v(0)) else Result.fail(new IllegalArgumentException("String must have exactly one character")) + } end Parser diff --git a/kyo-core/shared/src/main/scala/kyo/Timeout.scala b/kyo-core/shared/src/main/scala/kyo/Timeout.scala index d92e9df81..29ff95591 100644 --- a/kyo-core/shared/src/main/scala/kyo/Timeout.scala +++ b/kyo-core/shared/src/main/scala/kyo/Timeout.scala @@ -2,6 +2,4 @@ package kyo import scala.util.control.NoStackTrace -case class Timeout(frame: Frame) - extends Exception(frame.position.show) - with NoStackTrace +class Timeout()(using Frame) extends KyoException(t"Computation has timed out.") diff --git a/kyo-core/shared/src/main/scala/kyo/scheduler/IOPromise.scala b/kyo-core/shared/src/main/scala/kyo/scheduler/IOPromise.scala index 46cbb4601..8e9cd9230 100644 --- a/kyo-core/shared/src/main/scala/kyo/scheduler/IOPromise.scala +++ b/kyo-core/shared/src/main/scala/kyo/scheduler/IOPromise.scala @@ -49,7 +49,7 @@ private[kyo] class IOPromise[+E, +A](init: State[E, A]) extends Safepoint.Interc case l: Linked[E, A] @unchecked => interruptsLoop(l.p) case _ => - try discard(other.interrupt(Result.Panic(Interrupt(frame)))) + try discard(other.interrupt(Result.Panic(Interrupt()))) catch case ex if NonFatal(ex) => import AllowUnsafe.embrace.danger @@ -194,7 +194,7 @@ private[kyo] class IOPromise[+E, +A](init: State[E, A]) extends Safepoint.Interc import kyo.AllowUnsafe.embrace.danger if isNull(result) then if deadline.isOverdue() then - return Result.fail(Timeout(frame)) + return Result.fail(Timeout()) val timeLeft = deadline.timeLeft() if !timeLeft.isFinite then LockSupport.park(this) @@ -228,8 +228,6 @@ end IOPromise private[kyo] object IOPromise: - case class Interrupt(origin: Frame) extends Exception with NoStackTrace - type State[+E, +A] = Result[E, A] | Pending[E, A] | Linked[E, A] case class Linked[+E, +A](p: IOPromise[E, A]) diff --git a/kyo-core/shared/src/test/scala/kyo/AsyncTest.scala b/kyo-core/shared/src/test/scala/kyo/AsyncTest.scala index 30b807a99..cae360ce4 100644 --- a/kyo-core/shared/src/test/scala/kyo/AsyncTest.scala +++ b/kyo-core/shared/src/test/scala/kyo/AsyncTest.scala @@ -72,7 +72,7 @@ class AsyncTest extends Test: .pipe(Async.runAndBlock(Duration.Infinity)) .pipe(Abort.run[Timeout](_)) .map { - case Result.Fail(Timeout(_)) => succeed + case Result.Fail(_: Timeout) => succeed case v => fail(v.toString()) } } @@ -82,7 +82,7 @@ class AsyncTest extends Test: .pipe(Async.runAndBlock(10.millis)) .pipe(Abort.run[Timeout](_)) .map { - case Result.Fail(Timeout(_)) => succeed + case Result.Fail(_: Timeout) => succeed case v => fail(v.toString()) } } @@ -92,7 +92,7 @@ class AsyncTest extends Test: .pipe(Async.runAndBlock(10.millis)) .pipe(Abort.run[Timeout](_)) .map { - case Result.Fail(Timeout(_)) => succeed + case Result.Fail(_: Timeout) => succeed case v => fail(v.toString()) } } @@ -520,7 +520,7 @@ class AsyncTest extends Test: yield value Abort.run[Timeout](result).map { - case Result.Fail(Timeout(_)) => succeed + case Result.Fail(_: Timeout) => succeed case other => fail(s"Expected Timeout, got $other") } } diff --git a/kyo-core/shared/src/test/scala/kyo/ChannelTest.scala b/kyo-core/shared/src/test/scala/kyo/ChannelTest.scala index 99795662f..5173c85d3 100644 --- a/kyo-core/shared/src/test/scala/kyo/ChannelTest.scala +++ b/kyo-core/shared/src/test/scala/kyo/ChannelTest.scala @@ -531,9 +531,9 @@ class ChannelTest extends Test: stream = c.stream().mapChunk(ch => Chunk(ch)) v <- Abort.run(stream.run) yield v match - case Result.Success(v) => fail(s"Stream succeeded unexpectedly: ${v}") - case Result.Fail(Closed(_, _, _)) => assert(true) - case Result.Panic(ex) => fail(s"Stream panicked unexpectedly: ${ex}") + case Result.Success(v) => fail(s"Stream succeeded unexpectedly: ${v}") + case Result.Fail(_: Closed) => assert(true) + case Result.Panic(ex) => fail(s"Stream panicked unexpectedly: ${ex}") } } diff --git a/kyo-data/shared/src/main/scala/kyo/Duration.scala b/kyo-data/shared/src/main/scala/kyo/Duration.scala index 794940798..83cb19735 100644 --- a/kyo-data/shared/src/main/scala/kyo/Duration.scala +++ b/kyo-data/shared/src/main/scala/kyo/Duration.scala @@ -20,7 +20,7 @@ object Duration: inline given Flat[Duration] = Flat.unsafe.bypass /** Exception thrown for invalid duration parsing. */ - case class InvalidDuration(message: String) extends Exception(message) + class InvalidDuration(message: Text)(using Frame) extends KyoException(message) /** Parses a string representation of a duration. * @@ -29,7 +29,7 @@ object Duration: * @return * A Result containing either the parsed Duration or an InvalidDuration error */ - def parse(s: String): Result[InvalidDuration, Duration] = + def parse(s: String)(using Frame): Result[InvalidDuration, Duration] = val pattern = """(\d+)\s*([a-zA-Z]+)""".r s.trim.toLowerCase match case "infinity" | "inf" => Result.success(Infinity) diff --git a/kyo-data/shared/src/main/scala/kyo/KyoException.scala b/kyo-data/shared/src/main/scala/kyo/KyoException.scala new file mode 100644 index 000000000..0ae6078c1 --- /dev/null +++ b/kyo-data/shared/src/main/scala/kyo/KyoException.scala @@ -0,0 +1,27 @@ +package kyo + +import kyo.* +import kyo.Ansi.* +import scala.util.control.NoStackTrace + +class KyoException private[kyo] ( + message: Text = null, + cause: Text | Throwable = null +)(using val frame: Frame) extends NoStackTrace: + + override def getCause(): Throwable = + cause match + case cause: Throwable => cause + case _ => null + + override def getMessage(): String = + val detail = + cause match + case _: Throwable => Absent + case cause: Text @unchecked => Maybe(cause) + + val msg = frame.render(("⚠️ KyoException".red.bold :: Maybe(message).toList ::: detail.toList).map(_.show)*) + s"\n$msg\n" + end getMessage + +end KyoException diff --git a/kyo-prelude/shared/src/main/scala/kyo/Parse.scala b/kyo-prelude/shared/src/main/scala/kyo/Parse.scala index 82923330e..773524383 100644 --- a/kyo-prelude/shared/src/main/scala/kyo/Parse.scala +++ b/kyo-prelude/shared/src/main/scala/kyo/Parse.scala @@ -72,7 +72,7 @@ object Parse: Var.use[State](s => fail(Seq(s), message)) private def fail(states: Seq[State], message: String)(using frame: Frame): Nothing < Abort[ParseFailed] = - Abort.fail(ParseFailed(frame, states, message)) + Abort.fail(ParseFailed(states, message)) /** Reads and consumes characters from the input as long as they satisfy the given predicate. * @@ -915,7 +915,4 @@ object Parse: end Parse -case class ParseFailed(frame: Frame, states: Seq[Parse.State], message: String) extends Exception with Serializable: - - override def getMessage() = frame.render("Parse failed! ".red.bold + message, states) -end ParseFailed +case class ParseFailed(states: Seq[Parse.State], message: String)(using Frame) extends KyoException(message, Render.asText(states)) diff --git a/kyo-stm/shared/src/main/scala/kyo/STM.scala b/kyo-stm/shared/src/main/scala/kyo/STM.scala index 9b59c60ee..9843279c3 100644 --- a/kyo-stm/shared/src/main/scala/kyo/STM.scala +++ b/kyo-stm/shared/src/main/scala/kyo/STM.scala @@ -7,7 +7,7 @@ import scala.util.control.NoStackTrace /** A FailedTransaction exception that is thrown when a transaction fails to commit. Contains the frame where the failure occurred. */ -case class FailedTransaction(frame: Frame) extends Exception(frame.position.show) with NoStackTrace +class FailedTransaction()(using Frame) extends KyoException /** Software Transactional Memory (STM) provides concurrent access to shared state using optimistic locking. Rather than acquiring locks * upfront, transactions execute speculatively and automatically retry if conflicts are detected during commit. While this enables better @@ -53,7 +53,7 @@ object STM: * @return * Nothing, as this operation always aborts the transaction */ - def retry(using frame: Frame): Nothing < STM = Abort.fail(FailedTransaction(frame)) + def retry(using Frame): Nothing < STM = Abort.fail(FailedTransaction()) /** Conditionally retries a transaction based on a boolean condition. If the condition is true, the transaction will be retried. * Otherwise, execution continues normally. @@ -61,7 +61,7 @@ object STM: * @param cond * The condition that determines whether to retry */ - def retryIf(cond: Boolean)(using frame: Frame): Unit < STM = Abort.when(cond)(FailedTransaction(frame)) + def retryIf(cond: Boolean)(using Frame): Unit < STM = Abort.when(cond)(FailedTransaction()) /** Executes a transactional computation with explicit state isolation. This version of run supports additional effects beyond Abort and * Async through the provided isolate, which ensures proper state management during transaction retries and rollbacks. @@ -103,9 +103,7 @@ object STM: * @return * The result of the computation if successful */ - def run[E, A: Flat](retrySchedule: Schedule)(v: A < (STM & Abort[E] & Async))( - using frame: Frame - ): A < (Async & Abort[E | FailedTransaction]) = + def run[E, A: Flat](retrySchedule: Schedule)(v: A < (STM & Abort[E] & Async))(using Frame): A < (Async & Abort[E | FailedTransaction]) = TID.use { case -1L => // New transaction without a parent, use regular commit flow @@ -128,7 +126,7 @@ object STM: ref.unlock(entry) result else - Abort.fail(FailedTransaction(frame)) + Abort.fail(FailedTransaction()) end if } case size => @@ -138,7 +136,7 @@ object STM: val array = new Array[Any](size * 2) try - def fail = throw new FailedTransaction(frame) + def fail = throw new FailedTransaction() var i = 0 // Pre-validate and dump the log to the flat array diff --git a/kyo-sttp/shared/src/main/scala/kyo/Requests.scala b/kyo-sttp/shared/src/main/scala/kyo/Requests.scala index 44f737551..cd6a5e28e 100644 --- a/kyo-sttp/shared/src/main/scala/kyo/Requests.scala +++ b/kyo-sttp/shared/src/main/scala/kyo/Requests.scala @@ -9,17 +9,7 @@ import sttp.client3.* * @param cause * A string or throwable describing the reason for the failure */ -case class FailedRequest(cause: String | Throwable) extends Exception: - override def getCause(): Throwable = - cause match - case ex: Throwable => ex - case _ => null - - override def getMessage(): String = - cause match - case ex: Throwable => ex.getMessage - case str: String => str -end FailedRequest +class FailedRequest(cause: Text | Throwable)(using Frame) extends KyoException("Failed http request", cause) object Requests: @@ -73,7 +63,7 @@ object Requests: /** A basic request with error handling */ val basicRequest: BasicRequest = sttp.client3.basicRequest.mapResponse { - case Left(value) => Left(FailedRequest(value)) + case Left(value) => Left(FailedRequest(value)(using Frame.internal)) case Right(value) => Right(value) }