// TODO: // - [ ] squashTrace // - [ ] squashTraceWith export const CauseSym = Symbol.for("@effect/core/io/Cause") export type CauseSym = typeof CauseSym export const _E = Symbol.for("@effect/core/io/Cause/E") export type _E = typeof _E /** * @tsplus type effect/core/io/Cause */ export interface Cause extends Equals { readonly [CauseSym]: CauseSym readonly [_E]: () => E } /** * @tsplus type effect/core/io/Cause.Ops */ export interface CauseOps {} export const Cause: CauseOps = {} /** * @tsplus type effect/core/io/Cause.Aspects */ export interface CauseAspects {} /** * @tsplus unify effect/core/io/Cause */ export function unifyCause>( self: X ): Cause<[X] extends [{ [_E]: () => infer E }] ? E : never> { return self } export type RealCause = | Empty | Fail | Die | Interrupt | Stackless | Then | Both /** * @tsplus macro remove */ export function realCause(cause: Cause): asserts cause is RealCause { // } /** * @tsplus fluent effect/core/io/Cause isEmptyType */ export function isEmptyType(cause: Cause): cause is Empty { realCause(cause) return cause._tag === "Empty" } /** * @tsplus fluent effect/core/io/Cause isDieType */ export function isDieType(cause: Cause): cause is Die { realCause(cause) return cause._tag === "Die" } /** * @tsplus fluent effect/core/io/Cause isFailType */ export function isFailType(cause: Cause): cause is Fail { realCause(cause) return cause._tag === "Fail" } /** * @tsplus fluent effect/core/io/Cause isInterruptType */ export function isInterruptType(cause: Cause): cause is Interrupt { realCause(cause) return cause._tag === "Interrupt" } /** * @tsplus fluent effect/core/io/Cause isStacklessType */ export function isStacklessType(cause: Cause): cause is Stackless { realCause(cause) return cause._tag === "Stackless" } /** * @tsplus fluent effect/core/io/Cause isThenType */ export function isThenType(cause: Cause): cause is Then { realCause(cause) return cause._tag === "Then" } /** * @tsplus fluent effect/core/io/Cause isBothType */ export function isBothType(cause: Cause): cause is Both { realCause(cause) return cause._tag === "Both" } export class Empty implements Cause, Equals { readonly _tag = "Empty" readonly [CauseSym]: CauseSym = CauseSym readonly [_E]!: () => never; [Hash.sym](): number { return _emptyHash } [Equals.sym](that: unknown): boolean { return isCause(that) && this.__equalsSafe(that).run } __equalsSafe(that: Cause): Eval { realCause(that) switch (that._tag) { case "Empty": { return Eval.succeed(true) } case "Both": case "Then": { return Eval.suspend( this.__equalsSafe(that.left) ).zipWith( Eval.suspend(this.__equalsSafe(that.right)), (a, b) => a && b ) } case "Stackless": { return Eval.suspend(this.__equalsSafe(that.cause)) } default: { return Eval.succeed(false) } } } } export interface Fail extends Cause {} export class Fail implements Cause, Equals { readonly _tag = "Fail" readonly [CauseSym]: CauseSym = CauseSym readonly [_E]!: () => E constructor(readonly value: E) {} [Hash.sym](): number { return Hash.combine(Hash.string(this._tag), Hash.unknown(this.value)) } [Equals.sym](that: unknown): boolean { return isCause(that) && this.__equalsSafe(that).run } __equalsSafe(that: Cause): Eval { realCause(that) switch (that._tag) { case "Fail": { return Eval.succeed(Equals.equals(this.value, that.value)) } case "Both": case "Then": { return Eval.suspend(sym(zero)(this, that)) } case "Stackless": { return Eval.suspend(this.__equalsSafe(that.cause)) } default: { return Eval.succeed(false) } } } } export class Die implements Cause, Equals { readonly _tag = "Die" readonly [CauseSym]: CauseSym = CauseSym readonly [_E]!: () => never constructor(readonly value: unknown) {} [Hash.sym](): number { return Hash.combine(Hash.string(this._tag), Hash.unknown(this.value)) } [Equals.sym](that: unknown): boolean { return isCause(that) && this.__equalsSafe(that).run } __equalsSafe(that: Cause): Eval { realCause(that) switch (that._tag) { case "Die": { return Eval.succeed(Equals.equals(this.value, that.value)) } case "Both": case "Then": { return Eval.suspend(sym(zero)(this, that)) } case "Stackless": { return Eval.suspend(this.__equalsSafe(that.cause)) } default: { return Eval.succeed(false) } } } } export class Interrupt implements Cause, Equals { readonly _tag = "Interrupt" readonly [CauseSym]: CauseSym = CauseSym readonly [_E]!: () => never constructor(readonly fiberId: FiberId) {} [Hash.sym](): number { return Hash.combine(Hash.string(this._tag), Hash.unknown(this.fiberId)) } [Equals.sym](that: unknown): boolean { return isCause(that) && this.__equalsSafe(that).run } __equalsSafe(that: Cause): Eval { realCause(that) switch (that._tag) { case "Interrupt": { return Eval.succeed(Equals.equals(this.fiberId, that.fiberId)) } case "Both": case "Then": { return Eval.suspend(sym(zero)(this, that)) } case "Stackless": { return Eval.suspend(this.__equalsSafe(that.cause)) } default: { return Eval.succeed(false) } } } } export class Stackless implements Cause, Equals { readonly _tag = "Stackless" readonly [CauseSym]: CauseSym = CauseSym readonly [_E]!: () => E constructor(readonly cause: Cause, readonly stackless: boolean) {} [Hash.sym](): number { return this.cause[Hash.sym]() } [Equals.sym](that: unknown): boolean { return isCause(that) && this.__equalsSafe(that).run } __equalsSafe(that: Cause): Eval { realCause(this.cause) realCause(that) return that._tag === "Stackless" ? this.cause.__equalsSafe(that.cause) : this.cause.__equalsSafe(that) } } export class Then implements Cause, Equals { readonly _tag = "Then" readonly [CauseSym]: CauseSym = CauseSym readonly [_E]!: () => E constructor(readonly left: Cause, readonly right: Cause) {} [Hash.sym](): number { return hashCode(this) } [Equals.sym](that: unknown): boolean { return isCause(that) && this.__equalsSafe(that).run } __equalsSafe(that: Cause): Eval { // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this return Eval.gen(function*(_) { realCause(that) if (that._tag === "Stackless") { return yield* _(self.__equalsSafe(that.cause)) } return ( (yield* _(self.eq(that))) || (yield* _(sym(associativeThen)(self, that))) || (yield* _(sym(distributiveThen)(self, that))) || (yield* _(sym(zero)(self, that))) ) }) } private eq(that: Cause): Eval { // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this realCause(that) if (that._tag === "Then") { return Eval.gen(function*(_) { realCause(self.left) realCause(self.right) return ( (yield* _(self.left.__equalsSafe(that.left))) && (yield* _(self.right.__equalsSafe(that.right))) ) }) } return Eval.succeed(false) } } export class Both implements Cause, Equals { readonly _tag = "Both" readonly [CauseSym]: CauseSym = CauseSym readonly [_E]!: () => E constructor(readonly left: Cause, readonly right: Cause) {} [Hash.sym](): number { return hashCode(this) } [Equals.sym](that: unknown): boolean { return isCause(that) && this.__equalsSafe(that).run } __equalsSafe(that: Cause): Eval { // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this return Eval.gen(function*(_) { realCause(that) if (that._tag === "Stackless") { return yield* _(self.__equalsSafe(that.cause)) } return ( (yield* _(self.eq(that))) || (yield* _(sym(associativeBoth)(self, that))) || (yield* _(sym(distributiveBoth)(self, that))) || (yield* _(commutativeBoth(self, that))) || (yield* _(sym(zero)(self, that))) ) }) } private eq(that: Cause): Eval { // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this realCause(that) if (that._tag === "Both") { return Eval.gen(function*(_) { realCause(self.left) realCause(self.right) return ( (yield* _(self.left.__equalsSafe(that.left))) && (yield* _(self.right.__equalsSafe(that.right))) ) }) } return Eval.succeed(false) } } // ----------------------------------------------------------------------------- // Constructors // ----------------------------------------------------------------------------- /** * @tsplus static effect/core/io/Cause.Ops empty */ export const empty: Cause = new Empty() /** * @tsplus static effect/core/io/Cause.Ops die */ export function die(defect: unknown): Cause { return new Die(defect) } /** * @tsplus static effect/core/io/Cause.Ops fail */ export function fail(error: E): Cause { return new Fail(error) } /** * @tsplus static effect/core/io/Cause.Ops interrupt */ export function interrupt(fiberId: FiberId): Cause { return new Interrupt(fiberId) } /** * @tsplus static effect/core/io/Cause.Ops stack */ export function stack(cause: Cause): Cause { return new Stackless(cause, false) } /** * @tsplus static effect/core/io/Cause.Ops stackless */ export function stackless(cause: Cause): Cause { return new Stackless(cause, true) } /** * @tsplus operator effect/core/io/Cause + * @tsplus static effect/core/io/Cause.Ops then */ export function combineSeq(left: Cause, right: Cause): Cause { return isEmpty(left) ? right : isEmpty(right) ? left : left.equals(right) ? left : new Then(left, right) } /** * @tsplus operator effect/core/io/Cause & * @tsplus static effect/core/io/Cause.Ops both */ export function combinePar(left: Cause, right: Cause): Cause { // TODO(Mike/Max): discuss this, because ZIO does not flatten empty causes here return isEmpty(left) ? right : isEmpty(right) ? left : new Both(left, right) } // ----------------------------------------------------------------------------- // Utilities // ----------------------------------------------------------------------------- /** * Determines if the provided value is a `Cause`. * * @tsplus fluent effect/core/io/Cause isCause */ export function isCause(self: unknown): self is Cause { return typeof self === "object" && self != null && CauseSym in self } /** * Determines if the `Cause` is empty. * * @tsplus getter effect/core/io/Cause isEmpty */ export function isEmpty(cause: Cause): boolean { if (isEmptyType(cause) || (isStacklessType(cause) && isEmptyType(cause.cause))) { return true } let causes: Stack> | undefined = undefined realCause(cause) let current: RealCause | undefined = cause while (current) { switch (current._tag) { case "Die": return false case "Fail": return false case "Interrupt": return false case "Then": { causes = new Stack(current.right, causes) realCause(current.left) current = current.left break } case "Both": { causes = new Stack(current.right, causes) realCause(current.left) current = current.left break } case "Stackless": { realCause(current.cause) current = current.cause break } default: { current = undefined } } if (!current && causes) { realCause(causes.value) current = causes.value causes = causes.previous } } return true } const _emptyHash = Hash.optimize(Hash.random()) function stepLoop( cause: Cause, stack: List>, parallel: HashSet>, sequential: List> ): readonly [HashSet>, List>] { // eslint-disable-next-line no-constant-condition while (1) { realCause(cause) switch (cause._tag) { case "Empty": { if (stack.length === 0) { return [parallel, sequential] as const } else { cause = stack.unsafeHead! const tail = stack.unsafeTail stack = tail == null ? List.nil() : tail } break } case "Then": { const left = cause.left const right = cause.right realCause(left) switch (left._tag) { case "Empty": { cause = cause.right break } case "Then": { cause = new Then(left.left, new Then(left.right, right)) break } case "Both": { cause = new Both(new Then(left.left, right), new Then(left.right, right)) break } case "Stackless": { cause = new Then(left.cause, right) break } default: { cause = left sequential = sequential.prepend(right) } } break } case "Both": { stack = stack.prepend(cause.right) cause = cause.left break } case "Stackless": { cause = cause.cause break } default: { if (stack.length === 0) { return [parallel.add(cause), sequential] as const } else { parallel = parallel.add(cause) cause = stack.unsafeHead! const tail = stack.unsafeTail stack = tail == null ? List.nil() : tail break } } } } throw new Error("Bug") } /** * Takes one step in evaluating a cause, returning a set of causes that fail * in parallel and a list of causes that fail sequentially after those causes. */ function step(self: Cause): readonly [HashSet>, List>] { return stepLoop(self, List.empty(), HashSet(), List.empty()) } function flattenCauseLoop( causes: List>, flattened: List>> ): List>> { // eslint-disable-next-line no-constant-condition while (1) { const [parallel, sequential] = causes.reduce( [HashSet.empty>(), List.empty>()] as const, ([parallel, sequential], cause) => { const [set, seq] = step(cause) return [parallel.union(set), sequential + seq] } ) const updated = parallel.size > 0 ? flattened.prepend(parallel) : flattened if (sequential.length === 0) { return updated.reverse } else { causes = sequential flattened = updated } } throw new Error("Bug") } /** * Flattens a `Cause` to a sequence of sets of causes, where each set represents * causes that fail in parallel and sequential sets represent causes that fail * after each other. */ function flattenCause(self: Cause): List>> { return flattenCauseLoop(List(self), List.empty()) } function hashCode(self: Cause): number { const flat = flattenCause(self) const size = flat.length let head if (size === 0) { return _emptyHash } else if (size === 1 && (head = flat.unsafeHead!) && head.size === 1) { return List.from(head).unsafeHead![Hash.sym]() } else { return flat[Hash.sym]() } } function sym( f: (a: Cause, b: Cause) => Eval ): (a: Cause, b: Cause) => Eval { return (l, r) => f(l, r).zipWith(f(r, l), (a, b) => a || b) } function zero(self: Cause, that: Cause): Eval { if (isThenType(self) && isEmptyType(self.right)) { realCause(self.left) return self.left.__equalsSafe(that) } if (isThenType(self) && isEmptyType(self.left)) { realCause(self.right) return self.right.__equalsSafe(that) } if (isBothType(self) && isEmptyType(self.right)) { realCause(self.left) return self.left.__equalsSafe(that) } if (isBothType(self) && isEmptyType(self.left)) { realCause(self.right) return self.right.__equalsSafe(that) } return Eval.succeedNow(false) } function associativeThen(self: Cause, that: Cause): Eval { return Eval.gen(function*(_) { if ( isThenType(self) && isThenType(self.left) && isThenType(that) && isThenType(that.right) ) { const al = self.left.left const bl = self.left.right const cl = self.right const ar = that.left const br = that.right.left const cr = that.right.right realCause(al) realCause(bl) realCause(cl) return ( (yield* _(al.__equalsSafe(ar))) && (yield* _(bl.__equalsSafe(br))) && (yield* _(cl.__equalsSafe(cr))) ) } return false }) } function distributiveThen(self: Cause, that: Cause): Eval { return Eval.gen(function*(_) { if ( isThenType(self) && isBothType(self.right) && isBothType(that) && isThenType(that.left) && isThenType(that.right) ) { const al = self.left const bl = self.right.left const cl = self.right.right const ar1 = that.left.left const br = that.left.right const ar2 = that.right.left const cr = that.right.right realCause(ar1) realCause(al) realCause(bl) realCause(cl) if ( (yield* _(ar1.__equalsSafe(ar2))) && (yield* _(al.__equalsSafe(ar1))) && (yield* _(bl.__equalsSafe(br))) && (yield* _(cl.__equalsSafe(cr))) ) { return true } } if ( isThenType(self) && isBothType(self.left) && isBothType(that) && isThenType(that.left) && isThenType(that.right) ) { const al = self.left.left const bl = self.left.right const cl = self.right const ar = that.left.left const cr1 = that.left.right const br = that.right.left const cr2 = that.right.right realCause(cr1) realCause(al) realCause(bl) realCause(cl) if ( (yield* _(cr1.__equalsSafe(cr2))) && (yield* _(al.__equalsSafe(ar))) && (yield* _(bl.__equalsSafe(br))) && (yield* _(cl.__equalsSafe(cr1))) ) { return true } } return false }) } function associativeBoth(self: Cause, that: Cause): Eval { return Eval.gen(function*(_) { if ( isBothType(self) && isBothType(self.left) && isBothType(that) && isBothType(that.right) ) { const al = self.left.left const bl = self.left.right const cl = self.right const ar = that.left const br = that.right.left const cr = that.right.right realCause(al) realCause(bl) realCause(cl) return ( (yield* _(al.__equalsSafe(ar))) && (yield* _(bl.__equalsSafe(br))) && (yield* _(cl.__equalsSafe(cr))) ) } return false }) } function distributiveBoth(self: Cause, that: Cause): Eval { return Eval.gen(function*(_) { if ( isBothType(self) && isThenType(self.left) && isThenType(self.right) && isThenType(that) && isBothType(that.right) ) { const al1 = self.left.left const bl = self.left.right const al2 = self.right.left const cl = self.right.right const ar = that.left const br = that.right.left const cr = that.right.right realCause(al1) realCause(bl) realCause(cl) if ( (yield* _(al1.__equalsSafe(al2))) && (yield* _(al1.__equalsSafe(ar))) && (yield* _(bl.__equalsSafe(br))) && (yield* _(cl.__equalsSafe(cr))) ) { return true } } if ( isBothType(self) && isThenType(self.left) && isThenType(self.right) && isThenType(that) && isBothType(that.left) ) { const al = self.left.left const cl1 = self.left.right const bl = self.right.left const cl2 = self.right.right const ar = that.left.left const br = that.left.right const cr = that.right realCause(cl1) realCause(al) realCause(bl) if ( (yield* _(cl1.__equalsSafe(cl2))) && (yield* _(al.__equalsSafe(ar))) && (yield* _(bl.__equalsSafe(br))) && (yield* _(cl1.__equalsSafe(cr))) ) { return true } } return false }) } function commutativeBoth(self: Both, that: Cause): Eval { return Eval.gen(function*(_) { if (isBothType(that)) { realCause(self.left) realCause(self.right) return ( (yield* _(self.left.__equalsSafe(that.right))) && (yield* _(self.right.__equalsSafe(that.left))) ) } return false }) }