Skip to content

Backport coverage fixes to 3.2.0 #15573

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 14 additions & 10 deletions compiler/src/dotty/tools/dotc/transform/ElimRepeated.scala
Original file line number Diff line number Diff line change
Expand Up @@ -124,17 +124,21 @@ class ElimRepeated extends MiniPhase with InfoTransformer { thisPhase =>
tp

override def transformApply(tree: Apply)(using Context): Tree =
val args = tree.args.mapConserve {
case arg: Typed if isWildcardStarArg(arg) =>
val args = tree.args.mapConserve { arg =>
if isWildcardStarArg(arg) then
val expr = arg match
case t: Typed => t.expr
case _ => arg // if the argument has been lifted it's not a Typed (often it's an Ident)

val isJavaDefined = tree.fun.symbol.is(JavaDefined)
val tpe = arg.expr.tpe
if isJavaDefined then
adaptToArray(arg.expr)
else if tpe.derivesFrom(defn.ArrayClass) then
arrayToSeq(arg.expr)
adaptToArray(expr)
else if expr.tpe.derivesFrom(defn.ArrayClass) then
arrayToSeq(expr)
else
arg.expr
case arg => arg
expr
else
arg
}
cpy.Apply(tree)(tree.fun, args)

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


if element <:< defn.AnyRefType
|| ctx.mode.is(Mode.SafeNulls) && element.stripNull <:< defn.AnyRefType
|| element.typeSymbol.isPrimitiveValueClass then array
|| element.typeSymbol.isPrimitiveValueClass
then array
else defn.ArrayOf(TypeBounds.upper(AndType(element, defn.AnyRefType))) // Array[? <: T & AnyRef]
}
57 changes: 34 additions & 23 deletions compiler/src/dotty/tools/dotc/transform/InstrumentCoverage.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package transform
import java.io.File
import java.util.concurrent.atomic.AtomicInteger

import ast.tpd.*
import collection.mutable
import core.Flags.*
import core.Contexts.{Context, ctx, inContext}
Expand All @@ -17,13 +18,13 @@ import typer.LiftCoverage
import util.{SourcePosition, Property}
import util.Spans.Span
import coverage.*
import localopt.StringInterpolatorOpt.isCompilerIntrinsic

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

override def phaseName = InstrumentCoverage.name

Expand Down Expand Up @@ -60,7 +61,7 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:

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

case tree: DefDef =>
// Only transform the params (for the default values) and the rhs.
val paramss = transformParamss(tree.paramss)
val rhs = transform(tree.rhs)
val finalRhs =
if canInstrumentDefDef(tree) then
// Ensure that the rhs is always instrumented, if possible
instrumentBody(tree, rhs)
else
rhs
cpy.DefDef(tree)(tree.name, paramss, tree.tpt, finalRhs)

if tree.symbol.isOneOf(Inline | Erased) then
// Inline and erased definitions will not be in the generated code and therefore do not need to be instrumented.
// Note that a retained inline method will have a `$retained` variant that will be instrumented.
tree
else
// Only transform the params (for the default values) and the rhs.
val paramss = transformParamss(tree.paramss)
val rhs = transform(tree.rhs)
val finalRhs =
if canInstrumentDefDef(tree) then
// Ensure that the rhs is always instrumented, if possible
instrumentBody(tree, rhs)
else
rhs
cpy.DefDef(tree)(tree.name, paramss, tree.tpt, finalRhs)
end if
case tree: PackageDef =>
// only transform the statements of the package
cpy.PackageDef(tree)(tree.pid, transform(tree.stats))
Expand Down Expand Up @@ -273,24 +279,29 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
* should not be changed to {val $x = f(); T($x)}(1) but to {val $x = f(); val $y = 1; T($x)($y)}
*/
private def needsLift(tree: Apply)(using Context): Boolean =
def isBooleanOperator(fun: Tree) =
// We don't want to lift a || getB(), to avoid calling getB if a is true.
// Same idea with a && getB(): if a is false, getB shouldn't be called.
val sym = fun.symbol
sym.exists &&
def isShortCircuitedOp(sym: Symbol) =
sym == defn.Boolean_&& || sym == defn.Boolean_||

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

val fun = tree.fun
val nestedApplyNeedsLift = fun match
case a: Apply => needsLift(a)
case _ => false

nestedApplyNeedsLift ||
!isBooleanOperator(fun) && !tree.args.isEmpty && !tree.args.forall(LiftCoverage.noLift)
!isUnliftableFun(fun) && !tree.args.isEmpty && !tree.args.forall(LiftCoverage.noLift)

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

object InstrumentCoverage:
val name: String = "instrumentCoverage"
val description: String = "instrument code for coverage cheking"
val description: String = "instrument code for coverage checking"
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class StringInterpolatorOpt extends MiniPhase:
tree match
case tree: RefTree =>
val sym = tree.symbol
assert(sym != defn.StringContext_raw && sym != defn.StringContext_s && sym != defn.StringContext_f,
assert(!StringInterpolatorOpt.isCompilerIntrinsic(sym),
i"$tree in ${ctx.owner.showLocated} should have been rewritten by phase $phaseName")
case _ =>

Expand Down Expand Up @@ -162,3 +162,9 @@ class StringInterpolatorOpt extends MiniPhase:
object StringInterpolatorOpt:
val name: String = "interpolators"
val description: String = "optimize s, f, and raw string interpolators"

/** Is this symbol one of the s, f or raw string interpolator? */
def isCompilerIntrinsic(sym: Symbol)(using Context): Boolean =
sym == defn.StringContext_s ||
sym == defn.StringContext_f ||
sym == defn.StringContext_raw
34 changes: 25 additions & 9 deletions compiler/src/dotty/tools/dotc/typer/EtaExpansion.scala
Original file line number Diff line number Diff line change
Expand Up @@ -159,23 +159,39 @@ class LiftComplex extends Lifter {
}
object LiftComplex extends LiftComplex

/** Lift complex + lift the prefixes */
object LiftCoverage extends LiftComplex {
/** Lift impure + lift the prefixes */
object LiftCoverage extends LiftImpure {

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

private inline def liftEverything(using Context): Boolean =
ctx.property(LiftEverything).contains(true)
private inline def liftingArgs(using Context): Boolean =
ctx.property(LiftingArgs).contains(true)

private def liftEverythingContext(using Context): Context =
ctx.fresh.setProperty(LiftEverything, true)
private def liftingArgsContext(using Context): Context =
ctx.fresh.setProperty(LiftingArgs, true)

/** Variant of `noLift` for the arguments of applications.
* To produce the right coverage information (especially in case of exceptions), we must lift:
* - all the applications, except the erased ones
* - all the impure arguments
*
* There's no need to lift the other arguments.
*/
private def noLiftArg(arg: tpd.Tree)(using Context): Boolean =
arg match
case a: tpd.Apply => a.symbol.is(Erased) // don't lift erased applications, but lift all others
case tpd.Block(stats, expr) => stats.forall(noLiftArg) && noLiftArg(expr)
case tpd.Inlined(_, bindings, expr) => noLiftArg(expr)
case tpd.Typed(expr, _) => noLiftArg(expr)
case _ => super.noLift(arg)

override def noLift(expr: tpd.Tree)(using Context) =
!liftEverything && super.noLift(expr)
if liftingArgs then noLiftArg(expr) else super.noLift(expr)

def liftForCoverage(defs: mutable.ListBuffer[tpd.Tree], tree: tpd.Apply)(using Context) = {
val liftedFun = liftApp(defs, tree.fun)
val liftedArgs = liftArgs(defs, tree.fun.tpe, tree.args)(using liftEverythingContext)
val liftedArgs = liftArgs(defs, tree.fun.tpe, tree.args)(using liftingArgsContext)
tpd.cpy.Apply(tree)(liftedFun, liftedArgs)
}
}
Expand Down
4 changes: 3 additions & 1 deletion compiler/test/dotty/tools/dotc/coverage/CoverageTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class CoverageTests:
val expected = fixWindowsPaths(Files.readAllLines(expectFile).asScala)
val obtained = fixWindowsPaths(Files.readAllLines(targetFile).asScala)
if expected != obtained then
// 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)
for ((exp, actual),i) <- expected.zip(obtained).filter(_ != _).zipWithIndex do
Console.err.println(s"wrong line ${i+1}:")
Console.err.println(s" expected: $exp")
Expand All @@ -68,10 +69,11 @@ class CoverageTests:
def computeCoverageInTmp(inputFile: Path, sourceRoot: Path, run: Boolean)(using TestGroup): Path =
val target = Files.createTempDirectory("coverage")
val options = defaultOptions.and("-Ycheck:instrumentCoverage", "-coverage-out", target.toString, "-sourceroot", sourceRoot.toString)
val test = compileFile(inputFile.toString, options)
if run then
val test = compileDir(inputFile.getParent.toString, options)
test.checkRuns()
else
val test = compileFile(inputFile.toString, options)
test.checkCompile()
target

Expand Down
20 changes: 10 additions & 10 deletions tests/coverage/pos/ContextFunctions.scoverage.check
Original file line number Diff line number Diff line change
Expand Up @@ -92,16 +92,16 @@ covtest
Imperative
Class
covtest.Imperative
$anonfun
267
294
readPerson
252
295
13
invoked
Apply
false
0
false
readName2(using e)(using s)
OnError((e) => readName2(using e)(using s))

5
ContextFunctions.scala
Expand All @@ -113,7 +113,7 @@ $anonfun
267
294
13
apply
invoked
Apply
false
0
Expand All @@ -126,16 +126,16 @@ covtest
Imperative
Class
covtest.Imperative
readPerson
252
295
$anonfun
267
294
13
invoked
apply
Apply
false
0
false
OnError((e) => readName2(using e)(using s))
readName2(using e)(using s)

7
ContextFunctions.scala
Expand Down
68 changes: 0 additions & 68 deletions tests/coverage/pos/Inlined.scoverage.check
Original file line number Diff line number Diff line change
Expand Up @@ -273,71 +273,3 @@ false
false
def testInlined

15
Inlined.scala
covtest
Inlined$package$
Object
covtest.Inlined$package$
assert
288
315
10
Scala3RunTime
Select
false
0
false
scala.runtime.Scala3RunTime

16
Inlined.scala
covtest
Inlined$package$
Object
covtest.Inlined$package$
assert
288
330
10
assertFailed
Apply
false
0
false
scala.runtime.Scala3RunTime.assertFailed()

17
Inlined.scala
covtest
Inlined$package$
Object
covtest.Inlined$package$
assert
288
330
10
<none>
Block
true
0
false
scala.runtime.Scala3RunTime.assertFailed()

18
Inlined.scala
covtest
Inlined$package$
Object
covtest.Inlined$package$
assert
202
231
9
assert
DefDef
false
0
false
transparent inline def assert

3 changes: 3 additions & 0 deletions tests/coverage/run/erased/test.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
foo(a)(b)
identity(idem)
foo(a)(idem)
15 changes: 15 additions & 0 deletions tests/coverage/run/erased/test.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import scala.language.experimental.erasedDefinitions

erased def e(x: String): String = "x"
def foo(erased a: String)(b: String): String =
println(s"foo(a)($b)")
b

def identity(s: String): String =
println(s"identity($s)")
s

@main
def Test: Unit =
foo(e("a"))("b")
foo(e("a"))(identity("idem"))
Loading