Skip to content

Commit b6acd6c

Browse files
Fix #25624: Scoverage and separation checking failures (#25625)
Fixes #25624 Separation checking for captures relies on `unsafeAssumeSeparate` to opt out of separation checking. To detect such code, `SepCheck` uses structural matching on the trees. Coverage phase may lift some trees, breaking the assumptions about the structure. This PR: - Opts `unsafeAssumeSeparate` out of coverage instrumentation - we aren't really interested in coverage info on it anyway, only the body is interesting - On lifting time, marks coverage-lifted synthetic trees with an explicit attachment - On `SepCheck` time, opts-out synthetic trees created by coverage from separation checking - rationalle being we have already checked their rhs for separation on assignment time ## How much have you relied on LLM-based tools in this contribution? Moderately. ## How was the solution tested? - Ran the full bootstrapped `testCompilation --enable-coverage-phase` suite - Run the compiler `pos` suite - Reproduced failing tests individually before and after the fix ## Additional notes I am not familiar with the captures codebase, so can use help of someone familiar with it to review this PR.
1 parent a06284b commit b6acd6c

4 files changed

Lines changed: 91 additions & 57 deletions

File tree

compiler/src/dotty/tools/dotc/cc/SepCheck.scala

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import CaptureSet.{Refs, emptyRefs, HiddenSet}
1010
import NameKinds.WildcardParamName
1111
import config.Printers.capt
1212
import StdNames.nme
13+
import transform.LiftCoverage
1314
import util.{SimpleIdentitySet, EqHashMap, SrcPos}
1415
import tpd.*
1516
import reflect.ClassTag
@@ -599,13 +600,22 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser:
599600
case _ =>
600601
end checkAssign
601602

603+
/** Is `tree` a coverage-lifted local temp or a reference to one?
604+
* These aliases are identified through the attachment set by `LiftCoverage`,
605+
* not by broad synthetic checks.
606+
*/
607+
private def isCoverageLiftedTemp(tree: Tree)(using Context): Boolean = tree match
608+
case tree: ValDef => tree.symbol.exists && LiftCoverage.isCoverageLiftedTemp(tree.symbol)
609+
case tree: Ident => tree.symbol.exists && LiftCoverage.isCoverageLiftedTemp(tree.symbol)
610+
case _ => false
611+
602612
/** 1. Check that the capabilities used at `tree` don't overlap with
603613
* capabilities hidden by a previous definition.
604614
* 2. Also check that none of the used capabilities was consumed before.
605615
*/
606616
def checkUse(tree: Tree)(using Context): Unit =
607617
val used = tree.markedFree.elems
608-
if !used.isEmpty then
618+
if !used.isEmpty && !isCoverageLiftedTemp(tree) then
609619
capt.println(i"check use $tree: $used")
610620
val usedPeaks = used.allPeaks
611621
if !defsShadow.allPeaks.sharedPeaks(usedPeaks).isEmpty then
@@ -994,13 +1004,16 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser:
9941004
/** Check (result-) type of `tree` for separation conditions using `checkType`.
9951005
* Excluded are parameters and definitions that have an =unsafeAssumeSeparate
9961006
* application as right hand sides.
1007+
* Also excluded are local temps marked by `LiftCoverage`, which are aliases
1008+
* introduced solely to preserve coverage evaluation order.
9971009
* Hidden sets of checked definitions are added to `defsShadow`.
9981010
*/
9991011
def checkValOrDefDef(tree: ValOrDefDef)(using Context): Unit =
10001012
val sym = tree.symbol
10011013
if !sym.isOneOf(TermParamOrAccessor)
10021014
&& !sym.needsResultRefinement
10031015
&& !isUnsafeAssumeSeparate(tree.rhs)
1016+
&& !isCoverageLiftedTemp(tree)
10041017
then
10051018
checkType(tree.tpt, sym)
10061019
capt.println(i"sep check def $sym: ${tree.tpt} with ${spanCaptures(tree.tpt).transHiddenSet.directFootprint}")

compiler/src/dotty/tools/dotc/transform/InstrumentCoverage.scala

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package transform
44
import java.io.File
55
import java.nio.file.{Files, Path}
66

7+
import ast.tpd
78
import ast.tpd.*
89
import collection.mutable
910
import core.Comments.Comment
@@ -18,14 +19,75 @@ import core.StdNames.nme
1819
import core.Types.*
1920
import core.Decorators.*
2021
import coverage.*
21-
import typer.LiftCoverage
22-
import util.{SourcePosition, SourceFile}
22+
import typer.LiftImpure
23+
import util.{Property, SourcePosition, SourceFile}
2324
import util.Spans.Span
2425
import localopt.StringInterpolatorOpt
2526
import inlines.Inlines
2627
import scala.util.matching.Regex
2728
import java.util.regex.Pattern
2829

30+
/** Lift impure + lift the prefixes for coverage instrumentation. */
31+
object LiftCoverage extends LiftImpure:
32+
33+
// Property indicating whether we're currently lifting the arguments of an application
34+
private val LiftingArgs = new Property.Key[Boolean]
35+
val CoverageLiftedTemp = Property.StickyKey[Unit]()
36+
37+
private inline def liftingArgs(using Context): Boolean =
38+
ctx.property(LiftingArgs).contains(true)
39+
40+
private def liftingArgsContext(using Context): Context =
41+
ctx.fresh.setProperty(LiftingArgs, true)
42+
43+
/** Variant of `noLift` for the arguments of applications.
44+
* To produce the right coverage information (especially in case of exceptions), we must lift:
45+
* - all the applications, except the erased ones
46+
*/
47+
private def noLiftArg(arg: tpd.Tree)(using Context): Boolean =
48+
arg match
49+
case arg if isUnsafeAssumeSeparate(arg) => true
50+
case a: tpd.Apply => a.symbol.is(Erased) // don't lift erased applications, but lift all others
51+
case tpd.Block(stats, expr) => stats.forall(noLiftArg) && noLiftArg(expr)
52+
case tpd.Inlined(_, bindings, expr) => noLiftArg(expr)
53+
case tpd.Typed(expr, _) => noLiftArg(expr)
54+
case _ => super.noLift(arg)
55+
56+
def isUnsafeAssumeSeparate(tree: tpd.Tree)(using Context): Boolean = tree match
57+
case tree: tpd.Apply =>
58+
tree.symbol == defn.Caps_unsafeAssumeSeparate
59+
|| isUnsafeAssumeSeparate(tree.fun)
60+
case tpd.Select(qual, _) => isUnsafeAssumeSeparate(qual)
61+
case tpd.Block(_, expr) => isUnsafeAssumeSeparate(expr)
62+
case tpd.Inlined(_, _, expr) => isUnsafeAssumeSeparate(expr)
63+
case tpd.Typed(expr, _) => isUnsafeAssumeSeparate(expr)
64+
case _ => false
65+
66+
def isCoverageLiftedTemp(sym: Symbol)(using Context): Boolean =
67+
sym.defTree.hasAttachment(CoverageLiftedTemp)
68+
69+
override protected def onLiftedDef(tree: tpd.Tree)(using Context): Unit =
70+
tree.putAttachment(CoverageLiftedTemp, ())
71+
72+
override def noLift(expr: tpd.Tree)(using Context) =
73+
if liftingArgs then noLiftArg(expr)
74+
else isUnsafeAssumeSeparate(expr) || super.noLift(expr)
75+
76+
/** Preserve singleton precision for lifted coverage temps when the underlying value is a
77+
* compile-time constant (same notion ConstFold uses), so constant re-folding after lifting
78+
* still matches the original inferred singleton type. Everything else uses the base widen.
79+
*/
80+
override protected def liftedExprType(expr: tpd.Tree)(using Context): Type =
81+
val dealiased = expr.tpe.dealias.deskolemized
82+
dealiased.widenTermRefExpr.normalized.simplified match
83+
case _: ConstantType => dealiased
84+
case _ => super.liftedExprType(expr)
85+
86+
def liftForCoverage(defs: mutable.ListBuffer[tpd.Tree], tree: tpd.Apply)(using Context) =
87+
val liftedFun = liftApp(defs, tree.fun)
88+
val liftedArgs = liftArgs(defs, tree.fun.tpe, tree.args)(using liftingArgsContext)
89+
tpd.cpy.Apply(tree)(liftedFun, liftedArgs)
90+
2991
/** Implements code coverage by inserting calls to scala.runtime.coverage.Invoker
3092
* ("instruments" the source code).
3193
* The result can then be consumed by the Scoverage tool.
@@ -261,7 +323,10 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
261323
* @return instrumentation result, with the preparation statement, coverage call and tree separated
262324
*/
263325
private def tryInstrument(tree: Apply)(using Context): InstrumentedParts =
264-
if canInstrumentApply(tree) then
326+
if LiftCoverage.isUnsafeAssumeSeparate(tree) then
327+
val transformed = cpy.Apply(tree)(transformInnerApply(tree.fun), transformApplyArgs(tree.args, erasedParamStatuses(tree)))
328+
InstrumentedParts.notCovered(transformed)
329+
else if canInstrumentApply(tree) then
265330
// Create a call to Invoker.invoked(coverageDirectory, newStatementId)
266331
val coverageCall = createInvokeCall(tree, tree.sourcePos)
267332

@@ -501,7 +566,9 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
501566
tree.rhs
502567
else if sym.isClassConstructor then
503568
instrumentSecondaryCtor(tree)
504-
else if !sym.isOneOf(Accessor | Artifact | Synthetic) then
569+
else if !sym.isOneOf(Accessor | Artifact | Synthetic)
570+
&& !LiftCoverage.isUnsafeAssumeSeparate(tree.rhs)
571+
then
505572
// If the body can be instrumented, do it (i.e. insert a "coverage call" at the beginning)
506573
// This is useful because methods can be stored and called later, or called by reflection,
507574
// and if the rhs is too simple to be instrumented (like `def f = this`),

compiler/src/dotty/tools/dotc/typer/EtaExpansion.scala

Lines changed: 6 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import Symbols.*
1111
import Names.*
1212
import NameKinds.UniqueName
1313
import util.Spans.*
14-
import util.Property
1514
import collection.mutable
1615
import Trees.*
1716

@@ -52,6 +51,9 @@ abstract class Lifter {
5251
protected def liftedExprType(expr: Tree)(using Context): Type =
5352
expr.tpe.widen.deskolemized
5453

54+
/** Hook for lifters that need to record or mark freshly created lifted defs. */
55+
protected def onLiftedDef(tree: Tree)(using Context): Unit = ()
56+
5557
private def lift(defs: mutable.ListBuffer[Tree], expr: Tree, prefix: TermName = EmptyTermName)(using Context): Tree =
5658
if (noLift(expr)) expr
5759
else {
@@ -63,10 +65,12 @@ abstract class Lifter {
6365
// Lifted definitions will be added to a local block, so they need to be
6466
// at a higher nesting level to prevent leaks. See tests/pos/i15174.scala
6567
nestingLevel = ctx.nestingLevel + 1)
66-
defs += liftedDef(lifted, expr)
68+
val liftedTree = liftedDef(lifted, expr)
6769
.withSpan(expr.span)
6870
.changeNonLocalOwners(lifted)
6971
.setDefTree
72+
onLiftedDef(liftedTree)
73+
defs += liftedTree
7074
ref(lifted.termRef).withSpan(expr.span.focus)
7175
}
7276

@@ -187,53 +191,6 @@ class LiftComplex extends Lifter {
187191
}
188192
object LiftComplex extends LiftComplex
189193

190-
/** Lift impure + lift the prefixes */
191-
object LiftCoverage extends LiftImpure {
192-
193-
// Property indicating whether we're currently lifting the arguments of an application
194-
private val LiftingArgs = new Property.Key[Boolean]
195-
196-
private inline def liftingArgs(using Context): Boolean =
197-
ctx.property(LiftingArgs).contains(true)
198-
199-
private def liftingArgsContext(using Context): Context =
200-
ctx.fresh.setProperty(LiftingArgs, true)
201-
202-
/** Variant of `noLift` for the arguments of applications.
203-
* To produce the right coverage information (especially in case of exceptions), we must lift:
204-
* - all the applications, except the erased ones
205-
* - all the impure arguments
206-
*
207-
* There's no need to lift the other arguments.
208-
*/
209-
private def noLiftArg(arg: tpd.Tree)(using Context): Boolean =
210-
arg match
211-
case a: tpd.Apply => a.symbol.is(Erased) // don't lift erased applications, but lift all others
212-
case tpd.Block(stats, expr) => stats.forall(noLiftArg) && noLiftArg(expr)
213-
case tpd.Inlined(_, bindings, expr) => noLiftArg(expr)
214-
case tpd.Typed(expr, _) => noLiftArg(expr)
215-
case _ => super.noLift(arg)
216-
217-
override def noLift(expr: tpd.Tree)(using Context) =
218-
if liftingArgs then noLiftArg(expr) else super.noLift(expr)
219-
220-
/** Preserve singleton precision for lifted coverage temps when the underlying value is a
221-
* compile-time constant (same notion ConstFold uses), so constant re-folding after lifting
222-
* still matches the original inferred singleton type. Everything else uses the base widen.
223-
*/
224-
override protected def liftedExprType(expr: tpd.Tree)(using Context): Type =
225-
val dealiased = expr.tpe.dealias.deskolemized
226-
dealiased.widenTermRefExpr.normalized.simplified match
227-
case _: ConstantType => dealiased
228-
case _ => super.liftedExprType(expr)
229-
230-
def liftForCoverage(defs: mutable.ListBuffer[tpd.Tree], tree: tpd.Apply)(using Context) = {
231-
val liftedFun = liftApp(defs, tree.fun)
232-
val liftedArgs = liftArgs(defs, tree.fun.tpe, tree.args)(using liftingArgsContext)
233-
tpd.cpy.Apply(tree)(liftedFun, liftedArgs)
234-
}
235-
}
236-
237194
/** Lifter for eta expansion */
238195
object EtaExpansion extends LiftImpure {
239196
import tpd.*

compiler/test/dotc/scoverage-ignore.excludelist

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
applied_constructor_types.scala
99
capt1.scala
1010
capture.scala
11-
colltest5
1211
gadt-ycheck.scala
1312
help.scala
1413
i10889.scala
@@ -35,12 +34,10 @@ i8900a3.scala
3534
i9228.scala
3635
lazyVals_c3.0.0.scala
3736
lazyVals_c3.1.0.scala
38-
minicheck.scala
3937
minicheck-toplevel.scala
4038
mt-scrutinee-widen3.scala
4139
null.scala
4240
pos_valueclasses
43-
skolems2.scala
4441
spurious-overload.scala
4542
tailrec.scala
4643
traitParams.scala

0 commit comments

Comments
 (0)