import { realCause } from "@effect/core/io/Cause/definition" import { Doc } from "@effect/printer/Doc" import { FusionDepth } from "@effect/printer/Optimize" import { renderPretty } from "@effect/printer/Render" export interface Renderer { readonly lineWidth: number readonly ribbonFraction: number readonly renderError: (error: E) => string[] readonly renderUnknown: (error: unknown) => string[] } export type Segment = Sequential | Parallel | Failure export type Step = Parallel | Failure export interface Failure { readonly _tag: "Failure" readonly lines: ReadonlyArray> } export interface Parallel { readonly _tag: "Parallel" readonly all: ReadonlyArray } export interface Sequential { readonly _tag: "Sequential" readonly all: ReadonlyArray } export function Failure(lines: ReadonlyArray>): Failure { return { _tag: "Failure", lines } } export function Sequential(all: ReadonlyArray): Sequential { return { _tag: "Sequential", all } } export function Parallel(all: ReadonlyArray): Parallel { return { _tag: "Parallel", all } } interface NonEmptyReadonlyArray extends ReadonlyArray { readonly 0: A } const box = { horizontal: { light: Doc.char("─"), heavy: Doc.char("═") }, vertical: { heavy: Doc.char("║") }, branch: { right: { heavy: Doc.char("╠") }, down: { light: Doc.char("╥"), heavy: Doc.char("╦") } }, terminal: { down: { heavy: Doc.char("╗") } }, arrow: { down: Doc.char("▼") } } function isNonEmptyArray(array: ReadonlyArray): array is NonEmptyReadonlyArray { return array.length > 0 } function headTail(a: NonEmptyReadonlyArray): readonly [A, ReadonlyArray] { const x = [...a] // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const head = x.shift()! return [head, x] } function lines(s: string) { return s.split("\n").map((s) => s.replace("\r", "")) as string[] } function renderToString(u: unknown): string { if ( typeof u === "object" && u != null && "toString" in u && typeof u["toString"] === "function" && u["toString"] !== Object.prototype.toString ) { return u["toString"]() } return JSON.stringify(u, null, 2) } function times(value: A, n: number): ReadonlyArray { const array: Array = [] for (let i = 0; i < n; i = i + 1) { array.push(value) } return array } function renderFail( error: ReadonlyArray> // trace: O.Option, // traceRenderer: TraceRenderer ): Sequential { return Sequential([ Failure([ Doc.text("A checked error was not handled."), Doc.empty, ...error // ...renderTrace(trace, traceRenderer) ]) ]) } function renderDie( error: ReadonlyArray> // trace: O.Option, // traceRenderer: TraceRenderer ): Sequential { return Sequential([ Failure([ Doc.text("An unchecked error was produced."), Doc.empty, ...error // ...renderTrace(trace, traceRenderer) ]) ]) } function renderInterrupt( fiberId: FiberId // trace: O.Option, // traceRenderer: TraceRenderer ): Sequential { const ids = Array.from(fiberId.ids).map((id) => `#${id}`).join(", ") return Sequential([ Failure([ Doc.text(`An interrupt was produced by ${ids}.`) // "", // ...renderTrace(trace, traceRenderer) ]) ]) } function renderError(error: Error): string[] { return lines(error.stack ? error.stack : String(error)) } function prefixBlock( values: ReadonlyArray>, prefix1: Doc, prefix2: Doc ): ReadonlyArray> { if (isNonEmptyArray(values)) { const [head, tail] = headTail(values) const init = Doc.cat(prefix1, head) const rest = tail.map((value) => Doc.cat(prefix2, value)) return [init, ...rest] } return [] } function format(segment: Segment): ReadonlyArray> { switch (segment._tag) { case "Failure": { return prefixBlock(segment.lines, box.horizontal.light, Doc.char(" ")) } case "Parallel": { const spaces = Doc.spaces(2) const horizontalLines = Doc.cat(box.horizontal.heavy, box.horizontal.heavy) const verticalSeparator = Doc.cat(spaces, box.vertical.heavy) const junction = Doc.cat(horizontalLines, box.branch.down.heavy) const busTerminal = Doc.cat(horizontalLines, box.terminal.down.heavy) const fiberBus = Doc.hcat([...times(junction, segment.all.length - 1), busTerminal]) const segments = segment.all.reduceRight( (acc, curr) => [ ...prefixBlock(acc, verticalSeparator, verticalSeparator), ...prefixBlock(format(curr), spaces, spaces) ], [] as ReadonlyArray> ) return [fiberBus, ...segments] } case "Sequential": { return segment.all.flatMap((step) => [ box.vertical.heavy, ...prefixBlock(format(step), box.branch.right.heavy, box.vertical.heavy), box.arrow.down ]) } } } function linearSegments( cause: Cause, renderer: Renderer ): Eval> { realCause(cause) switch (cause._tag) { case "Then": { return linearSegments(cause.left, renderer).zipWith( linearSegments(cause.right, renderer), (left, right) => [...left, ...right] ) } default: { return causeToSequential(cause, renderer).map((sequential) => sequential.all) } } } function parallelSegments( cause: Cause, renderer: Renderer ): Eval> { realCause(cause) switch (cause._tag) { case "Both": { return parallelSegments(cause.left, renderer).zipWith( parallelSegments(cause.right, renderer), (left, right) => [...left, ...right] ) } default: { return causeToSequential(cause, renderer).map((sequential) => [sequential]) } } } function causeToSequential( cause: Cause, renderer: Renderer ): Eval { realCause(cause) switch (cause._tag) { case "Empty": { return Eval.succeed(Sequential([])) } case "Fail": { return Eval.succeed( renderFail(renderer.renderError(cause.value).map((line) => Doc.text(line))) ) } case "Die": { return Eval.succeed( renderDie(renderer.renderUnknown(cause.value).map((line) => Doc.text(line))) ) } case "Interrupt": { return Eval.succeed( renderInterrupt(cause.fiberId) ) } case "Then": { return linearSegments(cause, renderer) .map((segments) => Sequential(segments)) } case "Both": { return parallelSegments(cause, renderer) .map((segments) => Sequential([Parallel(segments)])) } case "Stackless": { // TODO: determine if this is correct for `Stackless` cause return Eval.suspend(causeToSequential(cause.cause, renderer)) } } } function defaultErrorToLines(error: unknown) { return error instanceof Error ? renderError(error) : lines(renderToString(error)) } export const defaultRenderer: Renderer = { lineWidth: 80, ribbonFraction: 1, renderError: defaultErrorToLines, renderUnknown: defaultErrorToLines } function prettyDocuments( cause: Cause, renderer: Renderer ): Eval>> { return causeToSequential(cause, renderer).map((sequential) => { if ( sequential.all.length === 1 && sequential.all[0] && sequential.all[0]._tag === "Failure" ) { return sequential.all[0].lines } const documents = format(sequential) return documents.length > 0 ? [box.branch.down.light, ...documents] : documents }) } function prettySafe( cause: Cause, renderer: Renderer ): Eval { return prettyDocuments(cause, renderer).map((docs) => { const document = Doc.cat( Doc.lineBreak, Doc.concatWith(docs, (x, y) => x.appendWithLineBreak(y)) ).optimize(FusionDepth.Deep) return renderPretty(renderer.lineWidth, renderer.ribbonFraction)(document) }) } /** * Returns a `String` with the cause pretty-printed. * * @tsplus static effect/core/io/Cause.Aspects pretty * @tsplus pipeable effect/core/io/Cause pretty */ export function pretty(renderer: Renderer = defaultRenderer) { return (self: Cause): string => prettySafe(self, renderer).run }