Skip to content

Commit 097ccd2

Browse files
authored
Merge pull request #15573 from TheElectronWill/release-3.2.0-coverage-1
Backport coverage fixes to 3.2.0
2 parents 6e7adfc + 09d5589 commit 097ccd2

23 files changed

+1522
-142
lines changed

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

+14-10
Original file line numberDiff line numberDiff line change
@@ -124,17 +124,21 @@ class ElimRepeated extends MiniPhase with InfoTransformer { thisPhase =>
124124
tp
125125

126126
override def transformApply(tree: Apply)(using Context): Tree =
127-
val args = tree.args.mapConserve {
128-
case arg: Typed if isWildcardStarArg(arg) =>
127+
val args = tree.args.mapConserve { arg =>
128+
if isWildcardStarArg(arg) then
129+
val expr = arg match
130+
case t: Typed => t.expr
131+
case _ => arg // if the argument has been lifted it's not a Typed (often it's an Ident)
132+
129133
val isJavaDefined = tree.fun.symbol.is(JavaDefined)
130-
val tpe = arg.expr.tpe
131134
if isJavaDefined then
132-
adaptToArray(arg.expr)
133-
else if tpe.derivesFrom(defn.ArrayClass) then
134-
arrayToSeq(arg.expr)
135+
adaptToArray(expr)
136+
else if expr.tpe.derivesFrom(defn.ArrayClass) then
137+
arrayToSeq(expr)
135138
else
136-
arg.expr
137-
case arg => arg
139+
expr
140+
else
141+
arg
138142
}
139143
cpy.Apply(tree)(tree.fun, args)
140144

@@ -287,9 +291,9 @@ class ElimRepeated extends MiniPhase with InfoTransformer { thisPhase =>
287291
val array = tp.translateFromRepeated(toArray = true) // Array[? <: T]
288292
val element = array.elemType.hiBound // T
289293

290-
291294
if element <:< defn.AnyRefType
292295
|| ctx.mode.is(Mode.SafeNulls) && element.stripNull <:< defn.AnyRefType
293-
|| element.typeSymbol.isPrimitiveValueClass then array
296+
|| element.typeSymbol.isPrimitiveValueClass
297+
then array
294298
else defn.ArrayOf(TypeBounds.upper(AndType(element, defn.AnyRefType))) // Array[? <: T & AnyRef]
295299
}

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

+34-23
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package transform
44
import java.io.File
55
import java.util.concurrent.atomic.AtomicInteger
66

7+
import ast.tpd.*
78
import collection.mutable
89
import core.Flags.*
910
import core.Contexts.{Context, ctx, inContext}
@@ -17,13 +18,13 @@ import typer.LiftCoverage
1718
import util.{SourcePosition, Property}
1819
import util.Spans.Span
1920
import coverage.*
21+
import localopt.StringInterpolatorOpt.isCompilerIntrinsic
2022

2123
/** Implements code coverage by inserting calls to scala.runtime.coverage.Invoker
2224
* ("instruments" the source code).
2325
* The result can then be consumed by the Scoverage tool.
2426
*/
2527
class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
26-
import ast.tpd._
2728

2829
override def phaseName = InstrumentCoverage.name
2930

@@ -60,7 +61,7 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
6061

6162
/** Transforms trees to insert calls to Invoker.invoked to compute the coverage when the code is called */
6263
private class CoverageTransformer extends Transformer:
63-
override def transform(tree: Tree)(using ctx: Context): Tree =
64+
override def transform(tree: Tree)(using Context): Tree =
6465
inContext(transformCtx(tree)) { // necessary to position inlined code properly
6566
tree match
6667
// simple cases
@@ -131,17 +132,22 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
131132
cpy.ValDef(tree)(rhs = rhs)
132133

133134
case tree: DefDef =>
134-
// Only transform the params (for the default values) and the rhs.
135-
val paramss = transformParamss(tree.paramss)
136-
val rhs = transform(tree.rhs)
137-
val finalRhs =
138-
if canInstrumentDefDef(tree) then
139-
// Ensure that the rhs is always instrumented, if possible
140-
instrumentBody(tree, rhs)
141-
else
142-
rhs
143-
cpy.DefDef(tree)(tree.name, paramss, tree.tpt, finalRhs)
144-
135+
if tree.symbol.isOneOf(Inline | Erased) then
136+
// Inline and erased definitions will not be in the generated code and therefore do not need to be instrumented.
137+
// Note that a retained inline method will have a `$retained` variant that will be instrumented.
138+
tree
139+
else
140+
// Only transform the params (for the default values) and the rhs.
141+
val paramss = transformParamss(tree.paramss)
142+
val rhs = transform(tree.rhs)
143+
val finalRhs =
144+
if canInstrumentDefDef(tree) then
145+
// Ensure that the rhs is always instrumented, if possible
146+
instrumentBody(tree, rhs)
147+
else
148+
rhs
149+
cpy.DefDef(tree)(tree.name, paramss, tree.tpt, finalRhs)
150+
end if
145151
case tree: PackageDef =>
146152
// only transform the statements of the package
147153
cpy.PackageDef(tree)(tree.pid, transform(tree.stats))
@@ -273,24 +279,29 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
273279
* should not be changed to {val $x = f(); T($x)}(1) but to {val $x = f(); val $y = 1; T($x)($y)}
274280
*/
275281
private def needsLift(tree: Apply)(using Context): Boolean =
276-
def isBooleanOperator(fun: Tree) =
277-
// We don't want to lift a || getB(), to avoid calling getB if a is true.
278-
// Same idea with a && getB(): if a is false, getB shouldn't be called.
279-
val sym = fun.symbol
280-
sym.exists &&
282+
def isShortCircuitedOp(sym: Symbol) =
281283
sym == defn.Boolean_&& || sym == defn.Boolean_||
282284

283-
def isContextual(fun: Apply): Boolean =
284-
val args = fun.args
285-
args.nonEmpty && args.head.symbol.isAllOf(Given | Implicit)
285+
def isUnliftableFun(fun: Tree) =
286+
/*
287+
* We don't want to lift a || getB(), to avoid calling getB if a is true.
288+
* Same idea with a && getB(): if a is false, getB shouldn't be called.
289+
*
290+
* On top of that, the `s`, `f` and `raw` string interpolators are special-cased
291+
* by the compiler and will disappear in phase StringInterpolatorOpt, therefore
292+
* they shouldn't be lifted.
293+
*/
294+
val sym = fun.symbol
295+
sym.exists && (isShortCircuitedOp(sym) || isCompilerIntrinsic(sym))
296+
end
286297

287298
val fun = tree.fun
288299
val nestedApplyNeedsLift = fun match
289300
case a: Apply => needsLift(a)
290301
case _ => false
291302

292303
nestedApplyNeedsLift ||
293-
!isBooleanOperator(fun) && !tree.args.isEmpty && !tree.args.forall(LiftCoverage.noLift)
304+
!isUnliftableFun(fun) && !tree.args.isEmpty && !tree.args.forall(LiftCoverage.noLift)
294305

295306
/** Check if the body of a DefDef can be instrumented with instrumentBody. */
296307
private def canInstrumentDefDef(tree: DefDef)(using Context): Boolean =
@@ -330,4 +341,4 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
330341

331342
object InstrumentCoverage:
332343
val name: String = "instrumentCoverage"
333-
val description: String = "instrument code for coverage cheking"
344+
val description: String = "instrument code for coverage checking"

compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala

+7-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class StringInterpolatorOpt extends MiniPhase:
3131
tree match
3232
case tree: RefTree =>
3333
val sym = tree.symbol
34-
assert(sym != defn.StringContext_raw && sym != defn.StringContext_s && sym != defn.StringContext_f,
34+
assert(!StringInterpolatorOpt.isCompilerIntrinsic(sym),
3535
i"$tree in ${ctx.owner.showLocated} should have been rewritten by phase $phaseName")
3636
case _ =>
3737

@@ -162,3 +162,9 @@ class StringInterpolatorOpt extends MiniPhase:
162162
object StringInterpolatorOpt:
163163
val name: String = "interpolators"
164164
val description: String = "optimize s, f, and raw string interpolators"
165+
166+
/** Is this symbol one of the s, f or raw string interpolator? */
167+
def isCompilerIntrinsic(sym: Symbol)(using Context): Boolean =
168+
sym == defn.StringContext_s ||
169+
sym == defn.StringContext_f ||
170+
sym == defn.StringContext_raw

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

+25-9
Original file line numberDiff line numberDiff line change
@@ -159,23 +159,39 @@ class LiftComplex extends Lifter {
159159
}
160160
object LiftComplex extends LiftComplex
161161

162-
/** Lift complex + lift the prefixes */
163-
object LiftCoverage extends LiftComplex {
162+
/** Lift impure + lift the prefixes */
163+
object LiftCoverage extends LiftImpure {
164164

165-
private val LiftEverything = new Property.Key[Boolean]
165+
// Property indicating whether we're currently lifting the arguments of an application
166+
private val LiftingArgs = new Property.Key[Boolean]
166167

167-
private inline def liftEverything(using Context): Boolean =
168-
ctx.property(LiftEverything).contains(true)
168+
private inline def liftingArgs(using Context): Boolean =
169+
ctx.property(LiftingArgs).contains(true)
169170

170-
private def liftEverythingContext(using Context): Context =
171-
ctx.fresh.setProperty(LiftEverything, true)
171+
private def liftingArgsContext(using Context): Context =
172+
ctx.fresh.setProperty(LiftingArgs, true)
173+
174+
/** Variant of `noLift` for the arguments of applications.
175+
* To produce the right coverage information (especially in case of exceptions), we must lift:
176+
* - all the applications, except the erased ones
177+
* - all the impure arguments
178+
*
179+
* There's no need to lift the other arguments.
180+
*/
181+
private def noLiftArg(arg: tpd.Tree)(using Context): Boolean =
182+
arg match
183+
case a: tpd.Apply => a.symbol.is(Erased) // don't lift erased applications, but lift all others
184+
case tpd.Block(stats, expr) => stats.forall(noLiftArg) && noLiftArg(expr)
185+
case tpd.Inlined(_, bindings, expr) => noLiftArg(expr)
186+
case tpd.Typed(expr, _) => noLiftArg(expr)
187+
case _ => super.noLift(arg)
172188

173189
override def noLift(expr: tpd.Tree)(using Context) =
174-
!liftEverything && super.noLift(expr)
190+
if liftingArgs then noLiftArg(expr) else super.noLift(expr)
175191

176192
def liftForCoverage(defs: mutable.ListBuffer[tpd.Tree], tree: tpd.Apply)(using Context) = {
177193
val liftedFun = liftApp(defs, tree.fun)
178-
val liftedArgs = liftArgs(defs, tree.fun.tpe, tree.args)(using liftEverythingContext)
194+
val liftedArgs = liftArgs(defs, tree.fun.tpe, tree.args)(using liftingArgsContext)
179195
tpd.cpy.Apply(tree)(liftedFun, liftedArgs)
180196
}
181197
}

compiler/test/dotty/tools/dotc/coverage/CoverageTests.scala

+3-1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class CoverageTests:
5656
val expected = fixWindowsPaths(Files.readAllLines(expectFile).asScala)
5757
val obtained = fixWindowsPaths(Files.readAllLines(targetFile).asScala)
5858
if expected != obtained then
59+
// FIXME: zip will drop part of the output if one is shorter (i.e. will not print anything of one is a refix of the other)
5960
for ((exp, actual),i) <- expected.zip(obtained).filter(_ != _).zipWithIndex do
6061
Console.err.println(s"wrong line ${i+1}:")
6162
Console.err.println(s" expected: $exp")
@@ -68,10 +69,11 @@ class CoverageTests:
6869
def computeCoverageInTmp(inputFile: Path, sourceRoot: Path, run: Boolean)(using TestGroup): Path =
6970
val target = Files.createTempDirectory("coverage")
7071
val options = defaultOptions.and("-Ycheck:instrumentCoverage", "-coverage-out", target.toString, "-sourceroot", sourceRoot.toString)
71-
val test = compileFile(inputFile.toString, options)
7272
if run then
73+
val test = compileDir(inputFile.getParent.toString, options)
7374
test.checkRuns()
7475
else
76+
val test = compileFile(inputFile.toString, options)
7577
test.checkCompile()
7678
target
7779

tests/coverage/pos/ContextFunctions.scoverage.check

+10-10
Original file line numberDiff line numberDiff line change
@@ -92,16 +92,16 @@ covtest
9292
Imperative
9393
Class
9494
covtest.Imperative
95-
$anonfun
96-
267
97-
294
95+
readPerson
96+
252
97+
295
9898
13
9999
invoked
100100
Apply
101101
false
102102
0
103103
false
104-
readName2(using e)(using s)
104+
OnError((e) => readName2(using e)(using s))
105105

106106
5
107107
ContextFunctions.scala
@@ -113,7 +113,7 @@ $anonfun
113113
267
114114
294
115115
13
116-
apply
116+
invoked
117117
Apply
118118
false
119119
0
@@ -126,16 +126,16 @@ covtest
126126
Imperative
127127
Class
128128
covtest.Imperative
129-
readPerson
130-
252
131-
295
129+
$anonfun
130+
267
131+
294
132132
13
133-
invoked
133+
apply
134134
Apply
135135
false
136136
0
137137
false
138-
OnError((e) => readName2(using e)(using s))
138+
readName2(using e)(using s)
139139

140140
7
141141
ContextFunctions.scala

tests/coverage/pos/Inlined.scoverage.check

-68
Original file line numberDiff line numberDiff line change
@@ -273,71 +273,3 @@ false
273273
false
274274
def testInlined
275275

276-
15
277-
Inlined.scala
278-
covtest
279-
Inlined$package$
280-
Object
281-
covtest.Inlined$package$
282-
assert
283-
288
284-
315
285-
10
286-
Scala3RunTime
287-
Select
288-
false
289-
0
290-
false
291-
scala.runtime.Scala3RunTime
292-
293-
16
294-
Inlined.scala
295-
covtest
296-
Inlined$package$
297-
Object
298-
covtest.Inlined$package$
299-
assert
300-
288
301-
330
302-
10
303-
assertFailed
304-
Apply
305-
false
306-
0
307-
false
308-
scala.runtime.Scala3RunTime.assertFailed()
309-
310-
17
311-
Inlined.scala
312-
covtest
313-
Inlined$package$
314-
Object
315-
covtest.Inlined$package$
316-
assert
317-
288
318-
330
319-
10
320-
<none>
321-
Block
322-
true
323-
0
324-
false
325-
scala.runtime.Scala3RunTime.assertFailed()
326-
327-
18
328-
Inlined.scala
329-
covtest
330-
Inlined$package$
331-
Object
332-
covtest.Inlined$package$
333-
assert
334-
202
335-
231
336-
9
337-
assert
338-
DefDef
339-
false
340-
0
341-
false
342-
transparent inline def assert
343-

tests/coverage/run/erased/test.check

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
foo(a)(b)
2+
identity(idem)
3+
foo(a)(idem)

tests/coverage/run/erased/test.scala

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import scala.language.experimental.erasedDefinitions
2+
3+
erased def e(x: String): String = "x"
4+
def foo(erased a: String)(b: String): String =
5+
println(s"foo(a)($b)")
6+
b
7+
8+
def identity(s: String): String =
9+
println(s"identity($s)")
10+
s
11+
12+
@main
13+
def Test: Unit =
14+
foo(e("a"))("b")
15+
foo(e("a"))(identity("idem"))

0 commit comments

Comments
 (0)