Skip to content
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

Fix lifting of arguments with -coverage-out #15530

Merged
merged 5 commits into from
Jul 2, 2022
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]
}
28 changes: 17 additions & 11 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 @@ -278,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(GivenOrImplicit)
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
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
3 changes: 2 additions & 1 deletion compiler/test/dotty/tools/dotc/coverage/CoverageTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -69,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
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