Skip to content

Commit ec9d062

Browse files
committed
Generate static inline accessors module
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 #13215 Fixes #15413
1 parent 2c893ff commit ec9d062

32 files changed

+376
-6
lines changed

compiler/src/dotty/tools/dotc/core/StdNames.scala

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ object StdNames {
2121
inline val AVOID_CLASH_SUFFIX = "$_avoid_name_clash_$"
2222
inline val MODULE_SUFFIX = "$"
2323
inline val TOPLEVEL_SUFFIX = "$package"
24+
inline val TOPLEVEL_INLINE_SUFFIX = "$inline$accessors"
2425
inline val NAME_JOIN = "$"
2526
inline val DEFAULT_GETTER = "$default$"
2627
inline val LOCALDUMMY_PREFIX = "<local " // owner of local blocks

compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala

+42-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package dotty.tools
22
package dotc
33
package inlines
44

5+
import scala.collection.mutable
6+
57
import dotty.tools.dotc.ast.{Trees, tpd, untpd}
68
import Trees._
79
import core._
@@ -22,14 +24,21 @@ import transform.SymUtils.*
2224
import config.Printers.inlining
2325
import util.Property
2426
import dotty.tools.dotc.transform.TreeMapWithStages._
27+
import dotty.tools.dotc.ast.desugar.packageObjectName
28+
import dotty.tools.dotc.core.StdNames.str
29+
import dotty.tools.dotc.util.{Spans, SrcPos}
30+
import dotty.tools.dotc.typer.TopLevelExtensionModules.newAccessorModule
2531

2632
object PrepareInlineable {
2733
import tpd._
2834

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

3138
def initContext(ctx: Context): Context =
32-
ctx.fresh.setProperty(InlineAccessorsKey, new InlineAccessors)
39+
ctx.fresh
40+
.setProperty(InlineAccessorsKey, new InlineAccessors)
41+
.setProperty(InlineAccessorsModuleKey, mutable.Map.empty)
3342

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

51+
def inlineAccessorsModule(topLevelClass: Symbol)(using Context): Symbol =
52+
assert(topLevelClass.asClass.owner.is(Package), topLevelClass)
53+
ctx.property(InlineAccessorsModuleKey).get.getOrElse(topLevelClass, NoSymbol)
54+
55+
def requiredInlineAccessorsModule(topLevelClass: Symbol)(using Context): Symbol =
56+
assert(topLevelClass.asClass.owner.is(Package), topLevelClass)
57+
ctx.property(InlineAccessorsModuleKey) match
58+
case Some(inlineAccessorsModule) =>
59+
inlineAccessorsModule.getOrElseUpdate(
60+
topLevelClass,
61+
newAccessorModule(str.TOPLEVEL_INLINE_SUFFIX))
62+
case None => NoSymbol
63+
4264
class InlineAccessors extends AccessProxies {
4365

4466
/** If an inline accessor name wraps a unique inline name, this is taken as indication
@@ -99,20 +121,32 @@ object PrepareInlineable {
99121
* advantage that we can re-use the receiver as is. But it is only
100122
* possible if the receiver is essentially this or an outer this, which is indicated
101123
* by the test that we can find a host for the accessor.
124+
*
125+
* @param inlineSym symbol of the inline method
126+
* @param compat use inline accessor format of 3.0-3.2
102127
*/
103-
class MakeInlineableDirect(inlineSym: Symbol) extends MakeInlineableMap(inlineSym) {
128+
class MakeInlineableDirect(inlineSym: Symbol, compat: Boolean) extends MakeInlineableMap(inlineSym) {
104129
def preTransform(tree: Tree)(using Context): Tree = tree match {
105130
case tree: RefTree if needsAccessor(tree.symbol) =>
106131
if tree.symbol.isConstructor then
107132
report.error("Implementation restriction: cannot use private constructors in inline methods", tree.srcPos)
108133
tree // TODO: create a proper accessor for the private constructor
109-
else
134+
else if compat then
135+
// Generate the accessor for backwards compatibility with 3.0-3.3
110136
val nearestHost = AccessProxies.hostForAccessorOf(tree.symbol)
111137
val host = if nearestHost.is(Package) then ctx.owner.topLevelClass else nearestHost
112138
useAccessor(tree, host)
139+
else
140+
// Generate the accessor for 3.4+
141+
val nearestHost = AccessProxies.hostForAccessorOf(tree.symbol)
142+
if nearestHost.is(Package) || (tree.symbol.owner.isStaticOwner && !nearestHost.isStaticOwner) then
143+
useAccessor(tree, requiredInlineAccessorsModule(ctx.owner.topLevelClass))
144+
else
145+
useAccessor(tree, nearestHost)
113146
case _ =>
114147
tree
115148
}
149+
116150
override def ifNoHost(reference: RefTree)(using Context): Tree = reference
117151
}
118152

@@ -228,8 +262,12 @@ object PrepareInlineable {
228262
// so no accessors are needed for them.
229263
tree
230264
else
265+
// Generate inline accessors for 3.0-3.3
266+
new MakeInlineablePassing(inlineSym).transform(
267+
new MakeInlineableDirect(inlineSym, compat = true).transform(tree))
268+
// Generate and use inline accessors for 3.4+
231269
new MakeInlineablePassing(inlineSym).transform(
232-
new MakeInlineableDirect(inlineSym).transform(tree))
270+
new MakeInlineableDirect(inlineSym, compat = false).transform(tree))
233271
}
234272
}
235273

compiler/src/dotty/tools/dotc/quoted/Interpreter.scala

+3-2
Original file line numberDiff line numberDiff line change
@@ -349,9 +349,10 @@ object Interpreter:
349349
if !ctx.compilationUnit.isSuspendable then None
350350
else targetException match
351351
case _: NoClassDefFoundError | _: ClassNotFoundException =>
352-
val className = targetException.getMessage
353-
if className eq null then None
352+
val msg = targetException.getMessage
353+
if msg eq null then None
354354
else
355+
val className = msg.stripSuffix(str.TOPLEVEL_INLINE_SUFFIX + str.MODULE_SUFFIX)
355356
val sym = staticRef(className.toTypeName).symbol
356357
if (sym.isDefinedInCurrentRun) Some(sym) else None
357358
case _ => None
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package dotty.tools.dotc.typer
2+
3+
import dotty.tools.dotc.ast.*, untpd.*
4+
import dotty.tools.dotc.core.Contexts.*
5+
import dotty.tools.dotc.core.Flags.*
6+
import dotty.tools.dotc.core.Names.*
7+
import dotty.tools.dotc.core.Scopes.*
8+
import dotty.tools.dotc.core.StdNames.*
9+
import dotty.tools.dotc.core.Symbols.*
10+
11+
/** Creation of top-level extension modules */
12+
object TopLevelExtensionModules:
13+
import tpd.*
14+
15+
/** Creates the symbols for a new extension module associated with the current
16+
* top-level class. These definitions are invisible in the source code.
17+
*
18+
* ```scala
19+
* class C
20+
* // symbols generated by `newAccessorModule("$extension")`
21+
* lazy val C$extension = new C$extension$
22+
* class C$extension$
23+
* ```
24+
*
25+
* @param suffix suffix that will be appended to the name of the top-level class
26+
* @return The class symbol of the new module
27+
*/
28+
def newAccessorModule(suffix: String)(using Context): ClassSymbol =
29+
val inlineAccessorObjectName: TermName =
30+
assert(suffix.startsWith("$"), "suffix should start with $")
31+
assert(suffix.size > 1, "suffix should start with $ followed by a name")
32+
val fileName = ctx.source.file.name
33+
val sourceName = ctx.owner.topLevelClass.name
34+
(sourceName ++ suffix).toTermName
35+
36+
val mod = newNormalizedModuleSymbol(
37+
ctx.owner.topLevelClass.owner,
38+
inlineAccessorObjectName,
39+
ModuleValCreationFlags & Invisible,
40+
ModuleClassCreationFlags & Invisible,
41+
List(defn.ObjectClass.typeRef),
42+
newScope,
43+
NoSymbol,
44+
coord = ctx.owner.topLevelClass.span
45+
)
46+
val cls = mod.moduleClass.asClass
47+
cls.enter(newConstructor(cls, Synthetic, Nil, Nil))
48+
cls
49+
50+
/** Generate a list with the ValDef and TypeDef trees of an extension module (created with `newAccessorModule`).
51+
*
52+
* ```scala
53+
* // given the moduleClassSym for `C$extension$` and `body` this generates the trees
54+
* lazy val C$extension = new C$extension$
55+
* class C$extension$:
56+
* <body*>
57+
* ```
58+
* @param moduleClassSym class symbol of the extension module
59+
* @param body list of definitions in the extension module
60+
*/
61+
def topLevelModuleDefTree(moduleClassSym: ClassSymbol, body: List[Tree])(using Context): List[Tree] =
62+
assert(moduleClassSym.owner.is(Package))
63+
val untpdCtr = untpd.DefDef(nme.CONSTRUCTOR, Nil, tpd.TypeTree(defn.UnitClass.typeRef), tpd.EmptyTree)
64+
val ctr = ctx.typeAssigner.assignType(untpdCtr, moduleClassSym.primaryConstructor)
65+
66+
val parents = List(TypeTree(defn.ObjectClass.typeRef))
67+
val clsDef =
68+
tpd.ClassDefWithParents(moduleClassSym.asClass, ctr, parents, body)
69+
.withSpan(moduleClassSym.span)
70+
71+
val newCls =
72+
Apply(New(ref(moduleClassSym)).select(moduleClassSym.primaryConstructor), Nil)
73+
val modVal =
74+
ValDef(moduleClassSym.companionModule.asTerm, newCls).withSpan(clsDef.span)
75+
.withSpan(moduleClassSym.companionModule.span)
76+
List(modVal, clsDef)

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

+12
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import util.Spans._
3434
import util.common._
3535
import util.{Property, SimpleIdentityMap, SrcPos}
3636
import Applications.{tupleComponentTypes, wrapDefs, defaultArgument}
37+
import TopLevelExtensionModules.topLevelModuleDefTree
38+
3739

3840
import collection.mutable
3941
import annotation.tailrec
@@ -2713,6 +2715,16 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
27132715
pkg.moduleClass.info.decls.lookup(topLevelClassName).ensureCompleted()
27142716
var stats1 = typedStats(tree.stats, pkg.moduleClass)._1
27152717
if (!ctx.isAfterTyper)
2718+
val inlineAccessorClasses = stats1.view.collect {
2719+
case tdef @ TypeDef(name, rhs) if tdef.symbol.isClass && PrepareInlineable.inlineAccessorsModule(tdef.symbol).exists =>
2720+
val inlineAccessorsModuleClass = PrepareInlineable.inlineAccessorsModule(tdef.symbol)
2721+
topLevelModuleDefTree(
2722+
inlineAccessorsModuleClass.asClass,
2723+
addAccessorDefs(inlineAccessorsModuleClass, Nil)
2724+
)
2725+
}.flatten
2726+
stats1 = stats1 ++ inlineAccessorClasses
2727+
27162728
stats1 = stats1 ++ typedBlockStats(MainProxies.proxies(stats1))._1
27172729
cpy.PackageDef(tree)(pid1, stats1).withType(pkg.termRef)
27182730
}

compiler/test/dotty/tools/backend/jvm/DottyBytecodeTests.scala

+77
Original file line numberDiff line numberDiff line change
@@ -1682,6 +1682,83 @@ class DottyBytecodeTests extends DottyBytecodeTest {
16821682
assertSameCode(instructions, expected)
16831683
}
16841684
}
1685+
1686+
@Test
1687+
def i13215(): Unit = {
1688+
val code =
1689+
"""package foo:
1690+
| trait Bar:
1691+
| inline def baz = Baz
1692+
| private[foo] object Baz
1693+
""".stripMargin
1694+
checkBCode(code) { dir =>
1695+
val privateAccessors = Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED
1696+
1697+
// For 3.0-3.3 compat
1698+
val barClass = loadClassNode(dir.subdirectoryNamed("foo").lookupName("Bar.class", directory = false).input, skipDebugInfo = false)
1699+
val accessorOld = getMethod(barClass, "foo$Bar$$inline$Baz")
1700+
assert(accessorOld.signature == "()Lfoo/Baz$;", accessorOld.signature)
1701+
assert((accessorOld.access & privateAccessors) == 0)
1702+
1703+
// For 3.4+
1704+
val accessorsClass = loadClassNode(dir.subdirectoryNamed("foo").lookupName("Bar$inline$accessors.class", directory = false).input, skipDebugInfo = false)
1705+
val accessorNew = getMethod(accessorsClass, "inline$Baz")
1706+
assert(accessorNew.signature == "()Lfoo/Baz$;", accessorNew.signature)
1707+
assert((accessorNew.access & privateAccessors) == 0)
1708+
}
1709+
}
1710+
1711+
@Test
1712+
def i15413(): Unit = {
1713+
val code =
1714+
"""import scala.quoted.*
1715+
|class Macro:
1716+
| inline def foo = Macro.fooImpl
1717+
|object Macro:
1718+
| private def fooImpl = {}
1719+
""".stripMargin
1720+
checkBCode(code) { dir =>
1721+
val privateAccessors = Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED
1722+
1723+
// For 3.0-3.3 compat
1724+
val macroClass = loadClassNode(dir.lookupName("Macro.class", directory = false).input, skipDebugInfo = false)
1725+
val accessorOld = getMethod(macroClass, "Macro$$inline$fooImpl")
1726+
assert(accessorOld.signature == "()V")
1727+
assert((accessorOld.access & privateAccessors) == 0)
1728+
1729+
// For 3.4+
1730+
val accessorsClass = loadClassNode(dir.lookupName("Macro$inline$accessors.class", directory = false).input, skipDebugInfo = false)
1731+
val accessorNew = getMethod(accessorsClass, "inline$fooImpl")
1732+
assert(accessorNew.signature == "()V")
1733+
assert((accessorNew.access & privateAccessors) == 0)
1734+
}
1735+
}
1736+
1737+
@Test
1738+
def i15413b(): Unit = {
1739+
val code =
1740+
"""package foo
1741+
|class C:
1742+
| inline def baz = D.bazImpl
1743+
|object D:
1744+
| private[foo] def bazImpl = {}
1745+
""".stripMargin
1746+
checkBCode(code) { dir =>
1747+
val privateAccessors = Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED
1748+
1749+
// For 3.0-3.3 compat
1750+
val barClass = loadClassNode(dir.subdirectoryNamed("foo").lookupName("C.class", directory = false).input, skipDebugInfo = false)
1751+
val accessorOld = getMethod(barClass, "inline$bazImpl$i1")
1752+
assert(accessorOld.desc == "(Lfoo/D$;)V", accessorOld.desc)
1753+
assert((accessorOld.access & privateAccessors) == 0)
1754+
1755+
// For 3.4+
1756+
val accessorsClass = loadClassNode(dir.subdirectoryNamed("foo").lookupName("C$inline$accessors.class", directory = false).input, skipDebugInfo = false)
1757+
val accessorNew = getMethod(accessorsClass, "inline$bazImpl")
1758+
assert(accessorNew.signature == "()V", accessorNew.signature)
1759+
assert((accessorNew.access & privateAccessors) == 0)
1760+
}
1761+
}
16851762
}
16861763

16871764
object invocationReceiversTestCode {

tests/pos-macros/i15413/Macro_1.scala

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import scala.quoted.*
2+
3+
class Macro:
4+
inline def foo = ${ Macro.fooImpl }
5+
6+
object Macro:
7+
private def fooImpl(using Quotes) = '{}

tests/pos-macros/i15413/Test_2.scala

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def test =
2+
new Macro().foo
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import scala.quoted.*
2+
3+
inline def foo = ${ fooImpl }
4+
5+
private def fooImpl(using Quotes) = '{}

tests/pos-macros/i15413b/Test_2.scala

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
def test = foo

tests/pos-macros/i15413c/Macro.scala

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import scala.quoted.*
2+
3+
class Macro:
4+
inline def foo = ${ Macro.fooImpl }
5+
6+
object Macro:
7+
private def fooImpl(using Quotes) = '{}

tests/pos-macros/i15413c/Test.scala

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def test =
2+
new Macro().foo

tests/pos-macros/i15413d/Macro.scala

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import scala.quoted.*
2+
3+
inline def foo = ${ fooImpl }
4+
5+
private def fooImpl(using Quotes) = '{}

tests/pos-macros/i15413d/Test.scala

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
def test = foo
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package foo
2+
3+
trait Bar:
4+
inline def baz = Baz
5+
6+
private[foo] object Baz
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package foo
2+
3+
trait Bar:
4+
inline def baz = Baz
5+
6+
private[foo] object Baz
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// We first compile A using 3.1 to generate the old accessors
2+
// Then we compile this file to link against the old accessors
3+
// Finally we recompile A using the current compiler to generate a version
4+
// of A that contains the new accessors (and the old for backwards compat)
5+
6+
@main def Test =
7+
val bar: foo.Bar = new foo.Bar{}
8+
bar.baz // test that old accessor links in 3.4+
9+
10+
// Check that both accessors exist in the bytecode
11+
val barMethods =
12+
java.lang.Class.forName("foo.Bar").getMethods()
13+
.filter(_.getName().contains("inline"))
14+
.filter(x => (x.getModifiers() & java.lang.reflect.Modifier.STATIC) == 0)
15+
.map(_.getName())
16+
val barObjectMethods =
17+
java.lang.Class.forName("foo.Bar$inline$accessors").getMethods()
18+
.filter(_.getName().contains("inline"))
19+
.map(_.getName())
20+
21+
println("3.0-3.3 inline accessor: " + barMethods.toList)
22+
println("3.4+ inline accessor: " + barObjectMethods.toList)
+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
3.0-3.3 inline accessor: List(Macro$$inline$fooImpl)
2+
3.4+ inline accessor: List(inline$fooImpl)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import scala.quoted.*
2+
3+
class Macro:
4+
inline def foo = /*${*/ Macro.fooImpl /*}*/
5+
6+
object Macro:
7+
private def fooImpl/*(using Quotes)*/ = {}

0 commit comments

Comments
 (0)