Skip to content

Commit

Permalink
Generate static inline accessors module
Browse files Browse the repository at this point in the history
If a class `C` needs inline accessors that would be added top-level
or if the accessor is to a static member, we place it in a new
invisible module `C$inline$accessors`.

If the accessor location in the new scheme is not the same as the
previous location, we also generate the old accessor for backward binary
compatibility but do not use it.

Fixes scala#13215
Fixes scala#15413
  • Loading branch information
nicolasstucki committed Feb 7, 2023
1 parent 2c893ff commit ec9d062
Show file tree
Hide file tree
Showing 32 changed files with 376 additions and 6 deletions.
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/core/StdNames.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ object StdNames {
inline val AVOID_CLASH_SUFFIX = "$_avoid_name_clash_$"
inline val MODULE_SUFFIX = "$"
inline val TOPLEVEL_SUFFIX = "$package"
inline val TOPLEVEL_INLINE_SUFFIX = "$inline$accessors"
inline val NAME_JOIN = "$"
inline val DEFAULT_GETTER = "$default$"
inline val LOCALDUMMY_PREFIX = "<local " // owner of local blocks
Expand Down
46 changes: 42 additions & 4 deletions compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package dotty.tools
package dotc
package inlines

import scala.collection.mutable

import dotty.tools.dotc.ast.{Trees, tpd, untpd}
import Trees._
import core._
Expand All @@ -22,14 +24,21 @@ import transform.SymUtils.*
import config.Printers.inlining
import util.Property
import dotty.tools.dotc.transform.TreeMapWithStages._
import dotty.tools.dotc.ast.desugar.packageObjectName
import dotty.tools.dotc.core.StdNames.str
import dotty.tools.dotc.util.{Spans, SrcPos}
import dotty.tools.dotc.typer.TopLevelExtensionModules.newAccessorModule

object PrepareInlineable {
import tpd._

private val InlineAccessorsKey = new Property.Key[InlineAccessors]
private val InlineAccessorsModuleKey = new Property.Key[mutable.Map[Symbol, Symbol]]

def initContext(ctx: Context): Context =
ctx.fresh.setProperty(InlineAccessorsKey, new InlineAccessors)
ctx.fresh
.setProperty(InlineAccessorsKey, new InlineAccessors)
.setProperty(InlineAccessorsModuleKey, mutable.Map.empty)

def makeInlineable(tree: Tree)(using Context): Tree =
ctx.property(InlineAccessorsKey).get.makeInlineable(tree)
Expand All @@ -39,6 +48,19 @@ object PrepareInlineable {
case Some(inlineAccessors) => inlineAccessors.addAccessorDefs(cls, body)
case _ => body

def inlineAccessorsModule(topLevelClass: Symbol)(using Context): Symbol =
assert(topLevelClass.asClass.owner.is(Package), topLevelClass)
ctx.property(InlineAccessorsModuleKey).get.getOrElse(topLevelClass, NoSymbol)

def requiredInlineAccessorsModule(topLevelClass: Symbol)(using Context): Symbol =
assert(topLevelClass.asClass.owner.is(Package), topLevelClass)
ctx.property(InlineAccessorsModuleKey) match
case Some(inlineAccessorsModule) =>
inlineAccessorsModule.getOrElseUpdate(
topLevelClass,
newAccessorModule(str.TOPLEVEL_INLINE_SUFFIX))
case None => NoSymbol

class InlineAccessors extends AccessProxies {

/** If an inline accessor name wraps a unique inline name, this is taken as indication
Expand Down Expand Up @@ -99,20 +121,32 @@ object PrepareInlineable {
* advantage that we can re-use the receiver as is. But it is only
* possible if the receiver is essentially this or an outer this, which is indicated
* by the test that we can find a host for the accessor.
*
* @param inlineSym symbol of the inline method
* @param compat use inline accessor format of 3.0-3.2
*/
class MakeInlineableDirect(inlineSym: Symbol) extends MakeInlineableMap(inlineSym) {
class MakeInlineableDirect(inlineSym: Symbol, compat: Boolean) extends MakeInlineableMap(inlineSym) {
def preTransform(tree: Tree)(using Context): Tree = tree match {
case tree: RefTree if needsAccessor(tree.symbol) =>
if tree.symbol.isConstructor then
report.error("Implementation restriction: cannot use private constructors in inline methods", tree.srcPos)
tree // TODO: create a proper accessor for the private constructor
else
else if compat then
// Generate the accessor for backwards compatibility with 3.0-3.3
val nearestHost = AccessProxies.hostForAccessorOf(tree.symbol)
val host = if nearestHost.is(Package) then ctx.owner.topLevelClass else nearestHost
useAccessor(tree, host)
else
// Generate the accessor for 3.4+
val nearestHost = AccessProxies.hostForAccessorOf(tree.symbol)
if nearestHost.is(Package) || (tree.symbol.owner.isStaticOwner && !nearestHost.isStaticOwner) then
useAccessor(tree, requiredInlineAccessorsModule(ctx.owner.topLevelClass))
else
useAccessor(tree, nearestHost)
case _ =>
tree
}

override def ifNoHost(reference: RefTree)(using Context): Tree = reference
}

Expand Down Expand Up @@ -228,8 +262,12 @@ object PrepareInlineable {
// so no accessors are needed for them.
tree
else
// Generate inline accessors for 3.0-3.3
new MakeInlineablePassing(inlineSym).transform(
new MakeInlineableDirect(inlineSym, compat = true).transform(tree))
// Generate and use inline accessors for 3.4+
new MakeInlineablePassing(inlineSym).transform(
new MakeInlineableDirect(inlineSym).transform(tree))
new MakeInlineableDirect(inlineSym, compat = false).transform(tree))
}
}

Expand Down
5 changes: 3 additions & 2 deletions compiler/src/dotty/tools/dotc/quoted/Interpreter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -349,9 +349,10 @@ object Interpreter:
if !ctx.compilationUnit.isSuspendable then None
else targetException match
case _: NoClassDefFoundError | _: ClassNotFoundException =>
val className = targetException.getMessage
if className eq null then None
val msg = targetException.getMessage
if msg eq null then None
else
val className = msg.stripSuffix(str.TOPLEVEL_INLINE_SUFFIX + str.MODULE_SUFFIX)
val sym = staticRef(className.toTypeName).symbol
if (sym.isDefinedInCurrentRun) Some(sym) else None
case _ => None
Expand Down
76 changes: 76 additions & 0 deletions compiler/src/dotty/tools/dotc/typer/TopLevelExtensionModules.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package dotty.tools.dotc.typer

import dotty.tools.dotc.ast.*, untpd.*
import dotty.tools.dotc.core.Contexts.*
import dotty.tools.dotc.core.Flags.*
import dotty.tools.dotc.core.Names.*
import dotty.tools.dotc.core.Scopes.*
import dotty.tools.dotc.core.StdNames.*
import dotty.tools.dotc.core.Symbols.*

/** Creation of top-level extension modules */
object TopLevelExtensionModules:
import tpd.*

/** Creates the symbols for a new extension module associated with the current
* top-level class. These definitions are invisible in the source code.
*
* ```scala
* class C
* // symbols generated by `newAccessorModule("$extension")`
* lazy val C$extension = new C$extension$
* class C$extension$
* ```
*
* @param suffix suffix that will be appended to the name of the top-level class
* @return The class symbol of the new module
*/
def newAccessorModule(suffix: String)(using Context): ClassSymbol =
val inlineAccessorObjectName: TermName =
assert(suffix.startsWith("$"), "suffix should start with $")
assert(suffix.size > 1, "suffix should start with $ followed by a name")
val fileName = ctx.source.file.name
val sourceName = ctx.owner.topLevelClass.name
(sourceName ++ suffix).toTermName

val mod = newNormalizedModuleSymbol(
ctx.owner.topLevelClass.owner,
inlineAccessorObjectName,
ModuleValCreationFlags & Invisible,
ModuleClassCreationFlags & Invisible,
List(defn.ObjectClass.typeRef),
newScope,
NoSymbol,
coord = ctx.owner.topLevelClass.span
)
val cls = mod.moduleClass.asClass
cls.enter(newConstructor(cls, Synthetic, Nil, Nil))
cls

/** Generate a list with the ValDef and TypeDef trees of an extension module (created with `newAccessorModule`).
*
* ```scala
* // given the moduleClassSym for `C$extension$` and `body` this generates the trees
* lazy val C$extension = new C$extension$
* class C$extension$:
* <body*>
* ```
* @param moduleClassSym class symbol of the extension module
* @param body list of definitions in the extension module
*/
def topLevelModuleDefTree(moduleClassSym: ClassSymbol, body: List[Tree])(using Context): List[Tree] =
assert(moduleClassSym.owner.is(Package))
val untpdCtr = untpd.DefDef(nme.CONSTRUCTOR, Nil, tpd.TypeTree(defn.UnitClass.typeRef), tpd.EmptyTree)
val ctr = ctx.typeAssigner.assignType(untpdCtr, moduleClassSym.primaryConstructor)

val parents = List(TypeTree(defn.ObjectClass.typeRef))
val clsDef =
tpd.ClassDefWithParents(moduleClassSym.asClass, ctr, parents, body)
.withSpan(moduleClassSym.span)

val newCls =
Apply(New(ref(moduleClassSym)).select(moduleClassSym.primaryConstructor), Nil)
val modVal =
ValDef(moduleClassSym.companionModule.asTerm, newCls).withSpan(clsDef.span)
.withSpan(moduleClassSym.companionModule.span)
List(modVal, clsDef)
12 changes: 12 additions & 0 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import util.Spans._
import util.common._
import util.{Property, SimpleIdentityMap, SrcPos}
import Applications.{tupleComponentTypes, wrapDefs, defaultArgument}
import TopLevelExtensionModules.topLevelModuleDefTree


import collection.mutable
import annotation.tailrec
Expand Down Expand Up @@ -2713,6 +2715,16 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
pkg.moduleClass.info.decls.lookup(topLevelClassName).ensureCompleted()
var stats1 = typedStats(tree.stats, pkg.moduleClass)._1
if (!ctx.isAfterTyper)
val inlineAccessorClasses = stats1.view.collect {
case tdef @ TypeDef(name, rhs) if tdef.symbol.isClass && PrepareInlineable.inlineAccessorsModule(tdef.symbol).exists =>
val inlineAccessorsModuleClass = PrepareInlineable.inlineAccessorsModule(tdef.symbol)
topLevelModuleDefTree(
inlineAccessorsModuleClass.asClass,
addAccessorDefs(inlineAccessorsModuleClass, Nil)
)
}.flatten
stats1 = stats1 ++ inlineAccessorClasses

stats1 = stats1 ++ typedBlockStats(MainProxies.proxies(stats1))._1
cpy.PackageDef(tree)(pid1, stats1).withType(pkg.termRef)
}
Expand Down
77 changes: 77 additions & 0 deletions compiler/test/dotty/tools/backend/jvm/DottyBytecodeTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1682,6 +1682,83 @@ class DottyBytecodeTests extends DottyBytecodeTest {
assertSameCode(instructions, expected)
}
}

@Test
def i13215(): Unit = {
val code =
"""package foo:
| trait Bar:
| inline def baz = Baz
| private[foo] object Baz
""".stripMargin
checkBCode(code) { dir =>
val privateAccessors = Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED

// For 3.0-3.3 compat
val barClass = loadClassNode(dir.subdirectoryNamed("foo").lookupName("Bar.class", directory = false).input, skipDebugInfo = false)
val accessorOld = getMethod(barClass, "foo$Bar$$inline$Baz")
assert(accessorOld.signature == "()Lfoo/Baz$;", accessorOld.signature)
assert((accessorOld.access & privateAccessors) == 0)

// For 3.4+
val accessorsClass = loadClassNode(dir.subdirectoryNamed("foo").lookupName("Bar$inline$accessors.class", directory = false).input, skipDebugInfo = false)
val accessorNew = getMethod(accessorsClass, "inline$Baz")
assert(accessorNew.signature == "()Lfoo/Baz$;", accessorNew.signature)
assert((accessorNew.access & privateAccessors) == 0)
}
}

@Test
def i15413(): Unit = {
val code =
"""import scala.quoted.*
|class Macro:
| inline def foo = Macro.fooImpl
|object Macro:
| private def fooImpl = {}
""".stripMargin
checkBCode(code) { dir =>
val privateAccessors = Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED

// For 3.0-3.3 compat
val macroClass = loadClassNode(dir.lookupName("Macro.class", directory = false).input, skipDebugInfo = false)
val accessorOld = getMethod(macroClass, "Macro$$inline$fooImpl")
assert(accessorOld.signature == "()V")
assert((accessorOld.access & privateAccessors) == 0)

// For 3.4+
val accessorsClass = loadClassNode(dir.lookupName("Macro$inline$accessors.class", directory = false).input, skipDebugInfo = false)
val accessorNew = getMethod(accessorsClass, "inline$fooImpl")
assert(accessorNew.signature == "()V")
assert((accessorNew.access & privateAccessors) == 0)
}
}

@Test
def i15413b(): Unit = {
val code =
"""package foo
|class C:
| inline def baz = D.bazImpl
|object D:
| private[foo] def bazImpl = {}
""".stripMargin
checkBCode(code) { dir =>
val privateAccessors = Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED

// For 3.0-3.3 compat
val barClass = loadClassNode(dir.subdirectoryNamed("foo").lookupName("C.class", directory = false).input, skipDebugInfo = false)
val accessorOld = getMethod(barClass, "inline$bazImpl$i1")
assert(accessorOld.desc == "(Lfoo/D$;)V", accessorOld.desc)
assert((accessorOld.access & privateAccessors) == 0)

// For 3.4+
val accessorsClass = loadClassNode(dir.subdirectoryNamed("foo").lookupName("C$inline$accessors.class", directory = false).input, skipDebugInfo = false)
val accessorNew = getMethod(accessorsClass, "inline$bazImpl")
assert(accessorNew.signature == "()V", accessorNew.signature)
assert((accessorNew.access & privateAccessors) == 0)
}
}
}

object invocationReceiversTestCode {
Expand Down
7 changes: 7 additions & 0 deletions tests/pos-macros/i15413/Macro_1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import scala.quoted.*

class Macro:
inline def foo = ${ Macro.fooImpl }

object Macro:
private def fooImpl(using Quotes) = '{}
2 changes: 2 additions & 0 deletions tests/pos-macros/i15413/Test_2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def test =
new Macro().foo
5 changes: 5 additions & 0 deletions tests/pos-macros/i15413b/Macro_1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import scala.quoted.*

inline def foo = ${ fooImpl }

private def fooImpl(using Quotes) = '{}
1 change: 1 addition & 0 deletions tests/pos-macros/i15413b/Test_2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
def test = foo
7 changes: 7 additions & 0 deletions tests/pos-macros/i15413c/Macro.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import scala.quoted.*

class Macro:
inline def foo = ${ Macro.fooImpl }

object Macro:
private def fooImpl(using Quotes) = '{}
2 changes: 2 additions & 0 deletions tests/pos-macros/i15413c/Test.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def test =
new Macro().foo
5 changes: 5 additions & 0 deletions tests/pos-macros/i15413d/Macro.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import scala.quoted.*

inline def foo = ${ fooImpl }

private def fooImpl(using Quotes) = '{}
1 change: 1 addition & 0 deletions tests/pos-macros/i15413d/Test.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
def test = foo
6 changes: 6 additions & 0 deletions tests/run-macros/i13215-compat-3.1/A_1_c3.1.0.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package foo

trait Bar:
inline def baz = Baz

private[foo] object Baz
6 changes: 6 additions & 0 deletions tests/run-macros/i13215-compat-3.1/A_3.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package foo

trait Bar:
inline def baz = Baz

private[foo] object Baz
22 changes: 22 additions & 0 deletions tests/run-macros/i13215-compat-3.1/B_2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// We first compile A using 3.1 to generate the old accessors
// Then we compile this file to link against the old accessors
// Finally we recompile A using the current compiler to generate a version
// of A that contains the new accessors (and the old for backwards compat)

@main def Test =
val bar: foo.Bar = new foo.Bar{}
bar.baz // test that old accessor links in 3.4+

// Check that both accessors exist in the bytecode
val barMethods =
java.lang.Class.forName("foo.Bar").getMethods()
.filter(_.getName().contains("inline"))
.filter(x => (x.getModifiers() & java.lang.reflect.Modifier.STATIC) == 0)
.map(_.getName())
val barObjectMethods =
java.lang.Class.forName("foo.Bar$inline$accessors").getMethods()
.filter(_.getName().contains("inline"))
.map(_.getName())

println("3.0-3.3 inline accessor: " + barMethods.toList)
println("3.4+ inline accessor: " + barObjectMethods.toList)
2 changes: 2 additions & 0 deletions tests/run-macros/i15413-compat-3.1.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
3.0-3.3 inline accessor: List(Macro$$inline$fooImpl)
3.4+ inline accessor: List(inline$fooImpl)
7 changes: 7 additions & 0 deletions tests/run-macros/i15413-compat-3.1/Macro_1_c3.1.0.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import scala.quoted.*

class Macro:
inline def foo = /*${*/ Macro.fooImpl /*}*/

object Macro:
private def fooImpl/*(using Quotes)*/ = {}
Loading

0 comments on commit ec9d062

Please sign in to comment.