From 0a86ec2325c16382b0c8516bb3c43d7af169c181 Mon Sep 17 00:00:00 2001 From: Simon Vergauwen Date: Thu, 16 Mar 2023 22:18:12 +0100 Subject: [PATCH] Tracing POC (#2946) Co-authored-by: Alejandro Serrano Co-authored-by: franciscodr Co-authored-by: Youssef Shoaib --- arrow-libs/core/arrow-core/api/arrow-core.api | 28 ++++-- .../kotlin/arrow/core/raise/Builders.kt | 32 ++----- .../kotlin/arrow/core/raise/Fold.kt | 89 ++++++++++++++++--- .../arrow/core/raise/RaiseAccumulate.kt | 1 - .../kotlin/arrow/core/raise/Trace.kt | 43 +++++++++ .../core/raise/StructuredConcurrencySpec.kt | 2 +- .../kotlin/arrow/core/raise/TraceSpec.kt | 46 ++++++++++ .../raise/CancellationExceptionNoTrace.kt | 2 +- .../raise/CancellationExceptionNoTrace.kt | 2 +- .../kotlin/arrow/core/raise/TraceJvmSpec.kt | 24 +++++ .../raise/CancellationExceptionNoTrace.kt | 2 +- 11 files changed, 227 insertions(+), 44 deletions(-) create mode 100644 arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Trace.kt create mode 100644 arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/raise/TraceSpec.kt create mode 100644 arrow-libs/core/arrow-core/src/jvmTest/kotlin/arrow/core/raise/TraceJvmSpec.kt diff --git a/arrow-libs/core/arrow-core/api/arrow-core.api b/arrow-libs/core/arrow-core/api/arrow-core.api index 597eed99947..75e75921f31 100644 --- a/arrow-libs/core/arrow-core/api/arrow-core.api +++ b/arrow-libs/core/arrow-core/api/arrow-core.api @@ -3231,13 +3231,8 @@ public final class arrow/core/continuations/result { public final fun invoke-gIAlu-s (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } -public class arrow/core/raise/CancellationExceptionNoTrace : java/util/concurrent/CancellationException { - public fun ()V - public fun fillInStackTrace ()Ljava/lang/Throwable; -} - public final class arrow/core/raise/DefaultRaise : arrow/core/raise/Raise { - public fun ()V + public fun (Z)V public fun attempt (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun bind (Larrow/core/Either;)Ljava/lang/Object; public fun bind (Larrow/core/Validated;)Ljava/lang/Object; @@ -3249,10 +3244,14 @@ public final class arrow/core/raise/DefaultRaise : arrow/core/raise/Raise { public final fun complete ()Z public fun invoke (Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; public fun invoke (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun isTraced ()Z public fun raise (Ljava/lang/Object;)Ljava/lang/Void; public fun shift (Ljava/lang/Object;)Ljava/lang/Object; } +public abstract interface annotation class arrow/core/raise/ExperimentalTraceApi : java/lang/annotation/Annotation { +} + public final class arrow/core/raise/IorRaise : arrow/core/raise/Raise { public fun (Lkotlin/jvm/functions/Function2;Ljava/util/concurrent/atomic/AtomicReference;Larrow/core/raise/Raise;)V public fun attempt (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -3456,6 +3455,7 @@ public final class arrow/core/raise/RaiseKt { public static final fun toResult (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun toValidated (Lkotlin/jvm/functions/Function1;)Larrow/core/Validated; public static final fun toValidated (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun traced (Larrow/core/raise/Raise;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; public static final fun zipOrAccumulate (Larrow/core/raise/Raise;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function9;)Ljava/lang/Object; public static final fun zipOrAccumulate (Larrow/core/raise/Raise;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function8;)Ljava/lang/Object; public static final fun zipOrAccumulate (Larrow/core/raise/Raise;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function7;)Ljava/lang/Object; @@ -3514,6 +3514,22 @@ public final class arrow/core/raise/ResultRaise : arrow/core/raise/Raise { public final synthetic fun unbox-impl ()Larrow/core/raise/Raise; } +public final class arrow/core/raise/Trace { + public static final synthetic fun box-impl (Ljava/util/concurrent/CancellationException;)Larrow/core/raise/Trace; + public static fun constructor-impl (Ljava/util/concurrent/CancellationException;)Ljava/util/concurrent/CancellationException; + public fun equals (Ljava/lang/Object;)Z + public static fun equals-impl (Ljava/util/concurrent/CancellationException;Ljava/lang/Object;)Z + public static final fun equals-impl0 (Ljava/util/concurrent/CancellationException;Ljava/util/concurrent/CancellationException;)Z + public fun hashCode ()I + public static fun hashCode-impl (Ljava/util/concurrent/CancellationException;)I + public static final fun printStackTrace-impl (Ljava/util/concurrent/CancellationException;)V + public static final fun stackTraceToString-impl (Ljava/util/concurrent/CancellationException;)Ljava/lang/String; + public static final fun suppressedExceptions-impl (Ljava/util/concurrent/CancellationException;)Ljava/util/List; + public fun toString ()Ljava/lang/String; + public static fun toString-impl (Ljava/util/concurrent/CancellationException;)Ljava/lang/String; + public final synthetic fun unbox-impl ()Ljava/util/concurrent/CancellationException; +} + public abstract interface class arrow/typeclasses/Monoid : arrow/typeclasses/Semigroup { public static final field Companion Larrow/typeclasses/Monoid$Companion; public static fun Boolean ()Larrow/typeclasses/Monoid; diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Builders.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Builders.kt index cbb5a5c3886..195b599d92d 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Builders.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Builders.kt @@ -13,11 +13,7 @@ import arrow.core.Option import arrow.core.Some import arrow.core.getOrElse import arrow.core.identity -import arrow.core.orElse -import arrow.typeclasses.Semigroup -import arrow.typeclasses.SemigroupDeprecation import kotlin.contracts.ExperimentalContracts -import kotlin.contracts.InvocationKind.EXACTLY_ONCE import kotlin.contracts.contract import kotlin.experimental.ExperimentalTypeInference import kotlin.jvm.JvmInline @@ -27,23 +23,16 @@ import kotlin.jvm.JvmName public inline fun either(@BuilderInference block: Raise.() -> A): Either = fold({ block.invoke(this) }, { Either.Left(it) }, { Either.Right(it) }) -public inline fun nullable(block: NullableRaise.() -> A): A? { - contract { callsInPlace(block, EXACTLY_ONCE) } - return fold({ block(NullableRaise(this)) }, { null }, ::identity) -} +public inline fun nullable(block: NullableRaise.() -> A): A? = + fold({ block(NullableRaise(this)) }, { null }, ::identity) -public inline fun result(block: ResultRaise.() -> A): Result { - contract { callsInPlace(block, EXACTLY_ONCE) } - return fold({ block(ResultRaise(this)) }, Result.Companion::failure, Result.Companion::failure, Result.Companion::success) -} +public inline fun result(block: ResultRaise.() -> A): Result = + fold({ block(ResultRaise(this)) }, Result.Companion::failure, Result.Companion::failure, Result.Companion::success) -public inline fun option(block: OptionRaise.() -> A): Option { - contract { callsInPlace(block, EXACTLY_ONCE) } - return fold({ block(OptionRaise(this)) }, ::identity, ::Some) -} +public inline fun option(block: OptionRaise.() -> A): Option = + fold({ block(OptionRaise(this)) }, ::identity, ::Some) public inline fun ior(noinline combineError: (E, E) -> E, @BuilderInference block: IorRaise.() -> A): Ior { - contract { callsInPlace(block, EXACTLY_ONCE) } val state: Atomic> = Atomic(None) return fold>( { block(IorRaise(combineError, state, this)) }, @@ -56,10 +45,9 @@ public inline fun ior(noinline combineError: (E, E) -> E, @BuilderInferen public typealias Null = Nothing? @JvmInline -public value class NullableRaise(private val cont: Raise) : Raise { +public value class NullableRaise(private val raise: Raise) : Raise by raise { @RaiseDSL public fun ensure(value: Boolean): Unit = ensure(value) { null } - override fun raise(r: Nothing?): Nothing = cont.raise(r) public fun Option.bind(): B = getOrElse { raise(null) } public fun B?.bind(): B { @@ -74,14 +62,12 @@ public value class NullableRaise(private val cont: Raise) : Raise { } @JvmInline -public value class ResultRaise(private val cont: Raise) : Raise { - override fun raise(r: Throwable): Nothing = cont.raise(r) +public value class ResultRaise(private val raise: Raise) : Raise by raise { public fun Result.bind(): B = fold(::identity) { raise(it) } } @JvmInline -public value class OptionRaise(private val cont: Raise) : Raise { - override fun raise(r: None): Nothing = cont.raise(r) +public value class OptionRaise(private val raise: Raise) : Raise by raise { public fun Option.bind(): B = getOrElse { raise(None) } public fun ensure(value: Boolean): Unit = ensure(value) { None } diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Fold.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Fold.kt index fdf1d48a2a4..f83b9f9808c 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Fold.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Fold.kt @@ -1,13 +1,14 @@ @file:JvmMultifileClass @file:JvmName("RaiseKt") @file:OptIn(ExperimentalTypeInference::class, ExperimentalContracts::class) + package arrow.core.raise import arrow.atomic.AtomicBoolean import arrow.core.nonFatalOrThrow +import arrow.core.Either import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind.AT_MOST_ONCE -import kotlin.contracts.InvocationKind.EXACTLY_ONCE import kotlin.contracts.contract import kotlin.coroutines.cancellation.CancellationException import kotlin.experimental.ExperimentalTypeInference @@ -75,7 +76,6 @@ public inline fun fold( transform: (value: A) -> B, ): B { contract { - callsInPlace(program, EXACTLY_ONCE) callsInPlace(recover, AT_MOST_ONCE) callsInPlace(transform, AT_MOST_ONCE) } @@ -94,7 +94,7 @@ public inline fun fold( callsInPlace(recover, AT_MOST_ONCE) callsInPlace(transform, AT_MOST_ONCE) } - val raise = DefaultRaise() + val raise = DefaultRaise(false) return try { val res = program(raise) raise.complete() @@ -108,27 +108,91 @@ public inline fun fold( } } +/** + * Inspect a [Trace] value of [R]. + * + * Tracing [R] can be useful to know where certain errors, or failures are coming from. + * Let's say you have a `DomainError`, but it might be raised from many places in the project. + * + * You would have to manually _trace_ where this error is coming from, + * instead [Trace] offers you ways to inspect the actual stacktrace of where the raised value occurred. + * + * Beware that tracing can only track the [Raise.bind] or [Raise.raise] call that resulted in the [R] value, + * and not any location of where the [R], or [Either.Left] value was created. + * + * ```kotlin + * public fun main() { + * val error = effect { raise("error") } + * error.traced { (trace, _: String) -> trace.printStackTrace() } + * .fold({ require(it == "error") }, { error("impossible") }) + * } + * ``` + * ```text + * arrow.core.continuations.RaiseCancellationException: Raised Continuation + * at arrow.core.continuations.DefaultRaise.raise(Fold.kt:77) + * at MainKtKt$main$error$1.invoke(MainKt.kt:6) + * at MainKtKt$main$error$1.invoke(MainKt.kt:6) + * at arrow.core.continuations.Raise$DefaultImpls.bind(Raise.kt:22) + * at arrow.core.continuations.DefaultRaise.bind(Fold.kt:74) + * at arrow.core.continuations.Effect__TracingKt$traced$2.invoke(Traced.kt:46) + * at arrow.core.continuations.Effect__TracingKt$traced$2.invoke(Traced.kt:46) + * at arrow.core.continuations.Effect__FoldKt.fold(Fold.kt:92) + * at arrow.core.continuations.Effect.fold(Unknown Source) + * at MainKtKt.main(MainKt.kt:8) + * at MainKtKt.main(MainKt.kt) + * ``` + * + * NOTE: + * This implies a performance penalty of creating a stacktrace when calling [Raise.raise], + * but **this only occurs** when composing `traced`. + * The stacktrace creation is disabled if no `traced` calls are made within the function composition. + */ +@ExperimentalTraceApi +public inline fun Raise.traced( + @BuilderInference program: Raise.() -> A, + trace: (traced: Trace, R) -> Unit +): A { + val isOuterTraced = this is DefaultRaise && isTraced + val nested = if (this is DefaultRaise && isTraced) this else DefaultRaise(true) + return try { + program.invoke(nested) + } catch (e: RaiseCancellationException) { + val r: R = e.raisedOrRethrow(nested) + trace(Trace(e), r) + if (isOuterTraced) throw e else raise(r) + } +} + /** Returns the raised value, rethrows the CancellationException if not our scope */ @PublishedApi @Suppress("UNCHECKED_CAST") internal fun CancellationException.raisedOrRethrow(raise: DefaultRaise): R = - if (this is RaiseCancellationException && this.raise === raise) raised as R - else throw this + when { + this is RaiseCancellationExceptionNoTrace && this.raise === raise -> raised as R + this is RaiseCancellationException && this.raise === raise -> raised as R + else -> throw this + } /** Serves as both purposes of a scope-reference token, and a default implementation for Raise. */ @PublishedApi -internal class DefaultRaise : Raise { +internal class DefaultRaise(@PublishedApi internal val isTraced: Boolean) : Raise { private val isActive = AtomicBoolean(true) + @PublishedApi internal fun complete(): Boolean = isActive.getAndSet(false) - override fun raise(r: Any?): Nothing = - if (isActive.value) throw RaiseCancellationException(r, this) else throw RaiseLeakedException() + override fun raise(r: Any?): Nothing = when { + isActive.value -> throw if (isTraced) RaiseCancellationException(r, this) else RaiseCancellationExceptionNoTrace(r, this) + else -> throw RaiseLeakedException() + } } /** CancellationException is required to cancel coroutines when raising from within them. */ -private class RaiseCancellationException(val raised: Any?, val raise: Raise) : CancellationExceptionNoTrace() +private class RaiseCancellationExceptionNoTrace(val raised: Any?, val raise: Raise) : + CancellationExceptionNoTrace() -public expect open class CancellationExceptionNoTrace() : CancellationException +private class RaiseCancellationException(val raised: Any?, val raise: Raise) : CancellationException() + +internal expect open class CancellationExceptionNoTrace() : CancellationException private class RaiseLeakedException : IllegalStateException( """ @@ -138,3 +202,8 @@ private class RaiseLeakedException : IllegalStateException( See: Effect documentation for additional information. """.trimIndent() ) + +internal const val RaiseCancellationExceptionCaptured: String = + "kotlin.coroutines.cancellation.CancellationException should never get cancelled. Always re-throw it if captured." + + "This swallows the exception of Arrow's Raise, and leads to unexpected behavior." + + "When working with Arrow prefer Either.catch or arrow.core.raise.catch to automatically rethrow CancellationException." diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/RaiseAccumulate.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/RaiseAccumulate.kt index af77863614b..302c6f1bf3b 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/RaiseAccumulate.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/RaiseAccumulate.kt @@ -22,7 +22,6 @@ import kotlin.experimental.ExperimentalTypeInference import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmName - /** * Accumulate the errors from running both [action1] and [action2] using the given [combine] function. */ diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Trace.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Trace.kt new file mode 100644 index 00000000000..33d303bb41a --- /dev/null +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Trace.kt @@ -0,0 +1,43 @@ +@file:JvmMultifileClass +@file:JvmName("RaiseKt") +package arrow.core.raise + +import kotlin.coroutines.cancellation.CancellationException +import kotlin.jvm.JvmInline +import kotlin.jvm.JvmMultifileClass +import kotlin.jvm.JvmName + +@RequiresOptIn("This API is experimental, and may change in the future.") +public annotation class ExperimentalTraceApi + +/** Tracing result. Allows to inspect the traces from where raise was called. */ +@ExperimentalTraceApi +@JvmInline +public value class Trace(private val exception: CancellationException) { + /** + * Returns the stacktrace as a [String] + * + * Note, the first line in the stacktrace will be the `RaiseCancellationException`. + * The users call to `raise` can found in the_second line of the stacktrace. + */ + public fun stackTraceToString(): String = exception.stackTraceToString() + + /** + * Prints the stacktrace. + * + * Note, the first line in the stacktrace will be the `RaiseCancellationException`. + * The users call to `raise` can found in the_second line of the stacktrace. + */ + public fun printStackTrace(): Unit = + exception.printStackTrace() + + /** + * Returns the suppressed exceptions that occurred during cancellation of the surrounding coroutines, + * + * For example when working with `Resource`, or `bracket`: + * When consuming a `Resource` fails due to [Raise.raise] it results in `ExitCase.Cancelled`, + * if the finalizer then results in a `Throwable` it will be added as a `suppressedException` to the [CancellationException]. + */ + public fun suppressedExceptions(): List = + exception.suppressedExceptions +} diff --git a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/raise/StructuredConcurrencySpec.kt b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/raise/StructuredConcurrencySpec.kt index dc4f36c7061..5ce8a987981 100644 --- a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/raise/StructuredConcurrencySpec.kt +++ b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/raise/StructuredConcurrencySpec.kt @@ -54,7 +54,7 @@ class StructuredConcurrencySpec : StringSpec({ }.fold(::identity) { fail("Should never be here") } shouldBe "hello" withTimeout(2.seconds) { - cancelled.await().shouldNotBeNull().message shouldBe "Raised Continuation" + cancelled.await().shouldNotBeNull().message shouldBe RaiseCancellationExceptionCaptured } } diff --git a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/raise/TraceSpec.kt b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/raise/TraceSpec.kt new file mode 100644 index 00000000000..7a488a1f2f7 --- /dev/null +++ b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/raise/TraceSpec.kt @@ -0,0 +1,46 @@ +package arrow.core.raise + +import arrow.core.right +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe +import io.kotest.property.Arb +import io.kotest.property.arbitrary.int +import io.kotest.property.arbitrary.string +import io.kotest.property.checkAll +import kotlinx.coroutines.CompletableDeferred + +@OptIn(ExperimentalTraceApi::class) +class TraceSpec : StringSpec({ + "trace is empty when no errors" { + checkAll(Arb.int()) { i -> + either { + traced({ i }) { _,_ -> unreachable() } + } shouldBe i.right() + } + } + + "trace is empty with exception" { + checkAll(Arb.string()) { msg -> + val error = RuntimeException(msg) + shouldThrow { + either { + traced({ throw error }) { _,_ -> unreachable() } + } + }.message shouldBe msg + } + } + + "nested tracing - identity" { + val inner = CompletableDeferred() + ior(String::plus) { + traced({ + traced({ raise("") }) { traced, _ -> + inner.complete(traced.stackTraceToString()) + } + }) { traced, _ -> + inner.await() shouldBe traced.stackTraceToString() + } + } + } +}) diff --git a/arrow-libs/core/arrow-core/src/jsMain/kotlin/arrow/core/raise/CancellationExceptionNoTrace.kt b/arrow-libs/core/arrow-core/src/jsMain/kotlin/arrow/core/raise/CancellationExceptionNoTrace.kt index 83d1a6a624d..747d2adac12 100644 --- a/arrow-libs/core/arrow-core/src/jsMain/kotlin/arrow/core/raise/CancellationExceptionNoTrace.kt +++ b/arrow-libs/core/arrow-core/src/jsMain/kotlin/arrow/core/raise/CancellationExceptionNoTrace.kt @@ -2,4 +2,4 @@ package arrow.core.raise import kotlin.coroutines.cancellation.CancellationException -public actual open class CancellationExceptionNoTrace : CancellationException("Raised Continuation") +internal actual open class CancellationExceptionNoTrace : CancellationException(RaiseCancellationExceptionCaptured) diff --git a/arrow-libs/core/arrow-core/src/jvmMain/kotlin/arrow/core/raise/CancellationExceptionNoTrace.kt b/arrow-libs/core/arrow-core/src/jvmMain/kotlin/arrow/core/raise/CancellationExceptionNoTrace.kt index 3b25b1c4b9e..226c21392d6 100644 --- a/arrow-libs/core/arrow-core/src/jvmMain/kotlin/arrow/core/raise/CancellationExceptionNoTrace.kt +++ b/arrow-libs/core/arrow-core/src/jvmMain/kotlin/arrow/core/raise/CancellationExceptionNoTrace.kt @@ -6,7 +6,7 @@ import kotlin.coroutines.cancellation.CancellationException * Inspired by KotlinX Coroutines: * https://github.com/Kotlin/kotlinx.coroutines/blob/3788889ddfd2bcfedbff1bbca10ee56039e024a2/kotlinx-coroutines-core/jvm/src/Exceptions.kt#L29 */ -public actual open class CancellationExceptionNoTrace : CancellationException("Raised Continuation") { +internal actual open class CancellationExceptionNoTrace : CancellationException(RaiseCancellationExceptionCaptured) { override fun fillInStackTrace(): Throwable { // Prevent Android <= 6.0 bug. stackTrace = emptyArray() diff --git a/arrow-libs/core/arrow-core/src/jvmTest/kotlin/arrow/core/raise/TraceJvmSpec.kt b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/arrow/core/raise/TraceJvmSpec.kt new file mode 100644 index 00000000000..caaf88af7ff --- /dev/null +++ b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/arrow/core/raise/TraceJvmSpec.kt @@ -0,0 +1,24 @@ +package arrow.core.raise + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe + +@OptIn(ExperimentalTraceApi::class) +class TraceJvmSpec : StringSpec({ + "Can trace a typed error" { + either { + traced({ raise(RuntimeException("")) }) { traced, raised -> + // Remove first 2 lines: + // arrow.core.raise.RaiseCancellationException + // at arrow.core.raise.DefaultRaise.raise(Fold.kt:187) + val trace = traced.stackTraceToString().lines().drop(2) + + // Remove first line: + // java.lang.RuntimeException: + val exceptionTrace = raised.stackTraceToString().lines().drop(1) + + trace shouldBe exceptionTrace + } + } + } +}) diff --git a/arrow-libs/core/arrow-core/src/nativeMain/kotlin/arrow/core/raise/CancellationExceptionNoTrace.kt b/arrow-libs/core/arrow-core/src/nativeMain/kotlin/arrow/core/raise/CancellationExceptionNoTrace.kt index 83d1a6a624d..747d2adac12 100644 --- a/arrow-libs/core/arrow-core/src/nativeMain/kotlin/arrow/core/raise/CancellationExceptionNoTrace.kt +++ b/arrow-libs/core/arrow-core/src/nativeMain/kotlin/arrow/core/raise/CancellationExceptionNoTrace.kt @@ -2,4 +2,4 @@ package arrow.core.raise import kotlin.coroutines.cancellation.CancellationException -public actual open class CancellationExceptionNoTrace : CancellationException("Raised Continuation") +internal actual open class CancellationExceptionNoTrace : CancellationException(RaiseCancellationExceptionCaptured)