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

Alternative fix for #6909: Use @cached annotation #6967

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from 7 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
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/Compiler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class Compiler {
new ExpandSAMs, // Expand single abstract method closures to anonymous classes
new ProtectedAccessors, // Add accessors for protected members
new ExtensionMethods, // Expand methods of value classes with extension methods
new CacheAliasImplicits, // Cache RHS of parameterless alias implicits
new CacheDefs , // Cache RHS of parameterless alias implicits and @cached methods
new ShortcutImplicits, // Allow implicit functions without creating closures
new ByNameClosures, // Expand arguments to by-name parameters to closures
new HoistSuperArgs, // Hoist complex arguments of supercalls to enclosing scope
Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,7 @@ class Definitions {
@threadUnsafe lazy val FunctionalInterfaceAnnot: ClassSymbolPerRun = perRunClass(ctx.requiredClassRef("java.lang.FunctionalInterface"))
@threadUnsafe lazy val InfixAnnot: ClassSymbolPerRun = perRunClass(ctx.requiredClassRef("scala.annotation.infix"))
@threadUnsafe lazy val AlphaAnnot: ClassSymbolPerRun = perRunClass(ctx.requiredClassRef("scala.annotation.alpha"))
@threadUnsafe lazy val CachedAnnot: ClassSymbol = ctx.requiredClass("scala.annotation.meta.cached")

// convenient one-parameter method types
def methOfAny(tp: Type): MethodType = MethodType(List(AnyType), tp)
Expand Down
8 changes: 6 additions & 2 deletions compiler/src/dotty/tools/dotc/core/SymDenotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,12 @@ trait SymDenotations { this: Context =>
}
}

/** Configurable: Accept stale symbol with warning if in IDE */
def staleOK: Boolean = Config.ignoreStaleInIDE && mode.is(Mode.Interactive)
/** Configurable: Accept stale symbol with warning if in IDE
* Always accept stale symbols when testing pickling.
*/
def staleOK: Boolean =
Config.ignoreStaleInIDE && mode.is(Mode.Interactive) ||
settings.YtestPickler.value

/** Possibly accept stale symbol with warning if in IDE */
def acceptStale(denot: SingleDenotation): Boolean =
Expand Down
25 changes: 14 additions & 11 deletions compiler/src/dotty/tools/dotc/core/Symbols.scala
Original file line number Diff line number Diff line change
Expand Up @@ -553,9 +553,10 @@ object Symbols {

/** This symbol entered into owner's scope (owner must be a class). */
final def entered(implicit ctx: Context): this.type = {
assert(this.owner.isClass, s"symbol ($this) entered the scope of non-class owner ${this.owner}") // !!! DEBUG
this.owner.asClass.enter(this)
if (this.is(Module)) this.owner.asClass.enter(this.moduleClass)
if (this.owner.isClass) {
this.owner.asClass.enter(this)
if (this.is(Module)) this.owner.asClass.enter(this.moduleClass)
}
this
}

Expand All @@ -566,14 +567,16 @@ object Symbols {
*/
def enteredAfter(phase: DenotTransformer)(implicit ctx: Context): this.type =
if (ctx.phaseId != phase.next.id) enteredAfter(phase)(ctx.withPhase(phase.next))
else {
if (this.owner.is(Package)) {
denot.validFor |= InitialPeriod
if (this.is(Module)) this.moduleClass.validFor |= InitialPeriod
}
else this.owner.asClass.ensureFreshScopeAfter(phase)
assert(isPrivate || phase.changesMembers, i"$this entered in ${this.owner} at undeclared phase $phase")
entered
else this.owner match {
case owner: ClassSymbol =>
if (owner.is(Package)) {
denot.validFor |= InitialPeriod
if (this.is(Module)) this.moduleClass.validFor |= InitialPeriod
}
else owner.ensureFreshScopeAfter(phase)
assert(isPrivate || phase.changesMembers, i"$this entered in $owner at undeclared phase $phase")
entered
case _ => this
}

/** Remove symbol from scope of owning class */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ class CacheAliasImplicits extends MiniPhase with IdentityDenotTransformer { this
val cacheFlags = if (ctx.owner.isClass) Private | Local | Mutable else Mutable
val cacheSym =
ctx.newSymbol(ctx.owner, CacheName(tree.name), cacheFlags, rhsType, coord = sym.coord)
if (ctx.owner.isClass) cacheSym.enteredAfter(thisPhase)
.enteredAfter(thisPhase)
val cacheDef = ValDef(cacheSym, tpd.defaultValue(rhsType))
val cachingDef = cpy.DefDef(tree)(rhs =
Block(
Expand Down
111 changes: 111 additions & 0 deletions compiler/src/dotty/tools/dotc/transform/CacheDefs.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package dotty.tools.dotc
package transform

import MegaPhase._
import core._
import DenotTransformers.{IdentityDenotTransformer}
import Symbols._, Contexts._, Types._, Flags._, NameOps._, Decorators._
import StdNames.nme
import NameKinds.CacheName
import Constants.Constant
import ast.tpd
import collection.mutable

object CacheDefs {
val name: String = "cacheDefs"

/** Flags that disable caching */
val NoCacheFlags =
StableRealizable | // It's a simple forwarder, leave it as one
Exported // Export forwarders are never cached
}

/** This phase ensures that the right hand side of parameterless alias implicits
* is cached. It applies to all alias implicits that have neither type parameters
* nor a given clause. Example: The alias
*
* implicit a for TC = rhs
*
* is expanded before this phase
*
* implicit def a: TC = rhs
*
* It is then expanded further as follows:
*
* 1. If `rhs` is a simple name `x` (possibly with a `this.` prefix), leave the definition as is.
* 2. Otherwise, if `rhs` is a pure path, replace the definition with
*
* implicit val a: TC = rhs
*
* 3. Otherwise, if `TC` is a reference type, replace the definition with
*
* private[this] var a$_cache: TC = null
* implicit def a: TC = { if (a$_cache == null) a$_cache = rhs; a$_cache }
*
* 4. Otherwise `TC` is a value type. Replace the definition with
*
* lazy implicit val a: TC = rhs
*/
class CacheDefs extends MiniPhase with IdentityDenotTransformer { thisPhase =>
import tpd._

override def phaseName: String = CacheDefs.name

override def transformDefDef(tree: DefDef) given (ctx: Context): Tree = {
val sym = tree.symbol.asTerm
val isCached = sym.hasAnnotation(defn.CachedAnnot) || {
sym.info match {
case _: ExprType if sym.is(Given, butNot = CacheAliasImplicits.NoCacheFlags) =>
tree.rhs.tpe match {
case rhsTpe @ TermRef(NoPrefix, _)
if rhsTpe.isStable => false
case rhsTpe @ TermRef(pre: ThisType, _)
if rhsTpe.isStable && pre.cls == sym.owner.enclosingClass => false
case _ => true
}
case _ => false
}
}
if (isCached) {
val cachedType = sym.info.widenExpr match {
case mt: MethodType => mt.finalResultType
case cachedType => cachedType
}
val cacheVar = ctx.newSymbol(
owner = sym.owner,
name = CacheName(sym.name),
flags = if (sym.owner.isClass) Private | Local | Mutable else Mutable,
info = OrType(cachedType, defn.NullType),
coord = tree.rhs.span
).enteredAfter(thisPhase)
val cacheSetter =
if (sym.owner.is(Trait))
ctx.newSymbol(
owner = sym.owner,
name = cacheVar.name.setterName,
flags = cacheVar.flags | Method | Accessor,
info = MethodType(cachedType :: Nil, defn.UnitType),
coord = tree.rhs.span
).enteredAfter(thisPhase)
else cacheVar
val cachedDefs = new mutable.ListBuffer[Tree]()
cachedDefs += ValDef(cacheVar, nullLiteral)
if (cacheSetter ne cacheVar)
cachedDefs += DefDef(cacheSetter, _ => Literal(Constant(())))
cachedDefs += cpy.DefDef(tree)(rhs =
Block(
If(
ref(cacheVar).select(defn.Any_==).appliedTo(nullLiteral),
ref(cacheSetter).becomes(tree.rhs),
unitLiteral) :: Nil,
ref(cacheVar).cast(cachedType)
)
)
Thicket(cachedDefs.toList)
}
else tree
}
}



6 changes: 3 additions & 3 deletions compiler/src/dotty/tools/dotc/transform/HoistSuperArgs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,13 @@ class HoistSuperArgs extends MiniPhase with IdentityDenotTransformer { thisPhase
val argTypeWrtConstr = argType.subst(origParams, allParamRefs(constr.info))
// argType with references to paramRefs of the primary constructor instead of
// local parameter accessors
val meth = ctx.newSymbol(
ctx.newSymbol(
owner = methOwner,
name = SuperArgName.fresh(cls.name.toTermName),
flags = Synthetic | Private | Method | staticFlag,
info = replaceResult(constr.info, argTypeWrtConstr),
coord = constr.coord)
if (methOwner.isClass) meth.enteredAfter(thisPhase) else meth
coord = constr.coord
).enteredAfter(thisPhase)
}

/** Type of a reference implies that it needs to be hoisted */
Expand Down
5 changes: 3 additions & 2 deletions compiler/src/dotty/tools/dotc/transform/LambdaLift.scala
Original file line number Diff line number Diff line change
Expand Up @@ -298,8 +298,9 @@ object LambdaLift {
proxyMap(owner) = {
for (fv <- freeValues.toList) yield {
val proxyName = newName(fv)
val proxy = ctx.newSymbol(owner, proxyName.asTermName, newFlags, fv.info, coord = fv.coord)
if (owner.isClass) proxy.enteredAfter(thisPhase)
val proxy =
ctx.newSymbol(owner, proxyName.asTermName, newFlags, fv.info, coord = fv.coord)
.enteredAfter(thisPhase)
(fv, proxy)
}
}.toMap
Expand Down
14 changes: 6 additions & 8 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2132,15 +2132,13 @@ class Typer extends Namer
buf += inlineExpansion(mdef1)
// replace body with expansion, because it will be used as inlined body
// from separately compiled files - the original BodyAnnotation is not kept.
case mdef1: TypeDef if mdef1.symbol.is(Enum, butNot = Case) =>
enumContexts(mdef1.symbol) = ctx
buf += mdef1
case EmptyTree =>
// clashing synthetic case methods are converted to empty trees, drop them here
case mdef1 =>
import untpd.modsDeco
mdef match {
case mdef: untpd.TypeDef if mdef.mods.isEnumClass =>
enumContexts(mdef1.symbol) = ctx
case _ =>
}
if (!mdef1.isEmpty) // clashing synthetic case methods are converted to empty trees
buf += mdef1
buf += mdef1
}
traverse(rest)
}
Expand Down
10 changes: 1 addition & 9 deletions docs/docs/reference/contextual/relationship-implicits.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,12 @@ Given instances can be mapped to combinations of implicit objects, classes and i
class ListOrd[T](implicit ord: Ord[T]) extends Ord[List[T]] { ... }
final implicit def ListOrd[T](implicit ord: Ord[T]): ListOrd[T] = new ListOrd[T]
```
3. Alias givens map to implicit methods. If an alias has neither type parameters nor a given clause, its right-hand side is cached in a variable. There are two cases that can be optimized:

- If the right hand side is a simple reference, we can
use a forwarder to that reference without caching it.
- If the right hand side is more complex, but still known to be pure, we can
create a `val` that computes it ahead of time.
3. Alias givens map to implicit methods. If an alias has neither type parameters nor a given clause, its right-hand side is cached in a variable, unless the right hand side is a simple reference in which case we can use a forwarder to that reference without caching it.

Examples:

```scala
given global as ExecutionContext = new ForkJoinContext()
given config as Config = default.config

val ctx: Context
given as Context = ctx
Expand All @@ -52,8 +46,6 @@ Given instances can be mapped to combinations of implicit objects, classes and i
global$cache
}

final implicit val config: Config = default.config

final implicit def Context_given = ctx
```

Expand Down
4 changes: 4 additions & 0 deletions library/src/scala/annotation/meta/cached.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package scala.annotation
package meta

class cached extends Annotation
7 changes: 7 additions & 0 deletions tests/pos/i6909.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import scala.compiletime
trait Foo[A]


trait Qux {
given as Foo[Int] = new Foo[Int] {}
}