Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@hearnadam
Copy link
Collaborator

@hearnadam hearnadam commented Aug 26, 2025

/fixes #10051

This change makes TestArrow.run stack safe by removing indirection with the attempt method on each iteration of run loop, as well as introducing TailCalls to trampoline where necessary.

The run method has been split to a secondary runLoop method.

This change roughly doubles the stack safety of TestArrow.run by removing indirection with the attempt method on each iteration of `run` loop.

I split out the `run` method to an inner `runLoop` method and a top level `run` that uses try/catch once. This will prevent method indirection and the allocation of a by-name parameter.
@hearnadam hearnadam force-pushed the test-arrow-stack-safety branch from dbd66f4 to 511209d Compare August 26, 2025 22:49
@hearnadam hearnadam force-pushed the test-arrow-stack-safety branch from 511209d to 134d6c8 Compare August 26, 2025 22:50
kyri-petrou
kyri-petrou previously approved these changes Aug 30, 2025
@kyri-petrou
Copy link
Contributor

kyri-petrou commented Aug 30, 2025

In case you want to make it fully stack-safe without making too many chances, you can also use scala.util.control.TailCalls

import scala.util.control.TailCalls

  private val onError: PartialFunction[Throwable, TailCalls.TailRec[TestTrace[Nothing]]] = {
    case ex if NonFatal(ex) =>
      ex.setStackTrace(ex.getStackTrace.filterNot { (ste: StackTraceElement) =>
        ste.getClassName.startsWith("zio.test.TestArrow")
      })
      TailCalls.done(TestTrace.die(ex))
  }

  private def runLoop[A, B](arrow: TestArrow[A, B], in: Either[Throwable, A]): TailCalls.TailRec[TestTrace[B]] =
    TailCalls.tailcall(arrow match {
      case TestArrowF(f) =>
        try TailCalls.done(f(in))
        catch onError

      case AndThen(f, g) =>
        runLoop(f, in).flatMap { t1 =>
          t1.result match {
            case _: Result.Fail.type   => TailCalls.done(t1.asInstanceOf[TestTrace[B]])
            case Result.Die(err)       => runLoop(g, Left(err)).map(t1 >>> _)
            case Result.Succeed(value) => runLoop(g, Right(value)).map(t1 >>> _)
          }
        }

      case And(lhs, rhs) =>
        for {
          l <- runLoop(lhs, in)
          r <- runLoop(rhs, in)
        } yield l && r

      case Or(lhs, rhs) =>
        for {
          l <- runLoop(lhs, in)
          r <- runLoop(rhs, in)
        } yield l || r

      case Not(arrow) =>
        runLoop(arrow, in).map(!_)

      case Suspend(f) =>
        in match {
          case Left(exception) =>
            TailCalls.done(TestTrace.die(exception))
          case Right(value) =>
            try runLoop(f(value), in)
            catch onError
        }

      case Meta(arrow, span, parentSpan, code, location, completeCode, customLabel, genFailureDetails) =>
        runLoop(arrow, in).map {
          _.withSpan(span)
            .withCode(code)
            .withParentSpan(parentSpan)
            .withLocation(location)
            .withCompleteCode(completeCode)
            .withCustomLabel(customLabel)
            .withGenFailureDetails(genFailureDetails)
        }
    })

  def run[A, B](arrow: TestArrow[A, B], in: Either[Throwable, A]): TestTrace[B] =
    (try runLoop(arrow, in)
    catch onError).result

@hearnadam hearnadam changed the title Optimize TestArrow.run to double stack safety Make TestArrow.run stack safe Sep 3, 2025
@kyri-petrou kyri-petrou merged commit 407ebae into zio:series/2.x Sep 4, 2025
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TestArrow.run/attempt is not stack safe

2 participants