Skip to content

Scoverage: instrument chained calls correctly#26166

Draft
anatoliykmetyuk wants to merge 5 commits into
scala:mainfrom
anatoliykmetyuk:fix/min12-freshcontext-addmode-ycheck
Draft

Scoverage: instrument chained calls correctly#26166
anatoliykmetyuk wants to merge 5 commits into
scala:mainfrom
anatoliykmetyuk:fix/min12-freshcontext-addmode-ycheck

Conversation

@anatoliykmetyuk
Copy link
Copy Markdown
Contributor

Fixes #26088

Root Issue

The issue in question is actually a manifestation of a larger issue:

addMode(foo())

Will correctly get instrumented by coverage to roughly:

val $1$: T = { Invoker.invoked("foo"); foo() }
Invoker.invoked("addMode")
addMode($1$)

But:

addMode(foo()).setA(...)

Will incorrectly get instrumented to roughly:

val $1$ = addMode(foo())
Invoker.invoked("setA")
$1.setA(...)

So, in case of chained calls foo(x).bar(y).baz(z), only the last call baz(z) will get proper instrumentation that respects argument evaluation order.

Reported Issue Impact

Consider code:

class C { def setA(a: Int): this.type = this }
def addMode(c: C): c.type = c
val x = addMode(identity(new C()).setA(???)).setA(???)

Its line:

val x = addMode(identity(new C()).setA(???)).setA(???)

Coverage instrumentation of that line:

val $2$: (?1 : C) =
  addMode(
    {
      val $1$: C = identity[C]({ Invoker.invoked(...); new C() })
      val a$1: Nothing = ???
      Invoker.invoked(...)
      $1$.setA(a$1)
    }
  )

Ycheck then fails because the result type is tied to the method parameter placeholder ?1, while the block result is tied to the lifted local $1$:

assertion failed: Type Mismatch
Found:    ($1$ : C)
Required: (?1 : C)

Solution

Ensure call chains such as foo(x).bar(y).baz(z) have all their calls lifted and instrumented properly to preserve evaluation order.

In the case of the issue in question, this will have the following effect:

val c$1: C =
  {
    val x$1: C = { Invoker.invoked(...); new C() }
    val $1$: C = identity[C](x$1)
    val a$1: Nothing = ???
    Invoker.invoked(...)
    $1$.setA(a$1)
  }
Invoker.invoked(...)
val $2$: (c$1 : C) = addMode(c$1)

c.type is preserved in addMode, and addMode(...) is traced the same way in addMode(...).setA(...) as it is in addMode(...).

How much have you relied on LLM-based tools in this contribution?

Moderately, for minimization, codebase analysis, tracing, test generation.

How was the solution tested?

New automated tests. Added tests/pos-custom-args/captures/coverage-freshcontext-addmode.scala and tests/coverage/run/chained-apply/test.scala. The coverage run test checks direct, single-chain, multi-chain, and receiver-dependent chained applications.

sbt "testCompilation --enable-coverage-phase coverage-freshcontext-addmode"
sbt "testCompilation coverage-freshcontext-addmode"
sbt "testCompilation --coverage"

@anatoliykmetyuk anatoliykmetyuk force-pushed the fix/min12-freshcontext-addmode-ycheck branch from bb42871 to f0dbb8d Compare May 29, 2026 02:09
@SolalPirelli
Copy link
Copy Markdown
Contributor

Is this still a draft or can it be reviewed?

@anatoliykmetyuk
Copy link
Copy Markdown
Contributor Author

anatoliykmetyuk commented Jun 1, 2026

Is this still a draft or can it be reviewed?

I still have not convinced myself this is the best solution, will come back to it later this week and mark it as review-ready when I'm done.

@anatoliykmetyuk anatoliykmetyuk force-pushed the fix/min12-freshcontext-addmode-ycheck branch from f0dbb8d to 01e309a Compare June 5, 2026 04:37
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.

Scoverage lifting does not preserve this.type properly

2 participants