Skip to content

Commit 3a80e3a

Browse files
authored
Merge pull request #15530 from TheElectronWill/fix-coverage-1
Fix lifting of arguments with -coverage-out
2 parents 41678e6 + 2d9d647 commit 3a80e3a

19 files changed

+1274
-62
lines changed

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

Lines changed: 14 additions & 10 deletions
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

Lines changed: 17 additions & 11 deletions
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
@@ -278,24 +279,29 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
278279
* should not be changed to {val $x = f(); T($x)}(1) but to {val $x = f(); val $y = 1; T($x)($y)}
279280
*/
280281
private def needsLift(tree: Apply)(using Context): Boolean =
281-
def isBooleanOperator(fun: Tree) =
282-
// We don't want to lift a || getB(), to avoid calling getB if a is true.
283-
// Same idea with a && getB(): if a is false, getB shouldn't be called.
284-
val sym = fun.symbol
285-
sym.exists &&
282+
def isShortCircuitedOp(sym: Symbol) =
286283
sym == defn.Boolean_&& || sym == defn.Boolean_||
287284

288-
def isContextual(fun: Apply): Boolean =
289-
val args = fun.args
290-
args.nonEmpty && args.head.symbol.isAllOf(GivenOrImplicit)
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
291297

292298
val fun = tree.fun
293299
val nestedApplyNeedsLift = fun match
294300
case a: Apply => needsLift(a)
295301
case _ => false
296302

297303
nestedApplyNeedsLift ||
298-
!isBooleanOperator(fun) && !tree.args.isEmpty && !tree.args.forall(LiftCoverage.noLift)
304+
!isUnliftableFun(fun) && !tree.args.isEmpty && !tree.args.forall(LiftCoverage.noLift)
299305

300306
/** Check if the body of a DefDef can be instrumented with instrumentBody. */
301307
private def canInstrumentDefDef(tree: DefDef)(using Context): Boolean =

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

Lines changed: 7 additions & 1 deletion
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

Lines changed: 25 additions & 9 deletions
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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,11 @@ class CoverageTests:
6969
def computeCoverageInTmp(inputFile: Path, sourceRoot: Path, run: Boolean)(using TestGroup): Path =
7070
val target = Files.createTempDirectory("coverage")
7171
val options = defaultOptions.and("-Ycheck:instrumentCoverage", "-coverage-out", target.toString, "-sourceroot", sourceRoot.toString)
72-
val test = compileFile(inputFile.toString, options)
7372
if run then
73+
val test = compileDir(inputFile.getParent.toString, options)
7474
test.checkRuns()
7575
else
76+
val test = compileFile(inputFile.toString, options)
7677
test.checkCompile()
7778
target
7879

tests/coverage/pos/ContextFunctions.scoverage.check

Lines changed: 10 additions & 10 deletions
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/run/erased/test.check

Lines changed: 3 additions & 0 deletions
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

Lines changed: 15 additions & 0 deletions
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)