-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Fix #4753: Ignore implicit shortcuts in RefChecks #4772
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
Changes from 4 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,7 +2,7 @@ package dotty.tools.dotc | |
package transform | ||
|
||
import MegaPhase._ | ||
import core.DenotTransformers.IdentityDenotTransformer | ||
import core.DenotTransformers.{DenotTransformer, IdentityDenotTransformer} | ||
import core.Symbols._ | ||
import core.Contexts._ | ||
import core.Types._ | ||
|
@@ -44,8 +44,20 @@ import collection.mutable | |
* (2) A reference `qual.apply` where `qual` has implicit function type and | ||
* `qual` refers to a method `m` is rewritten to a reference to `m$direct`, | ||
* keeping the same type and value arguments as they are found in `qual`. | ||
* | ||
* Note: The phase adds direct methods for all defined methods with IFT results, | ||
* as well as for all methods that are referenced. It does NOT do an | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe I miss something here, the part There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll try to express this clearer. |
||
* info transformer that adds these methods everywhere where an IFT returning | ||
* method exists. | ||
* Adding such an info transformer is impractical because it would mean | ||
* that we have to force the types of all members of classes that are referenced. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't quite understand how info transformer works. Maybe a future compiler session on the topic, I know several in the team are interested in the topic. |
||
* But not adding an info transformer can lead to inconsistencies in RefChecks. | ||
* We solve that by ignoring direct methods in Refchecks. | ||
* Another, related issue is bridge generation, where we also generate | ||
* shortcut methods on the fly. | ||
*/ | ||
class ShortcutImplicits extends MiniPhase with IdentityDenotTransformer { thisPhase => | ||
import ShortcutImplicits._ | ||
import tpd._ | ||
|
||
override def phaseName: String = ShortcutImplicits.name | ||
|
@@ -61,82 +73,42 @@ class ShortcutImplicits extends MiniPhase with IdentityDenotTransformer { thisPh | |
override def initContext(ctx: FreshContext) = | ||
DirectMeth = ctx.addLocation[MutableSymbolMap[Symbol]]() | ||
|
||
/** If this option is true, we don't specialize symbols that are known to be only | ||
* targets of monomorphic calls. | ||
* The reason for this option is that benchmarks show that on the JVM for monomorphic dispatch | ||
* scenarios inlining and escape analysis can often remove all calling overhead, so we might as | ||
* well not duplicate the code. We need more experience to decide on the best setting of this option. | ||
*/ | ||
final val specializeMonoTargets = true | ||
|
||
override def prepareForUnit(tree: Tree)(implicit ctx: Context) = | ||
ctx.fresh.updateStore(DirectMeth, newMutableSymbolMap[Symbol]) | ||
|
||
/** Should `sym` get a ..$direct companion? | ||
* This is the case if `sym` is a method with a non-nullary implicit function type as final result type. | ||
* However if `specializeMonoTargets` is false, we exclude symbols that are known | ||
* to be only targets of monomorphic calls because they are effectively | ||
* final and don't override anything. | ||
*/ | ||
private def shouldBeSpecialized(sym: Symbol)(implicit ctx: Context) = | ||
sym.is(Method, butNot = Accessor) && | ||
defn.isImplicitFunctionType(sym.info.finalResultType) && | ||
defn.functionArity(sym.info.finalResultType) > 0 && | ||
!sym.isAnonymousFunction && | ||
(specializeMonoTargets || !sym.isEffectivelyFinal || sym.allOverriddenSymbols.nonEmpty) | ||
|
||
/** @pre The type's final result type is an implicit function type `implicit Ts => R`. | ||
* @return The type of the `apply` member of `implicit Ts => R`. | ||
*/ | ||
private def directInfo(info: Type)(implicit ctx: Context): Type = info match { | ||
case info: PolyType => info.derivedLambdaType(resType = directInfo(info.resultType)) | ||
case info: MethodType => info.derivedLambdaType(resType = directInfo(info.resultType)) | ||
case info: ExprType => directInfo(info.resultType) | ||
case info => info.member(nme.apply).info | ||
} | ||
|
||
/** A new `m$direct` method to accompany the given method `m` */ | ||
private def newDirectMethod(sym: Symbol)(implicit ctx: Context): Symbol = { | ||
val direct = sym.copy( | ||
name = DirectMethodName(sym.name.asTermName).asInstanceOf[sym.ThisName], | ||
flags = sym.flags | Synthetic, | ||
info = directInfo(sym.info)) | ||
if (direct.allOverriddenSymbols.isEmpty) direct.resetFlag(Override) | ||
direct | ||
} | ||
|
||
/** The direct method `m$direct` that accompanies the given method `m`. | ||
* Create one if it does not exist already. | ||
*/ | ||
private def directMethod(sym: Symbol)(implicit ctx: Context): Symbol = | ||
if (sym.owner.isClass) { | ||
val direct = sym.owner.info.member(DirectMethodName(sym.name.asTermName)) | ||
.suchThat(_.info matches directInfo(sym.info)).symbol | ||
if (direct.maybeOwner == sym.owner) direct | ||
else newDirectMethod(sym).enteredAfter(thisPhase) | ||
} | ||
else directMeth.getOrElseUpdate(sym, newDirectMethod(sym)) | ||
if (sym.owner.isClass) shortcutMethod(sym, thisPhase) | ||
else directMeth.getOrElseUpdate(sym, newShortcutMethod(sym)) | ||
|
||
/** Transform `qual.apply` occurrences according to rewrite rule (2) above */ | ||
override def transformSelect(tree: Select)(implicit ctx: Context) = | ||
if (tree.name == nme.apply && | ||
defn.isImplicitFunctionType(tree.qualifier.tpe.widen) && | ||
shouldBeSpecialized(tree.qualifier.symbol)) { | ||
needsImplicitShortcut(tree.qualifier.symbol)) { | ||
def directQual(tree: Tree): Tree = tree match { | ||
case Apply(fn, args) => cpy.Apply(tree)(directQual(fn), args) | ||
case TypeApply(fn, args) => cpy.TypeApply(tree)(directQual(fn), args) | ||
case Block(stats, expr) => cpy.Block(tree)(stats, directQual(expr)) | ||
case tree: RefTree => | ||
def rewire(tp: Type, sym: Symbol) = tp match { | ||
case tp: NamedType => tp.prefix.select(sym) | ||
case _ => sym.termRef | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you explain when the last case is reachable? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not sure. Just to be defensive. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll try to eliminate the last case. It's better to fail fast if we misunderstand something. |
||
} | ||
cpy.Ref(tree)(DirectMethodName(tree.name.asTermName)) | ||
.withType(directMethod(tree.symbol).termRef) | ||
.withType(rewire(tree.tpe, directMethod(tree.symbol))) | ||
} | ||
directQual(tree.qualifier) | ||
} else tree | ||
|
||
/** Transform methods with implicit function type result according to rewrite rule (1) above */ | ||
override def transformDefDef(mdef: DefDef)(implicit ctx: Context): Tree = { | ||
val original = mdef.symbol | ||
if (shouldBeSpecialized(original)) { | ||
if (needsImplicitShortcut(original)) { | ||
val direct = directMethod(original) | ||
|
||
// Move @tailrec to the direct method | ||
|
@@ -179,4 +151,51 @@ class ShortcutImplicits extends MiniPhase with IdentityDenotTransformer { thisPh | |
|
||
object ShortcutImplicits { | ||
val name = "shortcutImplicits" | ||
|
||
/** If this option is true, we don't specialize symbols that are known to be only | ||
* targets of monomorphic calls. | ||
* The reason for this option is that benchmarks show that on the JVM for monomorphic dispatch | ||
* scenarios inlining and escape analysis can often remove all calling overhead, so we might as | ||
* well not duplicate the code. We need more experience to decide on the best setting of this option. | ||
*/ | ||
final val specializeMonoTargets = true | ||
|
||
/** Should `sym` get a ..$direct companion? | ||
* This is the case if `sym` is a method with a non-nullary implicit function type as final result type. | ||
* However if `specializeMonoTargets` is false, we exclude symbols that are known | ||
* to be only targets of monomorphic calls because they are effectively | ||
* final and don't override anything. | ||
*/ | ||
def needsImplicitShortcut(sym: Symbol)(implicit ctx: Context) = | ||
sym.is(Method, butNot = Accessor) && | ||
defn.isImplicitFunctionType(sym.info.finalResultType) && | ||
defn.functionArity(sym.info.finalResultType) > 0 && | ||
!sym.isAnonymousFunction && | ||
(specializeMonoTargets || !sym.isEffectivelyFinal || sym.allOverriddenSymbols.nonEmpty) | ||
|
||
/** @pre The type's final result type is an implicit function type `implicit Ts => R`. | ||
* @return The type of the `apply` member of `implicit Ts => R`. | ||
*/ | ||
private def directInfo(info: Type)(implicit ctx: Context): Type = info match { | ||
case info: PolyType => info.derivedLambdaType(resType = directInfo(info.resultType)) | ||
case info: MethodType => info.derivedLambdaType(resType = directInfo(info.resultType)) | ||
case info: ExprType => directInfo(info.resultType) | ||
case info => info.member(nme.apply).info | ||
} | ||
|
||
/** A new `m$direct` method to accompany the given method `m` */ | ||
private def newShortcutMethod(sym: Symbol)(implicit ctx: Context): Symbol = | ||
sym.copy( | ||
name = DirectMethodName(sym.name.asTermName).asInstanceOf[sym.ThisName], | ||
flags = sym.flags &~ Override | Synthetic, | ||
info = directInfo(sym.info)) | ||
|
||
def shortcutMethod(sym: Symbol, phase: DenotTransformer)(implicit ctx: Context) = | ||
sym.owner.info.decl(DirectMethodName(sym.name.asTermName)) | ||
.suchThat(_.info matches directInfo(sym.info)).symbol | ||
.orElse(newShortcutMethod(sym).enteredAfter(phase)) | ||
|
||
def isImplicitShortcut(sym: Symbol)(implicit ctx: Context) = sym.name.is(DirectMethodName) | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -336,8 +336,9 @@ object RefChecks { | |
} else if (!other.is(Deferred) && | ||
!other.name.is(DefaultGetterName) && | ||
!member.isAnyOverride) { | ||
// (*) Exclusion for default getters, fixes SI-5178. We cannot assign the Override flag to | ||
// Exclusion for default getters, fixes SI-5178. We cannot assign the Override flag to | ||
// the default getter: one default getter might sometimes override, sometimes not. Example in comment on ticket. | ||
// Also exclusion for implicit shortcut methods | ||
// Also excluded under Scala2 mode are overrides of default methods of Java traits. | ||
if (autoOverride(member) || | ||
other.owner.is(JavaTrait) && ctx.testScala2Mode("`override' modifier required when a Java 8 default method is re-implemented", member.pos)) | ||
|
@@ -446,9 +447,12 @@ object RefChecks { | |
} | ||
|
||
def ignoreDeferred(member: SingleDenotation) = | ||
member.isType || | ||
member.symbol.isSuperAccessor || // not yet synthesized | ||
member.symbol.is(JavaDefined) && hasJavaErasedOverriding(member.symbol) | ||
member.isType || { | ||
val mbr = member.symbol | ||
mbr.isSuperAccessor || // not yet synthesized | ||
ShortcutImplicits.isImplicitShortcut(mbr) || // only synthesized when referenced | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The comment "only synthesized when referenced" is confusing, if I understand correctly direct method trees are always synthesized. |
||
mbr.is(JavaDefined) && hasJavaErasedOverriding(mbr) | ||
} | ||
|
||
// 2. Check that only abstract classes have deferred members | ||
def checkNoAbstractMembers(): Unit = { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
class A | ||
|
||
trait Foo { | ||
def foo: implicit A => Int | ||
} | ||
|
||
class Test { | ||
new FooI{} | ||
} | ||
|
||
class FooI extends Foo { | ||
def foo: implicit A => Int = 3 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package implicitShortcut | ||
|
||
class C | ||
abstract class Base[T] { | ||
|
||
def foo(x: T): implicit C => T = x | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package implicitShortcut | ||
|
||
class Derived extends Base[Int] { | ||
override def foo(x: Int): implicit C => Int = 42 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import implicitShortcut._ | ||
object Test extends App { | ||
|
||
val d = new Derived | ||
val b: Base[Int] = d | ||
implicit val c: C = new C | ||
|
||
assert(b.foo(1) == 42) | ||
assert(d.foo(1) == 42) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we change the type of
myErasurePhase
inContext
toDenotTransformer
, so that we don't need to pass the duplicate information around and keep the design consistent?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm. But then we have to cast myErasurePhase there, as
phaseOf
gives a phase, not a DenotTransformer.