From 5b81038b9d276b3b33e6fb2989908363d0969fb8 Mon Sep 17 00:00:00 2001 From: Youssef Shoaib Date: Sun, 30 Nov 2025 03:58:28 +0000 Subject: [PATCH 1/3] Add holdsIn contracts --- .../commonMain/kotlin/arrow/core/Either.kt | 242 +++++++++++++----- .../src/commonMain/kotlin/arrow/core/Ior.kt | 37 ++- .../commonMain/kotlin/arrow/core/Option.kt | 48 +++- .../fx/arrow-fx-coroutines/build.gradle.kts | 3 + .../kotlin/arrow/fx/coroutines/Race3.kt | 7 +- .../src/main/kotlin/arrow.kotlin.gradle.kts | 1 + 6 files changed, 252 insertions(+), 86 deletions(-) diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Either.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Either.kt index 71dfc07492..4a60e1e3f4 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Either.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Either.kt @@ -1,6 +1,6 @@ @file:JvmMultifileClass @file:JvmName("EitherKt") -@file:OptIn(ExperimentalContracts::class) +@file:OptIn(ExperimentalContracts::class, ExperimentalExtendedContracts::class) @file:Suppress("API_NOT_AVAILABLE") package arrow.core @@ -12,6 +12,7 @@ import arrow.core.raise.Raise import arrow.core.raise.either import arrow.core.raise.fold import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.ExperimentalExtendedContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract import kotlin.experimental.ExperimentalTypeInference @@ -533,6 +534,7 @@ public sealed class Either { contract { returns(true) implies (this@Either is Left) callsInPlace(predicate, InvocationKind.AT_MOST_ONCE) + (this@Either is Left) holdsIn predicate } return this@Either is Left && predicate(value) } @@ -562,6 +564,7 @@ public sealed class Either { contract { returns(true) implies (this@Either is Right) callsInPlace(predicate, InvocationKind.AT_MOST_ONCE) + (this@Either is Right) holdsIn predicate } return this@Either is Right && predicate(value) } @@ -594,6 +597,8 @@ public sealed class Either { contract { callsInPlace(ifLeft, InvocationKind.AT_MOST_ONCE) callsInPlace(ifRight, InvocationKind.AT_MOST_ONCE) + (this@Either is Left) holdsIn ifLeft + (this@Either is Right) holdsIn ifRight } return when (this) { is Right -> ifRight(value) @@ -637,6 +642,7 @@ public sealed class Either { public inline fun map(f: (right: B) -> C): Either { contract { callsInPlace(f, InvocationKind.AT_MOST_ONCE) + (this@Either is Right) holdsIn f } return flatMap { Right(f(it)) } } @@ -658,7 +664,10 @@ public sealed class Either { * */ public inline fun mapLeft(f: (A) -> C): Either { - contract { callsInPlace(f, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(f, InvocationKind.AT_MOST_ONCE) + (this@Either is Left) holdsIn f + } return when (this) { is Left -> Left(f(value)) is Right -> Right(value) @@ -684,6 +693,7 @@ public sealed class Either { public inline fun onRight(action: (right: B) -> Unit): Either { contract { callsInPlace(action, InvocationKind.AT_MOST_ONCE) + (this@Either is Right) holdsIn action } return also { if (it.isRight()) action(it.value) } } @@ -707,6 +717,7 @@ public sealed class Either { public inline fun onLeft(action: (left: A) -> Unit): Either { contract { callsInPlace(action, InvocationKind.AT_MOST_ONCE) + (this@Either is Left) holdsIn action } return also { if (it.isLeft()) action(it.value) } } @@ -850,7 +861,11 @@ public sealed class Either { b: Either, transform: (A, B) -> Z, ): Either { - contract { callsInPlace(transform, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(transform, InvocationKind.AT_MOST_ONCE) + (a is Left && b is Left) holdsIn combine + (a is Right && b is Right) holdsIn transform + } return zipOrAccumulate(combine, a, b, unit, unit, unit, unit, unit, unit, unit, unit) { aa, bb, _, _, _, _, _, _, _, _ -> transform(aa, bb) } @@ -863,7 +878,10 @@ public sealed class Either { c: Either, transform: (A, B, C) -> Z, ): Either { - contract { callsInPlace(transform, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(transform, InvocationKind.AT_MOST_ONCE) + (a is Right && b is Right && c is Right) holdsIn transform + } return zipOrAccumulate(combine, a, b, c, unit, unit, unit, unit, unit, unit, unit) { aa, bb, cc, _, _, _, _, _, _, _ -> transform(aa, bb, cc) } @@ -877,7 +895,10 @@ public sealed class Either { d: Either, transform: (A, B, C, D) -> Z, ): Either { - contract { callsInPlace(transform, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(transform, InvocationKind.AT_MOST_ONCE) + (a is Right && b is Right && c is Right && d is Right) holdsIn transform + } return zipOrAccumulate(combine, a, b, c, d, unit, unit, unit, unit, unit, unit) { aa, bb, cc, dd, _, _, _, _, _, _ -> transform(aa, bb, cc, dd) } @@ -892,7 +913,10 @@ public sealed class Either { e: Either, transform: (A, B, C, D, EE) -> Z, ): Either { - contract { callsInPlace(transform, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(transform, InvocationKind.AT_MOST_ONCE) + (a is Right && b is Right && c is Right && d is Right && e is Right) holdsIn transform + } return zipOrAccumulate(combine, a, b, c, d, e, unit, unit, unit, unit, unit) { aa, bb, cc, dd, ee, _, _, _, _, _ -> transform(aa, bb, cc, dd, ee) } @@ -908,7 +932,10 @@ public sealed class Either { f: Either, transform: (A, B, C, D, EE, FF) -> Z, ): Either { - contract { callsInPlace(transform, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(transform, InvocationKind.AT_MOST_ONCE) + (a is Right && b is Right && c is Right && d is Right && e is Right && f is Right) holdsIn transform + } return zipOrAccumulate(combine, a, b, c, d, e, f, unit, unit, unit, unit) { aa, bb, cc, dd, ee, ff, _, _, _, _ -> transform(aa, bb, cc, dd, ee, ff) } @@ -925,7 +952,10 @@ public sealed class Either { g: Either, transform: (A, B, C, D, EE, F, G) -> Z, ): Either { - contract { callsInPlace(transform, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(transform, InvocationKind.AT_MOST_ONCE) + (a is Right && b is Right && c is Right && d is Right && e is Right && f is Right && g is Right) holdsIn transform + } return zipOrAccumulate(combine, a, b, c, d, e, f, g, unit, unit, unit) { aa, bb, cc, dd, ee, ff, gg, _, _, _ -> transform(aa, bb, cc, dd, ee, ff, gg) } @@ -943,7 +973,10 @@ public sealed class Either { h: Either, transform: (A, B, C, D, EE, F, G, H) -> Z, ): Either { - contract { callsInPlace(transform, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(transform, InvocationKind.AT_MOST_ONCE) + (a is Right && b is Right && c is Right && d is Right && e is Right && f is Right && g is Right && h is Right) holdsIn transform + } return zipOrAccumulate(combine, a, b, c, d, e, f, g, h, unit, unit) { aa, bb, cc, dd, ee, ff, gg, hh, _, _ -> transform(aa, bb, cc, dd, ee, ff, gg, hh) } @@ -962,7 +995,10 @@ public sealed class Either { i: Either, transform: (A, B, C, D, EE, F, G, H, I) -> Z, ): Either { - contract { callsInPlace(transform, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(transform, InvocationKind.AT_MOST_ONCE) + (a is Right && b is Right && c is Right && d is Right && e is Right && f is Right && g is Right && h is Right && i is Right) holdsIn transform + } return zipOrAccumulate(combine, a, b, c, d, e, f, g, h, i, unit) { aa, bb, cc, dd, ee, ff, gg, hh, ii, _ -> transform(aa, bb, cc, dd, ee, ff, gg, hh, ii) } @@ -983,12 +1019,14 @@ public sealed class Either { j: Either, transform: (A, B, C, D, EE, F, G, H, I, J) -> Z, ): Either { - contract { callsInPlace(transform, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(transform, InvocationKind.AT_MOST_ONCE) + (a is Right && b is Right && c is Right && d is Right && e is Right && f is Right && g is Right && h is Right && i is Right && j is Right) holdsIn transform + } return if (a is Right && b is Right && c is Right && d is Right && e is Right && f is Right && g is Right && h is Right && i is Right && j is Right) { Right(transform(a.value, b.value, c.value, d.value, e.value, f.value, g.value, h.value, i.value, j.value)) } else { - var accumulatedError: Any? = EmptyValue - accumulatedError = if (a is Left) a.value else accumulatedError + var accumulatedError: Any? = if (a is Left) a.value else EmptyValue accumulatedError = if (b is Left) EmptyValue.combine(accumulatedError, b.value, combine) else accumulatedError accumulatedError = if (c is Left) EmptyValue.combine(accumulatedError, c.value, combine) else accumulatedError accumulatedError = if (d is Left) EmptyValue.combine(accumulatedError, d.value, combine) else accumulatedError @@ -1009,7 +1047,10 @@ public sealed class Either { b: Either, transform: (A, B) -> Z, ): Either, Z> { - contract { callsInPlace(transform, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(transform, InvocationKind.AT_MOST_ONCE) + (a is Right && b is Right) holdsIn transform + } return zipOrAccumulate(a, b, unit, unit, unit, unit, unit, unit, unit, unit) { aa, bb, _, _, _, _, _, _, _, _ -> transform(aa, bb) } @@ -1021,7 +1062,10 @@ public sealed class Either { c: Either, transform: (A, B, C) -> Z, ): Either, Z> { - contract { callsInPlace(transform, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(transform, InvocationKind.AT_MOST_ONCE) + (a is Right && b is Right && c is Right) holdsIn transform + } return zipOrAccumulate(a, b, c, unit, unit, unit, unit, unit, unit, unit) { aa, bb, cc, _, _, _, _, _, _, _ -> transform(aa, bb, cc) } @@ -1034,7 +1078,10 @@ public sealed class Either { d: Either, transform: (A, B, C, D) -> Z, ): Either, Z> { - contract { callsInPlace(transform, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(transform, InvocationKind.AT_MOST_ONCE) + (a is Right && b is Right && c is Right && d is Right) holdsIn transform + } return zipOrAccumulate(a, b, c, d, unit, unit, unit, unit, unit, unit) { aa, bb, cc, dd, _, _, _, _, _, _ -> transform(aa, bb, cc, dd) } @@ -1048,7 +1095,10 @@ public sealed class Either { e: Either, transform: (A, B, C, D, EE) -> Z, ): Either, Z> { - contract { callsInPlace(transform, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(transform, InvocationKind.AT_MOST_ONCE) + (a is Right && b is Right && c is Right && d is Right && e is Right) holdsIn transform + } return zipOrAccumulate(a, b, c, d, e, unit, unit, unit, unit, unit) { aa, bb, cc, dd, ee, _, _, _, _, _ -> transform(aa, bb, cc, dd, ee) } @@ -1063,7 +1113,10 @@ public sealed class Either { f: Either, transform: (A, B, C, D, EE, FF) -> Z, ): Either, Z> { - contract { callsInPlace(transform, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(transform, InvocationKind.AT_MOST_ONCE) + (a is Right && b is Right && c is Right && d is Right && e is Right && f is Right) holdsIn transform + } return zipOrAccumulate(a, b, c, d, e, f, unit, unit, unit, unit) { aa, bb, cc, dd, ee, ff, _, _, _, _ -> transform(aa, bb, cc, dd, ee, ff) } @@ -1079,7 +1132,10 @@ public sealed class Either { g: Either, transform: (A, B, C, D, EE, F, G) -> Z, ): Either, Z> { - contract { callsInPlace(transform, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(transform, InvocationKind.AT_MOST_ONCE) + (a is Right && b is Right && c is Right && d is Right && e is Right && f is Right && g is Right) holdsIn transform + } return zipOrAccumulate(a, b, c, d, e, f, g, unit, unit, unit) { aa, bb, cc, dd, ee, ff, gg, _, _, _ -> transform(aa, bb, cc, dd, ee, ff, gg) } @@ -1096,7 +1152,10 @@ public sealed class Either { h: Either, transform: (A, B, C, D, EE, F, G, H) -> Z, ): Either, Z> { - contract { callsInPlace(transform, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(transform, InvocationKind.AT_MOST_ONCE) + (a is Right && b is Right && c is Right && d is Right && e is Right && f is Right && g is Right && h is Right) holdsIn transform + } return zipOrAccumulate(a, b, c, d, e, f, g, h, unit, unit) { aa, bb, cc, dd, ee, ff, gg, hh, _, _ -> transform(aa, bb, cc, dd, ee, ff, gg, hh) } @@ -1114,7 +1173,10 @@ public sealed class Either { i: Either, transform: (A, B, C, D, EE, F, G, H, I) -> Z, ): Either, Z> { - contract { callsInPlace(transform, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(transform, InvocationKind.AT_MOST_ONCE) + (a is Right && b is Right && c is Right && d is Right && e is Right && f is Right && g is Right && h is Right && i is Right) holdsIn transform + } return zipOrAccumulate(a, b, c, d, e, f, g, h, i, unit) { aa, bb, cc, dd, ee, ff, gg, hh, ii, _ -> transform(aa, bb, cc, dd, ee, ff, gg, hh, ii) } @@ -1134,24 +1196,24 @@ public sealed class Either { j: Either, transform: (A, B, C, D, EE, F, G, H, I, J) -> Z, ): Either, Z> { - contract { callsInPlace(transform, InvocationKind.AT_MOST_ONCE) } - return if (a is Right && b is Right && c is Right && d is Right && e is Right && f is Right && g is Right && h is Right && i is Right && j is Right) { - Right(transform(a.value, b.value, c.value, d.value, e.value, f.value, g.value, h.value, i.value, j.value)) - } else { - val list = buildList(9) { - if (a is Left) add(a.value) - if (b is Left) add(b.value) - if (c is Left) add(c.value) - if (d is Left) add(d.value) - if (e is Left) add(e.value) - if (f is Left) add(f.value) - if (g is Left) add(g.value) - if (h is Left) add(h.value) - if (i is Left) add(i.value) - if (j is Left) add(j.value) - } - Left(NonEmptyList(list[0], list.drop(1))) + contract { + callsInPlace(transform, InvocationKind.AT_MOST_ONCE) + (a is Right && b is Right && c is Right && d is Right && e is Right && f is Right && g is Right && h is Right && i is Right && j is Right) holdsIn transform } + return if (a is Right && b is Right && c is Right && d is Right && e is Right && f is Right && g is Right && h is Right && i is Right && j is Right) + Right(transform(a.value, b.value, c.value, d.value, e.value, f.value, g.value, h.value, i.value, j.value)) + else buildList(10) { + if (a is Left) add(a.value) + if (b is Left) add(b.value) + if (c is Left) add(c.value) + if (d is Left) add(d.value) + if (e is Left) add(e.value) + if (f is Left) add(f.value) + if (g is Left) add(g.value) + if (h is Left) add(h.value) + if (i is Left) add(i.value) + if (j is Left) add(j.value) + }.let(::NonEmptyList).left() } @JvmName("zipOrAccumulateNonEmptyList") @@ -1160,7 +1222,10 @@ public sealed class Either { b: EitherNel, transform: (A, B) -> Z, ): EitherNel { - contract { callsInPlace(transform, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(transform, InvocationKind.AT_MOST_ONCE) + (a is Right && b is Right) holdsIn transform + } return zipOrAccumulate(a, b, unit, unit, unit, unit, unit, unit, unit, unit) { aa, bb, _, _, _, _, _, _, _, _ -> transform(aa, bb) } @@ -1173,7 +1238,10 @@ public sealed class Either { c: EitherNel, transform: (A, B, C) -> Z, ): EitherNel { - contract { callsInPlace(transform, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(transform, InvocationKind.AT_MOST_ONCE) + (a is Right && b is Right && c is Right) holdsIn transform + } return zipOrAccumulate(a, b, c, unit, unit, unit, unit, unit, unit, unit) { aa, bb, cc, _, _, _, _, _, _, _ -> transform(aa, bb, cc) } @@ -1187,7 +1255,10 @@ public sealed class Either { d: EitherNel, transform: (A, B, C, D) -> Z, ): EitherNel { - contract { callsInPlace(transform, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(transform, InvocationKind.AT_MOST_ONCE) + (a is Right && b is Right && c is Right && d is Right) holdsIn transform + } return zipOrAccumulate(a, b, c, d, unit, unit, unit, unit, unit, unit) { aa, bb, cc, dd, _, _, _, _, _, _ -> transform(aa, bb, cc, dd) } @@ -1202,7 +1273,10 @@ public sealed class Either { e: EitherNel, transform: (A, B, C, D, EE) -> Z, ): EitherNel { - contract { callsInPlace(transform, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(transform, InvocationKind.AT_MOST_ONCE) + (a is Right && b is Right && c is Right && d is Right && e is Right) holdsIn transform + } return zipOrAccumulate(a, b, c, d, e, unit, unit, unit, unit, unit) { aa, bb, cc, dd, ee, _, _, _, _, _ -> transform(aa, bb, cc, dd, ee) } @@ -1218,7 +1292,10 @@ public sealed class Either { f: EitherNel, transform: (A, B, C, D, EE, FF) -> Z, ): EitherNel { - contract { callsInPlace(transform, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(transform, InvocationKind.AT_MOST_ONCE) + (a is Right && b is Right && c is Right && d is Right && e is Right && f is Right) holdsIn transform + } return zipOrAccumulate(a, b, c, d, e, f, unit, unit, unit, unit) { aa, bb, cc, dd, ee, ff, _, _, _, _ -> transform(aa, bb, cc, dd, ee, ff) } @@ -1235,7 +1312,10 @@ public sealed class Either { g: EitherNel, transform: (A, B, C, D, EE, F, G) -> Z, ): EitherNel { - contract { callsInPlace(transform, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(transform, InvocationKind.AT_MOST_ONCE) + (a is Right && b is Right && c is Right && d is Right && e is Right && f is Right && g is Right) holdsIn transform + } return zipOrAccumulate(a, b, c, d, e, f, g, unit, unit, unit) { aa, bb, cc, dd, ee, ff, gg, _, _, _ -> transform(aa, bb, cc, dd, ee, ff, gg) } @@ -1253,7 +1333,10 @@ public sealed class Either { h: EitherNel, transform: (A, B, C, D, EE, F, G, H) -> Z, ): EitherNel { - contract { callsInPlace(transform, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(transform, InvocationKind.AT_MOST_ONCE) + (a is Right && b is Right && c is Right && d is Right && e is Right && f is Right && g is Right && h is Right) holdsIn transform + } return zipOrAccumulate(a, b, c, d, e, f, g, h, unit, unit) { aa, bb, cc, dd, ee, ff, gg, hh, _, _ -> transform(aa, bb, cc, dd, ee, ff, gg, hh) } @@ -1272,7 +1355,10 @@ public sealed class Either { i: EitherNel, transform: (A, B, C, D, EE, F, G, H, I) -> Z, ): EitherNel { - contract { callsInPlace(transform, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(transform, InvocationKind.AT_MOST_ONCE) + (a is Right && b is Right && c is Right && d is Right && e is Right && f is Right && g is Right && h is Right && i is Right) holdsIn transform + } return zipOrAccumulate(a, b, c, d, e, f, g, h, i, unit) { aa, bb, cc, dd, ee, ff, gg, hh, ii, _ -> transform(aa, bb, cc, dd, ee, ff, gg, hh, ii) } @@ -1293,24 +1379,24 @@ public sealed class Either { j: EitherNel, transform: (A, B, C, D, EE, F, G, H, I, J) -> Z, ): EitherNel { - contract { callsInPlace(transform, InvocationKind.AT_MOST_ONCE) } - return if (a is Right && b is Right && c is Right && d is Right && e is Right && f is Right && g is Right && h is Right && i is Right && j is Right) { - Right(transform(a.value, b.value, c.value, d.value, e.value, f.value, g.value, h.value, i.value, j.value)) - } else { - val list = buildList { - if (a is Left) addAll(a.value) - if (b is Left) addAll(b.value) - if (c is Left) addAll(c.value) - if (d is Left) addAll(d.value) - if (e is Left) addAll(e.value) - if (f is Left) addAll(f.value) - if (g is Left) addAll(g.value) - if (h is Left) addAll(h.value) - if (i is Left) addAll(i.value) - if (j is Left) addAll(j.value) - } - Left(NonEmptyList(list[0], list.drop(1))) + contract { + callsInPlace(transform, InvocationKind.AT_MOST_ONCE) + (a is Right && b is Right && c is Right && d is Right && e is Right && f is Right && g is Right && h is Right && i is Right && j is Right) holdsIn transform } + return if (a is Right && b is Right && c is Right && d is Right && e is Right && f is Right && g is Right && h is Right && i is Right && j is Right) + Right(transform(a.value, b.value, c.value, d.value, e.value, f.value, g.value, h.value, i.value, j.value)) + else buildList(10) { + if (a is Left) addAll(a.value) + if (b is Left) addAll(b.value) + if (c is Left) addAll(c.value) + if (d is Left) addAll(d.value) + if (e is Left) addAll(e.value) + if (f is Left) addAll(f.value) + if (g is Left) addAll(g.value) + if (h is Left) addAll(h.value) + if (i is Left) addAll(i.value) + if (j is Left) addAll(j.value) + }.let(::NonEmptyList).left() } } } @@ -1323,7 +1409,10 @@ public sealed class Either { * @param f The function to bind across [Right]. */ public inline fun Either.flatMap(f: (right: B) -> Either): Either { - contract { callsInPlace(f, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(f, InvocationKind.AT_MOST_ONCE) + (this@flatMap is Right) holdsIn f + } return when (this) { is Right -> f(this.value) is Left -> this @@ -1338,7 +1427,10 @@ public inline fun Either.flatMap(f: (right: B) -> Either): * @param f The function to bind across [Left]. */ public inline fun Either.handleErrorWith(f: (A) -> Either): Either { - contract { callsInPlace(f, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(f, InvocationKind.AT_MOST_ONCE) + (this@handleErrorWith is Left) holdsIn f + } return when (this) { is Left -> f(this.value) is Right -> this @@ -1365,7 +1457,10 @@ public fun Either>.flatten(): Either = * */ public inline infix fun Either.getOrElse(default: (A) -> B): B { - contract { callsInPlace(default, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(default, InvocationKind.AT_MOST_ONCE) + (this@getOrElse is Left) holdsIn default + } return when (this) { is Left -> default(this.value) is Right -> this.value @@ -1411,6 +1506,8 @@ public inline fun Either.combine(other: Either, combineLeft: contract { callsInPlace(combineLeft, InvocationKind.AT_MOST_ONCE) callsInPlace(combineRight, InvocationKind.AT_MOST_ONCE) + (this@combine is Left && other is Left) holdsIn combineLeft + (this@combine is Right && other is Right) holdsIn combineRight } return when (val one = this) { is Left -> when (other) { @@ -1480,11 +1577,11 @@ public fun E.leftNel(): EitherNel = */ @OptIn(ExperimentalTypeInference::class) public inline fun Either.recover(@BuilderInference recover: Raise.(E) -> A): Either { - contract { callsInPlace(recover, InvocationKind.AT_MOST_ONCE) } - return when (this) { - is Left -> either { recover(this, value) } - is Right -> this@recover + contract { + callsInPlace(recover, InvocationKind.AT_MOST_ONCE) + (this@recover is Left) holdsIn recover } + return handleErrorWith { either { recover(it) } } } /** @@ -1518,6 +1615,9 @@ public inline fun Either.recover(@BuilderInference recover: Rai */ @OptIn(ExperimentalTypeInference::class) public inline fun Either.catch(@BuilderInference catch: Raise.(T) -> A): Either { - contract { callsInPlace(catch, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(catch, InvocationKind.AT_MOST_ONCE) + (this@catch is Left) holdsIn catch + } return recover { e -> if (e is T) catch(e) else throw e } } diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Ior.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Ior.kt index 9bd4b166a4..339ec6d592 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Ior.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Ior.kt @@ -1,4 +1,5 @@ -@file:OptIn(ExperimentalContracts::class) +@file:Suppress("API_NOT_AVAILABLE") +@file:OptIn(ExperimentalContracts::class, ExperimentalExtendedContracts::class) package arrow.core @@ -6,6 +7,7 @@ import arrow.core.Ior.Both import arrow.core.Ior.Left import arrow.core.Ior.Right import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.ExperimentalExtendedContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract import kotlin.jvm.JvmStatic @@ -149,6 +151,9 @@ public sealed class Ior { callsInPlace(fa, InvocationKind.AT_MOST_ONCE) callsInPlace(fb, InvocationKind.AT_MOST_ONCE) callsInPlace(fab, InvocationKind.AT_MOST_ONCE) + (this@Ior is Left) holdsIn fa + (this@Ior is Right) holdsIn fb + (this@Ior is Both) holdsIn fab } return when (this) { is Left -> fa(value) @@ -173,7 +178,10 @@ public sealed class Ior { * */ public inline fun map(f: (B) -> D): Ior { - contract { callsInPlace(f, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(f, InvocationKind.AT_MOST_ONCE) + (this@Ior is Right || this@Ior is Both) holdsIn f + } return when (this) { is Left -> this is Right -> Right(f(value)) @@ -197,7 +205,10 @@ public sealed class Ior { * */ public inline fun mapLeft(fa: (A) -> C): Ior { - contract { callsInPlace(fa, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(fa, InvocationKind.AT_MOST_ONCE) + (this@Ior is Left || this@Ior is Both) holdsIn fa + } return when (this) { is Left -> Left(fa(value)) is Right -> this @@ -337,6 +348,7 @@ public sealed class Ior { contract { returns(true) implies (this@Ior is Left) callsInPlace(predicate, InvocationKind.AT_MOST_ONCE) + (this@Ior is Left) holdsIn predicate } return this@Ior is Left && predicate(value) } @@ -362,6 +374,7 @@ public sealed class Ior { contract { returns(true) implies (this@Ior is Right) callsInPlace(predicate, InvocationKind.AT_MOST_ONCE) + (this@Ior is Right) holdsIn predicate } return this@Ior is Right && predicate(value) } @@ -389,6 +402,8 @@ public sealed class Ior { returns(true) implies (this@Ior is Both) callsInPlace(leftPredicate, InvocationKind.AT_MOST_ONCE) callsInPlace(rightPredicate, InvocationKind.AT_MOST_ONCE) + (this@Ior is Both) holdsIn leftPredicate + (this@Ior is Both) holdsIn rightPredicate } return this@Ior is Both && leftPredicate(leftValue) && rightPredicate(rightValue) } @@ -403,6 +418,8 @@ public inline fun Ior.flatMap(combine: (A, A) -> A, f: (B) -> Io contract { callsInPlace(combine, InvocationKind.AT_MOST_ONCE) callsInPlace(f, InvocationKind.AT_MOST_ONCE) + (this@flatMap is Both) holdsIn combine + (this@flatMap is Right || this@flatMap is Both) holdsIn f } return when (this) { is Left -> this @@ -424,6 +441,8 @@ public inline fun Ior.handleErrorWith(combine: (B, B) -> B, f: ( contract { callsInPlace(combine, InvocationKind.AT_MOST_ONCE) callsInPlace(f, InvocationKind.AT_MOST_ONCE) + (this@handleErrorWith is Both) holdsIn combine + (this@handleErrorWith is Left || this@handleErrorWith is Both) holdsIn f } return when (this) { is Left -> f(value) @@ -437,7 +456,10 @@ public inline fun Ior.handleErrorWith(combine: (B, B) -> B, f: ( } public inline fun Ior.getOrElse(default: (A) -> B): B { - contract { callsInPlace(default, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(default, InvocationKind.AT_MOST_ONCE) + (this@getOrElse is Left) holdsIn default + } return when (this) { is Left -> default(this.value) is Right -> this.value @@ -456,6 +478,8 @@ public inline fun Ior.combine(other: Ior, combineA: (A, A) -> contract { callsInPlace(combineA, InvocationKind.AT_MOST_ONCE) callsInPlace(combineB, InvocationKind.AT_MOST_ONCE) + ((this@combine is Left || this@combine is Both) && (other is Left || other is Both)) holdsIn combineA + ((this@combine is Right || this@combine is Both) && (other is Right || other is Both)) holdsIn combineB } return when (this) { is Ior.Left -> when (other) { @@ -479,7 +503,10 @@ public inline fun Ior.combine(other: Ior, combineA: (A, A) -> } public inline fun Ior>.flatten(combine: (A, A) -> A): Ior { - contract { callsInPlace(combine, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(combine, InvocationKind.AT_MOST_ONCE) + (this@flatten is Both) holdsIn combine + } return flatMap(combine, ::identity) } diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Option.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Option.kt index 9566a9b95e..23a690d08d 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Option.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Option.kt @@ -1,4 +1,4 @@ -@file:OptIn(ExperimentalContracts::class) +@file:OptIn(ExperimentalContracts::class, ExperimentalExtendedContracts::class) @file:Suppress("API_NOT_AVAILABLE") package arrow.core @@ -9,6 +9,7 @@ import arrow.core.raise.SingletonRaise import arrow.core.raise.option import arrow.core.raise.recover import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.ExperimentalExtendedContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract import kotlin.jvm.JvmName @@ -350,6 +351,7 @@ public sealed class Option { public inline fun onNone(action: () -> Unit): Option { contract { callsInPlace(action, InvocationKind.AT_MOST_ONCE) + (this@Option is None) holdsIn action } return also { if (it.isNone()) action() } } @@ -376,6 +378,7 @@ public sealed class Option { public inline fun onSome(action: (A) -> Unit): Option { contract { callsInPlace(action, InvocationKind.AT_MOST_ONCE) + (this@Option is Some) holdsIn action } return also { if (it.isSome()) action(it.value) } } @@ -431,6 +434,7 @@ public sealed class Option { contract { callsInPlace(predicate, InvocationKind.AT_MOST_ONCE) returns(true) implies (this@Option is Some) + (this@Option is Some) holdsIn predicate } return this@Option is Some && predicate(value) } @@ -473,7 +477,10 @@ public sealed class Option { * @see flatMap */ public inline fun map(f: (A) -> B): Option { - contract { callsInPlace(f, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(f, InvocationKind.AT_MOST_ONCE) + (this@Option is Some) holdsIn f + } return flatMap { a -> Some(f(a)) } } @@ -481,6 +488,8 @@ public sealed class Option { contract { callsInPlace(ifEmpty, InvocationKind.AT_MOST_ONCE) callsInPlace(ifSome, InvocationKind.AT_MOST_ONCE) + (this@Option is None) holdsIn ifEmpty + (this@Option is Some) holdsIn ifSome } return when (this) { is None -> ifEmpty() @@ -499,7 +508,10 @@ public sealed class Option { * @see map */ public inline fun flatMap(f: (A) -> Option): Option { - contract { callsInPlace(f, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(f, InvocationKind.AT_MOST_ONCE) + (this@Option is Some) holdsIn f + } return when (this) { is None -> this is Some -> f(value) @@ -513,7 +525,10 @@ public sealed class Option { * @param predicate the predicate used for testing. */ public inline fun filter(predicate: (A) -> Boolean): Option { - contract { callsInPlace(predicate, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(predicate, InvocationKind.AT_MOST_ONCE) + (this@Option is Some) holdsIn predicate + } return flatMap { a -> if (predicate(a)) Some(a) else None } } @@ -524,12 +539,18 @@ public sealed class Option { * @param predicate the predicate used for testing. */ public inline fun filterNot(predicate: (A) -> Boolean): Option { - contract { callsInPlace(predicate, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(predicate, InvocationKind.AT_MOST_ONCE) + (this@Option is Some) holdsIn predicate + } return flatMap { a -> if (!predicate(a)) Some(a) else None } } public inline fun toEither(ifEmpty: () -> L): Either { - contract { callsInPlace(ifEmpty, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(ifEmpty, InvocationKind.AT_MOST_ONCE) + (this@Option is None) holdsIn ifEmpty + } return fold({ ifEmpty().left() }, { it.right() }) } @@ -558,7 +579,10 @@ public data class Some(val value: T) : Option() { * @param default the default expression. */ public inline fun Option.getOrElse(default: () -> T): T { - contract { callsInPlace(default, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(default, InvocationKind.AT_MOST_ONCE) + (this@getOrElse is None) holdsIn default + } return when (this) { is Some -> value else -> default() @@ -594,7 +618,10 @@ public fun Option>.flatten(): Option = public fun Option>.toMap(): Map = this.toList().toMap() public inline fun Option.combine(other: Option, combine: (A, A) -> A): Option { - contract { callsInPlace(combine, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(combine, InvocationKind.AT_MOST_ONCE) + (this@combine is Some && other is Some) holdsIn combine + } return when (this) { is Some -> when (other) { is Some -> Some(combine(value, other.value)) @@ -657,7 +684,10 @@ public operator fun > Option.compareTo(other: Option): I * */ public inline fun Option.recover(recover: SingletonRaise.() -> A): Option { - contract { callsInPlace(recover, InvocationKind.AT_MOST_ONCE) } + contract { + callsInPlace(recover, InvocationKind.AT_MOST_ONCE) + (this@recover is None) holdsIn recover + } return when (this@recover) { is None -> option { recover() } is Some -> this@recover diff --git a/arrow-libs/fx/arrow-fx-coroutines/build.gradle.kts b/arrow-libs/fx/arrow-fx-coroutines/build.gradle.kts index 4471b67978..b554342af0 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/build.gradle.kts +++ b/arrow-libs/fx/arrow-fx-coroutines/build.gradle.kts @@ -3,6 +3,9 @@ plugins { } kotlin { + compilerOptions { + freeCompilerArgs.addAll("-Xallow-holdsin-contract") + } sourceSets { commonMain { dependencies { diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Race3.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Race3.kt index b5e428f557..6e8073bc67 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Race3.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Race3.kt @@ -1,4 +1,5 @@ -@file:OptIn(ExperimentalContracts::class) +@file:Suppress("API_NOT_AVAILABLE") +@file:OptIn(ExperimentalContracts::class, ExperimentalExtendedContracts::class) package arrow.fx.coroutines @@ -13,6 +14,7 @@ import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.selects.select import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.ExperimentalExtendedContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract import kotlin.coroutines.ContinuationInterceptor @@ -33,6 +35,9 @@ public sealed class Race3 { callsInPlace(ifA, InvocationKind.AT_MOST_ONCE) callsInPlace(ifB, InvocationKind.AT_MOST_ONCE) callsInPlace(ifC, InvocationKind.AT_MOST_ONCE) + (this@Race3 is First) holdsIn ifA + (this@Race3 is Second) holdsIn ifB + (this@Race3 is Third) holdsIn ifC } return when (this) { is First -> ifA(winner) diff --git a/buildSrc/src/main/kotlin/arrow.kotlin.gradle.kts b/buildSrc/src/main/kotlin/arrow.kotlin.gradle.kts index 1f81e9bb9c..11152870c1 100644 --- a/buildSrc/src/main/kotlin/arrow.kotlin.gradle.kts +++ b/buildSrc/src/main/kotlin/arrow.kotlin.gradle.kts @@ -105,6 +105,7 @@ fun KotlinCommonCompilerOptions.commonCompilerOptions() { "-Xreturn-value-checker=full", "-Xexpect-actual-classes", "-Xcontext-parameters", + "-Xallow-holdsin-contract", "-Xwarning-level=ERROR_SUPPRESSION:disabled", "-Xwarning-level=NOTHING_TO_INLINE:disabled", ) From 515ab13ca936f20e4cf4a51264de8f0e2b12f5c1 Mon Sep 17 00:00:00 2001 From: Youssef Shoaib Date: Sun, 7 Dec 2025 00:04:19 +0000 Subject: [PATCH 2/3] Add holdsIn contracts for ensure and ensureNotNull --- .../commonMain/kotlin/arrow/core/raise/Raise.kt | 5 +++++ .../arrow/core/raise/context/RaiseContext.kt | 15 +++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Raise.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Raise.kt index caf7b134af..b7b53c9bcd 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Raise.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Raise.kt @@ -13,6 +13,7 @@ import arrow.core.identity import arrow.core.nonFatalOrThrow import arrow.core.recover import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.ExperimentalExtendedContracts import kotlin.contracts.InvocationKind.AT_MOST_ONCE import kotlin.contracts.InvocationKind.EXACTLY_ONCE import kotlin.contracts.contract @@ -618,11 +619,13 @@ public inline fun catch(block: () -> A, transform: * @param raise a lambda that produces an error of type [Error] when the [condition] is false. * */ +@OptIn(ExperimentalExtendedContracts::class) @RaiseDSL public inline fun Raise.ensure(condition: Boolean, raise: () -> Error) { contract { callsInPlace(raise, AT_MOST_ONCE) returns() implies condition + !condition holdsIn raise } return if (condition) Unit else raise(raise()) } @@ -664,10 +667,12 @@ public inline fun Raise.ensure(condition: Boolean, raise: () -> E * @param raise a lambda that produces an error of type [Error] when the [value] is null. */ @RaiseDSL @IgnorableReturnValue +@OptIn(ExperimentalExtendedContracts::class) public inline fun Raise.ensureNotNull(value: B?, raise: () -> Error): B { contract { callsInPlace(raise, AT_MOST_ONCE) returns() implies (value != null) + (value == null) holdsIn raise } return value ?: raise(raise()) } diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/context/RaiseContext.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/context/RaiseContext.kt index 9320d832da..c23a01366d 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/context/RaiseContext.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/context/RaiseContext.kt @@ -16,6 +16,7 @@ import arrow.core.raise.ensure as ensureExt import arrow.core.raise.ensureNotNull as ensureNotNullExt import arrow.core.raise.withError as withErrorExt import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.ExperimentalExtendedContracts import kotlin.contracts.InvocationKind.AT_MOST_ONCE import kotlin.contracts.InvocationKind.EXACTLY_ONCE import kotlin.contracts.contract @@ -30,13 +31,23 @@ public typealias ResultRaise = arrow.core.raise.ResultRaise context(raise: Raise) @RaiseDSL public fun raise(e: Error): Nothing = raise.raise(e) +@OptIn(ExperimentalExtendedContracts::class) context(raise: Raise) @RaiseDSL public inline fun ensure(condition: Boolean, otherwise: () -> Error) { - contract { returns() implies condition } + contract { + callsInPlace(otherwise, AT_MOST_ONCE) + returns() implies condition + !condition holdsIn otherwise + } raise.ensureExt(condition, otherwise) } +@OptIn(ExperimentalExtendedContracts::class) context(raise: Raise) @RaiseDSL @IgnorableReturnValue public inline fun ensureNotNull(value: B?, otherwise: () -> Error): B { - contract { returns() implies (value != null) } + contract { + returns() implies (value != null) + callsInPlace(otherwise, AT_MOST_ONCE) + (value == null) holdsIn otherwise + } return raise.ensureNotNullExt(value, otherwise) } From f90fe99755299f7d4102ef5ab035feb4ad9baf3f Mon Sep 17 00:00:00 2001 From: Youssef Shoaib Date: Wed, 31 Dec 2025 19:53:59 +0000 Subject: [PATCH 3/3] Workaround KT-31886 --- .../src/commonMain/kotlin/arrow/core/Ior.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Ior.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Ior.kt index 339ec6d592..4921f87914 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Ior.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Ior.kt @@ -180,7 +180,7 @@ public sealed class Ior { public inline fun map(f: (B) -> D): Ior { contract { callsInPlace(f, InvocationKind.AT_MOST_ONCE) - (this@Ior is Right || this@Ior is Both) holdsIn f + (this@Ior !is Left) holdsIn f } return when (this) { is Left -> this @@ -207,7 +207,7 @@ public sealed class Ior { public inline fun mapLeft(fa: (A) -> C): Ior { contract { callsInPlace(fa, InvocationKind.AT_MOST_ONCE) - (this@Ior is Left || this@Ior is Both) holdsIn fa + (this@Ior !is Right) holdsIn fa } return when (this) { is Left -> Left(fa(value)) @@ -419,7 +419,7 @@ public inline fun Ior.flatMap(combine: (A, A) -> A, f: (B) -> Io callsInPlace(combine, InvocationKind.AT_MOST_ONCE) callsInPlace(f, InvocationKind.AT_MOST_ONCE) (this@flatMap is Both) holdsIn combine - (this@flatMap is Right || this@flatMap is Both) holdsIn f + (this@flatMap !is Left) holdsIn f } return when (this) { is Left -> this @@ -442,7 +442,7 @@ public inline fun Ior.handleErrorWith(combine: (B, B) -> B, f: ( callsInPlace(combine, InvocationKind.AT_MOST_ONCE) callsInPlace(f, InvocationKind.AT_MOST_ONCE) (this@handleErrorWith is Both) holdsIn combine - (this@handleErrorWith is Left || this@handleErrorWith is Both) holdsIn f + (this@handleErrorWith !is Right) holdsIn f } return when (this) { is Left -> f(value) @@ -478,8 +478,8 @@ public inline fun Ior.combine(other: Ior, combineA: (A, A) -> contract { callsInPlace(combineA, InvocationKind.AT_MOST_ONCE) callsInPlace(combineB, InvocationKind.AT_MOST_ONCE) - ((this@combine is Left || this@combine is Both) && (other is Left || other is Both)) holdsIn combineA - ((this@combine is Right || this@combine is Both) && (other is Right || other is Both)) holdsIn combineB + ((this@combine !is Right) && (other !is Right)) holdsIn combineA + ((this@combine !is Left) && (other !is Left)) holdsIn combineB } return when (this) { is Ior.Left -> when (other) {