aboutsummaryrefslogtreecommitdiff
path: root/compiler/src/dotty/tools/dotc/transform/ExplicitOuter.scala
blob: 7ad7fb34844754e36cc9762090194aa442e59ff6 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
package dotty.tools.dotc
package transform

import TreeTransforms._
import core.DenotTransformers._
import core.Symbols._
import core.Contexts._
import core.Types._
import core.Flags._
import core.Decorators._
import core.StdNames.nme
import core.Names._
import core.NameOps._
import core.NameKinds.OuterSelectName
import ast.Trees._
import SymUtils._
import dotty.tools.dotc.ast.tpd
import dotty.tools.dotc.core.Phases.Phase
import util.Property

import collection.mutable
import scala.annotation.tailrec

/** This phase adds outer accessors to classes and traits that need them.
 *  Compared to Scala 2.x, it tries to minimize the set of classes
 *  that take outer accessors by scanning class implementations for
 *  outer references.
 *
 *  The following things are delayed until erasure and are performed
 *  by class OuterOps:
 *
 *   - add outer parameters to constructors
 *   - pass outer arguments in constructor calls
 *
 *   replacement of outer this by outer paths is done in Erasure.
 *   needs to run after pattern matcher as it can add outer checks and force creation of $outer
 */
class ExplicitOuter extends MiniPhaseTransform with InfoTransformer { thisTransformer =>
  import ExplicitOuter._
  import ast.tpd._

  val Outer = new Property.Key[Tree]

  override def phaseName: String = "explicitOuter"

  /** List of names of phases that should have finished their processing of all compilation units
    * before this phase starts
    */
  override def runsAfter: Set[Class[_ <: Phase]] = Set(classOf[PatternMatcher])

  /** Add outer accessors if a class always needs an outer pointer */
  override def transformInfo(tp: Type, sym: Symbol)(implicit ctx: Context) = tp match {
    case tp @ ClassInfo(_, cls, _, decls, _) if needsOuterAlways(cls) && !sym.is(JavaDefined) =>
      val newDecls = decls.cloneScope
      newOuterAccessors(cls).foreach(newDecls.enter)
      tp.derivedClassInfo(decls = newDecls)
    case _ =>
      tp
  }

  override def mayChange(sym: Symbol)(implicit ctx: Context): Boolean = sym.isClass

  /** Convert a selection of the form `qual.C_<OUTER>` to an outer path from `qual` to `C` */
  override def transformSelect(tree: Select)(implicit ctx: Context, info: TransformerInfo) =
    tree.name match {
      case OuterSelectName(_, nhops) =>
        outer.path(start = tree.qualifier, count = nhops).ensureConforms(tree.tpe)
      case _ => tree
    }

  /** First, add outer accessors if a class does not have them yet and it references an outer this.
   *  If the class has outer accessors, implement them.
   *  Furthermore, if a parent trait might have an outer accessor,
   *  provide an implementation for the outer accessor by computing the parent's
   *  outer from the parent type prefix. If the trait ends up not having an outer accessor
   *  after all, the implementation is redundant, but does not harm.
   *  The same logic is not done for non-trait parent classes because for them the outer
   *  pointer is passed in the super constructor, which will be implemented later in
   *  a separate phase which needs to run after erasure. However, we make sure here
   *  that the super class constructor is indeed a New, and not just a type.
   */
  override def transformTemplate(impl: Template)(implicit ctx: Context, info: TransformerInfo): Tree = {
    val cls = ctx.owner.asClass
    val isTrait = cls.is(Trait)
    if (needsOuterIfReferenced(cls) &&
        !needsOuterAlways(cls) &&
        impl.existsSubTree(referencesOuter(cls, _)))
      ensureOuterAccessors(cls)

    val clsHasOuter = hasOuter(cls)
    if (clsHasOuter || cls.mixins.exists(needsOuterIfReferenced)) {
      val newDefs = new mutable.ListBuffer[Tree]

      if (clsHasOuter) {
        if (isTrait)
          newDefs += DefDef(outerAccessor(cls).asTerm, EmptyTree)
        else {
          val outerParamAcc = outerParamAccessor(cls)
          newDefs += ValDef(outerParamAcc, EmptyTree)
          newDefs += DefDef(outerAccessor(cls).asTerm, ref(outerParamAcc))
        }
      }

      for (parentTrait <- cls.mixins) {
        if (needsOuterIfReferenced(parentTrait)) {
          val parentTp = cls.denot.thisType.baseTypeRef(parentTrait)
          val outerAccImpl = newOuterAccessor(cls, parentTrait).enteredAfter(thisTransformer)
          newDefs += DefDef(outerAccImpl, singleton(fixThis(outerPrefix(parentTp))))
        }
      }

      val parents1 =
        for (parent <- impl.parents) yield {
          val parentCls = parent.tpe.classSymbol.asClass
          if (parentCls.is(Trait)) {
            parent
          }
          else parent match { // ensure class parent is a constructor
            case parent: TypeTree => New(parent.tpe, Nil).withPos(impl.pos)
            case _ => parent
          }
        }
      cpy.Template(impl)(parents = parents1, body = impl.body ++ newDefs)
    }
    else impl
  }

  override def transformClosure(tree: Closure)(implicit ctx: Context, info: TransformerInfo): tpd.Tree = {
    if (tree.tpt ne EmptyTree) {
      val cls = tree.tpt.asInstanceOf[TypeTree].tpe.classSymbol
      if (cls.exists && hasOuter(cls.asClass))
        ctx.error("Not a single abstract method type, requires an outer pointer", tree.pos)
    }
    tree
  }
}

object ExplicitOuter {
  import ast.tpd._

  /** Ensure that class `cls` has outer accessors */
  def ensureOuterAccessors(cls: ClassSymbol)(implicit ctx: Context): Unit = {
    //todo: implementing  #165 would simplify this logic
    val prevPhase = ctx.phase.prev
    assert(prevPhase.id <= ctx.explicitOuterPhase.id, "can add $outer symbols only before ExplicitOuter")
    assert(prevPhase.isInstanceOf[DenotTransformer], "adding outerAccessors requires being DenotTransformer")
    if (!hasOuter(cls)) {
      newOuterAccessors(cls).foreach(_.enteredAfter(prevPhase.asInstanceOf[DenotTransformer]))
    }
  }

  /** The outer accessor and potentially outer param accessor needed for class `cls` */
  private def newOuterAccessors(cls: ClassSymbol)(implicit ctx: Context) =
    newOuterAccessor(cls, cls) :: (if (cls is Trait) Nil else newOuterParamAccessor(cls) :: Nil)

  /** A new outer accessor or param accessor */
  private def newOuterSym(owner: ClassSymbol, cls: ClassSymbol, name: TermName, flags: FlagSet)(implicit ctx: Context) = {
    val target = cls.owner.enclosingClass.typeRef
    val info = if (flags.is(Method)) ExprType(target) else target
    ctx.withPhaseNoEarlier(ctx.explicitOuterPhase.next) // outer accessors are entered at explicitOuter + 1, should not be defined before.
       .newSymbol(owner, name, Synthetic | flags, info, coord = cls.coord)
  }

  /** A new param accessor for the outer field in class `cls` */
  private def newOuterParamAccessor(cls: ClassSymbol)(implicit ctx: Context) =
    newOuterSym(cls, cls, nme.OUTER, Private | ParamAccessor)

  /** A new outer accessor for class `cls` which is a member of `owner` */
  private def newOuterAccessor(owner: ClassSymbol, cls: ClassSymbol)(implicit ctx: Context) = {
    val deferredIfTrait = if (owner.is(Trait)) Deferred else EmptyFlags
    val outerAccIfOwn = if (owner == cls) OuterAccessor else EmptyFlags
    newOuterSym(owner, cls, outerAccName(cls),
      Final | Method | Stable | outerAccIfOwn | deferredIfTrait)
  }

  private def outerAccName(cls: ClassSymbol)(implicit ctx: Context): TermName =
    nme.OUTER.expandedName(cls)

  /** Class needs an outer pointer, provided there is a reference to an outer this in it. */
  def needsOuterIfReferenced(cls: ClassSymbol)(implicit ctx: Context): Boolean =
    !(cls.isStatic ||
      cls.owner.enclosingClass.isStaticOwner ||
      cls.is(PureInterface)
     )

  /** Class unconditionally needs an outer pointer. This is the case if
   *  the class needs an outer pointer if referenced and one of the following holds:
   *  - we might not know at all instantiation sites whether outer is referenced or not
   *  - we need to potentially pass along outer to a parent class or trait
   */
  private def needsOuterAlways(cls: ClassSymbol)(implicit ctx: Context): Boolean =
    needsOuterIfReferenced(cls) &&
    (!hasLocalInstantiation(cls) || // needs outer because we might not know whether outer is referenced or not
     cls.mixins.exists(needsOuterIfReferenced) || // needs outer for parent traits
     cls.classInfo.parents.exists(parent => // needs outer to potentially pass along to parent
       needsOuterIfReferenced(parent.classSymbol.asClass)))

  /** Class is always instantiated in the compilation unit where it is defined */
  private def hasLocalInstantiation(cls: ClassSymbol)(implicit ctx: Context): Boolean =
    // scala2x modules always take an outer pointer(as of 2.11)
    // dotty modules are always locally instantiated
    cls.owner.isTerm || cls.is(Private) || cls.is(Module, butNot = Scala2x)

  /** The outer parameter accessor of cass `cls` */
  private def outerParamAccessor(cls: ClassSymbol)(implicit ctx: Context): TermSymbol =
    cls.info.decl(nme.OUTER).symbol.asTerm

  /** The outer accessor of class `cls`. To find it is a bit tricky. The
   *  class might have been moved with new owners between ExplicitOuter and Erasure,
   *  where the method is also called. For instance, it might have been part
   *  of a by-name argument, and therefore be moved under a closure method
   *  by ElimByName. In that case looking up the method again at Erasure with the
   *  fully qualified name `outerAccName` will fail, because the `outerAccName`'s
   *  result is phase dependent. In that case we use a backup strategy where we search all
   *  definitions in the class to find the one with the OuterAccessor flag.
   */
  def outerAccessor(cls: ClassSymbol)(implicit ctx: Context): Symbol =
    if (cls.isStatic) NoSymbol // fast return to avoid scanning package decls
    else cls.info.member(outerAccName(cls)).suchThat(_ is OuterAccessor).symbol orElse
      cls.info.decls.find(_ is OuterAccessor)

  /** Class has an outer accessor. Can be called only after phase ExplicitOuter. */
  private def hasOuter(cls: ClassSymbol)(implicit ctx: Context): Boolean =
    needsOuterIfReferenced(cls) && outerAccessor(cls).exists

  /** Class constructor takes an outer argument. Can be called only after phase ExplicitOuter. */
  private def hasOuterParam(cls: ClassSymbol)(implicit ctx: Context): Boolean =
    !cls.is(Trait) && needsOuterIfReferenced(cls) && outerAccessor(cls).exists

  /** Tree references an outer class of `cls` which is not a static owner.
   */
  def referencesOuter(cls: Symbol, tree: Tree)(implicit ctx: Context): Boolean = {
    def isOuterSym(sym: Symbol) =
      !sym.isStaticOwner && cls.isProperlyContainedIn(sym)
    def isOuterRef(ref: Type): Boolean = ref match {
      case ref: ThisType =>
        isOuterSym(ref.cls)
      case ref: TermRef =>
        if (ref.prefix ne NoPrefix)
          !ref.symbol.isStatic && isOuterRef(ref.prefix)
        else (
          (ref.symbol is Hoistable) &&
            // ref.symbol will be placed in enclosing class scope by LambdaLift, so it might need
            // an outer path then.
            isOuterSym(ref.symbol.owner.enclosingClass)
          ||
            // If not hoistable, ref.symbol will get a proxy in immediately enclosing class. If this properly
            // contains the current class, it needs an outer path.
            // If the symbol is hoistable, it might have free variables for which the same
            // reasoning applies. See pos/i1664.scala
            ctx.owner.enclosingClass.owner.enclosingClass.isContainedIn(ref.symbol.owner)
          )
      case _ => false
    }
    def hasOuterPrefix(tp: Type) = tp match {
      case TypeRef(prefix, _) => isOuterRef(prefix)
      case _ => false
    }
    tree match {
      case _: This | _: Ident => isOuterRef(tree.tpe)
      case nw: New =>
        val newCls = nw.tpe.classSymbol
        isOuterSym(newCls.owner.enclosingClass) ||
        hasOuterPrefix(nw.tpe) ||
        newCls.owner.isTerm && cls.isProperlyContainedIn(newCls)
          // newCls might get proxies for free variables. If current class is
          // properly contained in newCls, it needs an outer path to newCls access the
          // proxies and forward them to the new instance.
      case _ =>
        false
    }
  }

  private final val Hoistable = Method | Lazy | Module

  /** The outer prefix implied by type `tpe` */
  private def outerPrefix(tpe: Type)(implicit ctx: Context): Type = tpe match {
    case tpe: TypeRef =>
      tpe.symbol match {
        case cls: ClassSymbol =>
          if (tpe.prefix eq NoPrefix) cls.owner.enclosingClass.thisType
          else tpe.prefix
        case _ =>
          // Need to be careful to dealias before erasure, otherwise we lose prefixes.
          outerPrefix(tpe.underlying(ctx.withPhaseNoLater(ctx.erasurePhase)))
      }
    case tpe: TypeProxy =>
      outerPrefix(tpe.underlying)
  }

  /** It's possible (i1755.scala gives an example) that the type
   *  given by outerPrefix contains a This-reference to a module outside
   *  the context where that module is defined. This needs to be translated
   *  to an access to the module object from the enclosing class or object.
   *
   *  This solution is a bit of a hack; it would be better to avoid
   *  such references to the This of a module from outside the module
   *  in the first place. I was not yet able to find out how such references
   *  arise and how to avoid them.
   */
  private def fixThis(tpe: Type)(implicit ctx: Context): Type = tpe match {
    case tpe: ThisType if tpe.cls.is(Module) && !ctx.owner.isContainedIn(tpe.cls) =>
      fixThis(TermRef(tpe.cls.owner.thisType, tpe.cls.sourceModule.asTerm))
    case tpe: TermRef =>
      tpe.derivedSelect(fixThis(tpe.prefix))
    case _ =>
      tpe
  }

  def outer(implicit ctx: Context): OuterOps = new OuterOps(ctx)

  /** The operations in this class
   *   - add outer parameters
   *   - pass outer arguments to these parameters
   *   - replace outer this references by outer paths.
   *  They are called from erasure. There are two constraints which
   *  suggest these operations should be done in erasure.
   *   - Replacing this references with outer paths loses aliasing information,
   *     so programs will not typecheck with unerased types unless a lot of type
   *     refinements are added. Therefore, outer paths should be computed no
   *     earlier than erasure.
   *   - outer parameters should not show up in signatures, so again
   *     they cannot be added before erasure.
   *   - outer arguments need access to outer parameters as well as to the
   *     original type prefixes of types in New expressions. These prefixes
   *     get erased during erasure. Therefore, outer arguments have to be passed
   *     no later than erasure.
   */
  class OuterOps(val ictx: Context) extends AnyVal {
    private implicit def ctx: Context = ictx

    /** If `cls` has an outer parameter add one to the method type `tp`. */
    def addParam(cls: ClassSymbol, tp: Type): Type =
      if (hasOuterParam(cls)) {
        val mt @ MethodTpe(pnames, ptypes, restpe) = tp
        mt.derivedLambdaType(
          nme.OUTER :: pnames, cls.owner.enclosingClass.typeRef :: ptypes, restpe)
      } else tp

    /** If function in an apply node is a constructor that needs to be passed an
     *  outer argument, the singleton list with the argument, otherwise Nil.
     */
    def args(fun: Tree): List[Tree] = {
      if (fun.symbol.isConstructor) {
        val cls = fun.symbol.owner.asClass
        def outerArg(receiver: Tree): Tree = receiver match {
          case New(_) | Super(_, _) =>
            singleton(fixThis(outerPrefix(receiver.tpe)))
          case This(_) =>
            ref(outerParamAccessor(cls)) // will be rewired to outer argument of secondary constructor in phase Constructors
          case TypeApply(Select(r, nme.asInstanceOf_), args) =>
            outerArg(r) // cast was inserted, skip
        }
        if (hasOuterParam(cls))
          methPart(fun) match {
            case Select(receiver, _) => outerArg(receiver).withPos(fun.pos) :: Nil
          }
        else Nil
      } else Nil
    }

    /** A path of outer accessors starting from node `start`. `start` defaults to the
     *  context owner's this node. There are two alternative conditions that determine
     *  where the path ends:
     *
     *   - if the initial `count` parameter is non-negative: where the number of
     *     outer accessors reaches count.
     *   - if the initial `count` parameter is negative: where the class symbol of
     *     the type of the reached tree matches `toCls`.
     */
    def path(start: Tree = This(ctx.owner.lexicallyEnclosingClass.asClass),
             toCls: Symbol = NoSymbol,
             count: Int = -1): Tree = try {
      @tailrec def loop(tree: Tree, count: Int): Tree = {
        val treeCls = tree.tpe.widen.classSymbol
        val outerAccessorCtx = ctx.withPhaseNoLater(ctx.lambdaLiftPhase) // lambdalift mangles local class names, which means we cannot reliably find outer acessors anymore
        ctx.log(i"outer to $toCls of $tree: ${tree.tpe}, looking for ${outerAccName(treeCls.asClass)(outerAccessorCtx)} in $treeCls")
        if (count == 0 || count < 0 && treeCls == toCls) tree
        else {
          val acc = outerAccessor(treeCls.asClass)(outerAccessorCtx)
          assert(acc.exists,
              i"failure to construct path from ${ctx.owner.ownersIterator.toList}%/% to `this` of ${toCls.showLocated};\n${treeCls.showLocated} does not have an outer accessor")
          loop(tree.select(acc).ensureApplied, count - 1)
        }
      }
      ctx.log(i"computing outerpath to $toCls from ${ctx.outersIterator.map(_.owner).toList}")
      loop(start, count)
    } catch {
      case ex: ClassCastException =>
        throw new ClassCastException(i"no path exists from ${ctx.owner.enclosingClass} to $toCls")
    }

    /** The outer parameter definition of a constructor if it needs one */
    def paramDefs(constr: Symbol): List[ValDef] =
      if (constr.isConstructor && hasOuterParam(constr.owner.asClass)) {
        val MethodTpe(outerName :: _, outerType :: _, _) = constr.info
        val outerSym = ctx.newSymbol(constr, outerName, Param, outerType)
        ValDef(outerSym) :: Nil
      }
      else Nil
  }
}