UNPKG

@effect-ts/system

Version:

Effect-TS is a zero dependency set of libraries to write highly productive, purely functional TypeScript at scale.

702 lines (634 loc) 18.8 kB
// ets_tracing: off import * as HS from "../Collections/Immutable/HashSet/index.js" import * as L from "../Collections/Immutable/List/core.js" import * as Tp from "../Collections/Immutable/Tuple/index.js" import type { FiberID } from "../Fiber/id.js" import { equalsFiberID } from "../Fiber/id.js" import type { Trace } from "../Fiber/tracing.js" import { tuple } from "../Function/index.js" import * as IO from "../IO/index.js" import * as O from "../Option/index.js" import { Stack } from "../Stack/index.js" import * as St from "../Structural/index.js" import type { HasUnify } from "../Utils/index.js" /** * Cause is a Free Semiring structure that allows tracking of multiple error causes. */ export type Cause<E> = Empty | Fail<E> | Die | Interrupt | Then<E> | Both<E> | Traced<E> export const CauseSym = Symbol() export function isCause(self: unknown): self is Cause<unknown> { return typeof self === "object" && self != null && CauseSym in self } const _emptyHash = St.opt(St.randomInt()) export interface Empty extends HasUnify {} export class Empty implements St.HasEquals, St.HasHash { readonly _tag = "Empty"; readonly [CauseSym]: typeof CauseSym = CauseSym; [St.equalsSym](that: unknown): boolean { return isCause(that) && IO.run(this.equalsSafe(that)) } get [St.hashSym](): number { return _emptyHash } equalsSafe(that: Cause<unknown>): IO.IO<boolean> { // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this return IO.gen(function* (_) { if (that._tag === "Empty") { return true } else if (that._tag === "Then") { return ( (yield* _(self.equalsSafe(that.left))) && (yield* _(self.equalsSafe(that.right))) ) } else if (that._tag === "Both") { return ( (yield* _(self.equalsSafe(that.left))) && (yield* _(self.equalsSafe(that.right))) ) } else { return false } }) } } export const empty: Cause<never> = new Empty() export interface Fail<E> extends HasUnify {} export class Fail<E> implements St.HasEquals, St.HasHash { readonly _tag = "Fail"; readonly [CauseSym]: typeof CauseSym = CauseSym constructor(readonly value: E) {} [St.equalsSym](that: unknown): boolean { return isCause(that) && IO.run(this.equalsSafe(that)) } get [St.hashSym](): number { return St.combineHash(St.hashString(this._tag), St.hash(this.value)) } equalsSafe(that: Cause<unknown>): IO.IO<boolean> { // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this return IO.gen(function* (_) { switch (that._tag) { case "Fail": { return St.equals(self.value, that.value) } case "Then": { return yield* _(sym(zero)(self, that)) } case "Both": { return yield* _(sym(zero)(self, that)) } case "Traced": { return yield* _(self.equalsSafe(that.cause)) } } return false }) } } export interface Die extends HasUnify {} export class Die implements St.HasEquals, St.HasHash, HasUnify { readonly _tag = "Die"; readonly [CauseSym]: typeof CauseSym = CauseSym constructor(readonly value: unknown) {} [St.equalsSym](that: unknown): boolean { return isCause(that) && IO.run(this.equalsSafe(that)) } get [St.hashSym](): number { return St.combineHash(St.hashString(this._tag), St.hash(this.value)) } equalsSafe(that: Cause<unknown>): IO.IO<boolean> { // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this return IO.gen(function* (_) { switch (that._tag) { case "Die": { return St.equals(self.value, that.value) } case "Then": { return yield* _(sym(zero)(self, that)) } case "Both": { return yield* _(sym(zero)(self, that)) } case "Traced": { return yield* _(self.equalsSafe(that.cause)) } } return false }) } } export interface Interrupt extends HasUnify {} export class Interrupt implements St.HasEquals, St.HasHash, HasUnify { readonly _tag = "Interrupt"; readonly [CauseSym]: typeof CauseSym = CauseSym constructor(readonly fiberId: FiberID) {} [St.equalsSym](that: unknown): boolean { return isCause(that) && IO.run(this.equalsSafe(that)) } get [St.hashSym](): number { return St.combineHash(St.hashString(this._tag), St.hash(this.fiberId)) } equalsSafe(that: Cause<unknown>): IO.IO<boolean> { // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this return IO.gen(function* (_) { switch (that._tag) { case "Interrupt": { return equalsFiberID(self.fiberId, that.fiberId) } case "Then": { return yield* _(sym(zero)(self, that)) } case "Both": { return yield* _(sym(zero)(self, that)) } case "Traced": { return yield* _(self.equalsSafe(that.cause)) } } return false }) } } export interface Traced<E> extends HasUnify {} export class Traced<E> implements St.HasEquals, St.HasHash { readonly _tag = "Traced"; readonly [CauseSym]: typeof CauseSym = CauseSym constructor(readonly cause: Cause<E>, readonly trace: Trace) {} [St.equalsSym](that: unknown): boolean { return isCause(that) && IO.run(this.equalsSafe(that)) } get [St.hashSym](): number { return this.cause[St.hashSym] } equalsSafe(that: Cause<unknown>): IO.IO<boolean> { // eslint-disable-next-line @typescript-eslint/no-this-alias const self: Traced<E> = this return IO.gen(function* (_) { if (that._tag === "Traced") { return yield* _(self.cause.equalsSafe(that.cause)) } return yield* _(self.cause.equalsSafe(that)) }) } } export interface Then<E> extends HasUnify {} export class Then<E> implements St.HasEquals, St.HasHash { readonly _tag = "Then"; readonly [CauseSym]: typeof CauseSym = CauseSym constructor(readonly left: Cause<E>, readonly right: Cause<E>) {} [St.equalsSym](that: unknown): boolean { return isCause(that) && IO.run(this.equalsSafe(that)) } get [St.hashSym](): number { return hashCode(this) } equalsSafe(that: Cause<unknown>): IO.IO<boolean> { // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this return IO.gen(function* (_) { switch (that._tag) { case "Traced": { 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<unknown>): IO.IO<boolean> { // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this if (that._tag === "Then") { return IO.gen(function* (_) { return ( (yield* _(self.left.equalsSafe(that.left))) && (yield* _(self.right.equalsSafe(that.right))) ) }) } return IO.succeed(false) } } export interface Both<E> extends HasUnify {} export class Both<E> implements St.HasEquals, St.HasHash { readonly _tag = "Both"; readonly [CauseSym]: typeof CauseSym = CauseSym constructor(readonly left: Cause<E>, readonly right: Cause<E>) {} [St.equalsSym](that: unknown): boolean { return isCause(that) && IO.run(this.equalsSafe(that)) } get [St.hashSym](): number { return hashCode(this) } equalsSafe(that: Cause<unknown>): IO.IO<boolean> { // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this return IO.gen(function* (_) { switch (that._tag) { case "Traced": { 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<unknown>): IO.IO<boolean> { // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this if (that._tag === "Both") { return IO.gen(function* (_) { return ( (yield* _(self.left.equalsSafe(that.left))) && (yield* _(self.right.equalsSafe(that.right))) ) }) } return IO.succeed(false) } } export function fail<E>(value: E): Cause<E> { return new Fail(value) } export function traced<E>(cause: Cause<E>, trace: Trace): Cause<E> { if ( L.isEmpty(trace.executionTrace) && L.isEmpty(trace.stackTrace) && O.isNone(trace.parentTrace) ) { return cause } return new Traced(cause, trace) } export function die(value: unknown): Cause<never> { return new Die(value) } export function interrupt(fiberId: FiberID): Cause<never> { return new Interrupt(fiberId) } export function combineSeq<E1, E2>(left: Cause<E1>, right: Cause<E2>): Cause<E1 | E2> { return isEmpty(left) ? right : isEmpty(right) ? left : new Then<E1 | E2>(left, right) } export function combinePar<E1, E2>(left: Cause<E1>, right: Cause<E2>): Cause<E1 | E2> { return isEmpty(left) ? right : isEmpty(right) ? left : new Both<E1 | E2>(left, right) } /** * Determines if the `Cause` is empty. */ export function isEmpty<E>(cause: Cause<E>) { if ( cause._tag === "Empty" || (cause._tag === "Traced" && cause.cause._tag === "Empty") ) { return true } let causes: Stack<Cause<E>> | undefined = undefined let current: Cause<E> | 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) current = current.left break } case "Both": { causes = new Stack(current.right, causes) current = current.left break } case "Traced": { current = current.cause break } default: { current = undefined } } if (!current && causes) { current = causes.value causes = causes.previous } } return true } function associativeThen<A>(self: Cause<A>, that: Cause<A>): IO.IO<boolean> { return IO.gen(function* (_) { if ( self._tag === "Then" && self.left._tag === "Then" && that._tag === "Then" && that.right._tag === "Then" ) { 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 return ( (yield* _(al.equalsSafe(ar))) && (yield* _(bl.equalsSafe(br))) && (yield* _(cl.equalsSafe(cr))) ) } return false }) } function distributiveThen<A>(self: Cause<A>, that: Cause<A>): IO.IO<boolean> { return IO.gen(function* (_) { if ( self._tag === "Then" && self.right._tag === "Both" && that._tag === "Both" && that.left._tag === "Then" && that.right._tag === "Then" ) { 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 if ( (yield* _(ar1.equalsSafe(ar2))) && (yield* _(al.equalsSafe(ar1))) && (yield* _(bl.equalsSafe(br))) && (yield* _(cl.equalsSafe(cr))) ) { return true } } if ( self._tag === "Then" && self.left._tag === "Both" && that._tag === "Both" && that.left._tag === "Then" && that.right._tag === "Then" ) { 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 if ( (yield* _(cr1.equalsSafe(cr2))) && (yield* _(al.equalsSafe(ar))) && (yield* _(bl.equalsSafe(br))) && (yield* _(cl.equalsSafe(cr1))) ) { return true } } return false }) } function associativeBoth<A>(self: Cause<A>, that: Cause<A>): IO.IO<boolean> { return IO.gen(function* (_) { if ( self._tag === "Both" && self.left._tag === "Both" && that._tag === "Both" && that.right._tag === "Both" ) { 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 return ( (yield* _(al.equalsSafe(ar))) && (yield* _(bl.equalsSafe(br))) && (yield* _(cl.equalsSafe(cr))) ) } return false }) } function distributiveBoth<A>(self: Cause<A>, that: Cause<A>): IO.IO<boolean> { return IO.gen(function* (_) { if ( self._tag === "Both" && self.left._tag === "Then" && self.right._tag === "Then" && that._tag === "Then" && that.right._tag === "Both" ) { 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 if ( (yield* _(al1.equalsSafe(al2))) && (yield* _(al1.equalsSafe(ar))) && (yield* _(bl.equalsSafe(br))) && (yield* _(cl.equalsSafe(cr))) ) { return true } } if ( self._tag === "Both" && self.left._tag === "Then" && self.right._tag === "Then" && that._tag === "Then" && that.left._tag === "Both" ) { 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 if ( (yield* _(cl1.equalsSafe(cl2))) && (yield* _(al.equalsSafe(ar))) && (yield* _(bl.equalsSafe(br))) && (yield* _(cl1.equalsSafe(cr))) ) { return true } } return false }) } function commutativeBoth<A>(self: Both<A>, that: Cause<A>): IO.IO<boolean> { return IO.gen(function* (_) { if (that._tag === "Both") { return ( (yield* _(self.left.equalsSafe(that.right))) && (yield* _(self.right.equalsSafe(that.left))) ) } return false }) } function zero<A>(self: Cause<A>, that: Cause<A>) { if (self._tag === "Then" && self.right._tag === "Empty") { return self.left.equalsSafe(that) } if (self._tag === "Then" && self.left._tag === "Empty") { return self.right.equalsSafe(that) } if (self._tag === "Both" && self.right._tag === "Empty") { return self.left.equalsSafe(that) } if (self._tag === "Both" && self.left._tag === "Empty") { return self.right.equalsSafe(that) } return IO.succeed(false) } function sym<A>( f: (a: Cause<A>, b: Cause<A>) => IO.IO<boolean> ): (a: Cause<A>, b: Cause<A>) => IO.IO<boolean> { return (l, r) => IO.gen(function* (_) { return (yield* _(f(l, r))) || (yield* _(f(r, l))) }) } export function equals<A>(self: Cause<A>, that: Cause<A>): boolean { return IO.run(self.equalsSafe(that)) } function stepLoop<A>( cause: Cause<A>, stack: L.List<Cause<A>>, parallel: HS.HashSet<Cause<A>>, sequential: L.List<Cause<A>> ): Tp.Tuple<[HS.HashSet<Cause<A>>, L.List<Cause<A>>]> { // eslint-disable-next-line no-constant-condition while (1) { switch (cause._tag) { case "Empty": { if (L.isEmpty(stack)) { return Tp.tuple(parallel, sequential) } else { cause = L.unsafeFirst(stack)! stack = L.tail(stack) } break } case "Traced": { cause = cause.cause break } case "Both": { stack = L.prepend_(stack, cause.right) cause = cause.left break } case "Then": { const left = cause.left const right = cause.right switch (left._tag) { case "Traced": { cause = combineSeq(left.cause, right) break } case "Empty": { cause = cause.right break } case "Then": { cause = combineSeq(left.left, combineSeq(left.right, right)) break } case "Both": { cause = combinePar( combineSeq(left.left, right), combineSeq(left.right, right) ) break } default: { cause = left sequential = L.prepend_(sequential, right) } } break } default: { if (L.isEmpty(stack)) { return Tp.tuple(HS.add_(parallel, cause), sequential) } else { parallel = HS.add_(parallel, cause) cause = L.unsafeFirst(stack)! stack = L.tail(stack) break } } } } throw new Error("Bug") } function step<A>(self: Cause<A>): Tp.Tuple<[HS.HashSet<Cause<A>>, L.List<Cause<A>>]> { return stepLoop(self, L.empty(), HS.make(), L.empty()) } function flattenLoop<A>( causes: L.List<Cause<A>>, flattened: L.List<HS.HashSet<Cause<A>>> ): L.List<HS.HashSet<Cause<A>>> { // eslint-disable-next-line no-constant-condition while (1) { const [parallel, sequential] = L.reduce_( causes, tuple(HS.make<Cause<A>>(), L.empty<Cause<A>>()), ([parallel, sequential], cause) => { const { tuple: [set, seq] } = step(cause) return tuple(HS.union_(parallel, set), L.concat_(sequential, seq)) } ) const updated = HS.size(parallel) > 0 ? L.prepend_(flattened, parallel) : flattened if (L.isEmpty(sequential)) { return L.reverse(updated) } else { causes = sequential flattened = updated } } throw new Error("Bug") } function flatten<A>(self: Cause<A>) { return flattenLoop(L.of(self), L.empty()) } function hashCode<A>(self: Cause<A>) { const flat = flatten(self) const size = L.size(flat) let head if (size === 0) { return _emptyHash } else if (size === 1 && (head = L.unsafeFirst(flat)!) && HS.size(head) === 1) { return L.unsafeFirst(L.from(head))![St.hashSym] } else { return St.hashIterator(flat[Symbol.iterator]()) } }