Skip to content

Enforce scoverage type lifting to be observationally transparent for later phases - #26895

Open
anatoliykmetyuk wants to merge 9 commits into
scala:mainfrom
anatoliykmetyuk:br/swe-111-type-transparent-lifting
Open

Enforce scoverage type lifting to be observationally transparent for later phases#26895
anatoliykmetyuk wants to merge 9 commits into
scala:mainfrom
anatoliykmetyuk:br/swe-111-type-transparent-lifting

Conversation

@anatoliykmetyuk

@anatoliykmetyuk anatoliykmetyuk commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Fixes #26780
Fixes #26781
Fixes #26782
Fixes #26784

Excludelist additions are the same as #26894 to address upstream regression independent of this work.

Problem

Coverage instrumentation runs after typing and lifts expressions into synthetic local values to preserve evaluation order while inserting probes. E.g.:

f(x)

becomes:

val temp$x = x
f(temp$x)

Those temporaries are then encountered by later rechecking and capture-checking phases as if they were ordinary source values.

One root cause for a whole class of bugs is scoverage computing the type of the temporary in an incorrect, incomplete or overly simplistic way, breaking the assumptions by later phases.

The central question to correctly implementing coverage phase is therefore the correct computation of the lifted expression's type. This type can be one of the two things:

  • Preserve original type verbatim
  • Recompute the type for the temporary val

Naively, we may always want to preserve the original type, however, in practice, this is often not possible as properties such as stability, laziness, source singleton path, skolem/capture identity would be genuinely different for the temporary val. Typing the temporary by the original type would be wrong.

Therefore, the central question of this PR is determining in which cases a lifted expression should preserve the original type, when to default to the eagerly-computed (recomputed) type, and what that eagerly-computed type should be.

Solution

The solution attempts to establish the following invariant: lifting of application parameters must be observationally transparent. This means a lifted argument should be possible to use at the same position as the original argument without changing the type of the application's other arguments, the application's result type, and the capture tracking behavior.

The PR defaults to eagerly computing the type of lifted expressions, preserving the original type only in either of following cases:

  1. Original type has a skolem
  2. Original type is Nothing
  3. Eager type does not conform to the application's formal parameter type
  4. Eager type will change the type of a later parameter or the application's result type

Any of the above, provided that:

  1. Original type can be declared as the synthetic value's type

Coverage lifting now distinguishes the eager type of the value actually stored in a temporary from the original semantic type observed by the already-typed application.

Review Guide

  • liftArgContext is where the decision of eager vs original type is made.
  • liftedDef and liftedRef construct the synthetic value definition tree and its substitution into the application.
  • liftedExprType - computes the lifted expression's type, according to the decision made by liftArgContext, also taking measures to make the operation transparent for capture-checking purposes via declareNestedCaptureRefinements.
  • declareNestedCaptureRefinements - checks whether capture checking phase will change any of the structural parts of the synthetic value's type during Setup, and if so, asks it not to do so by annotating it with @Declared.
  • liftApp is lifting entrypoint for applications, calling copyApply transitively; copyApply preserves skolems

Have you relied on LLM-based tools in this contribution?s

Yes, and I checked the output by review and regression tests.

How was the solution tested?

New automated tests cover the four targeted issues, exact typer skolems, dependent and path-dependent arguments, parameterless and lazy accessors, nested capture refinements, named arguments, repeated arguments, read-only adaptation, exactly-once left-to-right evaluation, and a genuine escaping-capture negative control.

sbt --client "scala3-bootstrapped/testCompilation --enable-coverage-phase coverage-class-token-root-capture"
sbt --client "scala3-bootstrapped/testCompilation --enable-coverage-phase coverage-this-type-accessor-capture"
sbt --client "scala3-bootstrapped/testCompilation --enable-coverage-phase coverage-quotes-context"
sbt --client "scala3-bootstrapped/testCompilation --enable-coverage-phase coverage-iterator-iterate-capture"
sbt --client "scala3-bootstrapped/testCompilation --enable-coverage-phase coverage-dependent-lazy"
sbt --client "scala3-bootstrapped/testCompilation --enable-coverage-phase coverage-dependent-named-args"
sbt --client "scala3-bootstrapped/testCompilation --enable-coverage-phase coverage-dependent-varargs"
sbt --client "scala3-bootstrapped/testCompilation --enable-coverage-phase coverage-stateful-readonly-argument"
sbt --client "scala3-compiler-bootstrapped/testOnly dotty.tools.dotc.coverage.CoverageTests"

[test_coverage]

@anatoliykmetyuk
anatoliykmetyuk force-pushed the br/swe-111-type-transparent-lifting branch from 627ca6d to 6cfae14 Compare August 25, 2026 03:53
@anatoliykmetyuk
anatoliykmetyuk force-pushed the br/swe-111-type-transparent-lifting branch from 6cfae14 to a8a1dff Compare August 25, 2026 10:19
@som-snytt

Copy link
Copy Markdown
Contributor

The challenges described here afflict other lifted expressions as well. My other (naive) observation is that coverage need not be a core feature, but maybe a mechanism for plugins to safely lift is a core feature. (Like unused warnings, coverage feels like a third party operation.)

@anatoliykmetyuk
anatoliykmetyuk force-pushed the br/swe-111-type-transparent-lifting branch from a8a1dff to 4946257 Compare August 26, 2026 02:11
@anatoliykmetyuk

Copy link
Copy Markdown
Contributor Author

The challenges described here afflict other lifted expressions as well. My other (naive) observation is that coverage need not be a core feature, but maybe a mechanism for plugins to safely lift is a core feature. (Like unused warnings, coverage feels like a third party operation.)

Makes sense - other lifting cases may also need to observe similar transparency guarantees. Exploring a reusable core lifting mechanism would be broader than this PR, which keeps the implementation scoped to coverage instrumentation.

@anatoliykmetyuk
anatoliykmetyuk marked this pull request as ready for review August 26, 2026 03:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

4 participants