diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 74d210a4bbad..125201c2540f 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1047,6 +1047,7 @@ class Definitions { @tu lazy val RequiresCapabilityAnnot: ClassSymbol = requiredClass("scala.annotation.internal.requiresCapability") @tu lazy val RetainsAnnot: ClassSymbol = requiredClass("scala.annotation.retains") @tu lazy val RetainsByNameAnnot: ClassSymbol = requiredClass("scala.annotation.retainsByName") + @tu lazy val InlineAccessibleAnnot: ClassSymbol = requiredClass("scala.annotation.inlineAccessible") @tu lazy val JavaRepeatableAnnot: ClassSymbol = requiredClass("java.lang.annotation.Repeatable") diff --git a/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala b/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala index 85293d4a82d7..409f5e2109ee 100644 --- a/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala +++ b/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala @@ -34,6 +34,9 @@ object PrepareInlineable { def makeInlineable(tree: Tree)(using Context): Tree = ctx.property(InlineAccessorsKey).get.makeInlineable(tree) + def makeInlineAccessible(sym: Symbol)(using Context): Unit = + ctx.property(InlineAccessorsKey).get.makeInlineAccessible(sym) + def addAccessorDefs(cls: Symbol, body: List[Tree])(using Context): List[Tree] = ctx.property(InlineAccessorsKey) match case Some(inlineAccessors) => inlineAccessors.addAccessorDefs(cls, body) @@ -95,6 +98,20 @@ object PrepareInlineable { case _ => ctx } + /** Inline accessible approach: use the accessors generated by @inlineAccessible */ + class MakeInlineableAccessible(inlineSym: Symbol) extends MakeInlineableMap(inlineSym) { + def preTransform(tree: Tree)(using Context): Tree = tree match { + case tree: RefTree if needsAccessor(tree.symbol) && tree.symbol.hasAnnotation(defn.InlineAccessibleAnnot) => + val ref = tpd.ref(tree.symbol).asInstanceOf[RefTree] + val accessorHost = tree.symbol.owner + val accessor = useAccessor(ref, accessorHost).symbol + rewire(tree, accessor) + case _ => + tree + } + override def ifNoHost(reference: RefTree)(using Context): Tree = reference + } + /** Direct approach: place the accessor with the accessed symbol. This has the * 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 @@ -103,11 +120,15 @@ object PrepareInlineable { class MakeInlineableDirect(inlineSym: Symbol) extends MakeInlineableMap(inlineSym) { def preTransform(tree: Tree)(using Context): Tree = tree match { case tree: RefTree if needsAccessor(tree.symbol) => - if (tree.symbol.isConstructor) { + 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 useAccessor(tree) + else + // if !tree.symbol.hasAnnotation(defn.InlineAccessibleAnnot) then + // report.error(em"Inline access to private definition.\n\n${tree.symbol} should be annotated with @inlineAccessible.\nAlternatively, create a private @inlineAccessible alias to this definition.", tree.srcPos) + val nearestHost = AccessProxies.hostForAccessorOf(tree.symbol) + val host = if nearestHost.is(Package) then ctx.owner.topLevelClass else nearestHost + useAccessor(tree, host) case _ => tree } @@ -125,12 +146,13 @@ object PrepareInlineable { * } * class TestPassing { * inline def foo[A](x: A): (A, Int) = { - * val c = new C[A](x) - * c.next(1) - * } - * inline def bar[A](x: A): (A, String) = { - * val c = new C[A](x) - * c.next("") + * val c = new C[A](x) + * c.next(1) + * } + * inline def bar[A](x: A): (A, String) = { + * val c = new C[A](x) + * c.next("") + * } * } * * `C` could be compiled separately, so we cannot place the inline accessor in it. @@ -153,7 +175,7 @@ object PrepareInlineable { val qual = qualifier(refPart) inlining.println(i"adding receiver passing inline accessor for $tree/$refPart -> (${qual.tpe}, $refPart: ${refPart.getClass}, $argss%, %") - // Need to dealias in order to cagtch all possible references to abstracted over types in + // Need to dealias in order to catch all possible references to abstracted over types in // substitutions val dealiasMap = new TypeMap { def apply(t: Type) = mapOver(t.dealias) @@ -211,6 +233,11 @@ object PrepareInlineable { } } + /** Create an inline accessor for this definition. */ + def makeInlineAccessible(sym: Symbol)(using Context): Unit = + val ref = tpd.ref(sym).asInstanceOf[RefTree] + MakeInlineableDirect(NoSymbol).useAccessor(ref, sym.owner) + /** Adds accessors for all non-public term members accessed * from `tree`. Non-public type members are currently left as they are. * This means that references to a private type will lead to typing failures @@ -226,8 +253,16 @@ object PrepareInlineable { // so no accessors are needed for them. tree else + // Make sure the old accessors are generated for binary compatibility new MakeInlineablePassing(inlineSym).transform( new MakeInlineableDirect(inlineSym).transform(tree)) + + // Transform the tree adding all inline accessors + // TODO: warn if MakeInlineablePassing or MakeInlineableDirect generate accessors in this pass + // TODO: add a setting to only allow inlineAccessible accessors + new MakeInlineablePassing(inlineSym).transform( + new MakeInlineableDirect(inlineSym).transform( + new MakeInlineableAccessible(inlineSym).transform(tree))) } } diff --git a/compiler/src/dotty/tools/dotc/transform/AccessProxies.scala b/compiler/src/dotty/tools/dotc/transform/AccessProxies.scala index 14362260d032..fb8c5bd9cb35 100644 --- a/compiler/src/dotty/tools/dotc/transform/AccessProxies.scala +++ b/compiler/src/dotty/tools/dotc/transform/AccessProxies.scala @@ -96,7 +96,7 @@ abstract class AccessProxies { } /** Rewire reference to refer to `accessor` symbol */ - private def rewire(reference: RefTree, accessor: Symbol)(using Context): Tree = { + protected def rewire(reference: RefTree, accessor: Symbol)(using Context): Tree = { reference match { case Select(qual, _) if qual.tpe.derivesFrom(accessor.owner) => qual.select(accessor) case _ => ref(accessor) @@ -127,15 +127,12 @@ abstract class AccessProxies { /** Create an accessor unless one exists already, and replace the original * access with a reference to the accessor. * - * @param reference The original reference to the non-public symbol - * @param onLHS The reference is on the left-hand side of an assignment + * @param reference The original reference to the non-public symbol + * @param accessorClass The owner of the accessor */ - def useAccessor(reference: RefTree)(using Context): Tree = { + def useAccessor(reference: RefTree, accessorClass: Symbol)(using Context): Tree = { val accessed = reference.symbol.asTerm - var accessorClass = hostForAccessorOf(accessed: Symbol) if (accessorClass.exists) { - if accessorClass.is(Package) then - accessorClass = ctx.owner.topLevelClass val accessorName = accessorNameOf(accessed.name, accessorClass) val accessorInfo = accessed.info.ensureMethodic.asSeenFrom(accessorClass.thisType, accessed.owner) @@ -152,7 +149,7 @@ abstract class AccessProxies { report.error("Implementation restriction: cannot use private constructors in inlineable methods", tree.srcPos) tree // TODO: create a proper accessor for the private constructor } - else useAccessor(tree) + else useAccessor(tree, hostForAccessorOf(tree.symbol)) case _ => tree } diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 9d8fdcc006c9..d5ce25384d44 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2347,6 +2347,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer } val vdef1 = assignType(cpy.ValDef(vdef)(name, tpt1, rhs1), sym) postProcessInfo(sym) + inlineAccessors(sym) vdef1.setDefTree } @@ -2450,6 +2451,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val ddef2 = assignType(cpy.DefDef(ddef)(name, paramss1, tpt1, rhs1), sym) postProcessInfo(sym) + inlineAccessors(sym) ddef2.setDefTree //todo: make sure dependent method types do not depend on implicits or by-name params } @@ -2463,6 +2465,11 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer if !sym.is(Module) && !sym.isConstructor && sym.info.finalResultType.isErasedClass then sym.setFlag(Erased) + /** Generate inline accessors for definitions annotated with @inlineAccessible */ + def inlineAccessors(sym: Symbol)(using Context): Unit = + if !ctx.isAfterTyper && !sym.is(Param) && sym.hasAnnotation(defn.InlineAccessibleAnnot) then + PrepareInlineable.makeInlineAccessible(sym) + def typedTypeDef(tdef: untpd.TypeDef, sym: Symbol)(using Context): Tree = { val TypeDef(name, rhs) = tdef completeAnnotations(tdef, sym) diff --git a/compiler/test/dotty/tools/backend/jvm/DottyBytecodeTests.scala b/compiler/test/dotty/tools/backend/jvm/DottyBytecodeTests.scala index ac4ba3ee0e75..2e3667028a62 100644 --- a/compiler/test/dotty/tools/backend/jvm/DottyBytecodeTests.scala +++ b/compiler/test/dotty/tools/backend/jvm/DottyBytecodeTests.scala @@ -1682,6 +1682,74 @@ class DottyBytecodeTests extends DottyBytecodeTest { assertSameCode(instructions, expected) } } + + @Test + def i15413(): Unit = { + val code = + """import scala.quoted.* + |import scala.annotation.inlineAccessible + |class Macro: + | inline def foo = Macro.fooImpl + | def test = foo + |object Macro: + | @inlineAccessible 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 oldAccessor = getMethod(macroClass, "Macro$$inline$fooImpl") + assert(oldAccessor.signature == "()V") + assert((oldAccessor.access & privateAccessors) == 0) + + // For 3.4+ + val moduleClass = loadClassNode(dir.lookupName("Macro$.class", directory = false).input, skipDebugInfo = false) + val newAccessor = getMethod(moduleClass, "inline$fooImpl") + assert(newAccessor.signature == "()V") + assert((newAccessor.access & privateAccessors) == 0) + + // Check that the @inlineAccessible generated accessor is used + val testMethod = getMethod(macroClass, "test") + val testInstructions = instructionsFromMethod(testMethod).filter(_.isInstanceOf[Invoke]) + assertSameCode(testInstructions, List( + Invoke(INVOKEVIRTUAL, "Macro$", "inline$fooImpl", "()V", false))) + } + } + + @Test + def i15413b(): Unit = { + val code = + """package foo + |import scala.annotation.inlineAccessible + |class C: + | inline def baz = D.bazImpl + | def test = baz + |object D: + | @inlineAccessible 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 oldAccessor = getMethod(barClass, "inline$bazImpl$i1") + assert(oldAccessor.desc == "(Lfoo/D$;)V", oldAccessor.desc) + assert((oldAccessor.access & privateAccessors) == 0) + + // For 3.4+ + val dModuleClass = loadClassNode(dir.subdirectoryNamed("foo").lookupName("D$.class", directory = false).input, skipDebugInfo = false) + val newAccessor = getMethod(dModuleClass, "inline$bazImpl") + assert(newAccessor.signature == "()V", newAccessor.signature) + assert((newAccessor.access & privateAccessors) == 0) + + // Check that the @inlineAccessible generated accessor is used + val testMethod = getMethod(barClass, "test") + val testInstructions = instructionsFromMethod(testMethod).filter(_.isInstanceOf[Invoke]) + assertSameCode(testInstructions, List( + Invoke(INVOKEVIRTUAL, "foo/D$", "inline$bazImpl", "()V", false))) + } + } } object invocationReceiversTestCode { diff --git a/library/src/scala/annotation/inlineAccessible.scala b/library/src/scala/annotation/inlineAccessible.scala new file mode 100644 index 000000000000..c593d51298fe --- /dev/null +++ b/library/src/scala/annotation/inlineAccessible.scala @@ -0,0 +1,6 @@ +package scala.annotation + +/** TODO + */ +@experimental +final class inlineAccessible extends StaticAnnotation diff --git a/tests/neg/inline-accessible.scala b/tests/neg/inline-accessible.scala new file mode 100644 index 000000000000..0ff83bf070f5 --- /dev/null +++ b/tests/neg/inline-accessible.scala @@ -0,0 +1,7 @@ +package foo + +import scala.annotation.inlineAccessible + +@inlineAccessible type A // error +@inlineAccessible object B // error // TODO support? +@inlineAccessible class C // error diff --git a/tests/pos-macros/i15413/Macro_1.scala b/tests/pos-macros/i15413/Macro_1.scala new file mode 100644 index 000000000000..1a402071fe8c --- /dev/null +++ b/tests/pos-macros/i15413/Macro_1.scala @@ -0,0 +1,8 @@ +import scala.quoted.* +import scala.annotation.inlineAccessible + +class Macro: + inline def foo = ${ Macro.fooImpl } + +object Macro: + @inlineAccessible private def fooImpl(using Quotes) = '{} diff --git a/tests/pos-macros/i15413/Test_2.scala b/tests/pos-macros/i15413/Test_2.scala new file mode 100644 index 000000000000..a8310a8970fd --- /dev/null +++ b/tests/pos-macros/i15413/Test_2.scala @@ -0,0 +1,2 @@ +def test = + new Macro().foo diff --git a/tests/pos-macros/i15413b/Macro_1.scala b/tests/pos-macros/i15413b/Macro_1.scala new file mode 100644 index 000000000000..e638ff15be6a --- /dev/null +++ b/tests/pos-macros/i15413b/Macro_1.scala @@ -0,0 +1,6 @@ +import scala.quoted.* +import scala.annotation.inlineAccessible + +inline def foo = ${ fooImpl } + +@inlineAccessible private def fooImpl(using Quotes) = '{} diff --git a/tests/pos-macros/i15413b/Test_2.scala b/tests/pos-macros/i15413b/Test_2.scala new file mode 100644 index 000000000000..54c769c9618f --- /dev/null +++ b/tests/pos-macros/i15413b/Test_2.scala @@ -0,0 +1 @@ +def test = foo diff --git a/tests/pos/inline-accessible.scala b/tests/pos/inline-accessible.scala new file mode 100644 index 000000000000..1cebcbda0b1f --- /dev/null +++ b/tests/pos/inline-accessible.scala @@ -0,0 +1,61 @@ +// package foo + +import scala.annotation.inlineAccessible + +class Foo(@inlineAccessible param: Int): + @inlineAccessible + private val privateVal: Int = 2 + + @inlineAccessible + protected val protectedVal: Int = 2 + + @inlineAccessible + private[foo] def packagePrivateVal: Int = 2 + inline def foo: Int = param + privateVal + protectedVal + packagePrivateVal + +class Bar() extends Foo(3): + override protected val protectedVal: Int = 2 + + override private[foo] def packagePrivateVal: Int = 2 + + inline def bar: Int = protectedVal + packagePrivateVal // TODO reuse accessor in Foo + +class Baz() extends Foo(4): + @inlineAccessible // TODO warn? : does not need an accessor and it overrides a field that has an accessor. + override protected val protectedVal: Int = 2 + + @inlineAccessible + override private[foo] def packagePrivateVal: Int = 2 + + inline def baz: Int = protectedVal + packagePrivateVal + + +class Qux() extends Foo(5): + inline def qux: Int = protectedVal + packagePrivateVal + +def test = + @inlineAccessible // noop + val a = 5 + + Foo(3).foo + Bar().bar + Baz().baz + Qux().qux + + +package inlines { + // Case that needed to be converted with MakeInlineablePassing + class C[T](x: T) { + @inlineAccessible private[inlines] def next[U](y: U): (T, U) = (x, y) + } + class TestPassing { + inline def foo[A](x: A): (A, Int) = { + val c = new C[A](x) + c.next(1) + } + inline def bar[A](x: A): (A, String) = { + val c = new C[A](x) + c.next("") + } + } +} diff --git a/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala b/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala index eff76720a7e2..c882cad4e6d3 100644 --- a/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala @@ -58,6 +58,9 @@ val experimentalDefinitionInLibrary = Set( //// New feature: into "scala.annotation.allowConversions", + //// Explicit inline accessors + "scala.annotation.inlineAccessible", + //// New APIs: Mirror // Can be stabilized in 3.3.0 or later. "scala.deriving.Mirror$.fromProductTyped", // This API is a bit convoluted. We may need some more feedback before we can stabilize it.