Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keep stack traces in withError #3573

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions arrow-libs/core/arrow-core/api/android/arrow-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -875,6 +875,10 @@ public final class arrow/core/UtilsKt {
public static final fun mapNullable (Lkotlin/jvm/functions/Function1;)Lkotlin/jvm/functions/Function1;
}

public final class arrow/core/raise/CancellationExceptionNoTraceKt {
public static final fun copyStacktrace (Ljava/lang/Throwable;Ljava/lang/Throwable;)V
}

public final class arrow/core/raise/DefaultRaise : arrow/core/raise/Raise {
public fun <init> (Z)V
public fun bind (Larrow/core/Either;)Ljava/lang/Object;
Expand Down
1 change: 1 addition & 0 deletions arrow-libs/core/arrow-core/api/arrow-core.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,7 @@ final const val arrow.core/RedundantAPI // arrow.core/RedundantAPI|{}RedundantAP

final fun (arrow.core.raise/Traced).arrow.core.raise/withCause(arrow.core.raise/Traced): arrow.core.raise/Traced // arrow.core.raise/withCause|[email protected](arrow.core.raise.Traced){}[0]
final fun (kotlin/String).arrow.core/escaped(): kotlin/String // arrow.core/escaped|[email protected](){}[0]
final fun (kotlin/Throwable).arrow.core.raise/copyStacktrace(kotlin/Throwable) // arrow.core.raise/copyStacktrace|[email protected](kotlin.Throwable){}[0]
final fun (kotlin/Throwable).arrow.core/nonFatalOrThrow(): kotlin/Throwable // arrow.core/nonFatalOrThrow|[email protected](){}[0]
final fun <#A: kotlin/Any> (kotlin/Function1<#A, kotlin/Boolean>).arrow.core/mapNullable(): kotlin/Function1<#A?, kotlin/Boolean> // arrow.core/mapNullable|[email protected]<0:0,kotlin.Boolean>(){0§<kotlin.Any>}[0]
final fun <#A: kotlin/Any?, #B: kotlin/Any?, #C: kotlin/Any?, #D: kotlin/Any?, #E: kotlin/Any?, #F: kotlin/Any?, #G: kotlin/Any?, #H: kotlin/Any?, #I: kotlin/Any?, #J: kotlin/Any?, #K: kotlin/Any?> (kotlin.sequences/Sequence<#A>).arrow.core/zip(kotlin.sequences/Sequence<#B>, kotlin.sequences/Sequence<#C>, kotlin.sequences/Sequence<#D>, kotlin.sequences/Sequence<#E>, kotlin.sequences/Sequence<#F>, kotlin.sequences/Sequence<#G>, kotlin.sequences/Sequence<#H>, kotlin.sequences/Sequence<#I>, kotlin.sequences/Sequence<#J>, kotlin/Function10<#A, #B, #C, #D, #E, #F, #G, #H, #I, #J, #K>): kotlin.sequences/Sequence<#K> // arrow.core/zip|[email protected]<0:0>(kotlin.sequences.Sequence<0:1>;kotlin.sequences.Sequence<0:2>;kotlin.sequences.Sequence<0:3>;kotlin.sequences.Sequence<0:4>;kotlin.sequences.Sequence<0:5>;kotlin.sequences.Sequence<0:6>;kotlin.sequences.Sequence<0:7>;kotlin.sequences.Sequence<0:8>;kotlin.sequences.Sequence<0:9>;kotlin.Function10<0:0,0:1,0:2,0:3,0:4,0:5,0:6,0:7,0:8,0:9,0:10>){0§<kotlin.Any?>;1§<kotlin.Any?>;2§<kotlin.Any?>;3§<kotlin.Any?>;4§<kotlin.Any?>;5§<kotlin.Any?>;6§<kotlin.Any?>;7§<kotlin.Any?>;8§<kotlin.Any?>;9§<kotlin.Any?>;10§<kotlin.Any?>}[0]
Expand Down
4 changes: 4 additions & 0 deletions arrow-libs/core/arrow-core/api/jvm/arrow-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -875,6 +875,10 @@ public final class arrow/core/UtilsKt {
public static final fun mapNullable (Lkotlin/jvm/functions/Function1;)Lkotlin/jvm/functions/Function1;
}

public final class arrow/core/raise/CancellationExceptionNoTraceKt {
public static final fun copyStacktrace (Ljava/lang/Throwable;Ljava/lang/Throwable;)V
}

public final class arrow/core/raise/DefaultRaise : arrow/core/raise/Raise {
public fun <init> (Z)V
public fun bind (Larrow/core/Either;)Ljava/lang/Object;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
package arrow.core.raise

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
*/
@OptIn(DelicateRaiseApi::class)
@DelicateRaiseApi
@Suppress(
"EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING",
"SEALED_INHERITOR_IN_DIFFERENT_MODULE"
Expand All @@ -19,3 +17,8 @@ internal actual class NoTrace actual constructor(raised: Any?, raise: Raise<Any?
return this
}
}

@PublishedApi
internal actual fun Throwable.copyStacktrace(from: Throwable) {
this.stackTrace = from.stackTrace
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ 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
Expand Down Expand Up @@ -152,6 +153,60 @@ public inline fun <Error, A, B> fold(
}
}

/**
* Execute the [Raise] context function resulting in [A] or any _logical error_ of type [OtherError],
* and transform any raised [OtherError] into [Error], which is raised to the outer [Raise].
*
* <!--- INCLUDE
* import arrow.core.Either
* import arrow.core.raise.either
* import arrow.core.raise.withError
* import io.kotest.matchers.shouldBe
* -->
* ```kotlin
* fun test() {
* either<Int, String> {
* withError(String::length) {
* raise("failed")
* }
* } shouldBe Either.Left(6)
* }
* ```
* <!--- KNIT example-raise-dsl-11.kt -->
* <!--- TEST lines.isEmpty() -->
*/
@RaiseDSL
@OptIn(DelicateRaiseApi::class)
@Suppress("UNCHECKED_CAST")
public inline fun <Error, OtherError, A> Raise<Error>.withError(
transform: (OtherError) -> Error,
@BuilderInference block: Raise<OtherError>.() -> A
): A {
contract {
callsInPlace(block, EXACTLY_ONCE)
callsInPlace(transform, AT_MOST_ONCE)
}
// recover({ return block(this) }) { raise(transform(it)) }
val raise = DefaultRaise(false)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this mean that innerException would always have no stack trace?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would that be the case? As I understand the inner stack trace comes from the point where the RaiseCancellationException is first thrown.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, the inner exception is always going to be a NoTrace because it's created by a DefaultRaise(false). Otherwise, we would be creating a stack trace every time, which would defeat our optimizations.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is definitely true!

return try {
block(raise).also { raise.complete() }
} catch (innerException: RaiseCancellationException) {
raise.complete()
val error = transform(innerException.raisedOrRethrow(raise))
try {
raise(error)
} catch (outerException: RaiseCancellationException) {
throw when (outerException) {
is Traced -> outerException.apply { copyStacktrace(innerException) }
else -> outerException
}
}
} catch (e: Throwable) {
raise.complete()
throw e.nonFatalOrThrow()
}
}

/**
* Inspect a [Trace] value of [Error].
*
Expand Down Expand Up @@ -269,6 +324,9 @@ internal expect class NoTrace(raised: Any?, raise: Raise<Any?>) : RaiseCancellat
@DelicateRaiseApi
internal class Traced(raised: Any?, raise: Raise<Any?>, override val cause: Traced? = null): RaiseCancellationException(raised, raise)

@PublishedApi
internal expect fun Throwable.copyStacktrace(from: Throwable)

private class RaiseLeakedException : IllegalStateException(
"""
'raise' or 'bind' was leaked outside of its context scope.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import arrow.core.recover
import kotlin.coroutines.cancellation.CancellationException
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind.AT_MOST_ONCE
import kotlin.contracts.InvocationKind.EXACTLY_ONCE
import kotlin.contracts.contract
import kotlin.experimental.ExperimentalTypeInference
import kotlin.jvm.JvmMultifileClass
Expand Down Expand Up @@ -629,39 +628,6 @@ public inline fun <Error, B : Any> Raise<Error>.ensureNotNull(value: B?, raise:
return value ?: raise(raise())
}

/**
* Execute the [Raise] context function resulting in [A] or any _logical error_ of type [OtherError],
* and transform any raised [OtherError] into [Error], which is raised to the outer [Raise].
*
* <!--- INCLUDE
* import arrow.core.Either
* import arrow.core.raise.either
* import arrow.core.raise.withError
* import io.kotest.matchers.shouldBe
* -->
* ```kotlin
* fun test() {
* either<Int, String> {
* withError(String::length) {
* raise("failed")
* }
* } shouldBe Either.Left(6)
* }
* ```
* <!--- KNIT example-raise-dsl-11.kt -->
* <!--- TEST lines.isEmpty() -->
*/
@RaiseDSL
public inline fun <Error, OtherError, A> Raise<Error>.withError(
transform: (OtherError) -> Error,
@BuilderInference block: Raise<OtherError>.() -> A
): A {
contract {
callsInPlace(block, EXACTLY_ONCE)
}
recover({ return block(this) }) { raise(transform(it)) }
}

/**
* Execute the [Raise] context function resulting in [A] or any _logical error_ of type [A].
* Does not distinguish between normal results and errors, thus you can consider
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package arrow.core.raise

@OptIn(DelicateRaiseApi::class)
@DelicateRaiseApi
@Suppress(
"EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING",
"SEALED_INHERITOR_IN_DIFFERENT_MODULE"
)
internal actual class NoTrace actual constructor(raised: Any?, raise: Raise<Any?>) : RaiseCancellationException(raised, raise)

@PublishedApi
internal actual fun Throwable.copyStacktrace(from: Throwable) { }
Loading