Skip to content

Commit

Permalink
Carry both call-site and definition site in Effect.fn, auto-trace to …
Browse files Browse the repository at this point in the history
…anon (#4155)

Co-authored-by: Tim <hello@timsmart.co>
  • Loading branch information
mikearnaldi and tim-smart committed Dec 22, 2024
1 parent 2d79805 commit cc9c8c0
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 43 deletions.
5 changes: 5 additions & 0 deletions .changeset/quiet-kings-impress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"effect": minor
---

add Effect.fnUntraced - an untraced version of Effect.fn
5 changes: 5 additions & 0 deletions .changeset/weak-pears-remember.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"effect": patch
---

Carry both call-site and definition site in Effect.fn, auto-trace to anon
148 changes: 113 additions & 35 deletions packages/effect/src/Effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import * as layer from "./internal/layer.js"
import * as query from "./internal/query.js"
import * as _runtime from "./internal/runtime.js"
import * as _schedule from "./internal/schedule.js"
import * as internalTracer from "./internal/tracer.js"
import type * as Layer from "./Layer.js"
import type { LogLevel } from "./LogLevel.js"
import type * as ManagedRuntime from "./ManagedRuntime.js"
Expand All @@ -60,7 +61,7 @@ import type * as Supervisor from "./Supervisor.js"
import type * as Tracer from "./Tracer.js"
import type { Concurrency, Contravariant, Covariant, NoExcessProperties, NoInfer, NotFunction } from "./Types.js"
import type * as Unify from "./Unify.js"
import { internalCall, isGeneratorFunction, type YieldWrap } from "./Utils.js"
import { isGeneratorFunction, type YieldWrap } from "./Utils.js"

/**
* @since 2.0.0
Expand Down Expand Up @@ -11576,9 +11577,28 @@ export const fn:
name: string,
options?: Tracer.SpanOptions
) => fn.Gen & fn.NonGen) = function(nameOrBody: Function | string, ...pipeables: Array<any>) {
const limit = Error.stackTraceLimit
Error.stackTraceLimit = 2
const errorDef = new Error()
Error.stackTraceLimit = limit
if (typeof nameOrBody !== "string") {
return function(this: any) {
return fnApply(this, nameOrBody, arguments as any, pipeables)
return function(this: any, ...args: Array<any>) {
const limit = Error.stackTraceLimit
Error.stackTraceLimit = 2
const errorCall = new Error()
Error.stackTraceLimit = limit
return fnApply({
self: this,
body: nameOrBody,
args,
pipeables,
spanName: "<anonymous>",
spanOptions: {
context: internalTracer.DisablePropagation.context(true)
},
errorDef,
errorCall
})
} as any
}
const name = nameOrBody
Expand All @@ -11587,53 +11607,111 @@ export const fn:
return function(this: any, ...args: Array<any>) {
const limit = Error.stackTraceLimit
Error.stackTraceLimit = 2
const error = new Error()
const errorCall = new Error()
Error.stackTraceLimit = limit
let cache: false | string = false
const captureStackTrace = () => {
if (cache !== false) {
return cache
}
if (error.stack) {
const stack = error.stack.trim().split("\n")
cache = stack.slice(2).join("\n").trim()
return cache
}
}
const effect = fnApply(this, body, args, pipeables)
const opts: any = (options && "captureStackTrace" in options) ? options : { captureStackTrace, ...options }
return withSpan(effect, name, opts)
return fnApply({
self: this,
body,
args,
pipeables,
spanName: name,
spanOptions: options,
errorDef,
errorCall
})
}
}
}

function fnApply(self: any, body: Function, args: Array<any>, pipeables: Array<any>) {
function fnApply(options: {
readonly self: any
readonly body: Function
readonly args: Array<any>
readonly pipeables: Array<any>
readonly spanName: string
readonly spanOptions: Tracer.SpanOptions
readonly errorDef: Error
readonly errorCall: Error
}) {
let effect: Effect<any, any, any>
let fnError: any = undefined
if (isGeneratorFunction(body)) {
effect = gen(() => internalCall(() => body.apply(self, args)))
if (isGeneratorFunction(options.body)) {
effect = core.fromIterator(() => options.body.apply(options.self, options.args))
} else {
try {
effect = body.apply(self, args)
effect = options.body.apply(options.self, options.args)
} catch (error) {
fnError = error
effect = die(error)
}
}
if (pipeables.length === 0) {
return effect
if (options.pipeables.length > 0) {
try {
for (const x of options.pipeables) {
effect = x(effect)
}
} catch (error) {
effect = fnError
? failCause(internalCause.sequential(
internalCause.die(fnError),
internalCause.die(error)
))
: die(error)
}
}
try {
for (const x of pipeables) {
effect = x(effect)

let cache: false | string = false
const captureStackTrace = () => {
if (cache !== false) {
return cache
}
if (options.errorCall.stack) {
const stackDef = options.errorDef.stack!.trim().split("\n")
const stackCall = options.errorCall.stack.trim().split("\n")
cache = `${stackDef.slice(2).join("\n").trim()}\n${stackCall.slice(2).join("\n").trim()}`
return cache
}
} catch (error) {
effect = fnError
? failCause(internalCause.sequential(
internalCause.die(fnError),
internalCause.die(error)
))
: die(error)
}
return effect
const opts: any = (options.spanOptions && "captureStackTrace" in options.spanOptions)
? options.spanOptions
: { captureStackTrace, ...options.spanOptions }
return withSpan(effect, options.spanName, opts)
}

/**
* Creates a function that returns an Effect.
*
* The function can be created using a generator function that can yield
* effects.
*
* `Effect.fnUntraced` also acts as a `pipe` function, allowing you to create a pipeline after the function definition.
*
* @example
* ```ts
* // Title: Creating a traced function with a generator function
* import { Effect } from "effect"
*
* const logExample = Effect.fnUntraced(function*<N extends number>(n: N) {
* yield* Effect.annotateCurrentSpan("n", n)
* yield* Effect.logInfo(`got: ${n}`)
* yield* Effect.fail(new Error())
* })
*
* Effect.runFork(logExample(100))
* ```
*
* @since 3.12.0
* @category function
*/
export const fnUntraced: fn.Gen = (body: Function, ...pipeables: Array<any>) =>
pipeables.length === 0
? function(this: any, ...args: Array<any>) {
return core.fromIterator(() => body.apply(this, args))
}
: function(this: any, ...args: Array<any>) {
let effect = core.fromIterator(() => body.apply(this, args))
for (const x of pipeables) {
effect = x(effect)
}
return effect
}
14 changes: 10 additions & 4 deletions packages/effect/src/internal/cause.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1073,7 +1073,7 @@ export const prettyErrorMessage = (u: unknown): string => {
return stringifyCircular(u)
}

const locationRegex = /\((.*)\)/
const locationRegex = /\((.*)\)/g

/** @internal */
export const spanToTrace = globalValue("effect/Tracer/spanToTrace", () => new WeakMap())
Expand Down Expand Up @@ -1105,9 +1105,15 @@ const prettyErrorStack = (message: string, stack: string, span?: Span | undefine
if (typeof stackFn === "function") {
const stack = stackFn()
if (typeof stack === "string") {
const locationMatch = stack.match(locationRegex)
const location = locationMatch ? locationMatch[1] : stack.replace(/^at /, "")
out.push(` at ${current.name} (${location})`)
const locationMatchAll = stack.matchAll(locationRegex)
let match = false
for (const [, location] of locationMatchAll) {
match = true
out.push(` at ${current.name} (${location})`)
}
if (!match) {
out.push(` at ${current.name} (${stack.replace(/^at /, "")})`)
}
} else {
out.push(` at ${current.name}`)
}
Expand Down
18 changes: 14 additions & 4 deletions packages/effect/src/internal/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1427,13 +1427,23 @@ export const whileLoop = <A, E, R>(
}

/* @internal */
export const gen: typeof Effect.gen = function() {
const f = arguments.length === 1 ? arguments[0] : arguments[1].bind(arguments[0])
return suspend(() => {
export const fromIterator = <Eff extends YieldWrap<Effect.Effect<any, any, any>>, AEff>(
iterator: LazyArg<Iterator<Eff, AEff, never>>
): Effect.Effect<
AEff,
[Eff] extends [never] ? never : [Eff] extends [YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
[Eff] extends [never] ? never : [Eff] extends [YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
> =>
suspend(() => {
const effect = new EffectPrimitive(OpCodes.OP_ITERATOR) as any
effect.effect_instruction_i0 = f(pipe)
effect.effect_instruction_i0 = iterator()
return effect
})

/* @internal */
export const gen: typeof Effect.gen = function() {
const f = arguments.length === 1 ? arguments[0] : arguments[1].bind(arguments[0])
return fromIterator(() => f(pipe))
}

/* @internal */
Expand Down

0 comments on commit cc9c8c0

Please sign in to comment.