Skip to content

Commit 64bd0fe

Browse files
authored
Merge pull request #4772 from dotty-staging/fix-#4753
Fix #4753: Ignore implicit shortcuts in RefChecks
2 parents 7a80060 + ca23f25 commit 64bd0fe

File tree

8 files changed

+133
-62
lines changed

8 files changed

+133
-62
lines changed

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

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ import ast.untpd
99
import collection.{mutable, immutable}
1010
import TypeErasure._
1111
import ValueClasses.isDerivedValueClass
12+
import ShortcutImplicits._
1213

1314
/** A helper class for generating bridge methods in class `root`. */
14-
class Bridges(root: ClassSymbol)(implicit ctx: Context) {
15+
class Bridges(root: ClassSymbol, thisPhase: DenotTransformer)(implicit ctx: Context) {
1516
import ast.tpd._
1617

1718
assert(ctx.phase == ctx.erasurePhase.next)
@@ -26,7 +27,11 @@ class Bridges(root: ClassSymbol)(implicit ctx: Context) {
2627
* only in classes, never in traits.
2728
*/
2829
override def parents = Array(root.superClass)
29-
override def exclude(sym: Symbol) = !sym.is(MethodOrModule) || super.exclude(sym)
30+
31+
override def exclude(sym: Symbol) =
32+
!sym.is(MethodOrModule) ||
33+
isImplicitShortcut(sym) ||
34+
super.exclude(sym)
3035
}
3136

3237
//val site = root.thisType
@@ -81,8 +86,7 @@ class Bridges(root: ClassSymbol)(implicit ctx: Context) {
8186
owner = root,
8287
flags = (member.flags | Method | Bridge | Artifact) &~
8388
(Accessor | ParamAccessor | CaseAccessor | Deferred | Lazy | Module),
84-
coord = bridgePosFor(member))
85-
.enteredAfter(ctx.erasurePhase.asInstanceOf[DenotTransformer]).asTerm
89+
coord = bridgePosFor(member)).enteredAfter(thisPhase).asTerm
8690

8791
ctx.debuglog(
8892
i"""generating bridge from ${other.showLocated}: ${other.info}
@@ -111,8 +115,19 @@ class Bridges(root: ClassSymbol)(implicit ctx: Context) {
111115
*/
112116
def add(stats: List[untpd.Tree]): List[untpd.Tree] = {
113117
val opc = new BridgesCursor()(preErasureCtx)
118+
val ectx = ctx.withPhase(thisPhase)
114119
while (opc.hasNext) {
115-
if (!opc.overriding.is(Deferred)) addBridgeIfNeeded(opc.overriding, opc.overridden)
120+
if (!opc.overriding.is(Deferred)) {
121+
addBridgeIfNeeded(opc.overriding, opc.overridden)
122+
123+
if (needsImplicitShortcut(opc.overriding)(ectx))
124+
// implicit shortcuts do not show up in the Bridges cursor, since they
125+
// are created only when referenced. Therefore we need to generate a bridge
126+
// for them specifically, if one is needed for the original methods.
127+
addBridgeIfNeeded(
128+
shortcutMethod(opc.overriding, thisPhase)(ectx),
129+
shortcutMethod(opc.overridden, thisPhase)(ectx))
130+
}
116131
opc.next()
117132
}
118133
if (bridges.isEmpty) stats

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ class Erasure extends Phase with DenotTransformer {
9191
ref.derivedSingleDenotation(ref.symbol, transformInfo(ref.symbol, ref.symbol.info))
9292
}
9393

94-
val eraser = new Erasure.Typer
94+
val eraser = new Erasure.Typer(this)
9595

9696
def run(implicit ctx: Context): Unit = {
9797
val unit = ctx.compilationUnit
@@ -314,7 +314,7 @@ object Erasure {
314314
}
315315
}
316316

317-
class Typer extends typer.ReTyper with NoChecking {
317+
class Typer(erasurePhase: DenotTransformer) extends typer.ReTyper with NoChecking {
318318
import Boxing._
319319

320320
def erasedType(tree: untpd.Tree)(implicit ctx: Context): Type = {
@@ -672,7 +672,7 @@ object Erasure {
672672

673673
override def typedStats(stats: List[untpd.Tree], exprOwner: Symbol)(implicit ctx: Context): List[Tree] = {
674674
val stats1 =
675-
if (takesBridges(ctx.owner)) new Bridges(ctx.owner.asClass).add(stats)
675+
if (takesBridges(ctx.owner)) new Bridges(ctx.owner.asClass, erasurePhase).add(stats)
676676
else stats
677677
super.typedStats(stats1, exprOwner).filter(!_.isEmpty)
678678
}

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

Lines changed: 66 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package dotty.tools.dotc
22
package transform
33

44
import MegaPhase._
5-
import core.DenotTransformers.IdentityDenotTransformer
5+
import core.DenotTransformers.{DenotTransformer, IdentityDenotTransformer}
66
import core.Symbols._
77
import core.Contexts._
88
import core.Types._
@@ -44,8 +44,21 @@ import collection.mutable
4444
* (2) A reference `qual.apply` where `qual` has implicit function type and
4545
* `qual` refers to a method `m` is rewritten to a reference to `m$direct`,
4646
* keeping the same type and value arguments as they are found in `qual`.
47+
*
48+
* Note: The phase adds direct methods for all methods with IFT results that
49+
* are defined in the transformed compilation unit, as well as for all
50+
* methods that are referenced from inside the unit. It does NOT do an
51+
* info transformer that adds these methods everywhere where an IFT returning
52+
* method exists (including in separately compiled classes).
53+
* Adding such an info transformer is impractical because it would mean
54+
* that we have to force the types of all members of classes that are referenced.
55+
* But not adding an info transformer can lead to inconsistencies in RefChecks.
56+
* We solve that by ignoring direct methods in Refchecks.
57+
* Another, related issue is bridge generation, where we also generate
58+
* shortcut methods on the fly.
4759
*/
4860
class ShortcutImplicits extends MiniPhase with IdentityDenotTransformer { thisPhase =>
61+
import ShortcutImplicits._
4962
import tpd._
5063

5164
override def phaseName: String = ShortcutImplicits.name
@@ -61,82 +74,38 @@ class ShortcutImplicits extends MiniPhase with IdentityDenotTransformer { thisPh
6174
override def initContext(ctx: FreshContext) =
6275
DirectMeth = ctx.addLocation[MutableSymbolMap[Symbol]]()
6376

64-
/** If this option is true, we don't specialize symbols that are known to be only
65-
* targets of monomorphic calls.
66-
* The reason for this option is that benchmarks show that on the JVM for monomorphic dispatch
67-
* scenarios inlining and escape analysis can often remove all calling overhead, so we might as
68-
* well not duplicate the code. We need more experience to decide on the best setting of this option.
69-
*/
70-
final val specializeMonoTargets = true
7177

7278
override def prepareForUnit(tree: Tree)(implicit ctx: Context) =
7379
ctx.fresh.updateStore(DirectMeth, newMutableSymbolMap[Symbol])
7480

75-
/** Should `sym` get a ..$direct companion?
76-
* This is the case if `sym` is a method with a non-nullary implicit function type as final result type.
77-
* However if `specializeMonoTargets` is false, we exclude symbols that are known
78-
* to be only targets of monomorphic calls because they are effectively
79-
* final and don't override anything.
80-
*/
81-
private def shouldBeSpecialized(sym: Symbol)(implicit ctx: Context) =
82-
sym.is(Method, butNot = Accessor) &&
83-
defn.isImplicitFunctionType(sym.info.finalResultType) &&
84-
defn.functionArity(sym.info.finalResultType) > 0 &&
85-
!sym.isAnonymousFunction &&
86-
(specializeMonoTargets || !sym.isEffectivelyFinal || sym.allOverriddenSymbols.nonEmpty)
87-
88-
/** @pre The type's final result type is an implicit function type `implicit Ts => R`.
89-
* @return The type of the `apply` member of `implicit Ts => R`.
90-
*/
91-
private def directInfo(info: Type)(implicit ctx: Context): Type = info match {
92-
case info: PolyType => info.derivedLambdaType(resType = directInfo(info.resultType))
93-
case info: MethodType => info.derivedLambdaType(resType = directInfo(info.resultType))
94-
case info: ExprType => directInfo(info.resultType)
95-
case info => info.member(nme.apply).info
96-
}
97-
98-
/** A new `m$direct` method to accompany the given method `m` */
99-
private def newDirectMethod(sym: Symbol)(implicit ctx: Context): Symbol = {
100-
val direct = sym.copy(
101-
name = DirectMethodName(sym.name.asTermName).asInstanceOf[sym.ThisName],
102-
flags = sym.flags | Synthetic,
103-
info = directInfo(sym.info))
104-
if (direct.allOverriddenSymbols.isEmpty) direct.resetFlag(Override)
105-
direct
106-
}
10781

10882
/** The direct method `m$direct` that accompanies the given method `m`.
10983
* Create one if it does not exist already.
11084
*/
11185
private def directMethod(sym: Symbol)(implicit ctx: Context): Symbol =
112-
if (sym.owner.isClass) {
113-
val direct = sym.owner.info.member(DirectMethodName(sym.name.asTermName))
114-
.suchThat(_.info matches directInfo(sym.info)).symbol
115-
if (direct.maybeOwner == sym.owner) direct
116-
else newDirectMethod(sym).enteredAfter(thisPhase)
117-
}
118-
else directMeth.getOrElseUpdate(sym, newDirectMethod(sym))
86+
if (sym.owner.isClass) shortcutMethod(sym, thisPhase)
87+
else directMeth.getOrElseUpdate(sym, newShortcutMethod(sym))
11988

12089
/** Transform `qual.apply` occurrences according to rewrite rule (2) above */
12190
override def transformSelect(tree: Select)(implicit ctx: Context) =
12291
if (tree.name == nme.apply &&
12392
defn.isImplicitFunctionType(tree.qualifier.tpe.widen) &&
124-
shouldBeSpecialized(tree.qualifier.symbol)) {
93+
needsImplicitShortcut(tree.qualifier.symbol)) {
12594
def directQual(tree: Tree): Tree = tree match {
12695
case Apply(fn, args) => cpy.Apply(tree)(directQual(fn), args)
12796
case TypeApply(fn, args) => cpy.TypeApply(tree)(directQual(fn), args)
12897
case Block(stats, expr) => cpy.Block(tree)(stats, directQual(expr))
12998
case tree: RefTree =>
13099
cpy.Ref(tree)(DirectMethodName(tree.name.asTermName))
131-
.withType(directMethod(tree.symbol).termRef)
100+
.withType(tree.tpe.asInstanceOf[NamedType].prefix.select(directMethod(tree.symbol)))
132101
}
133102
directQual(tree.qualifier)
134103
} else tree
135104

136105
/** Transform methods with implicit function type result according to rewrite rule (1) above */
137106
override def transformDefDef(mdef: DefDef)(implicit ctx: Context): Tree = {
138107
val original = mdef.symbol
139-
if (shouldBeSpecialized(original)) {
108+
if (needsImplicitShortcut(original)) {
140109
val direct = directMethod(original)
141110

142111
// Move @tailrec to the direct method
@@ -179,4 +148,51 @@ class ShortcutImplicits extends MiniPhase with IdentityDenotTransformer { thisPh
179148

180149
object ShortcutImplicits {
181150
val name = "shortcutImplicits"
151+
152+
/** If this option is true, we don't specialize symbols that are known to be only
153+
* targets of monomorphic calls.
154+
* The reason for this option is that benchmarks show that on the JVM for monomorphic dispatch
155+
* scenarios inlining and escape analysis can often remove all calling overhead, so we might as
156+
* well not duplicate the code. We need more experience to decide on the best setting of this option.
157+
*/
158+
final val specializeMonoTargets = true
159+
160+
/** Should `sym` get a ..$direct companion?
161+
* This is the case if `sym` is a method with a non-nullary implicit function type as final result type.
162+
* However if `specializeMonoTargets` is false, we exclude symbols that are known
163+
* to be only targets of monomorphic calls because they are effectively
164+
* final and don't override anything.
165+
*/
166+
def needsImplicitShortcut(sym: Symbol)(implicit ctx: Context) =
167+
sym.is(Method, butNot = Accessor) &&
168+
defn.isImplicitFunctionType(sym.info.finalResultType) &&
169+
defn.functionArity(sym.info.finalResultType) > 0 &&
170+
!sym.isAnonymousFunction &&
171+
(specializeMonoTargets || !sym.isEffectivelyFinal || sym.allOverriddenSymbols.nonEmpty)
172+
173+
/** @pre The type's final result type is an implicit function type `implicit Ts => R`.
174+
* @return The type of the `apply` member of `implicit Ts => R`.
175+
*/
176+
private def directInfo(info: Type)(implicit ctx: Context): Type = info match {
177+
case info: PolyType => info.derivedLambdaType(resType = directInfo(info.resultType))
178+
case info: MethodType => info.derivedLambdaType(resType = directInfo(info.resultType))
179+
case info: ExprType => directInfo(info.resultType)
180+
case info => info.member(nme.apply).info
181+
}
182+
183+
/** A new `m$direct` method to accompany the given method `m` */
184+
private def newShortcutMethod(sym: Symbol)(implicit ctx: Context): Symbol =
185+
sym.copy(
186+
name = DirectMethodName(sym.name.asTermName).asInstanceOf[sym.ThisName],
187+
flags = sym.flags &~ Override | Synthetic,
188+
info = directInfo(sym.info))
189+
190+
def shortcutMethod(sym: Symbol, phase: DenotTransformer)(implicit ctx: Context) =
191+
sym.owner.info.decl(DirectMethodName(sym.name.asTermName))
192+
.suchThat(_.info matches directInfo(sym.info)).symbol
193+
.orElse(newShortcutMethod(sym).enteredAfter(phase))
194+
195+
def isImplicitShortcut(sym: Symbol)(implicit ctx: Context) = sym.name.is(DirectMethodName)
182196
}
197+
198+

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -336,8 +336,9 @@ object RefChecks {
336336
} else if (!other.is(Deferred) &&
337337
!other.name.is(DefaultGetterName) &&
338338
!member.isAnyOverride) {
339-
// (*) Exclusion for default getters, fixes SI-5178. We cannot assign the Override flag to
339+
// Exclusion for default getters, fixes SI-5178. We cannot assign the Override flag to
340340
// the default getter: one default getter might sometimes override, sometimes not. Example in comment on ticket.
341+
// Also exclusion for implicit shortcut methods
341342
// Also excluded under Scala2 mode are overrides of default methods of Java traits.
342343
if (autoOverride(member) ||
343344
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 {
446447
}
447448

448449
def ignoreDeferred(member: SingleDenotation) =
449-
member.isType ||
450-
member.symbol.isSuperAccessor || // not yet synthesized
451-
member.symbol.is(JavaDefined) && hasJavaErasedOverriding(member.symbol)
450+
member.isType || {
451+
val mbr = member.symbol
452+
mbr.isSuperAccessor || // not yet synthesized
453+
ShortcutImplicits.isImplicitShortcut(mbr) || // only synthesized when referenced, see Note in ShortcutImplicits
454+
mbr.is(JavaDefined) && hasJavaErasedOverriding(mbr)
455+
}
452456

453457
// 2. Check that only abstract classes have deferred members
454458
def checkNoAbstractMembers(): Unit = {

tests/pos/i4753.scala

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
class A
2+
3+
trait Foo {
4+
def foo: implicit A => Int
5+
}
6+
7+
class Test {
8+
new FooI{}
9+
}
10+
11+
class FooI extends Foo {
12+
def foo: implicit A => Int = 3
13+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package implicitShortcut
2+
3+
class C
4+
abstract class Base[T] {
5+
6+
def foo(x: T): implicit C => T = x
7+
8+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package implicitShortcut
2+
3+
class Derived extends Base[Int] {
4+
override def foo(x: Int): implicit C => Int = 42
5+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import implicitShortcut._
2+
object Test extends App {
3+
4+
val d = new Derived
5+
val b: Base[Int] = d
6+
implicit val c: C = new C
7+
8+
assert(b.foo(1) == 42)
9+
assert(d.foo(1) == 42)
10+
}

0 commit comments

Comments
 (0)