From 53c771a532a0e42eee3d89d36f6f237f1e1a9f50 Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Wed, 22 Feb 2023 14:22:04 +0100 Subject: [PATCH 01/11] Add `@binaryAPI` The `@binaryAPI` annotation will make any package private or protected term definition public in the generated bytecode. Definitions that override an `@binaryAPI` will also become public. We cannot annotate `private[this]` definitions because we might have different definitions with the same name and signature in a subclass. These would clash if made public. Instead of using private/private[this], the user can write `private[C]` where `C` is the enclosing class. This is useful in combination with inline definitions. If an inline definition refers to a `private`/`protected` definition marked as `@binaryAPI` it does not need to use an accessor. We still generate the accessors for binary compatibility but do not use them. --- compiler/src/dotty/tools/dotc/Compiler.scala | 3 +- .../dotty/tools/dotc/core/Definitions.scala | 1 + .../tools/dotc/core/SymDenotations.scala | 7 + .../dotc/inlines/PrepareInlineable.scala | 32 +- .../dotc/transform/BinaryAPIAnnotations.scala | 32 ++ .../tools/dotc/transform/PostTyper.scala | 5 +- .../src/dotty/tools/dotc/typer/Checking.scala | 5 + .../dotty/tools/dotc/typer/TypeAssigner.scala | 4 +- .../tools/backend/jvm/BinaryAPITests.scala | 324 ++++++++++++++++++ library/src/scala/annotation/binaryAPI.scala | 10 + project/MiMaFilters.scala | 1 + tests/neg/binaryAPI-not-visible.check | 31 ++ tests/neg/binaryAPI-not-visible.scala | 21 ++ tests/neg/binaryAPI.check | 44 +++ tests/neg/binaryAPI.scala | 20 ++ tests/pos-macros/i15413/Macro_1.scala | 8 + tests/pos-macros/i15413/Test_2.scala | 2 + tests/pos-macros/i15413b/Macro_1.scala | 8 + tests/pos-macros/i15413b/Test_2.scala | 1 + tests/pos/binaryAPI.scala | 83 +++++ .../stdlibExperimentalDefinitions.scala | 1 - tests/run/i13215.scala | 12 + 22 files changed, 640 insertions(+), 15 deletions(-) create mode 100644 compiler/src/dotty/tools/dotc/transform/BinaryAPIAnnotations.scala create mode 100644 compiler/test/dotty/tools/backend/jvm/BinaryAPITests.scala create mode 100644 library/src/scala/annotation/binaryAPI.scala create mode 100644 tests/neg/binaryAPI-not-visible.check create mode 100644 tests/neg/binaryAPI-not-visible.scala create mode 100644 tests/neg/binaryAPI.check create mode 100644 tests/neg/binaryAPI.scala create mode 100644 tests/pos-macros/i15413/Macro_1.scala create mode 100644 tests/pos-macros/i15413/Test_2.scala create mode 100644 tests/pos-macros/i15413b/Macro_1.scala create mode 100644 tests/pos-macros/i15413b/Test_2.scala create mode 100644 tests/pos/binaryAPI.scala create mode 100644 tests/run/i13215.scala diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index a6118732d4ae..851d825352f6 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -72,7 +72,8 @@ class Compiler { new ElimRepeated, // Rewrite vararg parameters and arguments new RefChecks) :: // Various checks mostly related to abstract members and overriding List(new init.Checker) :: // Check initialization of objects - List(new ProtectedAccessors, // Add accessors for protected members + List(new BinaryAPIAnnotations, // Makes @binaryAPI definitions public + new ProtectedAccessors, // Add accessors for protected members new ExtensionMethods, // Expand methods of value classes with extension methods new UncacheGivenAliases, // Avoid caching RHS of simple parameterless given aliases new ElimByName, // Map by-name parameters to functions diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index b7211b3ce5e3..147236304db7 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1039,6 +1039,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 BinaryAPIAnnot: ClassSymbol = requiredClass("scala.annotation.binaryAPI") @tu lazy val JavaRepeatableAnnot: ClassSymbol = requiredClass("java.lang.annotation.Repeatable") diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index aa97435d64bb..dbe7d403d2e9 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -1038,6 +1038,13 @@ object SymDenotations { isOneOf(EffectivelyErased) || is(Inline) && !isRetainedInline && !hasAnnotation(defn.ScalaStaticAnnot) + /** Is this a member that will become public in the generated binary */ + def isBinaryAPI(using Context): Boolean = + isTerm && ( + hasAnnotation(defn.BinaryAPIAnnot) || + allOverriddenSymbols.exists(_.hasAnnotation(defn.BinaryAPIAnnot)) + ) + /** ()T and => T types should be treated as equivalent for this symbol. * Note: For the moment, we treat Scala-2 compiled symbols as loose matching, * because the Scala library does not always follow the right conventions. diff --git a/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala b/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala index 060c8d21f390..1e3320d68300 100644 --- a/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala +++ b/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala @@ -54,6 +54,9 @@ object PrepareInlineable { /** A tree map which inserts accessors for non-public term members accessed from inlined code. */ abstract class MakeInlineableMap(val inlineSym: Symbol) extends TreeMap with Insert { + + def useBinaryAPI: Boolean + def accessorNameOf(name: TermName, site: Symbol)(using Context): TermName = val accName = InlineAccessorName(name) if site.isExtensibleClass then accName.expandedName(site) else accName @@ -71,6 +74,7 @@ object PrepareInlineable { def needsAccessor(sym: Symbol)(using Context): Boolean = sym.isTerm && (sym.isOneOf(AccessFlags) || sym.privateWithin.exists) && + (!useBinaryAPI || !sym.isBinaryAPI) && !sym.isContainedIn(inlineSym) && !(sym.isStableMember && sym.info.widenTermRefExpr.isInstanceOf[ConstantType]) && !sym.isInlineMethod && @@ -94,7 +98,7 @@ object PrepareInlineable { * 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. */ - class MakeInlineableDirect(inlineSym: Symbol) extends MakeInlineableMap(inlineSym) { + class MakeInlineableDirect(inlineSym: Symbol, val useBinaryAPI: Boolean) extends MakeInlineableMap(inlineSym) { def preTransform(tree: Tree)(using Context): Tree = tree match { case tree: RefTree if needsAccessor(tree.symbol) => if (tree.symbol.isConstructor) { @@ -118,13 +122,14 @@ object PrepareInlineable { * 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("") + * 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("") + * } * } * * `C` could be compiled separately, so we cannot place the inline accessor in it. @@ -137,7 +142,7 @@ object PrepareInlineable { * Since different calls might have different receiver types, we need to generate one * such accessor per call, so they need to have unique names. */ - class MakeInlineablePassing(inlineSym: Symbol) extends MakeInlineableMap(inlineSym) { + class MakeInlineablePassing(inlineSym: Symbol, val useBinaryAPI: Boolean) extends MakeInlineableMap(inlineSym) { def preTransform(tree: Tree)(using Context): Tree = tree match { case _: Apply | _: TypeApply | _: RefTree @@ -220,8 +225,13 @@ object PrepareInlineable { // so no accessors are needed for them. tree else - new MakeInlineablePassing(inlineSym).transform( - new MakeInlineableDirect(inlineSym).transform(tree)) + // Make sure the old accessors are generated for binary compatibility + new MakeInlineablePassing(inlineSym, useBinaryAPI = false).transform( + new MakeInlineableDirect(inlineSym, useBinaryAPI = false).transform(tree)) + + // TODO: warn if MakeInlineablePassing or MakeInlineableDirect generate accessors when useBinaryAPI in enabled + new MakeInlineablePassing(inlineSym, useBinaryAPI = true).transform( + new MakeInlineableDirect(inlineSym, useBinaryAPI = true).transform(tree)) } } diff --git a/compiler/src/dotty/tools/dotc/transform/BinaryAPIAnnotations.scala b/compiler/src/dotty/tools/dotc/transform/BinaryAPIAnnotations.scala new file mode 100644 index 000000000000..857598deb00d --- /dev/null +++ b/compiler/src/dotty/tools/dotc/transform/BinaryAPIAnnotations.scala @@ -0,0 +1,32 @@ +package dotty.tools.dotc.transform + +import dotty.tools.dotc.core.Contexts.* +import dotty.tools.dotc.core.DenotTransformers.SymTransformer +import dotty.tools.dotc.core.Flags.* +import dotty.tools.dotc.core.Symbols.NoSymbol +import dotty.tools.dotc.core.SymDenotations.SymDenotation +import dotty.tools.dotc.transform.MegaPhase.MiniPhase +import dotty.tools.dotc.typer.RefChecks + +/** Makes @binaryAPI definitions public */ +class BinaryAPIAnnotations extends MiniPhase with SymTransformer: + + override def runsAfterGroupsOf: Set[String] = Set(RefChecks.name) + + override def phaseName: String = BinaryAPIAnnotations.name + override def description: String = BinaryAPIAnnotations.description + + def transformSym(d: SymDenotation)(using Context): SymDenotation = { + if d.isBinaryAPI then + d.resetFlag(Protected) + d.setPrivateWithin(NoSymbol) + if d.is(Module) then + val moduleClass = d.moduleClass + moduleClass.resetFlag(Protected) + moduleClass.setPrivateWithin(NoSymbol) + d + } + +object BinaryAPIAnnotations: + val name: String = "binaryAPIAnnotations" + val description: String = "makes @binaryAPI definitions public" diff --git a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala index d74392f201ba..44ff46831baf 100644 --- a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala +++ b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala @@ -165,10 +165,13 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase atPhase(thisPhase)(cls.annotationsCarrying(Set(defn.CompanionMethodMetaAnnot))) ++ sym.annotations) else + val binaryAPIAnnotOpt = sym.getAnnotation(defn.BinaryAPIAnnot) if sym.is(Param) then sym.keepAnnotationsCarrying(thisPhase, Set(defn.ParamMetaAnnot), orNoneOf = defn.NonBeanMetaAnnots) else if sym.is(ParamAccessor) then - sym.keepAnnotationsCarrying(thisPhase, Set(defn.GetterMetaAnnot, defn.FieldMetaAnnot)) + // FIXME: copyAndKeepAnnotationsCarrying is dropping defn.BinaryAPIAnnot + sym.keepAnnotationsCarrying(thisPhase, Set(defn.GetterMetaAnnot, defn.FieldMetaAnnot, defn.BinaryAPIAnnot)) + for binaryAPIAnnot <- binaryAPIAnnotOpt do sym.addAnnotation(binaryAPIAnnot) else sym.keepAnnotationsCarrying(thisPhase, Set(defn.GetterMetaAnnot, defn.FieldMetaAnnot), orNoneOf = defn.NonBeanMetaAnnots) if sym.isScala2Macro && !ctx.settings.XignoreScala2Macros.value then diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 436721a6774b..5349be5f44a9 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -526,6 +526,11 @@ object Checking { fail(em"Inline methods cannot be @tailrec") if sym.hasAnnotation(defn.TargetNameAnnot) && sym.isClass && sym.isTopLevelClass then fail(TargetNameOnTopLevelClass(sym)) + if sym.hasAnnotation(defn.BinaryAPIAnnot) then + if sym.is(Enum) then fail(em"@binaryAPI cannot be used on enum definitions.") + else if sym.isType && !sym.is(Module) && !(sym.is(Given) || sym.companionModule.is(Given)) then fail(em"@binaryAPI cannot be used on ${sym.showKind} definitions") + else if !sym.owner.isClass && !(sym.is(Param) && sym.owner.isConstructor) then fail(em"@binaryAPI cannot be used on local definitions.") + else if sym.is(Private) then fail(em"@binaryAPI cannot be used on private definitions.\n\nCould use private[${sym.owner.name}] or protected instead.") if (sym.hasAnnotation(defn.NativeAnnot)) { if (!sym.is(Deferred)) fail(NativeMembersMayNotHaveImplementation(sym)) diff --git a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala index 6ac45cbcf04d..40220f0ae577 100644 --- a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala +++ b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala @@ -99,7 +99,9 @@ trait TypeAssigner { val tpe1 = accessibleType(tpe, superAccess) if tpe1.exists then tpe1 else tpe match - case tpe: NamedType => inaccessibleErrorType(tpe, superAccess, pos) + case tpe: NamedType => + if tpe.termSymbol.isBinaryAPI then tpe + else inaccessibleErrorType(tpe, superAccess, pos) case NoType => tpe /** Return a potentially skolemized version of `qualTpe` to be used diff --git a/compiler/test/dotty/tools/backend/jvm/BinaryAPITests.scala b/compiler/test/dotty/tools/backend/jvm/BinaryAPITests.scala new file mode 100644 index 000000000000..278543daac6e --- /dev/null +++ b/compiler/test/dotty/tools/backend/jvm/BinaryAPITests.scala @@ -0,0 +1,324 @@ +package dotty.tools.backend.jvm + +import scala.language.unsafeNulls + +import org.junit.Assert._ +import org.junit.Test + +import scala.tools.asm +import asm._ +import asm.tree._ + +import scala.tools.asm.Opcodes +import scala.jdk.CollectionConverters._ +import Opcodes._ + +class BinaryAPITests extends DottyBytecodeTest { + import ASMConverters._ + + private def privateOrProtectedOpcode = Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED + + private def checkPublicMethod(classNode: ClassNode, methodName: String, desc: String): Unit = + val method = getMethod(classNode, methodName) + assert(method.desc == desc) + assert((method.access & privateOrProtectedOpcode) == 0) + + private def checkPublicField(classNode: ClassNode, fliedName: String): Unit = + val method = getField(classNode, fliedName) + assert((method.access & privateOrProtectedOpcode) == 0) + + @Test + def binaryAPIDef(): Unit = { + val code = + """import scala.annotation.binaryAPI + |class C: + | @binaryAPI private[C] def packagePrivateBinaryAPI: Int = 1 + | @binaryAPI protected def protectedBinaryAPI: Int = 1 + | inline def inlined = packagePrivateBinaryAPI + protectedBinaryAPI + | def testInlined = inlined + """.stripMargin + checkBCode(code) { dir => + val cClass = loadClassNode(dir.lookupName("C.class", directory = false).input, skipDebugInfo = false) + + checkPublicMethod(cClass, "packagePrivateBinaryAPI", "()I") + checkPublicMethod(cClass, "protectedBinaryAPI", "()I") + + // Check that the @binaryAPI annotated method is called + val testInlined = getMethod(cClass, "testInlined") + val testInlinedInstructions = instructionsFromMethod(testInlined).filter(_.isInstanceOf[Invoke]) + assertSameCode(testInlinedInstructions, List( + Invoke(INVOKEVIRTUAL, "C", "packagePrivateBinaryAPI", "()I", false), + Invoke(INVOKEVIRTUAL, "C", "protectedBinaryAPI", "()I", false), + )) + } + } + + @Test + def binaryAPIVal(): Unit = { + val code = + """import scala.annotation.binaryAPI + |class C: + | @binaryAPI private[C] val packagePrivateBinaryAPI: Int = 1 + | @binaryAPI protected val protectedBinaryAPI: Int = 1 + | @binaryAPI private[C] lazy val lazyPackagePrivateBinaryAPI: Int = 1 + | @binaryAPI protected lazy val lazyProtectedBinaryAPI: Int = 1 + | inline def inlined = packagePrivateBinaryAPI + protectedBinaryAPI + lazyPackagePrivateBinaryAPI + lazyProtectedBinaryAPI + | def testInlined = inlined + """.stripMargin + checkBCode(code) { dir => + val cClass = loadClassNode(dir.lookupName("C.class", directory = false).input, skipDebugInfo = false) + + checkPublicMethod(cClass, "packagePrivateBinaryAPI", "()I") + checkPublicMethod(cClass, "protectedBinaryAPI", "()I") + checkPublicMethod(cClass, "lazyPackagePrivateBinaryAPI", "()I") + checkPublicMethod(cClass, "lazyProtectedBinaryAPI", "()I") + + // Check that the @binaryAPI annotated method is called + val testInlined = getMethod(cClass, "testInlined") + val testInlinedInstructions = instructionsFromMethod(testInlined).filter(_.isInstanceOf[Invoke]) + assertSameCode(testInlinedInstructions, List( + Invoke(INVOKEVIRTUAL, "C", "packagePrivateBinaryAPI", "()I", false), + Invoke(INVOKEVIRTUAL, "C", "protectedBinaryAPI", "()I", false), + Invoke(INVOKEVIRTUAL, "C", "lazyPackagePrivateBinaryAPI", "()I", false), + Invoke(INVOKEVIRTUAL, "C", "lazyProtectedBinaryAPI", "()I", false), + )) + } + } + + @Test + def binaryAPIVar(): Unit = { + val code = + """import scala.annotation.binaryAPI + |class C: + | @binaryAPI private[C] var packagePrivateBinaryAPI: Int = 1 + | @binaryAPI protected var protectedBinaryAPI: Int = 1 + | inline def inlined = + | packagePrivateBinaryAPI = 1 + | protectedBinaryAPI = 1 + | packagePrivateBinaryAPI + protectedBinaryAPI + | def testInlined = inlined + """.stripMargin + checkBCode(code) { dir => + val cClass = loadClassNode(dir.lookupName("C.class", directory = false).input, skipDebugInfo = false) + + checkPublicMethod(cClass, "packagePrivateBinaryAPI", "()I") + checkPublicMethod(cClass, "packagePrivateBinaryAPI_$eq", "(I)V") + checkPublicMethod(cClass, "protectedBinaryAPI", "()I") + checkPublicMethod(cClass, "protectedBinaryAPI_$eq", "(I)V") + + // Check that the @binaryAPI annotated method is called + val testInlined = getMethod(cClass, "testInlined") + val testInlinedInstructions = instructionsFromMethod(testInlined).filter(_.isInstanceOf[Invoke]) + assertSameCode(testInlinedInstructions, List( + Invoke(INVOKEVIRTUAL, "C", "packagePrivateBinaryAPI_$eq", "(I)V", false), + Invoke(INVOKEVIRTUAL, "C", "protectedBinaryAPI_$eq", "(I)V", false), + Invoke(INVOKEVIRTUAL, "C", "packagePrivateBinaryAPI", "()I", false), + Invoke(INVOKEVIRTUAL, "C", "protectedBinaryAPI", "()I", false), + )) + } + } + + @Test + def binaryAPIGiven(): Unit = { + val code = + """import scala.annotation.binaryAPI + |class C: + | @binaryAPI private[C] given packagePrivateBinaryAPI1: Int = 1 + | @binaryAPI protected given protectedBinaryAPI1: Int = 1 + | @binaryAPI private[C] given packagePrivateBinaryAPI2(using Int): Int = 1 + | @binaryAPI protected given protectedBinaryAPI2(using Int): Int = 1 + | inline def inlined = + | packagePrivateBinaryAPI1 + protectedBinaryAPI1 + packagePrivateBinaryAPI2(using 1) + protectedBinaryAPI2(using 1) + | def testInlined = inlined + """.stripMargin + checkBCode(code) { dir => + val cClass = loadClassNode(dir.lookupName("C.class", directory = false).input, skipDebugInfo = false) + checkPublicMethod(cClass, "packagePrivateBinaryAPI1", "()I") + checkPublicMethod(cClass, "protectedBinaryAPI1", "()I") + checkPublicMethod(cClass, "packagePrivateBinaryAPI2", "(I)I") + checkPublicMethod(cClass, "protectedBinaryAPI2", "(I)I") + + // Check that the @binaryAPI annotated method is called + val testInlined = getMethod(cClass, "testInlined") + val testInlinedInstructions = instructionsFromMethod(testInlined).filter(_.isInstanceOf[Invoke]) + assertSameCode(testInlinedInstructions, List( + Invoke(INVOKEVIRTUAL, "C", "packagePrivateBinaryAPI1", "()I", false), + Invoke(INVOKEVIRTUAL, "C", "protectedBinaryAPI1", "()I", false), + Invoke(INVOKEVIRTUAL, "C", "packagePrivateBinaryAPI2", "(I)I", false), + Invoke(INVOKEVIRTUAL, "C", "protectedBinaryAPI2", "(I)I", false), + )) + } + } + + @Test + def binaryAPIClassParam(): Unit = { + val code = + """import scala.annotation.binaryAPI + |class C( + | @binaryAPI private[C] val packagePrivateBinaryAPI: Int = 1, + | @binaryAPI protected val protectedBinaryAPI: Int = 1 + |) { + | inline def inlined = + | packagePrivateBinaryAPI + protectedBinaryAPI + | def testInlined = inlined + |} + """.stripMargin + checkBCode(code) { dir => + val cClass = loadClassNode(dir.lookupName("C.class", directory = false).input, skipDebugInfo = false) + + checkPublicMethod(cClass, "packagePrivateBinaryAPI", "()I") + checkPublicMethod(cClass, "protectedBinaryAPI", "()I") + + // Check that the @binaryAPI annotated method is called + val testInlined = getMethod(cClass, "testInlined") + val testInlinedInstructions = instructionsFromMethod(testInlined).filter(_.isInstanceOf[Invoke]) + assertSameCode(testInlinedInstructions, List( + Invoke(INVOKEVIRTUAL, "C", "packagePrivateBinaryAPI", "()I", false), + Invoke(INVOKEVIRTUAL, "C", "protectedBinaryAPI", "()I", false), + )) + } + } + + @Test + def binaryAPIObject(): Unit = { + val code = + """package foo + |import scala.annotation.binaryAPI + |@binaryAPI private[foo] object PackagePrivateBinaryAPI + |@binaryAPI protected object ProtectedBinaryAPI + """.stripMargin + checkBCode(code) { dir => + val packagePrivateBinaryAPI = loadClassNode(dir.subdirectoryNamed("foo").lookupName("PackagePrivateBinaryAPI$.class", directory = false).input, skipDebugInfo = false) + checkPublicField(packagePrivateBinaryAPI, "MODULE$") + + val protectedBinaryAPI = loadClassNode(dir.subdirectoryNamed("foo").lookupName("ProtectedBinaryAPI$.class", directory = false).input, skipDebugInfo = false) + checkPublicField(protectedBinaryAPI, "MODULE$") + } + } + + @Test + def binaryAPITraitDefs(): Unit = { + val code = + """import scala.annotation.binaryAPI + |trait C: + | @binaryAPI private[C] val packagePrivateValBinaryAPI: Int = 1 + | @binaryAPI protected val protectedValBinaryAPI: Int = 1 + | @binaryAPI private[C] lazy val packagePrivateLazyValBinaryAPI: Int = 1 + | @binaryAPI protected lazy val protectedLazyValBinaryAPI: Int = 1 + | @binaryAPI private[C] var packagePrivateVarBinaryAPI: Int = 1 + | @binaryAPI protected var protectedVarBinaryAPI: Int = 1 + | @binaryAPI private[C] def packagePrivateDefBinaryAPI: Int = 1 + | @binaryAPI protected def protectedDefBinaryAPI: Int = 1 + | inline def inlined = + | packagePrivateVarBinaryAPI = 1 + | protectedVarBinaryAPI = 1 + | packagePrivateValBinaryAPI + + | protectedValBinaryAPI + + | packagePrivateLazyValBinaryAPI + + | protectedLazyValBinaryAPI + + | packagePrivateVarBinaryAPI + + | protectedVarBinaryAPI + + | packagePrivateDefBinaryAPI + + | protectedDefBinaryAPI + | def testInlined = inlined + """.stripMargin + checkBCode(code) { dir => + val cTrait = loadClassNode(dir.lookupName("C.class", directory = false).input, skipDebugInfo = false) + + checkPublicMethod(cTrait, "packagePrivateValBinaryAPI", "()I") + checkPublicMethod(cTrait, "protectedValBinaryAPI", "()I") + checkPublicMethod(cTrait, "packagePrivateLazyValBinaryAPI", "()I") + checkPublicMethod(cTrait, "protectedLazyValBinaryAPI", "()I") + checkPublicMethod(cTrait, "packagePrivateVarBinaryAPI", "()I") + checkPublicMethod(cTrait, "packagePrivateVarBinaryAPI_$eq", "(I)V") + checkPublicMethod(cTrait, "protectedVarBinaryAPI", "()I") + checkPublicMethod(cTrait, "protectedVarBinaryAPI_$eq", "(I)V") + checkPublicMethod(cTrait, "packagePrivateDefBinaryAPI", "()I") + checkPublicMethod(cTrait, "protectedDefBinaryAPI", "()I") + + // Check that the @binaryAPI annotated method is called + val testInlined = getMethod(cTrait, "testInlined") + val testInlinedInstructions = instructionsFromMethod(testInlined).filter(_.isInstanceOf[Invoke]) + assertSameCode(testInlinedInstructions, List( + Invoke(INVOKEINTERFACE, "C", "packagePrivateVarBinaryAPI_$eq", "(I)V", true), + Invoke(INVOKEINTERFACE, "C", "protectedVarBinaryAPI_$eq", "(I)V", true), + Invoke(INVOKEINTERFACE, "C", "packagePrivateValBinaryAPI", "()I", true), + Invoke(INVOKEINTERFACE, "C", "protectedValBinaryAPI", "()I", true), + Invoke(INVOKEINTERFACE, "C", "packagePrivateLazyValBinaryAPI", "()I", true), + Invoke(INVOKEINTERFACE, "C", "protectedLazyValBinaryAPI", "()I", true), + Invoke(INVOKEINTERFACE, "C", "packagePrivateVarBinaryAPI", "()I", true), + Invoke(INVOKEINTERFACE, "C", "protectedVarBinaryAPI", "()I", true), + Invoke(INVOKEINTERFACE, "C", "packagePrivateDefBinaryAPI", "()I", true), + Invoke(INVOKEINTERFACE, "C", "protectedDefBinaryAPI", "()I", true) + )) + } + } + + @Test + def i13215(): Unit = { + val code = + """import scala.annotation.binaryAPI + |package foo: + | trait Bar: + | inline def baz = Baz + | def testInlined = baz + | @binaryAPI private[foo] object Baz + """.stripMargin + checkBCode(code) { dir => + // For 3.0-3.3 compat + val barClass = loadClassNode(dir.subdirectoryNamed("foo").lookupName("Bar.class", directory = false).input, skipDebugInfo = false) + checkPublicMethod(barClass, "foo$Bar$$inline$Baz", "()Lfoo/Baz$;") + + // Check that the @binaryAPI annotated method is called + checkPublicMethod(barClass, "testInlined", "()Lfoo/Baz$;") + } + } + + @Test + def i15413(): Unit = { + val code = + """import scala.quoted.* + |import scala.annotation.binaryAPI + |class Macro: + | inline def foo = Macro.fooImpl + | def test = foo + |object Macro: + | @binaryAPI private[Macro] def fooImpl = {} + """.stripMargin + checkBCode(code) { dir => + // For 3.0-3.3 compat + val macroClass = loadClassNode(dir.lookupName("Macro.class", directory = false).input, skipDebugInfo = false) + checkPublicMethod(macroClass, "Macro$$inline$fooImpl", "()V") + + // Check that the @binaryAPI annotated method is called + val testMethod = getMethod(macroClass, "test") + val testInstructions = instructionsFromMethod(testMethod).filter(_.isInstanceOf[Invoke]) + assertSameCode(testInstructions, List( + Invoke(INVOKEVIRTUAL, "Macro$", "fooImpl", "()V", false))) + } + } + + @Test + def i15413b(): Unit = { + val code = + """package foo + |import scala.annotation.binaryAPI + |class C: + | inline def baz = D.bazImpl + | def test = baz + |object D: + | @binaryAPI private[foo] def bazImpl = {} + """.stripMargin + checkBCode(code) { dir => + // For 3.0-3.3 compat + val barClass = loadClassNode(dir.subdirectoryNamed("foo").lookupName("C.class", directory = false).input, skipDebugInfo = false) + checkPublicMethod(barClass, "inline$bazImpl$i1", "(Lfoo/D$;)V") + + // Check that the @binaryAPI annotated method is called + val testMethod = getMethod(barClass, "test") + val testInstructions = instructionsFromMethod(testMethod).filter(_.isInstanceOf[Invoke]) + assertSameCode(testInstructions, List( + Invoke(INVOKEVIRTUAL, "foo/D$", "bazImpl", "()V", false))) + } + } +} diff --git a/library/src/scala/annotation/binaryAPI.scala b/library/src/scala/annotation/binaryAPI.scala new file mode 100644 index 000000000000..c6f45a2287e7 --- /dev/null +++ b/library/src/scala/annotation/binaryAPI.scala @@ -0,0 +1,10 @@ +package scala.annotation + +/** A binary API is a definition that is annotated with `@binaryAPI` or overrides a definition annotated with `@binaryAPI`. + * This annotation can be placed on `def`, `val`, `lazy val`, `var`, `object`, and `given` definitions. + * A binary API will be publicly available in the bytecode. + * + * - `private`/`private[this]` definitions will get an accessor. + * - `private[T]` and `protected` definitions will become public in the bytecode. + */ +final class binaryAPI extends scala.annotation.StaticAnnotation diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index d53eeb7077a4..597dcd4f3ec6 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -17,6 +17,7 @@ object MiMaFilters { ProblemFilters.exclude[MissingClassProblem]("scala.util.boundary$Break"), ProblemFilters.exclude[MissingClassProblem]("scala.util.boundary$Label"), ProblemFilters.exclude[MissingClassProblem]("scala.quoted.runtime.QuoteMatching$"), + ProblemFilters.exclude[MissingClassProblem]("scala.annotation.binaryAPI"), // Scala.js only: new runtime support class in 3.2.3; not available to users ProblemFilters.exclude[MissingClassProblem]("scala.scalajs.runtime.AnonFunctionXXL"), diff --git a/tests/neg/binaryAPI-not-visible.check b/tests/neg/binaryAPI-not-visible.check new file mode 100644 index 000000000000..de0f9dc00108 --- /dev/null +++ b/tests/neg/binaryAPI-not-visible.check @@ -0,0 +1,31 @@ +-- [E173] Reference Error: tests/neg/binaryAPI-not-visible.scala:15:4 -------------------------------------------------- +15 | a.p // error + | ^^^ + | value p cannot be accessed as a member of (a : foo.A) from module class binaryAPI-not-visible$package$. +-- [E173] Reference Error: tests/neg/binaryAPI-not-visible.scala:16:4 -------------------------------------------------- +16 | a.a // error + | ^^^ + | value a cannot be accessed as a member of (a² : foo.A) from module class binaryAPI-not-visible$package$. + | + | where: a is a value in class A + | a² is a parameter in method test +-- [E173] Reference Error: tests/neg/binaryAPI-not-visible.scala:17:4 -------------------------------------------------- +17 | a.b // error + | ^^^ + | lazy value b cannot be accessed as a member of (a : foo.A) from module class binaryAPI-not-visible$package$. +-- [E173] Reference Error: tests/neg/binaryAPI-not-visible.scala:18:4 -------------------------------------------------- +18 | a.c // error + | ^^^ + | variable c cannot be accessed as a member of (a : foo.A) from module class binaryAPI-not-visible$package$. +-- [E173] Reference Error: tests/neg/binaryAPI-not-visible.scala:19:4 -------------------------------------------------- +19 | a.d // error + | ^^^ + | method d cannot be accessed as a member of (a : foo.A) from module class binaryAPI-not-visible$package$. +-- [E173] Reference Error: tests/neg/binaryAPI-not-visible.scala:20:4 -------------------------------------------------- +20 | a.e // error + | ^^^ + |given instance e cannot be accessed as a member of (a : foo.A) from module class binaryAPI-not-visible$package$. +-- [E173] Reference Error: tests/neg/binaryAPI-not-visible.scala:21:4 -------------------------------------------------- +21 | a.f(using 1.0) // error + | ^^^ + |given instance f cannot be accessed as a member of (a : foo.A) from module class binaryAPI-not-visible$package$. diff --git a/tests/neg/binaryAPI-not-visible.scala b/tests/neg/binaryAPI-not-visible.scala new file mode 100644 index 000000000000..8c5f47ab977c --- /dev/null +++ b/tests/neg/binaryAPI-not-visible.scala @@ -0,0 +1,21 @@ +package foo + +import scala.annotation.binaryAPI + +class A(@binaryAPI private[A] val p: Int): + @binaryAPI private[A] val a: Int = 1 + @binaryAPI private[A] lazy val b: Int = 1 + @binaryAPI private[A] var c: Int = 1 + @binaryAPI private[A] def d: Int = 1 + @binaryAPI private[A] given e: Int = 1 + @binaryAPI private[A] given f(using Double): Int = 1 + + +def test(a: A) = + a.p // error + a.a // error + a.b // error + a.c // error + a.d // error + a.e // error + a.f(using 1.0) // error diff --git a/tests/neg/binaryAPI.check b/tests/neg/binaryAPI.check new file mode 100644 index 000000000000..4500ae537b92 --- /dev/null +++ b/tests/neg/binaryAPI.check @@ -0,0 +1,44 @@ +-- Error: tests/neg/binaryAPI.scala:6:17 ------------------------------------------------------------------------------- +6 |@binaryAPI class C: // error + | ^ + | @binaryAPI cannot be used on class definitions +-- Error: tests/neg/binaryAPI.scala:7:25 ------------------------------------------------------------------------------- +7 | @binaryAPI private def f: Unit = // error + | ^ + | @binaryAPI cannot be used on private definitions. + | + | Could use private[C] or protected instead. +-- Error: tests/neg/binaryAPI.scala:8:19 ------------------------------------------------------------------------------- +8 | @binaryAPI def g = () // error + | ^ + | @binaryAPI cannot be used on local definitions. +-- Error: tests/neg/binaryAPI.scala:10:19 ------------------------------------------------------------------------------ +10 |class D(@binaryAPI x: Int) // error + | ^ + | @binaryAPI cannot be used on private definitions. + | + | Could use private[D] or protected instead. +-- Error: tests/neg/binaryAPI.scala:11:19 ------------------------------------------------------------------------------ +11 |class E[@binaryAPI T] // error + | ^ + | @binaryAPI cannot be used on type definitions +-- Error: tests/neg/binaryAPI.scala:15:16 ------------------------------------------------------------------------------ +15 |@binaryAPI enum Enum1: // error + | ^ + | @binaryAPI cannot be used on enum definitions. +-- Error: tests/neg/binaryAPI.scala:19:18 ------------------------------------------------------------------------------ +19 | @binaryAPI case A // error + | ^ + | @binaryAPI cannot be used on enum definitions. +-- Error: tests/neg/binaryAPI.scala:20:18 ------------------------------------------------------------------------------ +20 | @binaryAPI case B(a: Int) // error + | ^ + | @binaryAPI cannot be used on enum definitions. +-- Error: tests/neg/binaryAPI.scala:5:16 ------------------------------------------------------------------------------- +5 |@binaryAPI type A // error + | ^ + | @binaryAPI cannot be used on type definitions +-- Error: tests/neg/binaryAPI.scala:13:17 ------------------------------------------------------------------------------ +13 |def f(@binaryAPI x: Int) = 3 // error + | ^ + | @binaryAPI cannot be used on local definitions. diff --git a/tests/neg/binaryAPI.scala b/tests/neg/binaryAPI.scala new file mode 100644 index 000000000000..7cc77556cac0 --- /dev/null +++ b/tests/neg/binaryAPI.scala @@ -0,0 +1,20 @@ +package foo + +import scala.annotation.binaryAPI + +@binaryAPI type A // error +@binaryAPI class C: // error + @binaryAPI private def f: Unit = // error + @binaryAPI def g = () // error + () +class D(@binaryAPI x: Int) // error +class E[@binaryAPI T] // error + +def f(@binaryAPI x: Int) = 3 // error + +@binaryAPI enum Enum1: // error + case A + +enum Enum2: + @binaryAPI case A // error + @binaryAPI case B(a: Int) // 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..116095d0b71f --- /dev/null +++ b/tests/pos-macros/i15413/Macro_1.scala @@ -0,0 +1,8 @@ +import scala.quoted.* +import scala.annotation.binaryAPI + +class Macro: + inline def foo = ${ Macro.fooImpl } + +object Macro: + @binaryAPI private[Macro] 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..05e67eca7f83 --- /dev/null +++ b/tests/pos-macros/i15413b/Macro_1.scala @@ -0,0 +1,8 @@ +package bar + +import scala.quoted.* +import scala.annotation.binaryAPI + +inline def foo = ${ fooImpl } + +@binaryAPI private[bar] 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..5fc688c79b68 --- /dev/null +++ b/tests/pos-macros/i15413b/Test_2.scala @@ -0,0 +1 @@ +def test = bar.foo diff --git a/tests/pos/binaryAPI.scala b/tests/pos/binaryAPI.scala new file mode 100644 index 000000000000..7e9aea5ba207 --- /dev/null +++ b/tests/pos/binaryAPI.scala @@ -0,0 +1,83 @@ +package foo + +import scala.annotation.binaryAPI + +class Foo(@binaryAPI private[Foo] val param: Int, @binaryAPI private[Foo] var param2: Int): + @binaryAPI + protected val protectedVal: Int = 2 + @binaryAPI + private[foo] val packagePrivateVal: Int = 2 + @binaryAPI + protected var protectedVar: Int = 2 + @binaryAPI + private[foo] var packagePrivateVar: Int = 2 + + inline def foo: Int = + protectedVar = 3 + packagePrivateVar = 3 + param + param2 + protectedVal + packagePrivateVal + protectedVar + packagePrivateVar + +class Bar() extends Foo(3, 3): + override protected val protectedVal: Int = 2 + + override private[foo] val packagePrivateVal: Int = 2 + + inline def bar: Int = protectedVal + packagePrivateVal + +class Baz() extends Foo(4, 4): + @binaryAPI // TODO warn? Not needed because Foo.protectedVal is already @binaryAPI + override protected val protectedVal: Int = 2 + + @binaryAPI + override private[foo] val packagePrivateVal: Int = 2 + + inline def baz: Int = protectedVal + packagePrivateVal + + +class Qux() extends Foo(5, 5): + inline def qux: Int = protectedVal + packagePrivateVal + +def test = + Foo(3, 3).foo + Bar().bar + Baz().baz + Qux().qux + +@binaryAPI given Int = 1 +@binaryAPI given (using Double): Int = 1 + +trait A[T]: + def f: T +@binaryAPI given A[Int] with + def f: Int = 1 +@binaryAPI given (using Double): A[Int] with + def f: Int = 1 + +package inlines { + // Case that needed to be converted with MakeInlineablePassing + class C[T](x: T) { + @binaryAPI 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("") + } + } +} + +package foo { + private object Foo: + @binaryAPI private[foo] def x: Int = 1 + inline def f: Int = Foo.x +} +def testFoo = foo.f + +def localTest = + class Foo: + @annotation.binaryAPI private[Foo] val a: Int = 1 + @annotation.binaryAPI protected val b: Int = 1 diff --git a/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala b/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala index 644efb54c32e..3353b2647269 100644 --- a/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala @@ -36,7 +36,6 @@ val experimentalDefinitionInLibrary = Set( "scala.annotation.MainAnnotation$.Parameter", "scala.annotation.MainAnnotation$.ParameterAnnotation", - //// New feature: prototype of new version of @main // This will never be stabilized. When it is ready it should replace the old @main annotation (requires scala.annotation.MainAnnotation). // Needs user feedback. diff --git a/tests/run/i13215.scala b/tests/run/i13215.scala new file mode 100644 index 000000000000..d2055c2490dc --- /dev/null +++ b/tests/run/i13215.scala @@ -0,0 +1,12 @@ +import scala.annotation.binaryAPI + +package foo { + trait Bar: + inline def baz = Baz + + @binaryAPI private[foo] object Baz +} + +@main def Test: Unit = + val bar = new foo.Bar {} + bar.baz From 82b3e13eba1d9f80c4e1ecdcdbe4377957073368 Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Thu, 23 Feb 2023 18:25:12 +0100 Subject: [PATCH 02/11] Support `@binaryAPI` on `private[this]` definitions In this case the annotation will generate a public accessor (and setter). This accessor has a consistent name that avoids clashes (`$inline$`). It will have the same name as a private accessor that would have been generate in a non final class by the old inline accessor generation. --- .../dotc/inlines/PrepareInlineable.scala | 38 ++++- .../tools/dotc/transform/AccessProxies.scala | 4 +- .../dotc/transform/ProtectedAccessors.scala | 2 +- .../src/dotty/tools/dotc/typer/Checking.scala | 1 - .../src/dotty/tools/dotc/typer/Typer.scala | 7 + .../tools/backend/jvm/BinaryAPITests.scala | 141 +++++++++++++++++- tests/neg/binaryAPI.check | 30 ++-- tests/neg/binaryAPI.scala | 5 +- tests/pos/binaryAPI.scala | 30 +++- 9 files changed, 210 insertions(+), 48 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala b/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala index 1e3320d68300..f280ac51175e 100644 --- a/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala +++ b/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala @@ -35,6 +35,9 @@ object PrepareInlineable { def makeInlineable(tree: Tree)(using Context): Tree = ctx.property(InlineAccessorsKey).get.makeInlineable(tree) + def makePrivateBinaryAPIAccessor(sym: Symbol)(using Context): Unit = + ctx.property(InlineAccessorsKey).get.makePrivateBinaryAPIAccessor(sym) + def addAccessorDefs(cls: Symbol, body: List[Tree])(using Context): List[Tree] = ctx.property(InlineAccessorsKey) match case Some(inlineAccessors) => inlineAccessors.addAccessorDefs(cls, body) @@ -51,15 +54,14 @@ object PrepareInlineable { case _ => false } - /** A tree map which inserts accessors for non-public term members accessed from inlined code. - */ - abstract class MakeInlineableMap(val inlineSym: Symbol) extends TreeMap with Insert { + trait InsertInlineAccessors extends Insert { def useBinaryAPI: Boolean - def accessorNameOf(name: TermName, site: Symbol)(using Context): TermName = - val accName = InlineAccessorName(name) - if site.isExtensibleClass then accName.expandedName(site) else accName + def accessorNameOf(accessed: Symbol, site: Symbol)(using Context): TermName = + val accName = InlineAccessorName(accessed.name.asTermName) + if site.isExtensibleClass || useBinaryAPI then accName.expandedName(site) + else accName /** A definition needs an accessor if it is private, protected, or qualified private * and it is not part of the tree that gets inlined. The latter test is implemented @@ -74,11 +76,21 @@ object PrepareInlineable { def needsAccessor(sym: Symbol)(using Context): Boolean = sym.isTerm && (sym.isOneOf(AccessFlags) || sym.privateWithin.exists) && - (!useBinaryAPI || !sym.isBinaryAPI) && - !sym.isContainedIn(inlineSym) && + (!useBinaryAPI || !sym.isBinaryAPI || (sym.is(Private) && !sym.owner.is(Trait))) && !(sym.isStableMember && sym.info.widenTermRefExpr.isInstanceOf[ConstantType]) && !sym.isInlineMethod && (Inlines.inInlineMethod || StagingLevel.level > 0) + } + + class InsertPrivateBinaryAPIAccessors extends InsertInlineAccessors: + def useBinaryAPI: Boolean = true + + /** A tree map which inserts accessors for non-public term members accessed from inlined code. + */ + abstract class MakeInlineableMap(val inlineSym: Symbol) extends TreeMap with InsertInlineAccessors { + + override def needsAccessor(sym: Symbol)(using Context): Boolean = + !sym.isContainedIn(inlineSym) && super.needsAccessor(sym) def preTransform(tree: Tree)(using Context): Tree @@ -210,6 +222,16 @@ object PrepareInlineable { } } + /** Create an inline accessor for this definition. */ + def makePrivateBinaryAPIAccessor(sym: Symbol)(using Context): Unit = + assert(sym.is(Private)) + if !sym.owner.is(Trait) then + val ref = tpd.ref(sym).asInstanceOf[RefTree] + val insertPrivateBinaryAPIAccessors = new InsertPrivateBinaryAPIAccessors() + val accessor = insertPrivateBinaryAPIAccessors.useAccessor(ref) + if sym.is(Mutable) then + insertPrivateBinaryAPIAccessors.useSetter(accessor) + /** 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 diff --git a/compiler/src/dotty/tools/dotc/transform/AccessProxies.scala b/compiler/src/dotty/tools/dotc/transform/AccessProxies.scala index 3175ffceae49..5096681bce16 100644 --- a/compiler/src/dotty/tools/dotc/transform/AccessProxies.scala +++ b/compiler/src/dotty/tools/dotc/transform/AccessProxies.scala @@ -67,7 +67,7 @@ abstract class AccessProxies { import ast.tpd._ /** The name of the accessor for definition with given `name` in given `site` */ - def accessorNameOf(name: TermName, site: Symbol)(using Context): TermName + def accessorNameOf(accessed: Symbol, site: Symbol)(using Context): TermName def needsAccessor(sym: Symbol)(using Context): Boolean def ifNoHost(reference: RefTree)(using Context): Tree = { @@ -136,7 +136,7 @@ abstract class AccessProxies { if (accessorClass.exists) { if accessorClass.is(Package) then accessorClass = ctx.owner.topLevelClass - val accessorName = accessorNameOf(accessed.name, accessorClass) + val accessorName = accessorNameOf(accessed, accessorClass) val accessorInfo = accessed.info.ensureMethodic.asSeenFrom(accessorClass.thisType, accessed.owner) val accessor = accessorSymbol(accessorClass, accessorName, accessorInfo, accessed) diff --git a/compiler/src/dotty/tools/dotc/transform/ProtectedAccessors.scala b/compiler/src/dotty/tools/dotc/transform/ProtectedAccessors.scala index 6d8f7bdb32cb..b093cf9038d7 100644 --- a/compiler/src/dotty/tools/dotc/transform/ProtectedAccessors.scala +++ b/compiler/src/dotty/tools/dotc/transform/ProtectedAccessors.scala @@ -64,7 +64,7 @@ class ProtectedAccessors extends MiniPhase { private class Accessors extends AccessProxies { val insert: Insert = new Insert { - def accessorNameOf(name: TermName, site: Symbol)(using Context): TermName = ProtectedAccessorName(name) + def accessorNameOf(accessed: Symbol, site: Symbol)(using Context): TermName = ProtectedAccessorName(accessed.name.asTermName) def needsAccessor(sym: Symbol)(using Context) = ProtectedAccessors.needsAccessor(sym) override def ifNoHost(reference: RefTree)(using Context): Tree = { diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 5349be5f44a9..5cbd76725b5d 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -530,7 +530,6 @@ object Checking { if sym.is(Enum) then fail(em"@binaryAPI cannot be used on enum definitions.") else if sym.isType && !sym.is(Module) && !(sym.is(Given) || sym.companionModule.is(Given)) then fail(em"@binaryAPI cannot be used on ${sym.showKind} definitions") else if !sym.owner.isClass && !(sym.is(Param) && sym.owner.isConstructor) then fail(em"@binaryAPI cannot be used on local definitions.") - else if sym.is(Private) then fail(em"@binaryAPI cannot be used on private definitions.\n\nCould use private[${sym.owner.name}] or protected instead.") if (sym.hasAnnotation(defn.NativeAnnot)) { if (!sym.is(Deferred)) fail(NativeMembersMayNotHaveImplementation(sym)) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 7eb8519739c6..89bee37bf35f 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2429,6 +2429,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer } val vdef1 = assignType(cpy.ValDef(vdef)(name, tpt1, rhs1), sym) postProcessInfo(sym) + binaryAPI(sym) vdef1.setDefTree } @@ -2532,6 +2533,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val ddef2 = assignType(cpy.DefDef(ddef)(name, paramss1, tpt1, rhs1), sym) postProcessInfo(sym) + binaryAPI(sym) ddef2.setDefTree //todo: make sure dependent method types do not depend on implicits or by-name params } @@ -2545,6 +2547,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 binaryAPI(sym: Symbol)(using Context): Unit = + if !ctx.isAfterTyper && !sym.is(Param) && sym.is(Private) && sym.hasAnnotation(defn.BinaryAPIAnnot) then + PrepareInlineable.makePrivateBinaryAPIAccessor(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/BinaryAPITests.scala b/compiler/test/dotty/tools/backend/jvm/BinaryAPITests.scala index 278543daac6e..776316026548 100644 --- a/compiler/test/dotty/tools/backend/jvm/BinaryAPITests.scala +++ b/compiler/test/dotty/tools/backend/jvm/BinaryAPITests.scala @@ -23,23 +23,35 @@ class BinaryAPITests extends DottyBytecodeTest { assert(method.desc == desc) assert((method.access & privateOrProtectedOpcode) == 0) + private def checkPrivateMethod(classNode: ClassNode, methodName: String, desc: String): Unit = + val method = getMethod(classNode, methodName) + assert(method.desc == desc) + assert((method.access & Opcodes.ACC_PRIVATE) == Opcodes.ACC_PRIVATE) + private def checkPublicField(classNode: ClassNode, fliedName: String): Unit = val method = getField(classNode, fliedName) assert((method.access & privateOrProtectedOpcode) == 0) + private def checkPrivateField(classNode: ClassNode, fliedName: String): Unit = + val method = getField(classNode, fliedName) + assert((method.access & Opcodes.ACC_PRIVATE) == Opcodes.ACC_PRIVATE) + @Test def binaryAPIDef(): Unit = { val code = """import scala.annotation.binaryAPI |class C: + | @binaryAPI private def privateBinaryAPI: Int = 1 | @binaryAPI private[C] def packagePrivateBinaryAPI: Int = 1 | @binaryAPI protected def protectedBinaryAPI: Int = 1 - | inline def inlined = packagePrivateBinaryAPI + protectedBinaryAPI + | inline def inlined = privateBinaryAPI + packagePrivateBinaryAPI + protectedBinaryAPI | def testInlined = inlined """.stripMargin checkBCode(code) { dir => val cClass = loadClassNode(dir.lookupName("C.class", directory = false).input, skipDebugInfo = false) + checkPrivateMethod(cClass, "privateBinaryAPI", "()I") + checkPublicMethod(cClass, "C$$inline$privateBinaryAPI", "()I") checkPublicMethod(cClass, "packagePrivateBinaryAPI", "()I") checkPublicMethod(cClass, "protectedBinaryAPI", "()I") @@ -47,6 +59,7 @@ class BinaryAPITests extends DottyBytecodeTest { val testInlined = getMethod(cClass, "testInlined") val testInlinedInstructions = instructionsFromMethod(testInlined).filter(_.isInstanceOf[Invoke]) assertSameCode(testInlinedInstructions, List( + Invoke(INVOKEVIRTUAL, "C", "C$$inline$privateBinaryAPI", "()I", false), Invoke(INVOKEVIRTUAL, "C", "packagePrivateBinaryAPI", "()I", false), Invoke(INVOKEVIRTUAL, "C", "protectedBinaryAPI", "()I", false), )) @@ -58,18 +71,25 @@ class BinaryAPITests extends DottyBytecodeTest { val code = """import scala.annotation.binaryAPI |class C: + | @binaryAPI private val privateBinaryAPI: Int = 1 | @binaryAPI private[C] val packagePrivateBinaryAPI: Int = 1 | @binaryAPI protected val protectedBinaryAPI: Int = 1 + | @binaryAPI private lazy val lazyPrivateBinaryAPI: Int = 1 | @binaryAPI private[C] lazy val lazyPackagePrivateBinaryAPI: Int = 1 | @binaryAPI protected lazy val lazyProtectedBinaryAPI: Int = 1 - | inline def inlined = packagePrivateBinaryAPI + protectedBinaryAPI + lazyPackagePrivateBinaryAPI + lazyProtectedBinaryAPI + | inline def inlined = privateBinaryAPI + packagePrivateBinaryAPI + protectedBinaryAPI + lazyPrivateBinaryAPI + lazyPackagePrivateBinaryAPI + lazyProtectedBinaryAPI | def testInlined = inlined """.stripMargin checkBCode(code) { dir => val cClass = loadClassNode(dir.lookupName("C.class", directory = false).input, skipDebugInfo = false) + checkPrivateField(cClass, "privateBinaryAPI") + checkPublicMethod(cClass, "C$$inline$privateBinaryAPI", "()I") checkPublicMethod(cClass, "packagePrivateBinaryAPI", "()I") checkPublicMethod(cClass, "protectedBinaryAPI", "()I") + + checkPrivateMethod(cClass, "lazyPrivateBinaryAPI", "()I") + checkPublicMethod(cClass, "C$$inline$lazyPrivateBinaryAPI", "()I") checkPublicMethod(cClass, "lazyPackagePrivateBinaryAPI", "()I") checkPublicMethod(cClass, "lazyProtectedBinaryAPI", "()I") @@ -77,8 +97,10 @@ class BinaryAPITests extends DottyBytecodeTest { val testInlined = getMethod(cClass, "testInlined") val testInlinedInstructions = instructionsFromMethod(testInlined).filter(_.isInstanceOf[Invoke]) assertSameCode(testInlinedInstructions, List( + Invoke(INVOKEVIRTUAL, "C", "C$$inline$privateBinaryAPI", "()I", false), Invoke(INVOKEVIRTUAL, "C", "packagePrivateBinaryAPI", "()I", false), Invoke(INVOKEVIRTUAL, "C", "protectedBinaryAPI", "()I", false), + Invoke(INVOKEVIRTUAL, "C", "C$$inline$lazyPrivateBinaryAPI", "()I", false), Invoke(INVOKEVIRTUAL, "C", "lazyPackagePrivateBinaryAPI", "()I", false), Invoke(INVOKEVIRTUAL, "C", "lazyProtectedBinaryAPI", "()I", false), )) @@ -90,17 +112,22 @@ class BinaryAPITests extends DottyBytecodeTest { val code = """import scala.annotation.binaryAPI |class C: + | @binaryAPI private var privateBinaryAPI: Int = 1 | @binaryAPI private[C] var packagePrivateBinaryAPI: Int = 1 | @binaryAPI protected var protectedBinaryAPI: Int = 1 | inline def inlined = + | privateBinaryAPI = 1 | packagePrivateBinaryAPI = 1 | protectedBinaryAPI = 1 - | packagePrivateBinaryAPI + protectedBinaryAPI + | privateBinaryAPI + packagePrivateBinaryAPI + protectedBinaryAPI | def testInlined = inlined """.stripMargin checkBCode(code) { dir => val cClass = loadClassNode(dir.lookupName("C.class", directory = false).input, skipDebugInfo = false) + checkPrivateField(cClass, "privateBinaryAPI") + checkPublicMethod(cClass, "C$$inline$privateBinaryAPI", "()I") + checkPublicMethod(cClass, "C$$inline$privateBinaryAPI_$eq", "(I)V") checkPublicMethod(cClass, "packagePrivateBinaryAPI", "()I") checkPublicMethod(cClass, "packagePrivateBinaryAPI_$eq", "(I)V") checkPublicMethod(cClass, "protectedBinaryAPI", "()I") @@ -110,8 +137,10 @@ class BinaryAPITests extends DottyBytecodeTest { val testInlined = getMethod(cClass, "testInlined") val testInlinedInstructions = instructionsFromMethod(testInlined).filter(_.isInstanceOf[Invoke]) assertSameCode(testInlinedInstructions, List( + Invoke(INVOKEVIRTUAL, "C", "C$$inline$privateBinaryAPI_$eq", "(I)V", false), Invoke(INVOKEVIRTUAL, "C", "packagePrivateBinaryAPI_$eq", "(I)V", false), Invoke(INVOKEVIRTUAL, "C", "protectedBinaryAPI_$eq", "(I)V", false), + Invoke(INVOKEVIRTUAL, "C", "C$$inline$privateBinaryAPI", "()I", false), Invoke(INVOKEVIRTUAL, "C", "packagePrivateBinaryAPI", "()I", false), Invoke(INVOKEVIRTUAL, "C", "protectedBinaryAPI", "()I", false), )) @@ -123,8 +152,10 @@ class BinaryAPITests extends DottyBytecodeTest { val code = """import scala.annotation.binaryAPI |class C: + | @binaryAPI private given privateBinaryAPI1: Int = 1 | @binaryAPI private[C] given packagePrivateBinaryAPI1: Int = 1 | @binaryAPI protected given protectedBinaryAPI1: Int = 1 + | @binaryAPI private given privateBinaryAPI2(using Int): Int = 1 | @binaryAPI private[C] given packagePrivateBinaryAPI2(using Int): Int = 1 | @binaryAPI protected given protectedBinaryAPI2(using Int): Int = 1 | inline def inlined = @@ -133,8 +164,13 @@ class BinaryAPITests extends DottyBytecodeTest { """.stripMargin checkBCode(code) { dir => val cClass = loadClassNode(dir.lookupName("C.class", directory = false).input, skipDebugInfo = false) + checkPrivateMethod(cClass, "privateBinaryAPI1", "()I") + checkPublicMethod(cClass, "C$$inline$privateBinaryAPI1", "()I") checkPublicMethod(cClass, "packagePrivateBinaryAPI1", "()I") checkPublicMethod(cClass, "protectedBinaryAPI1", "()I") + + checkPrivateMethod(cClass, "privateBinaryAPI2", "(I)I") + checkPublicMethod(cClass, "C$$inline$privateBinaryAPI2", "(I)I") checkPublicMethod(cClass, "packagePrivateBinaryAPI2", "(I)I") checkPublicMethod(cClass, "protectedBinaryAPI2", "(I)I") @@ -155,17 +191,22 @@ class BinaryAPITests extends DottyBytecodeTest { val code = """import scala.annotation.binaryAPI |class C( + | @binaryAPI private val privateBinaryAPI: Int = 1, | @binaryAPI private[C] val packagePrivateBinaryAPI: Int = 1, - | @binaryAPI protected val protectedBinaryAPI: Int = 1 + | @binaryAPI protected val protectedBinaryAPI: Int = 1, + | @binaryAPI private var privateVarBinaryAPI: Int = 1 |) { | inline def inlined = - | packagePrivateBinaryAPI + protectedBinaryAPI + | privateVarBinaryAPI = 1 + | privateBinaryAPI + packagePrivateBinaryAPI + protectedBinaryAPI + privateVarBinaryAPI | def testInlined = inlined |} """.stripMargin checkBCode(code) { dir => val cClass = loadClassNode(dir.lookupName("C.class", directory = false).input, skipDebugInfo = false) + checkPrivateMethod(cClass, "privateBinaryAPI", "()I") + checkPublicMethod(cClass, "C$$inline$privateBinaryAPI", "()I") checkPublicMethod(cClass, "packagePrivateBinaryAPI", "()I") checkPublicMethod(cClass, "protectedBinaryAPI", "()I") @@ -173,8 +214,11 @@ class BinaryAPITests extends DottyBytecodeTest { val testInlined = getMethod(cClass, "testInlined") val testInlinedInstructions = instructionsFromMethod(testInlined).filter(_.isInstanceOf[Invoke]) assertSameCode(testInlinedInstructions, List( + Invoke(INVOKEVIRTUAL, "C", "C$$inline$privateVarBinaryAPI_$eq", "(I)V", false), + Invoke(INVOKEVIRTUAL, "C", "C$$inline$privateBinaryAPI", "()I", false), Invoke(INVOKEVIRTUAL, "C", "packagePrivateBinaryAPI", "()I", false), Invoke(INVOKEVIRTUAL, "C", "protectedBinaryAPI", "()I", false), + Invoke(INVOKEVIRTUAL, "C", "C$$inline$privateVarBinaryAPI", "()I", false), )) } } @@ -184,10 +228,14 @@ class BinaryAPITests extends DottyBytecodeTest { val code = """package foo |import scala.annotation.binaryAPI + |@binaryAPI private object PrivateBinaryAPI |@binaryAPI private[foo] object PackagePrivateBinaryAPI |@binaryAPI protected object ProtectedBinaryAPI """.stripMargin checkBCode(code) { dir => + val privateBinaryAPI = loadClassNode(dir.subdirectoryNamed("foo").lookupName("PrivateBinaryAPI$.class", directory = false).input, skipDebugInfo = false) + checkPublicField(privateBinaryAPI, "MODULE$") + val packagePrivateBinaryAPI = loadClassNode(dir.subdirectoryNamed("foo").lookupName("PackagePrivateBinaryAPI$.class", directory = false).input, skipDebugInfo = false) checkPublicField(packagePrivateBinaryAPI, "MODULE$") @@ -196,28 +244,37 @@ class BinaryAPITests extends DottyBytecodeTest { } } - @Test + @Test def binaryAPITraitDefs(): Unit = { val code = """import scala.annotation.binaryAPI |trait C: + | @binaryAPI private val privateValBinaryAPI: Int = 1 | @binaryAPI private[C] val packagePrivateValBinaryAPI: Int = 1 | @binaryAPI protected val protectedValBinaryAPI: Int = 1 + | @binaryAPI private lazy val privateLazyValBinaryAPI: Int = 1 | @binaryAPI private[C] lazy val packagePrivateLazyValBinaryAPI: Int = 1 | @binaryAPI protected lazy val protectedLazyValBinaryAPI: Int = 1 + | @binaryAPI private var privateVarBinaryAPI: Int = 1 | @binaryAPI private[C] var packagePrivateVarBinaryAPI: Int = 1 | @binaryAPI protected var protectedVarBinaryAPI: Int = 1 + | @binaryAPI private def privateDefBinaryAPI: Int = 1 | @binaryAPI private[C] def packagePrivateDefBinaryAPI: Int = 1 | @binaryAPI protected def protectedDefBinaryAPI: Int = 1 | inline def inlined = + | privateVarBinaryAPI = 1 | packagePrivateVarBinaryAPI = 1 | protectedVarBinaryAPI = 1 + | privateValBinaryAPI + | packagePrivateValBinaryAPI + | protectedValBinaryAPI + + | privateLazyValBinaryAPI + | packagePrivateLazyValBinaryAPI + | protectedLazyValBinaryAPI + + | privateVarBinaryAPI + | packagePrivateVarBinaryAPI + | protectedVarBinaryAPI + + | privateDefBinaryAPI + | packagePrivateDefBinaryAPI + | protectedDefBinaryAPI | def testInlined = inlined @@ -225,14 +282,22 @@ class BinaryAPITests extends DottyBytecodeTest { checkBCode(code) { dir => val cTrait = loadClassNode(dir.lookupName("C.class", directory = false).input, skipDebugInfo = false) + checkPublicMethod(cTrait, "C$$privateValBinaryAPI", "()I") + checkPublicMethod(cTrait, "C$$inline$privateValBinaryAPI", "()I") checkPublicMethod(cTrait, "packagePrivateValBinaryAPI", "()I") checkPublicMethod(cTrait, "protectedValBinaryAPI", "()I") + checkPublicMethod(cTrait, "C$$privateLazyValBinaryAPI", "()I") + checkPublicMethod(cTrait, "C$$inline$privateLazyValBinaryAPI", "()I") checkPublicMethod(cTrait, "packagePrivateLazyValBinaryAPI", "()I") checkPublicMethod(cTrait, "protectedLazyValBinaryAPI", "()I") + checkPublicMethod(cTrait, "C$$privateVarBinaryAPI", "()I") + checkPublicMethod(cTrait, "C$$inline$privateVarBinaryAPI", "()I") + checkPublicMethod(cTrait, "C$$inline$privateVarBinaryAPI_$eq", "(I)V") checkPublicMethod(cTrait, "packagePrivateVarBinaryAPI", "()I") checkPublicMethod(cTrait, "packagePrivateVarBinaryAPI_$eq", "(I)V") checkPublicMethod(cTrait, "protectedVarBinaryAPI", "()I") checkPublicMethod(cTrait, "protectedVarBinaryAPI_$eq", "(I)V") + checkPublicMethod(cTrait, "C$$inline$privateDefBinaryAPI", "()I") checkPublicMethod(cTrait, "packagePrivateDefBinaryAPI", "()I") checkPublicMethod(cTrait, "protectedDefBinaryAPI", "()I") @@ -240,20 +305,64 @@ class BinaryAPITests extends DottyBytecodeTest { val testInlined = getMethod(cTrait, "testInlined") val testInlinedInstructions = instructionsFromMethod(testInlined).filter(_.isInstanceOf[Invoke]) assertSameCode(testInlinedInstructions, List( + Invoke(INVOKEINTERFACE, "C", "C$$privateVarBinaryAPI_$eq", "(I)V", true), Invoke(INVOKEINTERFACE, "C", "packagePrivateVarBinaryAPI_$eq", "(I)V", true), Invoke(INVOKEINTERFACE, "C", "protectedVarBinaryAPI_$eq", "(I)V", true), + Invoke(INVOKEINTERFACE, "C", "C$$privateValBinaryAPI", "()I", true), Invoke(INVOKEINTERFACE, "C", "packagePrivateValBinaryAPI", "()I", true), Invoke(INVOKEINTERFACE, "C", "protectedValBinaryAPI", "()I", true), + Invoke(INVOKEINTERFACE, "C", "C$$privateLazyValBinaryAPI", "()I", true), Invoke(INVOKEINTERFACE, "C", "packagePrivateLazyValBinaryAPI", "()I", true), Invoke(INVOKEINTERFACE, "C", "protectedLazyValBinaryAPI", "()I", true), + Invoke(INVOKEINTERFACE, "C", "C$$privateVarBinaryAPI", "()I", true), Invoke(INVOKEINTERFACE, "C", "packagePrivateVarBinaryAPI", "()I", true), Invoke(INVOKEINTERFACE, "C", "protectedVarBinaryAPI", "()I", true), + Invoke(INVOKESPECIAL, "C", "privateDefBinaryAPI", "()I", true), Invoke(INVOKEINTERFACE, "C", "packagePrivateDefBinaryAPI", "()I", true), Invoke(INVOKEINTERFACE, "C", "protectedDefBinaryAPI", "()I", true) )) } } + @Test + def binaryAPIDefFinalPrivateAccessors(): Unit = { + val code = + """import scala.annotation.binaryAPI + |final class C(@binaryAPI paramBinaryAPI: Int): + | @binaryAPI private val valBinaryAPI: Int = 1 + | @binaryAPI private def defBinaryAPI: Int = 1 + | @binaryAPI private var varBinaryAPI: Int = 1 + | inline def inlined = + | varBinaryAPI = 1 + | paramBinaryAPI + valBinaryAPI + defBinaryAPI + varBinaryAPI + | def testInlined = inlined + """.stripMargin + checkBCode(code) { dir => + val cClass = loadClassNode(dir.lookupName("C.class", directory = false).input, skipDebugInfo = false) + + checkPrivateField(cClass, "paramBinaryAPI") + checkPublicMethod(cClass, "C$$inline$paramBinaryAPI", "()I") + checkPrivateField(cClass, "valBinaryAPI") + checkPublicMethod(cClass, "C$$inline$valBinaryAPI", "()I") + checkPrivateMethod(cClass, "defBinaryAPI", "()I") + checkPublicMethod(cClass, "C$$inline$defBinaryAPI", "()I") + checkPrivateField(cClass, "varBinaryAPI") + checkPublicMethod(cClass, "C$$inline$varBinaryAPI", "()I") + checkPublicMethod(cClass, "C$$inline$varBinaryAPI_$eq", "(I)V") + + // Check that the @binaryAPI annotated method is called + val testInlined = getMethod(cClass, "testInlined") + val testInlinedInstructions = instructionsFromMethod(testInlined).filter(_.isInstanceOf[Invoke]) + assertSameCode(testInlinedInstructions, List( + Invoke(INVOKEVIRTUAL, "C", "C$$inline$varBinaryAPI_$eq", "(I)V", false), + Invoke(INVOKEVIRTUAL, "C", "C$$inline$paramBinaryAPI", "()I", false), + Invoke(INVOKEVIRTUAL, "C", "C$$inline$valBinaryAPI", "()I", false), + Invoke(INVOKEVIRTUAL, "C", "C$$inline$defBinaryAPI", "()I", false), + Invoke(INVOKEVIRTUAL, "C", "C$$inline$varBinaryAPI", "()I", false), + )) + } + } + @Test def i13215(): Unit = { val code = @@ -274,6 +383,26 @@ class BinaryAPITests extends DottyBytecodeTest { } } + @Test + def i13215b(): Unit = { + val code = + """import scala.annotation.binaryAPI + |package foo: + | trait Bar: + | inline def baz = Baz + | def testInlined = baz + | @binaryAPI private object Baz + """.stripMargin + checkBCode(code) { dir => + // For 3.0-3.3 compat + val barClass = loadClassNode(dir.subdirectoryNamed("foo").lookupName("Bar.class", directory = false).input, skipDebugInfo = false) + checkPublicMethod(barClass, "foo$Bar$$inline$Baz", "()Lfoo/Baz$;") + + // Check that the @binaryAPI annotated method is called + checkPublicMethod(barClass, "testInlined", "()Lfoo/Baz$;") + } + } + @Test def i15413(): Unit = { val code = diff --git a/tests/neg/binaryAPI.check b/tests/neg/binaryAPI.check index 4500ae537b92..134b77e96bc7 100644 --- a/tests/neg/binaryAPI.check +++ b/tests/neg/binaryAPI.check @@ -2,43 +2,31 @@ 6 |@binaryAPI class C: // error | ^ | @binaryAPI cannot be used on class definitions --- Error: tests/neg/binaryAPI.scala:7:25 ------------------------------------------------------------------------------- -7 | @binaryAPI private def f: Unit = // error - | ^ - | @binaryAPI cannot be used on private definitions. - | - | Could use private[C] or protected instead. -- Error: tests/neg/binaryAPI.scala:8:19 ------------------------------------------------------------------------------- 8 | @binaryAPI def g = () // error | ^ | @binaryAPI cannot be used on local definitions. -- Error: tests/neg/binaryAPI.scala:10:19 ------------------------------------------------------------------------------ -10 |class D(@binaryAPI x: Int) // error - | ^ - | @binaryAPI cannot be used on private definitions. - | - | Could use private[D] or protected instead. --- Error: tests/neg/binaryAPI.scala:11:19 ------------------------------------------------------------------------------ -11 |class E[@binaryAPI T] // error +10 |class D[@binaryAPI T] // error | ^ | @binaryAPI cannot be used on type definitions --- Error: tests/neg/binaryAPI.scala:15:16 ------------------------------------------------------------------------------ -15 |@binaryAPI enum Enum1: // error +-- Error: tests/neg/binaryAPI.scala:14:16 ------------------------------------------------------------------------------ +14 |@binaryAPI enum Enum1: // error | ^ | @binaryAPI cannot be used on enum definitions. --- Error: tests/neg/binaryAPI.scala:19:18 ------------------------------------------------------------------------------ -19 | @binaryAPI case A // error +-- Error: tests/neg/binaryAPI.scala:18:18 ------------------------------------------------------------------------------ +18 | @binaryAPI case A // error | ^ | @binaryAPI cannot be used on enum definitions. --- Error: tests/neg/binaryAPI.scala:20:18 ------------------------------------------------------------------------------ -20 | @binaryAPI case B(a: Int) // error +-- Error: tests/neg/binaryAPI.scala:19:18 ------------------------------------------------------------------------------ +19 | @binaryAPI case B(a: Int) // error | ^ | @binaryAPI cannot be used on enum definitions. -- Error: tests/neg/binaryAPI.scala:5:16 ------------------------------------------------------------------------------- 5 |@binaryAPI type A // error | ^ | @binaryAPI cannot be used on type definitions --- Error: tests/neg/binaryAPI.scala:13:17 ------------------------------------------------------------------------------ -13 |def f(@binaryAPI x: Int) = 3 // error +-- Error: tests/neg/binaryAPI.scala:12:17 ------------------------------------------------------------------------------ +12 |def f(@binaryAPI x: Int) = 3 // error | ^ | @binaryAPI cannot be used on local definitions. diff --git a/tests/neg/binaryAPI.scala b/tests/neg/binaryAPI.scala index 7cc77556cac0..a6b4223b12a3 100644 --- a/tests/neg/binaryAPI.scala +++ b/tests/neg/binaryAPI.scala @@ -4,11 +4,10 @@ import scala.annotation.binaryAPI @binaryAPI type A // error @binaryAPI class C: // error - @binaryAPI private def f: Unit = // error + def f: Unit = @binaryAPI def g = () // error () -class D(@binaryAPI x: Int) // error -class E[@binaryAPI T] // error +class D[@binaryAPI T] // error def f(@binaryAPI x: Int) = 3 // error diff --git a/tests/pos/binaryAPI.scala b/tests/pos/binaryAPI.scala index 7e9aea5ba207..313a16e34b00 100644 --- a/tests/pos/binaryAPI.scala +++ b/tests/pos/binaryAPI.scala @@ -2,29 +2,34 @@ package foo import scala.annotation.binaryAPI -class Foo(@binaryAPI private[Foo] val param: Int, @binaryAPI private[Foo] var param2: Int): +class Foo(@binaryAPI param: Int, @binaryAPI private[Foo] val paramVal: Int, @binaryAPI private[Foo] var paramVar: Int): + @binaryAPI + private val privateVal: Int = 2 @binaryAPI protected val protectedVal: Int = 2 @binaryAPI private[foo] val packagePrivateVal: Int = 2 @binaryAPI + private val privateVar: Int = 2 + @binaryAPI protected var protectedVar: Int = 2 @binaryAPI private[foo] var packagePrivateVar: Int = 2 inline def foo: Int = + paramVar = 3 protectedVar = 3 packagePrivateVar = 3 - param + param2 + protectedVal + packagePrivateVal + protectedVar + packagePrivateVar + param + paramVal + paramVar + privateVal + protectedVal + packagePrivateVal + protectedVar + packagePrivateVar -class Bar() extends Foo(3, 3): +class Bar() extends Foo(3, 3, 3): override protected val protectedVal: Int = 2 override private[foo] val packagePrivateVal: Int = 2 inline def bar: Int = protectedVal + packagePrivateVal -class Baz() extends Foo(4, 4): +class Baz() extends Foo(4, 4, 4): @binaryAPI // TODO warn? Not needed because Foo.protectedVal is already @binaryAPI override protected val protectedVal: Int = 2 @@ -34,11 +39,11 @@ class Baz() extends Foo(4, 4): inline def baz: Int = protectedVal + packagePrivateVal -class Qux() extends Foo(5, 5): +class Qux() extends Foo(5, 5, 5): inline def qux: Int = protectedVal + packagePrivateVal def test = - Foo(3, 3).foo + Foo(3, 3, 3).foo Bar().bar Baz().baz Qux().qux @@ -81,3 +86,16 @@ def localTest = class Foo: @annotation.binaryAPI private[Foo] val a: Int = 1 @annotation.binaryAPI protected val b: Int = 1 + +trait Trait: + @annotation.binaryAPI private val myVal = 1 + @annotation.binaryAPI private lazy val myLazyVl = 2 + @annotation.binaryAPI private var myVar = 2 + @annotation.binaryAPI private def myDef = 3 + @annotation.binaryAPI private given myGiven: Int = 4 + + inline def inlined: Unit = + myVar = 1 + myVal + myLazyVl + myVar + myDef + myGiven + +def testTrait(t: Trait) = t.inlined \ No newline at end of file From 42b31a40ff72193f0ec4a4ee4a5f988b4fee861d Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Fri, 24 Feb 2023 11:09:13 +0100 Subject: [PATCH 03/11] Don't generate old inline accessors if we use BinaryAPIs Users should add the old accessor explicitly to keep binary compatibility. This accessor should be `@binaryAPI private[C]` where `C` is the enclosing class. --- .../dotc/inlines/PrepareInlineable.scala | 28 +++++++------------ .../tools/backend/jvm/BinaryAPITests.scala | 21 -------------- 2 files changed, 10 insertions(+), 39 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala b/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala index f280ac51175e..941d72535fc0 100644 --- a/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala +++ b/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala @@ -56,11 +56,9 @@ object PrepareInlineable { trait InsertInlineAccessors extends Insert { - def useBinaryAPI: Boolean - def accessorNameOf(accessed: Symbol, site: Symbol)(using Context): TermName = val accName = InlineAccessorName(accessed.name.asTermName) - if site.isExtensibleClass || useBinaryAPI then accName.expandedName(site) + if site.isExtensibleClass || accessed.isBinaryAPI then accName.expandedName(site) else accName /** A definition needs an accessor if it is private, protected, or qualified private @@ -76,14 +74,13 @@ object PrepareInlineable { def needsAccessor(sym: Symbol)(using Context): Boolean = sym.isTerm && (sym.isOneOf(AccessFlags) || sym.privateWithin.exists) && - (!useBinaryAPI || !sym.isBinaryAPI || (sym.is(Private) && !sym.owner.is(Trait))) && + (!sym.isBinaryAPI || (sym.is(Private) && !sym.owner.is(Trait))) && !(sym.isStableMember && sym.info.widenTermRefExpr.isInstanceOf[ConstantType]) && !sym.isInlineMethod && (Inlines.inInlineMethod || StagingLevel.level > 0) } - class InsertPrivateBinaryAPIAccessors extends InsertInlineAccessors: - def useBinaryAPI: Boolean = true + object InsertPrivateBinaryAPIAccessors extends InsertInlineAccessors /** A tree map which inserts accessors for non-public term members accessed from inlined code. */ @@ -110,7 +107,7 @@ object PrepareInlineable { * 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. */ - class MakeInlineableDirect(inlineSym: Symbol, val useBinaryAPI: Boolean) extends MakeInlineableMap(inlineSym) { + 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) { @@ -154,7 +151,7 @@ object PrepareInlineable { * Since different calls might have different receiver types, we need to generate one * such accessor per call, so they need to have unique names. */ - class MakeInlineablePassing(inlineSym: Symbol, val useBinaryAPI: Boolean) extends MakeInlineableMap(inlineSym) { + class MakeInlineablePassing(inlineSym: Symbol) extends MakeInlineableMap(inlineSym) { def preTransform(tree: Tree)(using Context): Tree = tree match { case _: Apply | _: TypeApply | _: RefTree @@ -227,10 +224,9 @@ object PrepareInlineable { assert(sym.is(Private)) if !sym.owner.is(Trait) then val ref = tpd.ref(sym).asInstanceOf[RefTree] - val insertPrivateBinaryAPIAccessors = new InsertPrivateBinaryAPIAccessors() - val accessor = insertPrivateBinaryAPIAccessors.useAccessor(ref) + val accessor = InsertPrivateBinaryAPIAccessors.useAccessor(ref) if sym.is(Mutable) then - insertPrivateBinaryAPIAccessors.useSetter(accessor) + InsertPrivateBinaryAPIAccessors.useSetter(accessor) /** Adds accessors for all non-public term members accessed * from `tree`. Non-public type members are currently left as they are. @@ -247,13 +243,9 @@ object PrepareInlineable { // so no accessors are needed for them. tree else - // Make sure the old accessors are generated for binary compatibility - new MakeInlineablePassing(inlineSym, useBinaryAPI = false).transform( - new MakeInlineableDirect(inlineSym, useBinaryAPI = false).transform(tree)) - - // TODO: warn if MakeInlineablePassing or MakeInlineableDirect generate accessors when useBinaryAPI in enabled - new MakeInlineablePassing(inlineSym, useBinaryAPI = true).transform( - new MakeInlineableDirect(inlineSym, useBinaryAPI = true).transform(tree)) + // TODO: warn if MakeInlineablePassing or MakeInlineableDirect generate accessors + new MakeInlineablePassing(inlineSym).transform( + new MakeInlineableDirect(inlineSym).transform(tree)) } } diff --git a/compiler/test/dotty/tools/backend/jvm/BinaryAPITests.scala b/compiler/test/dotty/tools/backend/jvm/BinaryAPITests.scala index 776316026548..ea1dadc0b5a5 100644 --- a/compiler/test/dotty/tools/backend/jvm/BinaryAPITests.scala +++ b/compiler/test/dotty/tools/backend/jvm/BinaryAPITests.scala @@ -283,21 +283,16 @@ class BinaryAPITests extends DottyBytecodeTest { val cTrait = loadClassNode(dir.lookupName("C.class", directory = false).input, skipDebugInfo = false) checkPublicMethod(cTrait, "C$$privateValBinaryAPI", "()I") - checkPublicMethod(cTrait, "C$$inline$privateValBinaryAPI", "()I") checkPublicMethod(cTrait, "packagePrivateValBinaryAPI", "()I") checkPublicMethod(cTrait, "protectedValBinaryAPI", "()I") checkPublicMethod(cTrait, "C$$privateLazyValBinaryAPI", "()I") - checkPublicMethod(cTrait, "C$$inline$privateLazyValBinaryAPI", "()I") checkPublicMethod(cTrait, "packagePrivateLazyValBinaryAPI", "()I") checkPublicMethod(cTrait, "protectedLazyValBinaryAPI", "()I") checkPublicMethod(cTrait, "C$$privateVarBinaryAPI", "()I") - checkPublicMethod(cTrait, "C$$inline$privateVarBinaryAPI", "()I") - checkPublicMethod(cTrait, "C$$inline$privateVarBinaryAPI_$eq", "(I)V") checkPublicMethod(cTrait, "packagePrivateVarBinaryAPI", "()I") checkPublicMethod(cTrait, "packagePrivateVarBinaryAPI_$eq", "(I)V") checkPublicMethod(cTrait, "protectedVarBinaryAPI", "()I") checkPublicMethod(cTrait, "protectedVarBinaryAPI_$eq", "(I)V") - checkPublicMethod(cTrait, "C$$inline$privateDefBinaryAPI", "()I") checkPublicMethod(cTrait, "packagePrivateDefBinaryAPI", "()I") checkPublicMethod(cTrait, "protectedDefBinaryAPI", "()I") @@ -374,11 +369,7 @@ class BinaryAPITests extends DottyBytecodeTest { | @binaryAPI private[foo] object Baz """.stripMargin checkBCode(code) { dir => - // For 3.0-3.3 compat val barClass = loadClassNode(dir.subdirectoryNamed("foo").lookupName("Bar.class", directory = false).input, skipDebugInfo = false) - checkPublicMethod(barClass, "foo$Bar$$inline$Baz", "()Lfoo/Baz$;") - - // Check that the @binaryAPI annotated method is called checkPublicMethod(barClass, "testInlined", "()Lfoo/Baz$;") } } @@ -394,11 +385,7 @@ class BinaryAPITests extends DottyBytecodeTest { | @binaryAPI private object Baz """.stripMargin checkBCode(code) { dir => - // For 3.0-3.3 compat val barClass = loadClassNode(dir.subdirectoryNamed("foo").lookupName("Bar.class", directory = false).input, skipDebugInfo = false) - checkPublicMethod(barClass, "foo$Bar$$inline$Baz", "()Lfoo/Baz$;") - - // Check that the @binaryAPI annotated method is called checkPublicMethod(barClass, "testInlined", "()Lfoo/Baz$;") } } @@ -415,11 +402,7 @@ class BinaryAPITests extends DottyBytecodeTest { | @binaryAPI private[Macro] def fooImpl = {} """.stripMargin checkBCode(code) { dir => - // For 3.0-3.3 compat val macroClass = loadClassNode(dir.lookupName("Macro.class", directory = false).input, skipDebugInfo = false) - checkPublicMethod(macroClass, "Macro$$inline$fooImpl", "()V") - - // Check that the @binaryAPI annotated method is called val testMethod = getMethod(macroClass, "test") val testInstructions = instructionsFromMethod(testMethod).filter(_.isInstanceOf[Invoke]) assertSameCode(testInstructions, List( @@ -439,11 +422,7 @@ class BinaryAPITests extends DottyBytecodeTest { | @binaryAPI private[foo] def bazImpl = {} """.stripMargin checkBCode(code) { dir => - // For 3.0-3.3 compat val barClass = loadClassNode(dir.subdirectoryNamed("foo").lookupName("C.class", directory = false).input, skipDebugInfo = false) - checkPublicMethod(barClass, "inline$bazImpl$i1", "(Lfoo/D$;)V") - - // Check that the @binaryAPI annotated method is called val testMethod = getMethod(barClass, "test") val testInstructions = instructionsFromMethod(testMethod).filter(_.isInstanceOf[Invoke]) assertSameCode(testInstructions, List( From ab1a18f0152b88b39a824272b2c51de10da468ce Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Fri, 24 Feb 2023 11:46:51 +0100 Subject: [PATCH 04/11] Warn if inline accessors are generated --- community-build/community-projects/scodec | 2 +- .../jvm/PostProcessorFrontendAccess.scala | 3 + .../tools/dotc/core/ConstraintHandling.scala | 10 +- .../src/dotty/tools/dotc/core/Contexts.scala | 6 +- .../dotty/tools/dotc/core/TypeComparer.scala | 10 +- .../src/dotty/tools/dotc/core/Types.scala | 3 +- .../dotc/inlines/PrepareInlineable.scala | 33 +++++- .../dotty/tools/dotc/parsing/Scanners.scala | 4 +- .../dotc/parsing/xml/MarkupParsers.scala | 7 +- .../tools/dotc/printing/RefinedPrinter.scala | 4 +- .../dotty/tools/dotc/reporting/trace.scala | 5 +- .../tools/dotc/transform/AccessProxies.scala | 50 ++++---- .../dotc/transform/sjs/PrepJSInterop.scala | 3 +- .../dotty/tools/dotc/typer/EtaExpansion.scala | 4 +- .../tools/dotc/typer/QuotesAndSplices.scala | 4 +- .../tools/dotc/util/ReusableInstance.scala | 8 +- .../quoted/runtime/impl/QuotesImpl.scala | 2 + .../tools/backend/jvm/BinaryAPITests.scala | 10 +- tests/init/pos/i15465.scala | 4 +- .../inline-unstable-accessors.check | 108 ++++++++++++++++++ .../inline-unstable-accessors.scala | 46 ++++++++ .../neg-macros/delegate-match-1/Macro_1.scala | 4 +- .../neg-macros/delegate-match-2/Macro_1.scala | 4 +- .../neg-macros/delegate-match-3/Macro_1.scala | 4 +- tests/neg-macros/ill-abort.check | 4 +- tests/neg-macros/ill-abort/quoted_1.scala | 3 +- tests/pos-custom-args/i13405/Macro.scala | 4 +- tests/pos-macros/i9570.scala | 3 +- tests/pos/binaryAPI.scala | 48 ++++++-- 29 files changed, 317 insertions(+), 83 deletions(-) create mode 100644 tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.check create mode 100644 tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.scala diff --git a/community-build/community-projects/scodec b/community-build/community-projects/scodec index 96a77ecaaf91..ba025a604ca7 160000 --- a/community-build/community-projects/scodec +++ b/community-build/community-projects/scodec @@ -1 +1 @@ -Subproject commit 96a77ecaaf913f195bb4079966a2e9fb41ce214e +Subproject commit ba025a604ca798918680bf583bf44aa3ee464ea0 diff --git a/compiler/src/dotty/tools/backend/jvm/PostProcessorFrontendAccess.scala b/compiler/src/dotty/tools/backend/jvm/PostProcessorFrontendAccess.scala index 80ee68bc94c3..28ff781442ec 100644 --- a/compiler/src/dotty/tools/backend/jvm/PostProcessorFrontendAccess.scala +++ b/compiler/src/dotty/tools/backend/jvm/PostProcessorFrontendAccess.scala @@ -9,6 +9,8 @@ import dotty.tools.dotc.core.Contexts.Context import dotty.tools.dotc.report import dotty.tools.dotc.core.Phases +import scala.annotation.binaryAPI + /** * Functionality needed in the post-processor whose implementation depends on the compiler * frontend. All methods are synchronized. @@ -20,6 +22,7 @@ sealed abstract class PostProcessorFrontendAccess { def backendReporting: BackendReporting def getEntryPoints: List[String] + @binaryAPI private val frontendLock: AnyRef = new Object() inline final def frontendSynch[T](inline x: => T): T = frontendLock.synchronized(x) } diff --git a/compiler/src/dotty/tools/dotc/core/ConstraintHandling.scala b/compiler/src/dotty/tools/dotc/core/ConstraintHandling.scala index 9ffe2bda73cb..5edbdd3e8350 100644 --- a/compiler/src/dotty/tools/dotc/core/ConstraintHandling.scala +++ b/compiler/src/dotty/tools/dotc/core/ConstraintHandling.scala @@ -15,6 +15,8 @@ import NameKinds.AvoidNameKind import util.SimpleIdentitySet import NullOpsDecorator.stripNull +import scala.annotation.binaryAPI + /** Methods for adding constraints and solving them. * * What goes into a Constraint as opposed to a ConstrainHandler? @@ -39,12 +41,12 @@ trait ConstraintHandling { private var addConstraintInvocations = 0 /** If the constraint is frozen we cannot add new bounds to the constraint. */ - protected var frozenConstraint: Boolean = false + @binaryAPI protected var frozenConstraint: Boolean = false /** Potentially a type lambda that is still instantiatable, even though the constraint * is generally frozen. */ - protected var caseLambda: Type = NoType + @binaryAPI protected var caseLambda: Type = NoType /** If set, align arguments `S1`, `S2`when taking the glb * `T1 { X = S1 } & T2 { X = S2 }` of a constraint upper bound for some type parameter. @@ -56,7 +58,7 @@ trait ConstraintHandling { /** We are currently comparing type lambdas. Used as a flag for * optimization: when `false`, no need to do an expensive `pruneLambdaParams` */ - protected var comparedTypeLambdas: Set[TypeLambda] = Set.empty + @binaryAPI protected var comparedTypeLambdas: Set[TypeLambda] = Set.empty /** Used for match type reduction: If false, we don't recognize an abstract type * to be a subtype type of any of its base classes. This is in place only at the @@ -110,7 +112,7 @@ trait ConstraintHandling { * of `1`. So the lower bound is `1 | x.M` and when we level-avoid that we * get `1 | Int & String`, which simplifies to `Int`. */ - private var myTrustBounds = true + @binaryAPI private var myTrustBounds = true inline def withUntrustedBounds(op: => Type): Type = val saved = myTrustBounds diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index e0e43169820a..d0d493af69c9 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -30,6 +30,7 @@ import classfile.ReusableDataReader import StdNames.nme import compiletime.uninitialized +import scala.annotation.binaryAPI import scala.annotation.internal.sharable import DenotTransformers.DenotTransformer @@ -805,6 +806,7 @@ object Contexts { * Note: plain TypeComparers always take on the kind of the outer comparer if they are in the same context. * In other words: tracking or explaining is a sticky property in the same context. */ + @binaryAPI private def comparer(using Context): TypeComparer = util.Stats.record("comparing") val base = ctx.base @@ -980,7 +982,7 @@ object Contexts { private[core] var phasesPlan: List[List[Phase]] = uninitialized /** Phases by id */ - private[dotc] var phases: Array[Phase] = uninitialized + @binaryAPI private[dotc] var phases: Array[Phase] = uninitialized /** Phases with consecutive Transforms grouped into a single phase, Empty array if fusion is disabled */ private[core] var fusedPhases: Array[Phase] = Array.empty[Phase] @@ -1019,7 +1021,7 @@ object Contexts { val generalContextPool = ContextPool() private[Contexts] val comparers = new mutable.ArrayBuffer[TypeComparer] - private[Contexts] var comparersInUse: Int = 0 + @binaryAPI private[Contexts] var comparersInUse: Int = 0 private var charArray = new Array[Char](256) diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 6857e3da38ed..933bd07f37f4 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -25,6 +25,8 @@ import reporting.trace import annotation.constructorOnly import cc.{CapturingType, derivedCapturingType, CaptureSet, stripCapturing, isBoxedCapturing, boxed, boxedUnlessFun, boxedIfTypeParam, isAlwaysPure} +import scala.annotation.binaryAPI + /** Provides methods to compare types. */ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling, PatternTypeConstrainer { @@ -34,7 +36,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling private var myContext: Context = initctx def comparerContext: Context = myContext - protected given [DummySoItsADef]: Context = myContext + @binaryAPI protected given [DummySoItsADef]: Context = myContext protected var state: TyperState = compiletime.uninitialized def constraint: Constraint = state.constraint @@ -115,7 +117,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling private def isBottom(tp: Type) = tp.widen.isRef(NothingClass) - protected def gadtBounds(sym: Symbol)(using Context) = ctx.gadt.bounds(sym) + @binaryAPI protected def gadtBounds(sym: Symbol)(using Context) = ctx.gadt.bounds(sym) protected def gadtAddBound(sym: Symbol, b: Type, isUpper: Boolean): Boolean = ctx.gadtState.addBound(sym, b, isUpper) protected def typeVarInstance(tvar: TypeVar)(using Context): Type = tvar.underlying @@ -156,7 +158,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling private [this] var leftRoot: Type | Null = null /** Are we forbidden from recording GADT constraints? */ - private var frozenGadt = false + @binaryAPI private var frozenGadt = false private inline def inFrozenGadt[T](inline op: T): T = inFrozenGadtIf(true)(op) @@ -187,7 +189,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling inFrozenGadtIf(true)(inFrozenConstraint(op)) extension (sym: Symbol) - private inline def onGadtBounds(inline op: TypeBounds => Boolean): Boolean = + @binaryAPI private inline def onGadtBounds(inline op: TypeBounds => Boolean): Boolean = val bounds = gadtBounds(sym) bounds != null && op(bounds) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index f000fe53f239..456eb732b71c 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -39,6 +39,7 @@ import compiletime.uninitialized import cc.{CapturingType, CaptureSet, derivedCapturingType, isBoxedCapturing, EventuallyCapturingType, boxedUnlessFun} import CaptureSet.{CompareResult, IdempotentCaptRefMap, IdentityCaptRefMap} +import scala.annotation.binaryAPI import scala.annotation.internal.sharable import scala.annotation.threadUnsafe @@ -5542,7 +5543,7 @@ object Types { /** Common base class of TypeMap and TypeAccumulator */ abstract class VariantTraversal: - protected[dotc] var variance: Int = 1 + @binaryAPI protected[dotc] var variance: Int = 1 inline protected def atVariance[T](v: Int)(op: => T): T = { val saved = variance diff --git a/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala b/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala index 941d72535fc0..7ba815e83424 100644 --- a/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala +++ b/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala @@ -21,6 +21,7 @@ import transform.{AccessProxies, Splicer} import staging.CrossStageSafety import transform.SymUtils.* import config.Printers.inlining +import util.SrcPos import util.Property import staging.StagingLevel @@ -74,7 +75,7 @@ object PrepareInlineable { def needsAccessor(sym: Symbol)(using Context): Boolean = sym.isTerm && (sym.isOneOf(AccessFlags) || sym.privateWithin.exists) && - (!sym.isBinaryAPI || (sym.is(Private) && !sym.owner.is(Trait))) && + (!sym.isBinaryAPI || sym.is(Private)) && !(sym.isStableMember && sym.info.widenTermRefExpr.isInstanceOf[ConstantType]) && !sym.isInlineMethod && (Inlines.inInlineMethod || StagingLevel.level > 0) @@ -100,6 +101,22 @@ object PrepareInlineable { override def transform(tree: Tree)(using Context): Tree = postTransform(super.transform(preTransform(tree))) + + protected def unstableAccessorWarning(accessor: Symbol, accessed: Symbol, srcPos: SrcPos)(using Context): Unit = + val accessorDefTree = accessorDef(accessor, accessed) + val solution = + if accessed.is(Private) then + s"Annotate ${accessed.name} with `@binaryAPI` to generate a stable accessor." + else + s"Annotate ${accessed.name} with `@binaryAPI` to make it accessible." + val binaryCompat = + s"""Adding @binaryAPI may break binary compatibility if a previous version of this + |library was compiled with Scala 3.0-3.3, Binary compatibility should be checked + |using MiMa. To keep binary you can add the following accessor to ${accessor.owner.showKind} ${accessor.owner.name.stripModuleClassSuffix}: + | @binaryAPI private[${accessor.owner.name.stripModuleClassSuffix}] ${accessorDefTree.show} + | + |""".stripMargin + report.warning(em"Generated unstable inline accessor for $accessed defined in ${accessed.owner}.\n\n$solution\n\n$binaryCompat", srcPos) } /** Direct approach: place the accessor with the accessed symbol. This has the @@ -114,7 +131,11 @@ object PrepareInlineable { 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 + val accessorTree = useAccessor(tree) + if !tree.symbol.isBinaryAPI && tree.symbol != accessorTree.symbol then + unstableAccessorWarning(accessorTree.symbol, tree.symbol, tree.srcPos) + accessorTree case _ => tree } @@ -199,7 +220,9 @@ object PrepareInlineable { localRefs.map(TypeTree(_)) ++ leadingTypeArgs, // TODO: pass type parameters in two sections? (qual :: Nil) :: otherArgss ) - ref(accessor).appliedToArgss(argss1).withSpan(tree.span) + val accessorTree = ref(accessor).appliedToArgss(argss1).withSpan(tree.span) + + unstableAccessorWarning(accessorTree.symbol, tree.symbol, tree.srcPos) // TODO: Handle references to non-public types. // This is quite tricky, as such types can appear anywhere, including as parts @@ -211,6 +234,7 @@ object PrepareInlineable { // myAccessors += TypeDef(accessor).withPos(tree.pos.focus) // ref(accessor).withSpan(tree.span) // + accessorTree case _: TypeDef if tree.symbol.is(Case) => report.error(reporting.CaseClassInInlinedCode(tree), tree) tree @@ -222,7 +246,7 @@ object PrepareInlineable { /** Create an inline accessor for this definition. */ def makePrivateBinaryAPIAccessor(sym: Symbol)(using Context): Unit = assert(sym.is(Private)) - if !sym.owner.is(Trait) then + if !sym.is(Accessor) then val ref = tpd.ref(sym).asInstanceOf[RefTree] val accessor = InsertPrivateBinaryAPIAccessors.useAccessor(ref) if sym.is(Mutable) then @@ -243,7 +267,6 @@ object PrepareInlineable { // so no accessors are needed for them. tree else - // TODO: warn if MakeInlineablePassing or MakeInlineableDirect generate accessors new MakeInlineablePassing(inlineSym).transform( new MakeInlineableDirect(inlineSym).transform(tree)) } diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 933661b7c5c9..babef5da63f5 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -21,6 +21,8 @@ import config.Feature.{migrateTo3, fewerBracesEnabled} import config.SourceVersion.`3.0` import reporting.{NoProfile, Profile, Message} +import scala.annotation.binaryAPI + import java.util.Objects object Scanners { @@ -1597,7 +1599,7 @@ object Scanners { protected def coversIndent(w: IndentWidth): Boolean = knownWidth != null && w == indentWidth - private var myCommasExpected: Boolean = false + @binaryAPI private var myCommasExpected: Boolean = false inline def withCommasExpected[T](inline op: => T): T = val saved = myCommasExpected diff --git a/compiler/src/dotty/tools/dotc/parsing/xml/MarkupParsers.scala b/compiler/src/dotty/tools/dotc/parsing/xml/MarkupParsers.scala index b3f41fab9eaa..a97747e0caff 100644 --- a/compiler/src/dotty/tools/dotc/parsing/xml/MarkupParsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/xml/MarkupParsers.scala @@ -19,6 +19,7 @@ import Decorators.{em, toMessage} import util.SourceFile import Utility._ +import scala.annotation.binaryAPI // XXX/Note: many/most of the functions in here are almost direct cut and pastes // from another file - scala.xml.parsing.MarkupParser, it looks like. @@ -52,7 +53,7 @@ object MarkupParsers { override def getMessage: String = "input ended while parsing XML" } - class MarkupParser(parser: Parser, final val preserveWS: Boolean)(using Context) extends MarkupParserCommon { + class MarkupParser(@binaryAPI parser: Parser, final val preserveWS: Boolean)(using @binaryAPI c: Context) extends MarkupParserCommon { import Tokens.{ LBRACE, RBRACE } @@ -93,8 +94,8 @@ object MarkupParsers { var xEmbeddedBlock: Boolean = false private var debugLastStartElement = List.empty[(Int, String)] - private def debugLastPos = debugLastStartElement.head._1 - private def debugLastElem = debugLastStartElement.head._2 + @binaryAPI private def debugLastPos = debugLastStartElement.head._1 + @binaryAPI private def debugLastElem = debugLastStartElement.head._2 private def errorBraces() = { reportSyntaxError("in XML content, please use '}}' to express '}'") diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 93de33778750..3a31d225c675 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -31,11 +31,13 @@ import dotty.tools.dotc.util.SourcePosition import dotty.tools.dotc.ast.untpd.{MemberDef, Modifiers, PackageDef, RefTree, Template, TypeDef, ValOrDefDef} import cc.{CaptureSet, toCaptureSet, IllegalCaptureRef} +import scala.annotation.binaryAPI + class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { /** A stack of enclosing DefDef, TypeDef, or ClassDef, or ModuleDefs nodes */ private var enclosingDef: untpd.Tree = untpd.EmptyTree - private var myCtx: Context = super.printerContext + @binaryAPI private var myCtx: Context = super.printerContext private var printPos = ctx.settings.YprintPos.value private val printLines = ctx.settings.printLines.value diff --git a/compiler/src/dotty/tools/dotc/reporting/trace.scala b/compiler/src/dotty/tools/dotc/reporting/trace.scala index 8e8d3efb8b40..2d1df32161f6 100644 --- a/compiler/src/dotty/tools/dotc/reporting/trace.scala +++ b/compiler/src/dotty/tools/dotc/reporting/trace.scala @@ -8,6 +8,7 @@ import core.*, Contexts.*, Decorators.* import config.* import printing.Formatting.* +import scala.annotation.binaryAPI import scala.compiletime.* /** Exposes the {{{ trace("question") { op } }}} syntax. @@ -76,9 +77,9 @@ trait TraceSyntax: inline def apply[T](inline question: String)(inline op: T)(using Context): T = apply[T](question, false)(op) - private val alwaysToString = (x: Any) => String.valueOf(x) + @binaryAPI private val alwaysToString = (x: Any) => String.valueOf(x) - private def doTrace[T](question: => String, + @binaryAPI private def doTrace[T](question: => String, printer: Printers.Printer = Printers.default, showOp: T => String) (op: => T)(using Context): T = diff --git a/compiler/src/dotty/tools/dotc/transform/AccessProxies.scala b/compiler/src/dotty/tools/dotc/transform/AccessProxies.scala index 5096681bce16..fe8c45a8d16c 100644 --- a/compiler/src/dotty/tools/dotc/transform/AccessProxies.scala +++ b/compiler/src/dotty/tools/dotc/transform/AccessProxies.scala @@ -32,29 +32,33 @@ abstract class AccessProxies { /** The accessor definitions that need to be added to class `cls` */ private def accessorDefs(cls: Symbol)(using Context): Iterator[DefDef] = for accessor <- cls.info.decls.iterator; accessed <- accessedBy.get(accessor) yield - DefDef(accessor.asTerm, prefss => { - def numTypeParams = accessed.info match { - case info: PolyType => info.paramNames.length - case _ => 0 - } - val (targs, argss) = splitArgs(prefss) - val (accessRef, forwardedTpts, forwardedArgss) = - if (passReceiverAsArg(accessor.name)) - (argss.head.head.select(accessed), targs.takeRight(numTypeParams), argss.tail) - else - (if (accessed.isStatic) ref(accessed) else ref(TermRef(cls.thisType, accessed)), - targs, argss) - val rhs = - if (accessor.name.isSetterName && - forwardedArgss.nonEmpty && forwardedArgss.head.nonEmpty) // defensive conditions - accessRef.becomes(forwardedArgss.head.head) - else - accessRef - .appliedToTypeTrees(forwardedTpts) - .appliedToArgss(forwardedArgss) - .etaExpandCFT(using ctx.withOwner(accessor)) - rhs.withSpan(accessed.span) - }) + accessorDef(accessor, accessed) + + /** The accessor definitions that need to be added to class `cls` */ + protected def accessorDef(accessor: Symbol, accessed: Symbol)(using Context): DefDef = + DefDef(accessor.asTerm, prefss => { + def numTypeParams = accessed.info match { + case info: PolyType => info.paramNames.length + case _ => 0 + } + val (targs, argss) = splitArgs(prefss) + val (accessRef, forwardedTpts, forwardedArgss) = + if (passReceiverAsArg(accessor.name)) + (argss.head.head.select(accessed), targs.takeRight(numTypeParams), argss.tail) + else + (if (accessed.isStatic) ref(accessed) else ref(TermRef(accessor.owner.thisType, accessed)), + targs, argss) + val rhs = + if (accessor.name.isSetterName && + forwardedArgss.nonEmpty && forwardedArgss.head.nonEmpty) // defensive conditions + accessRef.becomes(forwardedArgss.head.head) + else + accessRef + .appliedToTypeTrees(forwardedTpts) + .appliedToArgss(forwardedArgss) + .etaExpandCFT(using ctx.withOwner(accessor)) + rhs.withSpan(accessed.span) + }) /** Add all needed accessors to the `body` of class `cls` */ def addAccessorDefs(cls: Symbol, body: List[Tree])(using Context): List[Tree] = { diff --git a/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSInterop.scala b/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSInterop.scala index a2f9a0fb45a3..671784121845 100644 --- a/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSInterop.scala +++ b/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSInterop.scala @@ -2,6 +2,7 @@ package dotty.tools.dotc package transform package sjs +import scala.annotation.binaryAPI import scala.collection.mutable import ast.tpd @@ -1153,7 +1154,7 @@ object PrepJSInterop { val name: String = "prepjsinterop" val description: String = "additional checks and transformations for Scala.js" - private final class OwnerKind private (private val baseKinds: Int) extends AnyVal { + private final class OwnerKind private (@binaryAPI private val baseKinds: Int) extends AnyVal { inline def isBaseKind: Boolean = Integer.lowestOneBit(baseKinds) == baseKinds && baseKinds != 0 // exactly 1 bit on diff --git a/compiler/src/dotty/tools/dotc/typer/EtaExpansion.scala b/compiler/src/dotty/tools/dotc/typer/EtaExpansion.scala index b1513df777ec..0a16d535aa19 100644 --- a/compiler/src/dotty/tools/dotc/typer/EtaExpansion.scala +++ b/compiler/src/dotty/tools/dotc/typer/EtaExpansion.scala @@ -15,6 +15,8 @@ import util.Property import collection.mutable import Trees._ +import scala.annotation.binaryAPI + /** A class that handles argument lifting. Argument lifting is needed in the following * scenarios: * - eta expansion @@ -163,7 +165,7 @@ object LiftComplex extends LiftComplex object LiftCoverage extends LiftImpure { // Property indicating whether we're currently lifting the arguments of an application - private val LiftingArgs = new Property.Key[Boolean] + @binaryAPI private val LiftingArgs = new Property.Key[Boolean] private inline def liftingArgs(using Context): Boolean = ctx.property(LiftingArgs).contains(true) diff --git a/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala b/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala index 3a5ea05726a5..71b2726d0775 100644 --- a/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala +++ b/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala @@ -22,6 +22,8 @@ import dotty.tools.dotc.typer.Inferencing._ import dotty.tools.dotc.util.Spans._ import dotty.tools.dotc.util.Stats.record import dotty.tools.dotc.reporting.IllegalVariableInPatternAlternative + +import scala.annotation.binaryAPI import scala.collection.mutable @@ -213,7 +215,7 @@ trait QuotesAndSplices { }) object splitter extends tpd.TreeMap { - private var variance: Int = 1 + @binaryAPI private var variance: Int = 1 inline private def atVariance[T](v: Int)(op: => T): T = { val saved = variance diff --git a/compiler/src/dotty/tools/dotc/util/ReusableInstance.scala b/compiler/src/dotty/tools/dotc/util/ReusableInstance.scala index 4dd897dd082a..166cfa3bcea2 100644 --- a/compiler/src/dotty/tools/dotc/util/ReusableInstance.scala +++ b/compiler/src/dotty/tools/dotc/util/ReusableInstance.scala @@ -3,6 +3,8 @@ package dotty.tools.dotc.util import scala.collection.mutable.ArrayBuffer import scala.util.chaining._ +import scala.annotation.binaryAPI + /** A wrapper for a list of cached instances of a type `T`. * The wrapper is recursion-reentrant: several instances are kept, so * at each depth of reentrance we are reusing the instance for that. @@ -14,9 +16,9 @@ import scala.util.chaining._ * * Ported from scala.reflect.internal.util.ReusableInstance */ -final class ReusableInstance[T <: AnyRef] private (make: => T) { - private[this] val cache = new ArrayBuffer[T](ReusableInstance.InitialSize).tap(_.addOne(make)) - private[this] var taken = 0 +final class ReusableInstance[T <: AnyRef] private (@binaryAPI make: => T) { + @binaryAPI private[this] val cache = new ArrayBuffer[T](ReusableInstance.InitialSize).tap(_.addOne(make)) + @binaryAPI private[this] var taken = 0 inline def withInstance[R](action: T => R): R ={ if (taken == cache.size) diff --git a/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala b/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala index df7ec3da51af..c2ecc317231c 100644 --- a/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala +++ b/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala @@ -22,6 +22,7 @@ import scala.quoted.runtime.{QuoteUnpickler, QuoteMatching} import scala.quoted.runtime.impl.printers._ import scala.reflect.TypeTest +import scala.annotation.binaryAPI object QuotesImpl { @@ -38,6 +39,7 @@ object QuotesImpl { class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler, QuoteMatching: + @binaryAPI private val xCheckMacro: Boolean = ctx.settings.XcheckMacros.value extension [T](self: scala.quoted.Expr[T]) diff --git a/compiler/test/dotty/tools/backend/jvm/BinaryAPITests.scala b/compiler/test/dotty/tools/backend/jvm/BinaryAPITests.scala index ea1dadc0b5a5..46a3f6ef7156 100644 --- a/compiler/test/dotty/tools/backend/jvm/BinaryAPITests.scala +++ b/compiler/test/dotty/tools/backend/jvm/BinaryAPITests.scala @@ -300,19 +300,19 @@ class BinaryAPITests extends DottyBytecodeTest { val testInlined = getMethod(cTrait, "testInlined") val testInlinedInstructions = instructionsFromMethod(testInlined).filter(_.isInstanceOf[Invoke]) assertSameCode(testInlinedInstructions, List( - Invoke(INVOKEINTERFACE, "C", "C$$privateVarBinaryAPI_$eq", "(I)V", true), + Invoke(INVOKEINTERFACE, "C", "C$$inline$privateVarBinaryAPI_$eq", "(I)V", true), Invoke(INVOKEINTERFACE, "C", "packagePrivateVarBinaryAPI_$eq", "(I)V", true), Invoke(INVOKEINTERFACE, "C", "protectedVarBinaryAPI_$eq", "(I)V", true), - Invoke(INVOKEINTERFACE, "C", "C$$privateValBinaryAPI", "()I", true), + Invoke(INVOKEINTERFACE, "C", "C$$inline$privateValBinaryAPI", "()I", true), Invoke(INVOKEINTERFACE, "C", "packagePrivateValBinaryAPI", "()I", true), Invoke(INVOKEINTERFACE, "C", "protectedValBinaryAPI", "()I", true), - Invoke(INVOKEINTERFACE, "C", "C$$privateLazyValBinaryAPI", "()I", true), + Invoke(INVOKEINTERFACE, "C", "C$$inline$privateLazyValBinaryAPI", "()I", true), Invoke(INVOKEINTERFACE, "C", "packagePrivateLazyValBinaryAPI", "()I", true), Invoke(INVOKEINTERFACE, "C", "protectedLazyValBinaryAPI", "()I", true), - Invoke(INVOKEINTERFACE, "C", "C$$privateVarBinaryAPI", "()I", true), + Invoke(INVOKEINTERFACE, "C", "C$$inline$privateVarBinaryAPI", "()I", true), Invoke(INVOKEINTERFACE, "C", "packagePrivateVarBinaryAPI", "()I", true), Invoke(INVOKEINTERFACE, "C", "protectedVarBinaryAPI", "()I", true), - Invoke(INVOKESPECIAL, "C", "privateDefBinaryAPI", "()I", true), + Invoke(INVOKEINTERFACE, "C", "C$$inline$privateDefBinaryAPI", "()I", true), Invoke(INVOKEINTERFACE, "C", "packagePrivateDefBinaryAPI", "()I", true), Invoke(INVOKEINTERFACE, "C", "protectedDefBinaryAPI", "()I", true) )) diff --git a/tests/init/pos/i15465.scala b/tests/init/pos/i15465.scala index 5b99670e9027..cb619802b700 100644 --- a/tests/init/pos/i15465.scala +++ b/tests/init/pos/i15465.scala @@ -1,10 +1,12 @@ +import scala.annotation.binaryAPI + class TestSuite: protected val it = new ItWord protected final class ItWord: def should(string: String) = new ItVerbString("should", string) - private def registerTestToRun(fun: => Any): Unit = () + @binaryAPI private def registerTestToRun(fun: => Any): Unit = () protected final class ItVerbString(verb: String, name: String): inline def in(testFun: => Any): Unit = registerTestToRun(testFun) diff --git a/tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.check b/tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.check new file mode 100644 index 000000000000..bac9ffa5310e --- /dev/null +++ b/tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.check @@ -0,0 +1,108 @@ +-- Error: tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.scala:9:4 ------------------------------------- +9 | valBinaryAPI1 + // error + | ^^^^^^^^^^^^^ + | Generated unstable inline accessor for value valBinaryAPI1 defined in class A. + | + | Annotate valBinaryAPI1 with `@binaryAPI` to generate a stable accessor. + | + | Adding @binaryAPI may break binary compatibility if a previous version of this + | library was compiled with Scala 3.0-3.3, Binary compatibility should be checked + | using MiMa. To keep binary you can add the following accessor to class A: + | @binaryAPI private[A] final def foo$A$$inline$valBinaryAPI1: Int = this.valBinaryAPI1 + | +-- Error: tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.scala:10:4 ------------------------------------ +10 | valBinaryAPI2 + // error + | ^^^^^^^^^^^^^ + | Generated unstable inline accessor for value valBinaryAPI2 defined in class A. + | + | Annotate valBinaryAPI2 with `@binaryAPI` to make it accessible. + | + | Adding @binaryAPI may break binary compatibility if a previous version of this + | library was compiled with Scala 3.0-3.3, Binary compatibility should be checked + | using MiMa. To keep binary you can add the following accessor to class A: + | @binaryAPI private[A] def foo$A$$inline$valBinaryAPI2: Int = this.valBinaryAPI2 + | +-- Error: tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.scala:15:6 ------------------------------------ +15 | a.valBinaryAPI2 + // error + | ^^^^^^^^^^^^^^^ + | Generated unstable inline accessor for value valBinaryAPI2 defined in class A. + | + | Annotate valBinaryAPI2 with `@binaryAPI` to make it accessible. + | + | Adding @binaryAPI may break binary compatibility if a previous version of this + | library was compiled with Scala 3.0-3.3, Binary compatibility should be checked + | using MiMa. To keep binary you can add the following accessor to class B: + | @binaryAPI private[B] def inline$valBinaryAPI2$i1(x$0: foo.A): Int = x$0.valBinaryAPI2 + | +-- Error: tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.scala:24:4 ------------------------------------ +24 | valBinaryAPI1 + // error + | ^^^^^^^^^^^^^ + | Generated unstable inline accessor for value valBinaryAPI1 defined in class C. + | + | Annotate valBinaryAPI1 with `@binaryAPI` to generate a stable accessor. + | + | Adding @binaryAPI may break binary compatibility if a previous version of this + | library was compiled with Scala 3.0-3.3, Binary compatibility should be checked + | using MiMa. To keep binary you can add the following accessor to class C: + | @binaryAPI private[C] final def inline$valBinaryAPI1: Int = this.valBinaryAPI1 + | +-- Error: tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.scala:25:4 ------------------------------------ +25 | valBinaryAPI2 + // error + | ^^^^^^^^^^^^^ + | Generated unstable inline accessor for value valBinaryAPI2 defined in class C. + | + | Annotate valBinaryAPI2 with `@binaryAPI` to make it accessible. + | + | Adding @binaryAPI may break binary compatibility if a previous version of this + | library was compiled with Scala 3.0-3.3, Binary compatibility should be checked + | using MiMa. To keep binary you can add the following accessor to class C: + | @binaryAPI private[C] def inline$valBinaryAPI2: Int = this.valBinaryAPI2 + | +-- Error: tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.scala:30:6 ------------------------------------ +30 | c.valBinaryAPI2 + // error + | ^^^^^^^^^^^^^^^ + | Generated unstable inline accessor for value valBinaryAPI2 defined in class C. + | + | Annotate valBinaryAPI2 with `@binaryAPI` to make it accessible. + | + | Adding @binaryAPI may break binary compatibility if a previous version of this + | library was compiled with Scala 3.0-3.3, Binary compatibility should be checked + | using MiMa. To keep binary you can add the following accessor to class D: + | @binaryAPI private[D] def inline$valBinaryAPI2$i2(x$0: foo.C): Int = x$0.valBinaryAPI2 + | +-- Error: tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.scala:39:4 ------------------------------------ +39 | valBinaryAPI1 + // error + | ^^^^^^^^^^^^^ + | Generated unstable inline accessor for value valBinaryAPI1 defined in object E. + | + | Annotate valBinaryAPI1 with `@binaryAPI` to generate a stable accessor. + | + | Adding @binaryAPI may break binary compatibility if a previous version of this + | library was compiled with Scala 3.0-3.3, Binary compatibility should be checked + | using MiMa. To keep binary you can add the following accessor to object E: + | @binaryAPI private[E] final def inline$valBinaryAPI1: Int = foo.E.valBinaryAPI1 + | +-- Error: tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.scala:40:4 ------------------------------------ +40 | valBinaryAPI2 + // error + | ^^^^^^^^^^^^^ + | Generated unstable inline accessor for value valBinaryAPI2 defined in object E. + | + | Annotate valBinaryAPI2 with `@binaryAPI` to make it accessible. + | + | Adding @binaryAPI may break binary compatibility if a previous version of this + | library was compiled with Scala 3.0-3.3, Binary compatibility should be checked + | using MiMa. To keep binary you can add the following accessor to object E: + | @binaryAPI private[E] def inline$valBinaryAPI2: Int = foo.E.valBinaryAPI2 + | +-- Error: tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.scala:45:6 ------------------------------------ +45 | E.valBinaryAPI2 + // error + | ^^^^^^^^^^^^^^^ + | Generated unstable inline accessor for value valBinaryAPI2 defined in object E. + | + | Annotate valBinaryAPI2 with `@binaryAPI` to make it accessible. + | + | Adding @binaryAPI may break binary compatibility if a previous version of this + | library was compiled with Scala 3.0-3.3, Binary compatibility should be checked + | using MiMa. To keep binary you can add the following accessor to object F: + | @binaryAPI private[F] def inline$valBinaryAPI2$i3(x$0: foo.E): Int = x$0.valBinaryAPI2 + | diff --git a/tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.scala b/tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.scala new file mode 100644 index 000000000000..c1cd2effad2c --- /dev/null +++ b/tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.scala @@ -0,0 +1,46 @@ +package foo +import scala.annotation.binaryAPI +class A: + private val valBinaryAPI1: Int = 1 + private[foo] val valBinaryAPI2: Int = 1 + @binaryAPI private val valBinaryAPI3: Int = 1 + @binaryAPI private[foo] val valBinaryAPI4: Int = 1 + inline def inlined = + valBinaryAPI1 + // error + valBinaryAPI2 + // error + valBinaryAPI3 + + valBinaryAPI4 +class B(val a: A): + inline def inlined = + a.valBinaryAPI2 + // error + a.valBinaryAPI4 + +final class C: + private val valBinaryAPI1: Int = 1 + private[foo] val valBinaryAPI2: Int = 1 + @binaryAPI private val valBinaryAPI3: Int = 1 + @binaryAPI private[foo] val valBinaryAPI4: Int = 1 + inline def inlined = + valBinaryAPI1 + // error + valBinaryAPI2 + // error + valBinaryAPI3 + + valBinaryAPI4 +final class D(val c: C): + inline def inlined = + c.valBinaryAPI2 + // error + c.valBinaryAPI4 + +object E: + private val valBinaryAPI1: Int = 1 + private[foo] val valBinaryAPI2: Int = 1 + @binaryAPI private val valBinaryAPI3: Int = 1 + @binaryAPI private[foo] val valBinaryAPI4: Int = 1 + inline def inlined = + valBinaryAPI1 + // error + valBinaryAPI2 + // error + valBinaryAPI3 + + valBinaryAPI4 +object F: + inline def inlined = + E.valBinaryAPI2 + // error + E.valBinaryAPI4 diff --git a/tests/neg-macros/delegate-match-1/Macro_1.scala b/tests/neg-macros/delegate-match-1/Macro_1.scala index 7c5702b56fc6..63eff61b4017 100644 --- a/tests/neg-macros/delegate-match-1/Macro_1.scala +++ b/tests/neg-macros/delegate-match-1/Macro_1.scala @@ -1,9 +1,9 @@ import scala.quoted.* - +import scala.annotation.binaryAPI inline def f: Any = ${ fImpl } -private def fImpl(using Quotes): Expr[Unit] = { +@binaryAPI private def fImpl(using Quotes): Expr[Unit] = { import quotes.reflect.* Implicits.search(TypeRepr.of[A]) match { case x: ImplicitSearchSuccess => diff --git a/tests/neg-macros/delegate-match-2/Macro_1.scala b/tests/neg-macros/delegate-match-2/Macro_1.scala index 6c7e6917900e..5c006cfddc54 100644 --- a/tests/neg-macros/delegate-match-2/Macro_1.scala +++ b/tests/neg-macros/delegate-match-2/Macro_1.scala @@ -1,9 +1,9 @@ import scala.quoted.* - +import scala.annotation.binaryAPI inline def f: Any = ${ fImpl } -private def fImpl (using Quotes) : Expr[Unit] = { +@binaryAPI private def fImpl (using Quotes) : Expr[Unit] = { import quotes.reflect.* Implicits.search(TypeRepr.of[A]) match { case x: ImplicitSearchSuccess => diff --git a/tests/neg-macros/delegate-match-3/Macro_1.scala b/tests/neg-macros/delegate-match-3/Macro_1.scala index 89e467388a6f..9fa7e5ba31c5 100644 --- a/tests/neg-macros/delegate-match-3/Macro_1.scala +++ b/tests/neg-macros/delegate-match-3/Macro_1.scala @@ -1,9 +1,9 @@ import scala.quoted.* - +import scala.annotation.binaryAPI inline def f: Any = ${ fImpl } -private def fImpl(using Quotes) : Expr[Unit] = { +@binaryAPI private def fImpl(using Quotes) : Expr[Unit] = { import quotes.reflect.* Implicits.search(TypeRepr.of[A]) match { case x: ImplicitSearchSuccess => diff --git a/tests/neg-macros/ill-abort.check b/tests/neg-macros/ill-abort.check index c267c2e79ecf..3e449cda0f08 100644 --- a/tests/neg-macros/ill-abort.check +++ b/tests/neg-macros/ill-abort.check @@ -6,7 +6,7 @@ |--------------------------------------------------------------------------------------------------------------------- |Inline stack trace |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |This location contains code that was inlined from quoted_1.scala:3 -3 |inline def fail(): Unit = ${ impl } + |This location contains code that was inlined from quoted_1.scala:4 +4 |inline def fail(): Unit = ${ impl } | ^^^^^^^^^ --------------------------------------------------------------------------------------------------------------------- diff --git a/tests/neg-macros/ill-abort/quoted_1.scala b/tests/neg-macros/ill-abort/quoted_1.scala index 0392ef5ae571..88dc19115882 100644 --- a/tests/neg-macros/ill-abort/quoted_1.scala +++ b/tests/neg-macros/ill-abort/quoted_1.scala @@ -1,7 +1,8 @@ import scala.quoted.* +import scala.annotation.binaryAPI inline def fail(): Unit = ${ impl } -private def impl(using Quotes) : Expr[Unit] = +@binaryAPI private def impl(using Quotes) : Expr[Unit] = // should never be done without reporting error before (see docs) throw new scala.quoted.runtime.StopMacroExpansion diff --git a/tests/pos-custom-args/i13405/Macro.scala b/tests/pos-custom-args/i13405/Macro.scala index 2996555a6e0c..8a2539e7391e 100644 --- a/tests/pos-custom-args/i13405/Macro.scala +++ b/tests/pos-custom-args/i13405/Macro.scala @@ -1,9 +1,9 @@ import scala.quoted.* +import scala.annotation.binaryAPI sealed class Foo() inline def hh(): Unit = ${ interpMacro() } - -private def interpMacro()(using Quotes): Expr[Unit] = +@binaryAPI private def interpMacro()(using Quotes): Expr[Unit] = import quotes.reflect.* '{ val res: Either[String, (Foo, Foo)] = diff --git a/tests/pos-macros/i9570.scala b/tests/pos-macros/i9570.scala index 295969813df6..b668eb0aae01 100644 --- a/tests/pos-macros/i9570.scala +++ b/tests/pos-macros/i9570.scala @@ -1,4 +1,5 @@ import scala.quoted.* +import scala.annotation.binaryAPI object Macros { @@ -7,7 +8,7 @@ object Macros { case class HCons[+HD, TL <: HList](hd: HD, tl: TL) extends HList case object HNil extends HList - private def sizeImpl(e: Expr[HList], n:Int)(using qctx:Quotes): Expr[Int] = { + @binaryAPI private def sizeImpl(e: Expr[HList], n:Int)(using qctx:Quotes): Expr[Int] = { import quotes.reflect.* e match { case '{HCons(_,$t)} => // error if run with fatal warinings in BootstrappedOnlyCompilationTests diff --git a/tests/pos/binaryAPI.scala b/tests/pos/binaryAPI.scala index 313a16e34b00..2aabbdb7f306 100644 --- a/tests/pos/binaryAPI.scala +++ b/tests/pos/binaryAPI.scala @@ -87,15 +87,39 @@ def localTest = @annotation.binaryAPI private[Foo] val a: Int = 1 @annotation.binaryAPI protected val b: Int = 1 -trait Trait: - @annotation.binaryAPI private val myVal = 1 - @annotation.binaryAPI private lazy val myLazyVl = 2 - @annotation.binaryAPI private var myVar = 2 - @annotation.binaryAPI private def myDef = 3 - @annotation.binaryAPI private given myGiven: Int = 4 - - inline def inlined: Unit = - myVar = 1 - myVal + myLazyVl + myVar + myDef + myGiven - -def testTrait(t: Trait) = t.inlined \ No newline at end of file +package traits { + trait Trait: + @annotation.binaryAPI private val myVal = 1 + @annotation.binaryAPI private lazy val myLazyVl = 2 + @annotation.binaryAPI private var myVar = 2 + @annotation.binaryAPI private def myDef = 3 + @annotation.binaryAPI private given myGiven: Int = 4 + + @annotation.binaryAPI protected val myVal2 = 1 + @annotation.binaryAPI protected lazy val myLazyVl2 = 2 + @annotation.binaryAPI protected var myVar2 = 2 + @annotation.binaryAPI protected def myDef2 = 3 + @annotation.binaryAPI protected given myGiven2: Int = 4 + + inline def inlined: Unit = + myVar2 = 1 + myVar = 1 + myVal + myLazyVl + myVar + myDef + myGiven + + myVal2 + myLazyVl2 + myVar2 + myDef2 + myGiven2 + + def testTrait(t: Trait) = t.inlined + + class Baz extends Foo + object Baz extends Foo + + trait Foo: + inline def foo: Any = bar + @binaryAPI private def bar: Any = ??? + end Foo + + def test = + Baz.foo + (new Baz).foo + val baz = new Baz + baz.foo +} From 04ded7e52d930ee62b89a5ded0ebb533dd77efb9 Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Tue, 28 Feb 2023 08:30:32 +0100 Subject: [PATCH 05/11] Split into `@binaryAPI` and `@binaryAPIAccessor` * `@binaryAPI` makes the API public. Cannot be used on `private[this]`. * `@binaryAPIAccessor` generates the private accessor. --- community-build/community-projects/scodec | 2 +- .../jvm/PostProcessorFrontendAccess.scala | 4 +- .../tools/dotc/core/ConstraintHandling.scala | 5 +- .../src/dotty/tools/dotc/core/Contexts.scala | 4 +- .../dotty/tools/dotc/core/Definitions.scala | 1 + .../tools/dotc/core/SymDenotations.scala | 6 +- .../dotty/tools/dotc/core/TypeComparer.scala | 7 +- .../dotc/inlines/PrepareInlineable.scala | 14 +-- .../dotty/tools/dotc/parsing/Scanners.scala | 5 +- .../dotc/parsing/xml/MarkupParsers.scala | 8 +- .../tools/dotc/printing/RefinedPrinter.scala | 4 +- .../dotty/tools/dotc/reporting/trace.scala | 8 +- .../tools/dotc/transform/PostTyper.scala | 4 +- .../dotc/transform/sjs/PrepJSInterop.scala | 4 +- .../src/dotty/tools/dotc/typer/Checking.scala | 5 + .../dotty/tools/dotc/typer/EtaExpansion.scala | 4 +- .../tools/dotc/typer/QuotesAndSplices.scala | 4 +- .../src/dotty/tools/dotc/typer/Typer.scala | 8 +- .../tools/dotc/util/ReusableInstance.scala | 10 +- .../quoted/runtime/impl/QuotesImpl.scala | 4 +- .../tools/backend/jvm/BinaryAPITests.scala | 97 +++++++++++++------ library/src/scala/annotation/binaryAPI.scala | 7 +- .../scala/annotation/binaryAPIAccessor.scala | 12 +++ project/MiMaFilters.scala | 1 + tests/init/pos/i15465.scala | 4 +- .../inline-unstable-accessors.check | 12 +-- .../inline-unstable-accessors.scala | 8 +- tests/pos-macros/i9570.scala | 5 +- tests/pos/binaryAPI.scala | 36 +++---- 29 files changed, 186 insertions(+), 107 deletions(-) create mode 100644 library/src/scala/annotation/binaryAPIAccessor.scala diff --git a/community-build/community-projects/scodec b/community-build/community-projects/scodec index ba025a604ca7..d3d193b51229 160000 --- a/community-build/community-projects/scodec +++ b/community-build/community-projects/scodec @@ -1 +1 @@ -Subproject commit ba025a604ca798918680bf583bf44aa3ee464ea0 +Subproject commit d3d193b51229ebe9a3f2b6caaa47d973c95c8323 diff --git a/compiler/src/dotty/tools/backend/jvm/PostProcessorFrontendAccess.scala b/compiler/src/dotty/tools/backend/jvm/PostProcessorFrontendAccess.scala index 28ff781442ec..7992f77e5508 100644 --- a/compiler/src/dotty/tools/backend/jvm/PostProcessorFrontendAccess.scala +++ b/compiler/src/dotty/tools/backend/jvm/PostProcessorFrontendAccess.scala @@ -9,7 +9,7 @@ import dotty.tools.dotc.core.Contexts.Context import dotty.tools.dotc.report import dotty.tools.dotc.core.Phases -import scala.annotation.binaryAPI +import scala.annotation.binaryAPIAccessor /** * Functionality needed in the post-processor whose implementation depends on the compiler @@ -22,7 +22,7 @@ sealed abstract class PostProcessorFrontendAccess { def backendReporting: BackendReporting def getEntryPoints: List[String] - @binaryAPI + @binaryAPIAccessor private val frontendLock: AnyRef = new Object() inline final def frontendSynch[T](inline x: => T): T = frontendLock.synchronized(x) } diff --git a/compiler/src/dotty/tools/dotc/core/ConstraintHandling.scala b/compiler/src/dotty/tools/dotc/core/ConstraintHandling.scala index 5edbdd3e8350..e58456ec61a6 100644 --- a/compiler/src/dotty/tools/dotc/core/ConstraintHandling.scala +++ b/compiler/src/dotty/tools/dotc/core/ConstraintHandling.scala @@ -15,7 +15,7 @@ import NameKinds.AvoidNameKind import util.SimpleIdentitySet import NullOpsDecorator.stripNull -import scala.annotation.binaryAPI +import scala.annotation.{binaryAPI, binaryAPIAccessor} /** Methods for adding constraints and solving them. * @@ -112,7 +112,8 @@ trait ConstraintHandling { * of `1`. So the lower bound is `1 | x.M` and when we level-avoid that we * get `1 | Int & String`, which simplifies to `Int`. */ - @binaryAPI private var myTrustBounds = true + @binaryAPIAccessor + private var myTrustBounds = true inline def withUntrustedBounds(op: => Type): Type = val saved = myTrustBounds diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index d0d493af69c9..5e7f5642988d 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -30,7 +30,7 @@ import classfile.ReusableDataReader import StdNames.nme import compiletime.uninitialized -import scala.annotation.binaryAPI +import scala.annotation.{binaryAPI, binaryAPIAccessor} import scala.annotation.internal.sharable import DenotTransformers.DenotTransformer @@ -806,7 +806,7 @@ object Contexts { * Note: plain TypeComparers always take on the kind of the outer comparer if they are in the same context. * In other words: tracking or explaining is a sticky property in the same context. */ - @binaryAPI + @binaryAPIAccessor private def comparer(using Context): TypeComparer = util.Stats.record("comparing") val base = ctx.base diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 147236304db7..1615b413fc44 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1040,6 +1040,7 @@ class Definitions { @tu lazy val RetainsAnnot: ClassSymbol = requiredClass("scala.annotation.retains") @tu lazy val RetainsByNameAnnot: ClassSymbol = requiredClass("scala.annotation.retainsByName") @tu lazy val BinaryAPIAnnot: ClassSymbol = requiredClass("scala.annotation.binaryAPI") + @tu lazy val BinaryAPIAccessorAnnot: ClassSymbol = requiredClass("scala.annotation.binaryAPIAccessor") @tu lazy val JavaRepeatableAnnot: ClassSymbol = requiredClass("java.lang.annotation.Repeatable") diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index dbe7d403d2e9..eba60b7c65ec 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -1042,9 +1042,13 @@ object SymDenotations { def isBinaryAPI(using Context): Boolean = isTerm && ( hasAnnotation(defn.BinaryAPIAnnot) || - allOverriddenSymbols.exists(_.hasAnnotation(defn.BinaryAPIAnnot)) + allOverriddenSymbols.exists(sym => sym.hasAnnotation(defn.BinaryAPIAnnot)) ) + /** Is this a member that will have an accessor in the generated binary */ + def isBinaryAPIAccessor(using Context): Boolean = + isTerm && hasAnnotation(defn.BinaryAPIAccessorAnnot) + /** ()T and => T types should be treated as equivalent for this symbol. * Note: For the moment, we treat Scala-2 compiled symbols as loose matching, * because the Scala library does not always follow the right conventions. diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 933bd07f37f4..2275ca2721df 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -25,7 +25,7 @@ import reporting.trace import annotation.constructorOnly import cc.{CapturingType, derivedCapturingType, CaptureSet, stripCapturing, isBoxedCapturing, boxed, boxedUnlessFun, boxedIfTypeParam, isAlwaysPure} -import scala.annotation.binaryAPI +import scala.annotation.{binaryAPI, binaryAPIAccessor} /** Provides methods to compare types. */ @@ -158,7 +158,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling private [this] var leftRoot: Type | Null = null /** Are we forbidden from recording GADT constraints? */ - @binaryAPI private var frozenGadt = false + @binaryAPIAccessor + private var frozenGadt = false private inline def inFrozenGadt[T](inline op: T): T = inFrozenGadtIf(true)(op) @@ -189,7 +190,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling inFrozenGadtIf(true)(inFrozenConstraint(op)) extension (sym: Symbol) - @binaryAPI private inline def onGadtBounds(inline op: TypeBounds => Boolean): Boolean = + @binaryAPIAccessor private inline def onGadtBounds(inline op: TypeBounds => Boolean): Boolean = val bounds = gadtBounds(sym) bounds != null && op(bounds) diff --git a/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala b/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala index 7ba815e83424..1acd41017890 100644 --- a/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala +++ b/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala @@ -59,7 +59,7 @@ object PrepareInlineable { def accessorNameOf(accessed: Symbol, site: Symbol)(using Context): TermName = val accName = InlineAccessorName(accessed.name.asTermName) - if site.isExtensibleClass || accessed.isBinaryAPI then accName.expandedName(site) + if site.isExtensibleClass || accessed.isBinaryAPIAccessor then accName.expandedName(site) else accName /** A definition needs an accessor if it is private, protected, or qualified private @@ -75,7 +75,7 @@ object PrepareInlineable { def needsAccessor(sym: Symbol)(using Context): Boolean = sym.isTerm && (sym.isOneOf(AccessFlags) || sym.privateWithin.exists) && - (!sym.isBinaryAPI || sym.is(Private)) && + !sym.isBinaryAPI && !(sym.isStableMember && sym.info.widenTermRefExpr.isInstanceOf[ConstantType]) && !sym.isInlineMethod && (Inlines.inInlineMethod || StagingLevel.level > 0) @@ -104,13 +104,14 @@ object PrepareInlineable { protected def unstableAccessorWarning(accessor: Symbol, accessed: Symbol, srcPos: SrcPos)(using Context): Unit = val accessorDefTree = accessorDef(accessor, accessed) + val annot = if accessed.is(Private) then "@binaryAPIAccessor" else "@binaryAPI" val solution = if accessed.is(Private) then - s"Annotate ${accessed.name} with `@binaryAPI` to generate a stable accessor." + s"Annotate ${accessed.name} with `$annot` to generate a stable accessor." else - s"Annotate ${accessed.name} with `@binaryAPI` to make it accessible." + s"Annotate ${accessed.name} with `$annot` to make it accessible." val binaryCompat = - s"""Adding @binaryAPI may break binary compatibility if a previous version of this + s"""Adding $annot may break binary compatibility if a previous version of this |library was compiled with Scala 3.0-3.3, Binary compatibility should be checked |using MiMa. To keep binary you can add the following accessor to ${accessor.owner.showKind} ${accessor.owner.name.stripModuleClassSuffix}: | @binaryAPI private[${accessor.owner.name.stripModuleClassSuffix}] ${accessorDefTree.show} @@ -133,7 +134,7 @@ object PrepareInlineable { } else val accessorTree = useAccessor(tree) - if !tree.symbol.isBinaryAPI && tree.symbol != accessorTree.symbol then + if !tree.symbol.isBinaryAPI && !tree.symbol.isBinaryAPIAccessor && tree.symbol != accessorTree.symbol then unstableAccessorWarning(accessorTree.symbol, tree.symbol, tree.srcPos) accessorTree case _ => @@ -245,7 +246,6 @@ object PrepareInlineable { /** Create an inline accessor for this definition. */ def makePrivateBinaryAPIAccessor(sym: Symbol)(using Context): Unit = - assert(sym.is(Private)) if !sym.is(Accessor) then val ref = tpd.ref(sym).asInstanceOf[RefTree] val accessor = InsertPrivateBinaryAPIAccessors.useAccessor(ref) diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index babef5da63f5..c94743581e6e 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -21,7 +21,7 @@ import config.Feature.{migrateTo3, fewerBracesEnabled} import config.SourceVersion.`3.0` import reporting.{NoProfile, Profile, Message} -import scala.annotation.binaryAPI +import scala.annotation.binaryAPIAccessor import java.util.Objects @@ -1599,7 +1599,8 @@ object Scanners { protected def coversIndent(w: IndentWidth): Boolean = knownWidth != null && w == indentWidth - @binaryAPI private var myCommasExpected: Boolean = false + @binaryAPIAccessor + private var myCommasExpected: Boolean = false inline def withCommasExpected[T](inline op: => T): T = val saved = myCommasExpected diff --git a/compiler/src/dotty/tools/dotc/parsing/xml/MarkupParsers.scala b/compiler/src/dotty/tools/dotc/parsing/xml/MarkupParsers.scala index a97747e0caff..c737df682e43 100644 --- a/compiler/src/dotty/tools/dotc/parsing/xml/MarkupParsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/xml/MarkupParsers.scala @@ -19,7 +19,7 @@ import Decorators.{em, toMessage} import util.SourceFile import Utility._ -import scala.annotation.binaryAPI +import scala.annotation.binaryAPIAccessor // XXX/Note: many/most of the functions in here are almost direct cut and pastes // from another file - scala.xml.parsing.MarkupParser, it looks like. @@ -53,7 +53,7 @@ object MarkupParsers { override def getMessage: String = "input ended while parsing XML" } - class MarkupParser(@binaryAPI parser: Parser, final val preserveWS: Boolean)(using @binaryAPI c: Context) extends MarkupParserCommon { + class MarkupParser(@binaryAPIAccessor parser: Parser, final val preserveWS: Boolean)(using @binaryAPIAccessor c: Context) extends MarkupParserCommon { import Tokens.{ LBRACE, RBRACE } @@ -94,8 +94,8 @@ object MarkupParsers { var xEmbeddedBlock: Boolean = false private var debugLastStartElement = List.empty[(Int, String)] - @binaryAPI private def debugLastPos = debugLastStartElement.head._1 - @binaryAPI private def debugLastElem = debugLastStartElement.head._2 + @binaryAPIAccessor private def debugLastPos = debugLastStartElement.head._1 + @binaryAPIAccessor private def debugLastElem = debugLastStartElement.head._2 private def errorBraces() = { reportSyntaxError("in XML content, please use '}}' to express '}'") diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 3a31d225c675..59d5ce1b4cb5 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -31,13 +31,13 @@ import dotty.tools.dotc.util.SourcePosition import dotty.tools.dotc.ast.untpd.{MemberDef, Modifiers, PackageDef, RefTree, Template, TypeDef, ValOrDefDef} import cc.{CaptureSet, toCaptureSet, IllegalCaptureRef} -import scala.annotation.binaryAPI +import scala.annotation.binaryAPIAccessor class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { /** A stack of enclosing DefDef, TypeDef, or ClassDef, or ModuleDefs nodes */ private var enclosingDef: untpd.Tree = untpd.EmptyTree - @binaryAPI private var myCtx: Context = super.printerContext + @binaryAPIAccessor private var myCtx: Context = super.printerContext private var printPos = ctx.settings.YprintPos.value private val printLines = ctx.settings.printLines.value diff --git a/compiler/src/dotty/tools/dotc/reporting/trace.scala b/compiler/src/dotty/tools/dotc/reporting/trace.scala index 2d1df32161f6..7ebec9238a45 100644 --- a/compiler/src/dotty/tools/dotc/reporting/trace.scala +++ b/compiler/src/dotty/tools/dotc/reporting/trace.scala @@ -8,7 +8,7 @@ import core.*, Contexts.*, Decorators.* import config.* import printing.Formatting.* -import scala.annotation.binaryAPI +import scala.annotation.binaryAPIAccessor import scala.compiletime.* /** Exposes the {{{ trace("question") { op } }}} syntax. @@ -77,9 +77,11 @@ trait TraceSyntax: inline def apply[T](inline question: String)(inline op: T)(using Context): T = apply[T](question, false)(op) - @binaryAPI private val alwaysToString = (x: Any) => String.valueOf(x) + @binaryAPIAccessor + private val alwaysToString = (x: Any) => String.valueOf(x) - @binaryAPI private def doTrace[T](question: => String, + @binaryAPIAccessor + private def doTrace[T](question: => String, printer: Printers.Printer = Printers.default, showOp: T => String) (op: => T)(using Context): T = diff --git a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala index 44ff46831baf..d54fa0c91b4d 100644 --- a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala +++ b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala @@ -166,12 +166,14 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase ++ sym.annotations) else val binaryAPIAnnotOpt = sym.getAnnotation(defn.BinaryAPIAnnot) + val binaryAPIWithPrivateAccessorAnnotOpt = sym.getAnnotation(defn.BinaryAPIAccessorAnnot) if sym.is(Param) then sym.keepAnnotationsCarrying(thisPhase, Set(defn.ParamMetaAnnot), orNoneOf = defn.NonBeanMetaAnnots) else if sym.is(ParamAccessor) then // FIXME: copyAndKeepAnnotationsCarrying is dropping defn.BinaryAPIAnnot - sym.keepAnnotationsCarrying(thisPhase, Set(defn.GetterMetaAnnot, defn.FieldMetaAnnot, defn.BinaryAPIAnnot)) + sym.keepAnnotationsCarrying(thisPhase, Set(defn.GetterMetaAnnot, defn.FieldMetaAnnot, defn.BinaryAPIAnnot, defn.BinaryAPIAccessorAnnot)) for binaryAPIAnnot <- binaryAPIAnnotOpt do sym.addAnnotation(binaryAPIAnnot) + for binaryAPIWithPrivateAccessorAnnot <- binaryAPIWithPrivateAccessorAnnotOpt do sym.addAnnotation(binaryAPIWithPrivateAccessorAnnot) // TODO is this one necessary? else sym.keepAnnotationsCarrying(thisPhase, Set(defn.GetterMetaAnnot, defn.FieldMetaAnnot), orNoneOf = defn.NonBeanMetaAnnots) if sym.isScala2Macro && !ctx.settings.XignoreScala2Macros.value then diff --git a/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSInterop.scala b/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSInterop.scala index 671784121845..46abd92fd234 100644 --- a/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSInterop.scala +++ b/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSInterop.scala @@ -2,7 +2,7 @@ package dotty.tools.dotc package transform package sjs -import scala.annotation.binaryAPI +import scala.annotation.binaryAPIAccessor import scala.collection.mutable import ast.tpd @@ -1154,7 +1154,7 @@ object PrepJSInterop { val name: String = "prepjsinterop" val description: String = "additional checks and transformations for Scala.js" - private final class OwnerKind private (@binaryAPI private val baseKinds: Int) extends AnyVal { + private final class OwnerKind private (@binaryAPIAccessor private val baseKinds: Int) extends AnyVal { inline def isBaseKind: Boolean = Integer.lowestOneBit(baseKinds) == baseKinds && baseKinds != 0 // exactly 1 bit on diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 5cbd76725b5d..cb3a194a3e53 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -530,6 +530,11 @@ object Checking { if sym.is(Enum) then fail(em"@binaryAPI cannot be used on enum definitions.") else if sym.isType && !sym.is(Module) && !(sym.is(Given) || sym.companionModule.is(Given)) then fail(em"@binaryAPI cannot be used on ${sym.showKind} definitions") else if !sym.owner.isClass && !(sym.is(Param) && sym.owner.isConstructor) then fail(em"@binaryAPI cannot be used on local definitions.") + else if sym.is(Private) then fail(em"@binaryAPI cannot be used on private definitions.\n\nCould the definition `private[${sym.owner.name}]` or `protected` instead or use `@binaryAPIAccessor` instead of `@binaryAPI`.") + if sym.hasAnnotation(defn.BinaryAPIAccessorAnnot) then + if sym.is(Enum) then fail(em"@binaryAPIAccessor cannot be used on enum definitions.") + else if sym.isType && !sym.is(Module) && !(sym.is(Given) || sym.companionModule.is(Given)) then fail(em"@binaryAPIAccessor cannot be used on ${sym.showKind} definitions") + else if !sym.owner.isClass && !(sym.is(Param) && sym.owner.isConstructor) then fail(em"@binaryAPIAccessor cannot be used on local definitions.") if (sym.hasAnnotation(defn.NativeAnnot)) { if (!sym.is(Deferred)) fail(NativeMembersMayNotHaveImplementation(sym)) diff --git a/compiler/src/dotty/tools/dotc/typer/EtaExpansion.scala b/compiler/src/dotty/tools/dotc/typer/EtaExpansion.scala index 0a16d535aa19..380085cc2b70 100644 --- a/compiler/src/dotty/tools/dotc/typer/EtaExpansion.scala +++ b/compiler/src/dotty/tools/dotc/typer/EtaExpansion.scala @@ -15,7 +15,7 @@ import util.Property import collection.mutable import Trees._ -import scala.annotation.binaryAPI +import scala.annotation.binaryAPIAccessor /** A class that handles argument lifting. Argument lifting is needed in the following * scenarios: @@ -165,7 +165,7 @@ object LiftComplex extends LiftComplex object LiftCoverage extends LiftImpure { // Property indicating whether we're currently lifting the arguments of an application - @binaryAPI private val LiftingArgs = new Property.Key[Boolean] + @binaryAPIAccessor private val LiftingArgs = new Property.Key[Boolean] private inline def liftingArgs(using Context): Boolean = ctx.property(LiftingArgs).contains(true) diff --git a/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala b/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala index 71b2726d0775..ff63c887ef55 100644 --- a/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala +++ b/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala @@ -23,7 +23,7 @@ import dotty.tools.dotc.util.Spans._ import dotty.tools.dotc.util.Stats.record import dotty.tools.dotc.reporting.IllegalVariableInPatternAlternative -import scala.annotation.binaryAPI +import scala.annotation.binaryAPIAccessor import scala.collection.mutable @@ -215,7 +215,7 @@ trait QuotesAndSplices { }) object splitter extends tpd.TreeMap { - @binaryAPI private var variance: Int = 1 + @binaryAPIAccessor private var variance: Int = 1 inline private def atVariance[T](v: Int)(op: => T): T = { val saved = variance diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 89bee37bf35f..a3162b6b7fa1 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2429,7 +2429,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer } val vdef1 = assignType(cpy.ValDef(vdef)(name, tpt1, rhs1), sym) postProcessInfo(sym) - binaryAPI(sym) + binaryAPIAccessors(sym) vdef1.setDefTree } @@ -2533,7 +2533,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val ddef2 = assignType(cpy.DefDef(ddef)(name, paramss1, tpt1, rhs1), sym) postProcessInfo(sym) - binaryAPI(sym) + binaryAPIAccessors(sym) ddef2.setDefTree //todo: make sure dependent method types do not depend on implicits or by-name params } @@ -2548,8 +2548,8 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer sym.setFlag(Erased) /** Generate inline accessors for definitions annotated with @inlineAccessible */ - def binaryAPI(sym: Symbol)(using Context): Unit = - if !ctx.isAfterTyper && !sym.is(Param) && sym.is(Private) && sym.hasAnnotation(defn.BinaryAPIAnnot) then + def binaryAPIAccessors(sym: Symbol)(using Context): Unit = + if !ctx.isAfterTyper && !sym.is(Param) && sym.hasAnnotation(defn.BinaryAPIAccessorAnnot) then PrepareInlineable.makePrivateBinaryAPIAccessor(sym) def typedTypeDef(tdef: untpd.TypeDef, sym: Symbol)(using Context): Tree = { diff --git a/compiler/src/dotty/tools/dotc/util/ReusableInstance.scala b/compiler/src/dotty/tools/dotc/util/ReusableInstance.scala index 166cfa3bcea2..da27d7b9d2a7 100644 --- a/compiler/src/dotty/tools/dotc/util/ReusableInstance.scala +++ b/compiler/src/dotty/tools/dotc/util/ReusableInstance.scala @@ -3,7 +3,7 @@ package dotty.tools.dotc.util import scala.collection.mutable.ArrayBuffer import scala.util.chaining._ -import scala.annotation.binaryAPI +import scala.annotation.binaryAPIAccessor /** A wrapper for a list of cached instances of a type `T`. * The wrapper is recursion-reentrant: several instances are kept, so @@ -16,9 +16,11 @@ import scala.annotation.binaryAPI * * Ported from scala.reflect.internal.util.ReusableInstance */ -final class ReusableInstance[T <: AnyRef] private (@binaryAPI make: => T) { - @binaryAPI private[this] val cache = new ArrayBuffer[T](ReusableInstance.InitialSize).tap(_.addOne(make)) - @binaryAPI private[this] var taken = 0 +final class ReusableInstance[T <: AnyRef] private (@binaryAPIAccessor make: => T) { + @binaryAPIAccessor + private[this] val cache = new ArrayBuffer[T](ReusableInstance.InitialSize).tap(_.addOne(make)) + @binaryAPIAccessor + private[this] var taken = 0 inline def withInstance[R](action: T => R): R ={ if (taken == cache.size) diff --git a/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala b/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala index c2ecc317231c..569d6030c5e2 100644 --- a/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala +++ b/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala @@ -22,7 +22,7 @@ import scala.quoted.runtime.{QuoteUnpickler, QuoteMatching} import scala.quoted.runtime.impl.printers._ import scala.reflect.TypeTest -import scala.annotation.binaryAPI +import scala.annotation.binaryAPIAccessor object QuotesImpl { @@ -39,7 +39,7 @@ object QuotesImpl { class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler, QuoteMatching: - @binaryAPI + @binaryAPIAccessor private val xCheckMacro: Boolean = ctx.settings.XcheckMacros.value extension [T](self: scala.quoted.Expr[T]) diff --git a/compiler/test/dotty/tools/backend/jvm/BinaryAPITests.scala b/compiler/test/dotty/tools/backend/jvm/BinaryAPITests.scala index 46a3f6ef7156..751f2f94cbcb 100644 --- a/compiler/test/dotty/tools/backend/jvm/BinaryAPITests.scala +++ b/compiler/test/dotty/tools/backend/jvm/BinaryAPITests.scala @@ -39,9 +39,9 @@ class BinaryAPITests extends DottyBytecodeTest { @Test def binaryAPIDef(): Unit = { val code = - """import scala.annotation.binaryAPI + """import scala.annotation.{binaryAPI, binaryAPIAccessor} |class C: - | @binaryAPI private def privateBinaryAPI: Int = 1 + | @binaryAPIAccessor private def privateBinaryAPI: Int = 1 | @binaryAPI private[C] def packagePrivateBinaryAPI: Int = 1 | @binaryAPI protected def protectedBinaryAPI: Int = 1 | inline def inlined = privateBinaryAPI + packagePrivateBinaryAPI + protectedBinaryAPI @@ -69,12 +69,12 @@ class BinaryAPITests extends DottyBytecodeTest { @Test def binaryAPIVal(): Unit = { val code = - """import scala.annotation.binaryAPI + """import scala.annotation.{binaryAPI, binaryAPIAccessor} |class C: - | @binaryAPI private val privateBinaryAPI: Int = 1 + | @binaryAPIAccessor private val privateBinaryAPI: Int = 1 | @binaryAPI private[C] val packagePrivateBinaryAPI: Int = 1 | @binaryAPI protected val protectedBinaryAPI: Int = 1 - | @binaryAPI private lazy val lazyPrivateBinaryAPI: Int = 1 + | @binaryAPIAccessor private lazy val lazyPrivateBinaryAPI: Int = 1 | @binaryAPI private[C] lazy val lazyPackagePrivateBinaryAPI: Int = 1 | @binaryAPI protected lazy val lazyProtectedBinaryAPI: Int = 1 | inline def inlined = privateBinaryAPI + packagePrivateBinaryAPI + protectedBinaryAPI + lazyPrivateBinaryAPI + lazyPackagePrivateBinaryAPI + lazyProtectedBinaryAPI @@ -110,9 +110,9 @@ class BinaryAPITests extends DottyBytecodeTest { @Test def binaryAPIVar(): Unit = { val code = - """import scala.annotation.binaryAPI + """import scala.annotation.{binaryAPI, binaryAPIAccessor} |class C: - | @binaryAPI private var privateBinaryAPI: Int = 1 + | @binaryAPIAccessor private var privateBinaryAPI: Int = 1 | @binaryAPI private[C] var packagePrivateBinaryAPI: Int = 1 | @binaryAPI protected var protectedBinaryAPI: Int = 1 | inline def inlined = @@ -150,12 +150,12 @@ class BinaryAPITests extends DottyBytecodeTest { @Test def binaryAPIGiven(): Unit = { val code = - """import scala.annotation.binaryAPI + """import scala.annotation.{binaryAPI, binaryAPIAccessor} |class C: - | @binaryAPI private given privateBinaryAPI1: Int = 1 + | @binaryAPIAccessor private given privateBinaryAPI1: Int = 1 | @binaryAPI private[C] given packagePrivateBinaryAPI1: Int = 1 | @binaryAPI protected given protectedBinaryAPI1: Int = 1 - | @binaryAPI private given privateBinaryAPI2(using Int): Int = 1 + | @binaryAPIAccessor private given privateBinaryAPI2(using Int): Int = 1 | @binaryAPI private[C] given packagePrivateBinaryAPI2(using Int): Int = 1 | @binaryAPI protected given protectedBinaryAPI2(using Int): Int = 1 | inline def inlined = @@ -189,12 +189,12 @@ class BinaryAPITests extends DottyBytecodeTest { @Test def binaryAPIClassParam(): Unit = { val code = - """import scala.annotation.binaryAPI + """import scala.annotation.{binaryAPI, binaryAPIAccessor} |class C( - | @binaryAPI private val privateBinaryAPI: Int = 1, + | @binaryAPIAccessor private val privateBinaryAPI: Int = 1, | @binaryAPI private[C] val packagePrivateBinaryAPI: Int = 1, | @binaryAPI protected val protectedBinaryAPI: Int = 1, - | @binaryAPI private var privateVarBinaryAPI: Int = 1 + | @binaryAPIAccessor private var privateVarBinaryAPI: Int = 1 |) { | inline def inlined = | privateVarBinaryAPI = 1 @@ -227,8 +227,8 @@ class BinaryAPITests extends DottyBytecodeTest { def binaryAPIObject(): Unit = { val code = """package foo - |import scala.annotation.binaryAPI - |@binaryAPI private object PrivateBinaryAPI + |import scala.annotation.{binaryAPI, binaryAPIAccessor} + |@binaryAPIAccessor private object PrivateBinaryAPI |@binaryAPI private[foo] object PackagePrivateBinaryAPI |@binaryAPI protected object ProtectedBinaryAPI """.stripMargin @@ -247,18 +247,18 @@ class BinaryAPITests extends DottyBytecodeTest { @Test def binaryAPITraitDefs(): Unit = { val code = - """import scala.annotation.binaryAPI + """import scala.annotation.{binaryAPI, binaryAPIAccessor} |trait C: - | @binaryAPI private val privateValBinaryAPI: Int = 1 + | @binaryAPIAccessor private val privateValBinaryAPI: Int = 1 | @binaryAPI private[C] val packagePrivateValBinaryAPI: Int = 1 | @binaryAPI protected val protectedValBinaryAPI: Int = 1 - | @binaryAPI private lazy val privateLazyValBinaryAPI: Int = 1 + | @binaryAPIAccessor private lazy val privateLazyValBinaryAPI: Int = 1 | @binaryAPI private[C] lazy val packagePrivateLazyValBinaryAPI: Int = 1 | @binaryAPI protected lazy val protectedLazyValBinaryAPI: Int = 1 - | @binaryAPI private var privateVarBinaryAPI: Int = 1 + | @binaryAPIAccessor private var privateVarBinaryAPI: Int = 1 | @binaryAPI private[C] var packagePrivateVarBinaryAPI: Int = 1 | @binaryAPI protected var protectedVarBinaryAPI: Int = 1 - | @binaryAPI private def privateDefBinaryAPI: Int = 1 + | @binaryAPIAccessor private def privateDefBinaryAPI: Int = 1 | @binaryAPI private[C] def packagePrivateDefBinaryAPI: Int = 1 | @binaryAPI protected def protectedDefBinaryAPI: Int = 1 | inline def inlined = @@ -322,11 +322,11 @@ class BinaryAPITests extends DottyBytecodeTest { @Test def binaryAPIDefFinalPrivateAccessors(): Unit = { val code = - """import scala.annotation.binaryAPI - |final class C(@binaryAPI paramBinaryAPI: Int): - | @binaryAPI private val valBinaryAPI: Int = 1 - | @binaryAPI private def defBinaryAPI: Int = 1 - | @binaryAPI private var varBinaryAPI: Int = 1 + """import scala.annotation.binaryAPIAccessor + |final class C(@binaryAPIAccessor paramBinaryAPI: Int): + | @binaryAPIAccessor private val valBinaryAPI: Int = 1 + | @binaryAPIAccessor private def defBinaryAPI: Int = 1 + | @binaryAPIAccessor private var varBinaryAPI: Int = 1 | inline def inlined = | varBinaryAPI = 1 | paramBinaryAPI + valBinaryAPI + defBinaryAPI + varBinaryAPI @@ -358,6 +358,49 @@ class BinaryAPITests extends DottyBytecodeTest { } } + @Test + def binaryAPIDefFinalPrivateAccessorsOnPublic(): Unit = { + val code = + """import scala.annotation.binaryAPIAccessor + |final class C(@binaryAPIAccessor val paramBinaryAPI: Int): + | @binaryAPIAccessor val valBinaryAPI: Int = 1 + | @binaryAPIAccessor def defBinaryAPI: Int = 1 + | @binaryAPIAccessor var varBinaryAPI: Int = 1 + | inline def inlined = + | varBinaryAPI = 1 + | paramBinaryAPI + valBinaryAPI + defBinaryAPI + varBinaryAPI + | def testInlined = inlined + """.stripMargin + checkBCode(code) { dir => + val cClass = loadClassNode(dir.lookupName("C.class", directory = false).input, skipDebugInfo = false) + + checkPrivateField(cClass, "paramBinaryAPI") + checkPublicMethod(cClass, "paramBinaryAPI", "()I") + checkPublicMethod(cClass, "C$$inline$paramBinaryAPI", "()I") + checkPrivateField(cClass, "valBinaryAPI") + checkPublicMethod(cClass, "valBinaryAPI", "()I") + checkPublicMethod(cClass, "C$$inline$valBinaryAPI", "()I") + checkPublicMethod(cClass, "defBinaryAPI", "()I") + checkPublicMethod(cClass, "C$$inline$defBinaryAPI", "()I") + checkPrivateField(cClass, "varBinaryAPI") + checkPublicMethod(cClass, "varBinaryAPI", "()I") + checkPublicMethod(cClass, "C$$inline$varBinaryAPI", "()I") + checkPublicMethod(cClass, "varBinaryAPI_$eq", "(I)V") + checkPublicMethod(cClass, "C$$inline$varBinaryAPI_$eq", "(I)V") + + // Check that the @binaryAPI annotated method is called + val testInlined = getMethod(cClass, "testInlined") + val testInlinedInstructions = instructionsFromMethod(testInlined).filter(_.isInstanceOf[Invoke]) + assertSameCode(testInlinedInstructions, List( + Invoke(INVOKEVIRTUAL, "C", "varBinaryAPI_$eq", "(I)V", false), + Invoke(INVOKEVIRTUAL, "C", "paramBinaryAPI", "()I", false), + Invoke(INVOKEVIRTUAL, "C", "valBinaryAPI", "()I", false), + Invoke(INVOKEVIRTUAL, "C", "defBinaryAPI", "()I", false), + Invoke(INVOKEVIRTUAL, "C", "varBinaryAPI", "()I", false), + )) + } + } + @Test def i13215(): Unit = { val code = @@ -377,12 +420,12 @@ class BinaryAPITests extends DottyBytecodeTest { @Test def i13215b(): Unit = { val code = - """import scala.annotation.binaryAPI + """import scala.annotation.binaryAPIAccessor |package foo: | trait Bar: | inline def baz = Baz | def testInlined = baz - | @binaryAPI private object Baz + | @binaryAPIAccessor private object Baz """.stripMargin checkBCode(code) { dir => val barClass = loadClassNode(dir.subdirectoryNamed("foo").lookupName("Bar.class", directory = false).input, skipDebugInfo = false) diff --git a/library/src/scala/annotation/binaryAPI.scala b/library/src/scala/annotation/binaryAPI.scala index c6f45a2287e7..1a946347e44a 100644 --- a/library/src/scala/annotation/binaryAPI.scala +++ b/library/src/scala/annotation/binaryAPI.scala @@ -4,7 +4,10 @@ package scala.annotation * This annotation can be placed on `def`, `val`, `lazy val`, `var`, `object`, and `given` definitions. * A binary API will be publicly available in the bytecode. * - * - `private`/`private[this]` definitions will get an accessor. - * - `private[T]` and `protected` definitions will become public in the bytecode. + * This annotation cannot be used on `private`/`private[this]` definitions. See `scala.annotation.binaryAPIAccessor`. + * + * This can be used to access private/protected definitions within inline definitions. + * + * Removing this annotation from a non-public definition is a binary incompatible change. */ final class binaryAPI extends scala.annotation.StaticAnnotation diff --git a/library/src/scala/annotation/binaryAPIAccessor.scala b/library/src/scala/annotation/binaryAPIAccessor.scala new file mode 100644 index 000000000000..673d27cafab5 --- /dev/null +++ b/library/src/scala/annotation/binaryAPIAccessor.scala @@ -0,0 +1,12 @@ +package scala.annotation + +/** A binary API with accessor is a definition that is annotated with `@binaryAPIAccessor`. + * This annotation can be placed on `def`, `val`, `lazy val`, `var`, `object`, and `given` definitions. + * The annotated definition will get a public accessor. + * + * This can be used to access `private`/`private[this]` definitions within inline definitions. + * To access other private/protected definition see `scala.annotation.binaryAPI`. + * + * Removing this annotation is a binary incompatible change. + */ +final class binaryAPIAccessor extends scala.annotation.StaticAnnotation diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index 597dcd4f3ec6..ca200f4e76f4 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -18,6 +18,7 @@ object MiMaFilters { ProblemFilters.exclude[MissingClassProblem]("scala.util.boundary$Label"), ProblemFilters.exclude[MissingClassProblem]("scala.quoted.runtime.QuoteMatching$"), ProblemFilters.exclude[MissingClassProblem]("scala.annotation.binaryAPI"), + ProblemFilters.exclude[MissingClassProblem]("scala.annotation.binaryAPIAccessor"), // Scala.js only: new runtime support class in 3.2.3; not available to users ProblemFilters.exclude[MissingClassProblem]("scala.scalajs.runtime.AnonFunctionXXL"), diff --git a/tests/init/pos/i15465.scala b/tests/init/pos/i15465.scala index cb619802b700..67e486032ae8 100644 --- a/tests/init/pos/i15465.scala +++ b/tests/init/pos/i15465.scala @@ -1,4 +1,4 @@ -import scala.annotation.binaryAPI +import scala.annotation.binaryAPIAccessor class TestSuite: protected val it = new ItWord @@ -6,7 +6,7 @@ class TestSuite: protected final class ItWord: def should(string: String) = new ItVerbString("should", string) - @binaryAPI private def registerTestToRun(fun: => Any): Unit = () + @binaryAPIAccessor private def registerTestToRun(fun: => Any): Unit = () protected final class ItVerbString(verb: String, name: String): inline def in(testFun: => Any): Unit = registerTestToRun(testFun) diff --git a/tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.check b/tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.check index bac9ffa5310e..caa42a08ee14 100644 --- a/tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.check +++ b/tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.check @@ -3,9 +3,9 @@ | ^^^^^^^^^^^^^ | Generated unstable inline accessor for value valBinaryAPI1 defined in class A. | - | Annotate valBinaryAPI1 with `@binaryAPI` to generate a stable accessor. + | Annotate valBinaryAPI1 with `@binaryAPIAccessor` to generate a stable accessor. | - | Adding @binaryAPI may break binary compatibility if a previous version of this + | Adding @binaryAPIAccessor may break binary compatibility if a previous version of this | library was compiled with Scala 3.0-3.3, Binary compatibility should be checked | using MiMa. To keep binary you can add the following accessor to class A: | @binaryAPI private[A] final def foo$A$$inline$valBinaryAPI1: Int = this.valBinaryAPI1 @@ -39,9 +39,9 @@ | ^^^^^^^^^^^^^ | Generated unstable inline accessor for value valBinaryAPI1 defined in class C. | - | Annotate valBinaryAPI1 with `@binaryAPI` to generate a stable accessor. + | Annotate valBinaryAPI1 with `@binaryAPIAccessor` to generate a stable accessor. | - | Adding @binaryAPI may break binary compatibility if a previous version of this + | Adding @binaryAPIAccessor may break binary compatibility if a previous version of this | library was compiled with Scala 3.0-3.3, Binary compatibility should be checked | using MiMa. To keep binary you can add the following accessor to class C: | @binaryAPI private[C] final def inline$valBinaryAPI1: Int = this.valBinaryAPI1 @@ -75,9 +75,9 @@ | ^^^^^^^^^^^^^ | Generated unstable inline accessor for value valBinaryAPI1 defined in object E. | - | Annotate valBinaryAPI1 with `@binaryAPI` to generate a stable accessor. + | Annotate valBinaryAPI1 with `@binaryAPIAccessor` to generate a stable accessor. | - | Adding @binaryAPI may break binary compatibility if a previous version of this + | Adding @binaryAPIAccessor may break binary compatibility if a previous version of this | library was compiled with Scala 3.0-3.3, Binary compatibility should be checked | using MiMa. To keep binary you can add the following accessor to object E: | @binaryAPI private[E] final def inline$valBinaryAPI1: Int = foo.E.valBinaryAPI1 diff --git a/tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.scala b/tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.scala index c1cd2effad2c..fd4fd322ad8d 100644 --- a/tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.scala +++ b/tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.scala @@ -1,9 +1,9 @@ package foo -import scala.annotation.binaryAPI +import scala.annotation.{binaryAPI, binaryAPIAccessor} class A: private val valBinaryAPI1: Int = 1 private[foo] val valBinaryAPI2: Int = 1 - @binaryAPI private val valBinaryAPI3: Int = 1 + @binaryAPIAccessor private val valBinaryAPI3: Int = 1 @binaryAPI private[foo] val valBinaryAPI4: Int = 1 inline def inlined = valBinaryAPI1 + // error @@ -18,7 +18,7 @@ class B(val a: A): final class C: private val valBinaryAPI1: Int = 1 private[foo] val valBinaryAPI2: Int = 1 - @binaryAPI private val valBinaryAPI3: Int = 1 + @binaryAPIAccessor private val valBinaryAPI3: Int = 1 @binaryAPI private[foo] val valBinaryAPI4: Int = 1 inline def inlined = valBinaryAPI1 + // error @@ -33,7 +33,7 @@ final class D(val c: C): object E: private val valBinaryAPI1: Int = 1 private[foo] val valBinaryAPI2: Int = 1 - @binaryAPI private val valBinaryAPI3: Int = 1 + @binaryAPIAccessor private val valBinaryAPI3: Int = 1 @binaryAPI private[foo] val valBinaryAPI4: Int = 1 inline def inlined = valBinaryAPI1 + // error diff --git a/tests/pos-macros/i9570.scala b/tests/pos-macros/i9570.scala index b668eb0aae01..e5644a3effa0 100644 --- a/tests/pos-macros/i9570.scala +++ b/tests/pos-macros/i9570.scala @@ -1,5 +1,5 @@ import scala.quoted.* -import scala.annotation.binaryAPI +import scala.annotation.binaryAPIAccessor object Macros { @@ -8,7 +8,8 @@ object Macros { case class HCons[+HD, TL <: HList](hd: HD, tl: TL) extends HList case object HNil extends HList - @binaryAPI private def sizeImpl(e: Expr[HList], n:Int)(using qctx:Quotes): Expr[Int] = { + @binaryAPIAccessor + private def sizeImpl(e: Expr[HList], n:Int)(using qctx:Quotes): Expr[Int] = { import quotes.reflect.* e match { case '{HCons(_,$t)} => // error if run with fatal warinings in BootstrappedOnlyCompilationTests diff --git a/tests/pos/binaryAPI.scala b/tests/pos/binaryAPI.scala index 2aabbdb7f306..a96a065355f0 100644 --- a/tests/pos/binaryAPI.scala +++ b/tests/pos/binaryAPI.scala @@ -1,15 +1,15 @@ package foo -import scala.annotation.binaryAPI +import scala.annotation.{binaryAPI, binaryAPIAccessor} -class Foo(@binaryAPI param: Int, @binaryAPI private[Foo] val paramVal: Int, @binaryAPI private[Foo] var paramVar: Int): - @binaryAPI +class Foo(@binaryAPIAccessor param: Int, @binaryAPI private[Foo] val paramVal: Int, @binaryAPI private[Foo] var paramVar: Int): + @binaryAPIAccessor private val privateVal: Int = 2 @binaryAPI protected val protectedVal: Int = 2 @binaryAPI private[foo] val packagePrivateVal: Int = 2 - @binaryAPI + @binaryAPIAccessor private val privateVar: Int = 2 @binaryAPI protected var protectedVar: Int = 2 @@ -84,22 +84,22 @@ def testFoo = foo.f def localTest = class Foo: - @annotation.binaryAPI private[Foo] val a: Int = 1 - @annotation.binaryAPI protected val b: Int = 1 + @binaryAPI private[Foo] val a: Int = 1 + @binaryAPI protected val b: Int = 1 package traits { trait Trait: - @annotation.binaryAPI private val myVal = 1 - @annotation.binaryAPI private lazy val myLazyVl = 2 - @annotation.binaryAPI private var myVar = 2 - @annotation.binaryAPI private def myDef = 3 - @annotation.binaryAPI private given myGiven: Int = 4 - - @annotation.binaryAPI protected val myVal2 = 1 - @annotation.binaryAPI protected lazy val myLazyVl2 = 2 - @annotation.binaryAPI protected var myVar2 = 2 - @annotation.binaryAPI protected def myDef2 = 3 - @annotation.binaryAPI protected given myGiven2: Int = 4 + @binaryAPIAccessor private val myVal = 1 + @binaryAPIAccessor private lazy val myLazyVl = 2 + @binaryAPIAccessor private var myVar = 2 + @binaryAPIAccessor private def myDef = 3 + @binaryAPIAccessor private given myGiven: Int = 4 + + @binaryAPI protected val myVal2 = 1 + @binaryAPI protected lazy val myLazyVl2 = 2 + @binaryAPI protected var myVar2 = 2 + @binaryAPI protected def myDef2 = 3 + @binaryAPI protected given myGiven2: Int = 4 inline def inlined: Unit = myVar2 = 1 @@ -114,7 +114,7 @@ package traits { trait Foo: inline def foo: Any = bar - @binaryAPI private def bar: Any = ??? + @binaryAPIAccessor private def bar: Any = ??? end Foo def test = From 9b14707a5e7012083c2517ef9d831989a848c66b Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Thu, 23 Mar 2023 09:44:24 +0100 Subject: [PATCH 06/11] Test support of `@binaryAPI` on constructors --- .../dotc/inlines/PrepareInlineable.scala | 2 +- .../tools/dotc/transform/AccessProxies.scala | 2 +- .../src/dotty/tools/dotc/typer/Checking.scala | 1 + tests/neg/binaryAPI.check | 24 ++++++++++ tests/neg/binaryAPI.scala | 7 +++ tests/neg/binaryAPIAccessor.check | 48 +++++++++++++++++++ tests/neg/binaryAPIAccessor.scala | 25 ++++++++++ tests/pos/binaryAPI.scala | 13 +++++ 8 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 tests/neg/binaryAPIAccessor.check create mode 100644 tests/neg/binaryAPIAccessor.scala diff --git a/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala b/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala index 1acd41017890..8b3ea176d333 100644 --- a/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala +++ b/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala @@ -246,7 +246,7 @@ object PrepareInlineable { /** Create an inline accessor for this definition. */ def makePrivateBinaryAPIAccessor(sym: Symbol)(using Context): Unit = - if !sym.is(Accessor) then + if !sym.is(Accessor) && sym.owner.isClass then val ref = tpd.ref(sym).asInstanceOf[RefTree] val accessor = InsertPrivateBinaryAPIAccessors.useAccessor(ref) if sym.is(Mutable) then diff --git a/compiler/src/dotty/tools/dotc/transform/AccessProxies.scala b/compiler/src/dotty/tools/dotc/transform/AccessProxies.scala index fe8c45a8d16c..a199ebd3f93b 100644 --- a/compiler/src/dotty/tools/dotc/transform/AccessProxies.scala +++ b/compiler/src/dotty/tools/dotc/transform/AccessProxies.scala @@ -153,7 +153,7 @@ abstract class AccessProxies { def accessorIfNeeded(tree: Tree)(using Context): Tree = tree match { case tree: RefTree if needsAccessor(tree.symbol) => if (tree.symbol.isConstructor) { - report.error("Implementation restriction: cannot use private constructors in inlineable methods", tree.srcPos) + report.error("Cannot use private constructors in inline methods. You can use @binaryAPI to make constructor accessible in inline methods.", tree.srcPos) tree // TODO: create a proper accessor for the private constructor } else useAccessor(tree) diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index cb3a194a3e53..4c2258f2137b 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -533,6 +533,7 @@ object Checking { else if sym.is(Private) then fail(em"@binaryAPI cannot be used on private definitions.\n\nCould the definition `private[${sym.owner.name}]` or `protected` instead or use `@binaryAPIAccessor` instead of `@binaryAPI`.") if sym.hasAnnotation(defn.BinaryAPIAccessorAnnot) then if sym.is(Enum) then fail(em"@binaryAPIAccessor cannot be used on enum definitions.") + else if sym.isConstructor then fail(em"@binaryAPIAccessor cannot be used on constructors.") else if sym.isType && !sym.is(Module) && !(sym.is(Given) || sym.companionModule.is(Given)) then fail(em"@binaryAPIAccessor cannot be used on ${sym.showKind} definitions") else if !sym.owner.isClass && !(sym.is(Param) && sym.owner.isConstructor) then fail(em"@binaryAPIAccessor cannot be used on local definitions.") if (sym.hasAnnotation(defn.NativeAnnot)) { diff --git a/tests/neg/binaryAPI.check b/tests/neg/binaryAPI.check index 134b77e96bc7..3ebe096e1b4a 100644 --- a/tests/neg/binaryAPI.check +++ b/tests/neg/binaryAPI.check @@ -22,6 +22,30 @@ 19 | @binaryAPI case B(a: Int) // error | ^ | @binaryAPI cannot be used on enum definitions. +-- Error: tests/neg/binaryAPI.scala:22:9 ------------------------------------------------------------------------------- +22 |class Foo @binaryAPI private (x: Int): // error + | ^ + | @binaryAPI cannot be used on private definitions. + | + | Could the definition `private[Foo]` or `protected` instead or use `@binaryAPIAccessor` instead of `@binaryAPI`. +-- Error: tests/neg/binaryAPI.scala:23:25 ------------------------------------------------------------------------------ +23 | @binaryAPI private def this(x: Int, y: Int) = this(x + y) // error + | ^ + | @binaryAPI cannot be used on private definitions. + | + | Could the definition `private[Foo]` or `protected` instead or use `@binaryAPIAccessor` instead of `@binaryAPI`. +-- Error: tests/neg/binaryAPI.scala:25:9 ------------------------------------------------------------------------------- +25 |class Bar @binaryAPI private[this] (x: Int): // error + | ^ + | @binaryAPI cannot be used on private definitions. + | + | Could the definition `private[Bar]` or `protected` instead or use `@binaryAPIAccessor` instead of `@binaryAPI`. +-- Error: tests/neg/binaryAPI.scala:26:31 ------------------------------------------------------------------------------ +26 | @binaryAPI private[this] def this(x: Int, y: Int) = this(x + y) // error + | ^ + | @binaryAPI cannot be used on private definitions. + | + | Could the definition `private[Bar]` or `protected` instead or use `@binaryAPIAccessor` instead of `@binaryAPI`. -- Error: tests/neg/binaryAPI.scala:5:16 ------------------------------------------------------------------------------- 5 |@binaryAPI type A // error | ^ diff --git a/tests/neg/binaryAPI.scala b/tests/neg/binaryAPI.scala index a6b4223b12a3..1e5573e8e48a 100644 --- a/tests/neg/binaryAPI.scala +++ b/tests/neg/binaryAPI.scala @@ -17,3 +17,10 @@ def f(@binaryAPI x: Int) = 3 // error enum Enum2: @binaryAPI case A // error @binaryAPI case B(a: Int) // error + + +class Foo @binaryAPI private (x: Int): // error + @binaryAPI private def this(x: Int, y: Int) = this(x + y) // error + +class Bar @binaryAPI private[this] (x: Int): // error + @binaryAPI private[this] def this(x: Int, y: Int) = this(x + y) // error diff --git a/tests/neg/binaryAPIAccessor.check b/tests/neg/binaryAPIAccessor.check new file mode 100644 index 000000000000..c2fdd20e2a07 --- /dev/null +++ b/tests/neg/binaryAPIAccessor.check @@ -0,0 +1,48 @@ +-- Error: tests/neg/binaryAPIAccessor.scala:6:25 ----------------------------------------------------------------------- +6 |@binaryAPIAccessor class C: // error + | ^ + | @binaryAPIAccessor cannot be used on class definitions +-- Error: tests/neg/binaryAPIAccessor.scala:8:27 ----------------------------------------------------------------------- +8 | @binaryAPIAccessor def g = () // error + | ^ + | @binaryAPIAccessor cannot be used on local definitions. +-- Error: tests/neg/binaryAPIAccessor.scala:10:27 ---------------------------------------------------------------------- +10 |class D[@binaryAPIAccessor T] // error + | ^ + | @binaryAPIAccessor cannot be used on type definitions +-- Error: tests/neg/binaryAPIAccessor.scala:14:24 ---------------------------------------------------------------------- +14 |@binaryAPIAccessor enum Enum1: // error + | ^ + | @binaryAPIAccessor cannot be used on enum definitions. +-- Error: tests/neg/binaryAPIAccessor.scala:18:26 ---------------------------------------------------------------------- +18 | @binaryAPIAccessor case A // error + | ^ + | @binaryAPIAccessor cannot be used on enum definitions. +-- Error: tests/neg/binaryAPIAccessor.scala:19:26 ---------------------------------------------------------------------- +19 | @binaryAPIAccessor case B(a: Int) // error + | ^ + | @binaryAPIAccessor cannot be used on enum definitions. +-- Error: tests/neg/binaryAPIAccessor.scala:21:9 ----------------------------------------------------------------------- +21 |class Foo @binaryAPIAccessor private (x: Int): // error + | ^ + | @binaryAPIAccessor cannot be used on constructors. +-- Error: tests/neg/binaryAPIAccessor.scala:22:33 ---------------------------------------------------------------------- +22 | @binaryAPIAccessor private def this(x: Int, y: Int) = this(x + y) // error + | ^ + | @binaryAPIAccessor cannot be used on constructors. +-- Error: tests/neg/binaryAPIAccessor.scala:24:9 ----------------------------------------------------------------------- +24 |class Bar @binaryAPIAccessor private[this] (x: Int): // error + | ^ + | @binaryAPIAccessor cannot be used on constructors. +-- Error: tests/neg/binaryAPIAccessor.scala:25:39 ---------------------------------------------------------------------- +25 | @binaryAPIAccessor private[this] def this(x: Int, y: Int) = this(x + y) // error + | ^ + | @binaryAPIAccessor cannot be used on constructors. +-- Error: tests/neg/binaryAPIAccessor.scala:5:24 ----------------------------------------------------------------------- +5 |@binaryAPIAccessor type A // error + | ^ + | @binaryAPIAccessor cannot be used on type definitions +-- Error: tests/neg/binaryAPIAccessor.scala:12:25 ---------------------------------------------------------------------- +12 |def f(@binaryAPIAccessor x: Int) = 3 // error + | ^ + | @binaryAPIAccessor cannot be used on local definitions. diff --git a/tests/neg/binaryAPIAccessor.scala b/tests/neg/binaryAPIAccessor.scala new file mode 100644 index 000000000000..a7a0fd6b22a4 --- /dev/null +++ b/tests/neg/binaryAPIAccessor.scala @@ -0,0 +1,25 @@ +package foo + +import scala.annotation.binaryAPIAccessor + +@binaryAPIAccessor type A // error +@binaryAPIAccessor class C: // error + def f: Unit = + @binaryAPIAccessor def g = () // error + () +class D[@binaryAPIAccessor T] // error + +def f(@binaryAPIAccessor x: Int) = 3 // error + +@binaryAPIAccessor enum Enum1: // error + case A + +enum Enum2: + @binaryAPIAccessor case A // error + @binaryAPIAccessor case B(a: Int) // error + +class Foo @binaryAPIAccessor private (x: Int): // error + @binaryAPIAccessor private def this(x: Int, y: Int) = this(x + y) // error + +class Bar @binaryAPIAccessor private[this] (x: Int): // error + @binaryAPIAccessor private[this] def this(x: Int, y: Int) = this(x + y) // error diff --git a/tests/pos/binaryAPI.scala b/tests/pos/binaryAPI.scala index a96a065355f0..8059ea9ad788 100644 --- a/tests/pos/binaryAPI.scala +++ b/tests/pos/binaryAPI.scala @@ -123,3 +123,16 @@ package traits { val baz = new Baz baz.foo } + +package constructors { + class Foo @binaryAPI private[constructors] (x: Int): + @binaryAPI private[constructors] def this(x: Int, y: Int) = this(x + y) + + inline def newFoo(x: Int) = new Foo(x) + inline def newFoo(x: Int, y: Int) = new Foo(x, y) +} + +def testConstructors = + import constructors.* + val f = newFoo(1) + val g = newFoo(1, 2) From 22e600ecfa945e20ee0eaa9b15ee38fcd3e8010b Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Thu, 23 Mar 2023 09:51:26 +0100 Subject: [PATCH 07/11] Support `@binaryAPI` on `private` constructors --- .../src/dotty/tools/dotc/typer/Checking.scala | 2 +- tests/neg/binaryAPI.check | 24 ------------------- tests/neg/binaryAPI.scala | 7 ------ tests/pos/binaryAPI.scala | 10 +++++--- 4 files changed, 8 insertions(+), 35 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 4c2258f2137b..2b62d4a9c63a 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -530,7 +530,7 @@ object Checking { if sym.is(Enum) then fail(em"@binaryAPI cannot be used on enum definitions.") else if sym.isType && !sym.is(Module) && !(sym.is(Given) || sym.companionModule.is(Given)) then fail(em"@binaryAPI cannot be used on ${sym.showKind} definitions") else if !sym.owner.isClass && !(sym.is(Param) && sym.owner.isConstructor) then fail(em"@binaryAPI cannot be used on local definitions.") - else if sym.is(Private) then fail(em"@binaryAPI cannot be used on private definitions.\n\nCould the definition `private[${sym.owner.name}]` or `protected` instead or use `@binaryAPIAccessor` instead of `@binaryAPI`.") + else if sym.is(Private) && !sym.isConstructor then fail(em"@binaryAPI cannot be used on private definitions.\n\nCould the definition `private[${sym.owner.name}]` or `protected` instead or use `@binaryAPIAccessor` instead of `@binaryAPI`.") if sym.hasAnnotation(defn.BinaryAPIAccessorAnnot) then if sym.is(Enum) then fail(em"@binaryAPIAccessor cannot be used on enum definitions.") else if sym.isConstructor then fail(em"@binaryAPIAccessor cannot be used on constructors.") diff --git a/tests/neg/binaryAPI.check b/tests/neg/binaryAPI.check index 3ebe096e1b4a..134b77e96bc7 100644 --- a/tests/neg/binaryAPI.check +++ b/tests/neg/binaryAPI.check @@ -22,30 +22,6 @@ 19 | @binaryAPI case B(a: Int) // error | ^ | @binaryAPI cannot be used on enum definitions. --- Error: tests/neg/binaryAPI.scala:22:9 ------------------------------------------------------------------------------- -22 |class Foo @binaryAPI private (x: Int): // error - | ^ - | @binaryAPI cannot be used on private definitions. - | - | Could the definition `private[Foo]` or `protected` instead or use `@binaryAPIAccessor` instead of `@binaryAPI`. --- Error: tests/neg/binaryAPI.scala:23:25 ------------------------------------------------------------------------------ -23 | @binaryAPI private def this(x: Int, y: Int) = this(x + y) // error - | ^ - | @binaryAPI cannot be used on private definitions. - | - | Could the definition `private[Foo]` or `protected` instead or use `@binaryAPIAccessor` instead of `@binaryAPI`. --- Error: tests/neg/binaryAPI.scala:25:9 ------------------------------------------------------------------------------- -25 |class Bar @binaryAPI private[this] (x: Int): // error - | ^ - | @binaryAPI cannot be used on private definitions. - | - | Could the definition `private[Bar]` or `protected` instead or use `@binaryAPIAccessor` instead of `@binaryAPI`. --- Error: tests/neg/binaryAPI.scala:26:31 ------------------------------------------------------------------------------ -26 | @binaryAPI private[this] def this(x: Int, y: Int) = this(x + y) // error - | ^ - | @binaryAPI cannot be used on private definitions. - | - | Could the definition `private[Bar]` or `protected` instead or use `@binaryAPIAccessor` instead of `@binaryAPI`. -- Error: tests/neg/binaryAPI.scala:5:16 ------------------------------------------------------------------------------- 5 |@binaryAPI type A // error | ^ diff --git a/tests/neg/binaryAPI.scala b/tests/neg/binaryAPI.scala index 1e5573e8e48a..a6b4223b12a3 100644 --- a/tests/neg/binaryAPI.scala +++ b/tests/neg/binaryAPI.scala @@ -17,10 +17,3 @@ def f(@binaryAPI x: Int) = 3 // error enum Enum2: @binaryAPI case A // error @binaryAPI case B(a: Int) // error - - -class Foo @binaryAPI private (x: Int): // error - @binaryAPI private def this(x: Int, y: Int) = this(x + y) // error - -class Bar @binaryAPI private[this] (x: Int): // error - @binaryAPI private[this] def this(x: Int, y: Int) = this(x + y) // error diff --git a/tests/pos/binaryAPI.scala b/tests/pos/binaryAPI.scala index 8059ea9ad788..6ae93b800666 100644 --- a/tests/pos/binaryAPI.scala +++ b/tests/pos/binaryAPI.scala @@ -128,11 +128,15 @@ package constructors { class Foo @binaryAPI private[constructors] (x: Int): @binaryAPI private[constructors] def this(x: Int, y: Int) = this(x + y) + class Bar @binaryAPI(x: Int): + @binaryAPI private def this(x: Int, y: Int) = this(x + y) + inline def bar: Bar = new Bar(x, x) + inline def newFoo(x: Int) = new Foo(x) inline def newFoo(x: Int, y: Int) = new Foo(x, y) } def testConstructors = - import constructors.* - val f = newFoo(1) - val g = newFoo(1, 2) + val f = constructors.newFoo(1) + val g = constructors.newFoo(1, 2) + val h = new constructors.Bar(1).bar From 6ae7082af89c11a37863845587849a877e437e83 Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Tue, 9 May 2023 09:17:18 +0200 Subject: [PATCH 08/11] Detect when old accessor is compatible with `@binaryAPIAccessor` --- .../dotc/inlines/PrepareInlineable.scala | 24 ++++++++++++++----- .../inline-unstable-accessors.check | 24 +++++++------------ 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala b/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala index 8b3ea176d333..42ae0050e6d7 100644 --- a/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala +++ b/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala @@ -110,13 +110,25 @@ object PrepareInlineable { s"Annotate ${accessed.name} with `$annot` to generate a stable accessor." else s"Annotate ${accessed.name} with `$annot` to make it accessible." + val binaryCompat = - s"""Adding $annot may break binary compatibility if a previous version of this - |library was compiled with Scala 3.0-3.3, Binary compatibility should be checked - |using MiMa. To keep binary you can add the following accessor to ${accessor.owner.showKind} ${accessor.owner.name.stripModuleClassSuffix}: - | @binaryAPI private[${accessor.owner.name.stripModuleClassSuffix}] ${accessorDefTree.show} - | - |""".stripMargin + val accessorClass = AccessProxies.hostForAccessorOf(accessed: Symbol) + val inlineAccessorMatches = + accessor.name == InlineAccessorName(accessed.name.asTermName).expandedName(accessorClass) + if !inlineAccessorMatches then + s"""Adding $annot may break binary compatibility if a previous version of this + |library was compiled with Scala 3.0-3.3, Binary compatibility should be checked + |using MiMa. To keep binary compatibility you can add the following accessor to ${accessor.owner.showKind} ${accessor.owner.name.stripModuleClassSuffix}: + | @binaryAPI private[${accessor.owner.name.stripModuleClassSuffix}] ${accessorDefTree.show} + | + |""".stripMargin + else if !accessed.is(Private) then + s"""Adding $annot may break binary compatibility if a previous version of this + |library was compiled with Scala 3.0-3.3, Binary compatibility should be checked + |using MiMa. To keep binary compatibility you can use @binaryAPIAccessor on + |$accessed.""".stripMargin + else + "" report.warning(em"Generated unstable inline accessor for $accessed defined in ${accessed.owner}.\n\n$solution\n\n$binaryCompat", srcPos) } diff --git a/tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.check b/tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.check index caa42a08ee14..7e62fb91239a 100644 --- a/tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.check +++ b/tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.check @@ -5,11 +5,6 @@ | | Annotate valBinaryAPI1 with `@binaryAPIAccessor` to generate a stable accessor. | - | Adding @binaryAPIAccessor may break binary compatibility if a previous version of this - | library was compiled with Scala 3.0-3.3, Binary compatibility should be checked - | using MiMa. To keep binary you can add the following accessor to class A: - | @binaryAPI private[A] final def foo$A$$inline$valBinaryAPI1: Int = this.valBinaryAPI1 - | -- Error: tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.scala:10:4 ------------------------------------ 10 | valBinaryAPI2 + // error | ^^^^^^^^^^^^^ @@ -19,9 +14,8 @@ | | Adding @binaryAPI may break binary compatibility if a previous version of this | library was compiled with Scala 3.0-3.3, Binary compatibility should be checked - | using MiMa. To keep binary you can add the following accessor to class A: - | @binaryAPI private[A] def foo$A$$inline$valBinaryAPI2: Int = this.valBinaryAPI2 - | + | using MiMa. To keep binary compatibility you can use @binaryAPIAccessor on + | val valBinaryAPI2. -- Error: tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.scala:15:6 ------------------------------------ 15 | a.valBinaryAPI2 + // error | ^^^^^^^^^^^^^^^ @@ -31,7 +25,7 @@ | | Adding @binaryAPI may break binary compatibility if a previous version of this | library was compiled with Scala 3.0-3.3, Binary compatibility should be checked - | using MiMa. To keep binary you can add the following accessor to class B: + | using MiMa. To keep binary compatibility you can add the following accessor to class B: | @binaryAPI private[B] def inline$valBinaryAPI2$i1(x$0: foo.A): Int = x$0.valBinaryAPI2 | -- Error: tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.scala:24:4 ------------------------------------ @@ -43,7 +37,7 @@ | | Adding @binaryAPIAccessor may break binary compatibility if a previous version of this | library was compiled with Scala 3.0-3.3, Binary compatibility should be checked - | using MiMa. To keep binary you can add the following accessor to class C: + | using MiMa. To keep binary compatibility you can add the following accessor to class C: | @binaryAPI private[C] final def inline$valBinaryAPI1: Int = this.valBinaryAPI1 | -- Error: tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.scala:25:4 ------------------------------------ @@ -55,7 +49,7 @@ | | Adding @binaryAPI may break binary compatibility if a previous version of this | library was compiled with Scala 3.0-3.3, Binary compatibility should be checked - | using MiMa. To keep binary you can add the following accessor to class C: + | using MiMa. To keep binary compatibility you can add the following accessor to class C: | @binaryAPI private[C] def inline$valBinaryAPI2: Int = this.valBinaryAPI2 | -- Error: tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.scala:30:6 ------------------------------------ @@ -67,7 +61,7 @@ | | Adding @binaryAPI may break binary compatibility if a previous version of this | library was compiled with Scala 3.0-3.3, Binary compatibility should be checked - | using MiMa. To keep binary you can add the following accessor to class D: + | using MiMa. To keep binary compatibility you can add the following accessor to class D: | @binaryAPI private[D] def inline$valBinaryAPI2$i2(x$0: foo.C): Int = x$0.valBinaryAPI2 | -- Error: tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.scala:39:4 ------------------------------------ @@ -79,7 +73,7 @@ | | Adding @binaryAPIAccessor may break binary compatibility if a previous version of this | library was compiled with Scala 3.0-3.3, Binary compatibility should be checked - | using MiMa. To keep binary you can add the following accessor to object E: + | using MiMa. To keep binary compatibility you can add the following accessor to object E: | @binaryAPI private[E] final def inline$valBinaryAPI1: Int = foo.E.valBinaryAPI1 | -- Error: tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.scala:40:4 ------------------------------------ @@ -91,7 +85,7 @@ | | Adding @binaryAPI may break binary compatibility if a previous version of this | library was compiled with Scala 3.0-3.3, Binary compatibility should be checked - | using MiMa. To keep binary you can add the following accessor to object E: + | using MiMa. To keep binary compatibility you can add the following accessor to object E: | @binaryAPI private[E] def inline$valBinaryAPI2: Int = foo.E.valBinaryAPI2 | -- Error: tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.scala:45:6 ------------------------------------ @@ -103,6 +97,6 @@ | | Adding @binaryAPI may break binary compatibility if a previous version of this | library was compiled with Scala 3.0-3.3, Binary compatibility should be checked - | using MiMa. To keep binary you can add the following accessor to object F: + | using MiMa. To keep binary compatibility you can add the following accessor to object F: | @binaryAPI private[F] def inline$valBinaryAPI2$i3(x$0: foo.E): Int = x$0.valBinaryAPI2 | From 6de17356fbb56d30ffdab513e0a4b7d07a14a6aa Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Tue, 9 May 2023 10:12:51 +0200 Subject: [PATCH 09/11] Migrate scodec using the latest migration warnings --- community-build/community-projects/scodec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/community-build/community-projects/scodec b/community-build/community-projects/scodec index d3d193b51229..706de4608c47 160000 --- a/community-build/community-projects/scodec +++ b/community-build/community-projects/scodec @@ -1 +1 @@ -Subproject commit d3d193b51229ebe9a3f2b6caaa47d973c95c8323 +Subproject commit 706de4608c474b292907495f1896c4cb9c377bcc From 78df97e764d94bcf7a15628cc84830dbc400a95b Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Thu, 11 May 2023 08:38:32 +0200 Subject: [PATCH 10/11] Use binary compatible migration accessor names in objects In objects, we can use `inline$` for the name of this accessor. This is the same name as would have been generated automatically. --- community-build/community-projects/scodec | 2 +- .../dotty/tools/dotc/inlines/PrepareInlineable.scala | 7 +++++-- .../fatal-warnings/inline-unstable-accessors.check | 10 ++-------- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/community-build/community-projects/scodec b/community-build/community-projects/scodec index 706de4608c47..07f814f5f0d3 160000 --- a/community-build/community-projects/scodec +++ b/community-build/community-projects/scodec @@ -1 +1 @@ -Subproject commit 706de4608c474b292907495f1896c4cb9c377bcc +Subproject commit 07f814f5f0d3e889b0b79cbb50a928fb62d8a955 diff --git a/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala b/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala index 42ae0050e6d7..6cc0944b0c66 100644 --- a/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala +++ b/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala @@ -59,7 +59,7 @@ object PrepareInlineable { def accessorNameOf(accessed: Symbol, site: Symbol)(using Context): TermName = val accName = InlineAccessorName(accessed.name.asTermName) - if site.isExtensibleClass || accessed.isBinaryAPIAccessor then accName.expandedName(site) + if site.isExtensibleClass || (accessed.isBinaryAPIAccessor && !site.is(Module)) then accName.expandedName(site) else accName /** A definition needs an accessor if it is private, protected, or qualified private @@ -114,7 +114,10 @@ object PrepareInlineable { val binaryCompat = val accessorClass = AccessProxies.hostForAccessorOf(accessed: Symbol) val inlineAccessorMatches = - accessor.name == InlineAccessorName(accessed.name.asTermName).expandedName(accessorClass) + def binaryAccessorName = + if accessed.owner.is(Module) then InlineAccessorName(accessed.name.asTermName) + else InlineAccessorName(accessed.name.asTermName).expandedName(accessorClass) + accessor.owner == accessed.owner && accessor.name == binaryAccessorName if !inlineAccessorMatches then s"""Adding $annot may break binary compatibility if a previous version of this |library was compiled with Scala 3.0-3.3, Binary compatibility should be checked diff --git a/tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.check b/tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.check index 7e62fb91239a..83879f8befad 100644 --- a/tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.check +++ b/tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.check @@ -71,11 +71,6 @@ | | Annotate valBinaryAPI1 with `@binaryAPIAccessor` to generate a stable accessor. | - | Adding @binaryAPIAccessor may break binary compatibility if a previous version of this - | library was compiled with Scala 3.0-3.3, Binary compatibility should be checked - | using MiMa. To keep binary compatibility you can add the following accessor to object E: - | @binaryAPI private[E] final def inline$valBinaryAPI1: Int = foo.E.valBinaryAPI1 - | -- Error: tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.scala:40:4 ------------------------------------ 40 | valBinaryAPI2 + // error | ^^^^^^^^^^^^^ @@ -85,9 +80,8 @@ | | Adding @binaryAPI may break binary compatibility if a previous version of this | library was compiled with Scala 3.0-3.3, Binary compatibility should be checked - | using MiMa. To keep binary compatibility you can add the following accessor to object E: - | @binaryAPI private[E] def inline$valBinaryAPI2: Int = foo.E.valBinaryAPI2 - | + | using MiMa. To keep binary compatibility you can use @binaryAPIAccessor on + | val valBinaryAPI2. -- Error: tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.scala:45:6 ------------------------------------ 45 | E.valBinaryAPI2 + // error | ^^^^^^^^^^^^^^^ From 7b2dc038401d4d9cc7445ce6211a3cf00aadc06c Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Tue, 16 May 2023 08:50:07 +0200 Subject: [PATCH 11/11] Fix migration message with explicit accessor within packages --- .../dotc/inlines/PrepareInlineable.scala | 5 +- .../inline-unstable-accessors.check | 68 +++++++++++++++++++ .../inline-unstable-accessors.scala | 30 ++++++++ 3 files changed, 102 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala b/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala index 6cc0944b0c66..4c21275f82ef 100644 --- a/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala +++ b/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala @@ -119,10 +119,13 @@ object PrepareInlineable { else InlineAccessorName(accessed.name.asTermName).expandedName(accessorClass) accessor.owner == accessed.owner && accessor.name == binaryAccessorName if !inlineAccessorMatches then + val within = + if accessor.owner.name.isPackageObjectName then accessor.owner.owner.name.stripModuleClassSuffix + else accessor.owner.name.stripModuleClassSuffix s"""Adding $annot may break binary compatibility if a previous version of this |library was compiled with Scala 3.0-3.3, Binary compatibility should be checked |using MiMa. To keep binary compatibility you can add the following accessor to ${accessor.owner.showKind} ${accessor.owner.name.stripModuleClassSuffix}: - | @binaryAPI private[${accessor.owner.name.stripModuleClassSuffix}] ${accessorDefTree.show} + | @binaryAPI private[$within] ${accessorDefTree.show} | |""".stripMargin else if !accessed.is(Private) then diff --git a/tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.check b/tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.check index 83879f8befad..50de1d9fc1fc 100644 --- a/tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.check +++ b/tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.check @@ -94,3 +94,71 @@ | using MiMa. To keep binary compatibility you can add the following accessor to object F: | @binaryAPI private[F] def inline$valBinaryAPI2$i3(x$0: foo.E): Int = x$0.valBinaryAPI2 | +-- Error: tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.scala:54:4 ------------------------------------ +54 | valBinaryAPI1 + // error + | ^^^^^^^^^^^^^ + | Generated unstable inline accessor for value valBinaryAPI1 defined in package object G. + | + | Annotate valBinaryAPI1 with `@binaryAPI` to make it accessible. + | + | Adding @binaryAPI may break binary compatibility if a previous version of this + | library was compiled with Scala 3.0-3.3, Binary compatibility should be checked + | using MiMa. To keep binary compatibility you can use @binaryAPIAccessor on + | val valBinaryAPI1. +-- Error: tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.scala:55:4 ------------------------------------ +55 | valBinaryAPI2 + // error + | ^^^^^^^^^^^^^ + | Generated unstable inline accessor for value valBinaryAPI2 defined in package object G. + | + | Annotate valBinaryAPI2 with `@binaryAPI` to make it accessible. + | + | Adding @binaryAPI may break binary compatibility if a previous version of this + | library was compiled with Scala 3.0-3.3, Binary compatibility should be checked + | using MiMa. To keep binary compatibility you can use @binaryAPIAccessor on + | val valBinaryAPI2. +-- Error: tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.scala:60:6 ------------------------------------ +60 | G.valBinaryAPI2 + // error + | ^^^^^^^^^^^^^^^ + | Generated unstable inline accessor for value valBinaryAPI2 defined in package object G. + | + | Annotate valBinaryAPI2 with `@binaryAPI` to make it accessible. + | + | Adding @binaryAPI may break binary compatibility if a previous version of this + | library was compiled with Scala 3.0-3.3, Binary compatibility should be checked + | using MiMa. To keep binary compatibility you can add the following accessor to package object package: + | @binaryAPI private[H] def inline$valBinaryAPI2$i4(x$0: foo.G): Int = x$0.valBinaryAPI2 + | +-- Error: tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.scala:69:4 ------------------------------------ +69 | valBinaryAPI1 + // error + | ^^^^^^^^^^^^^ + |Generated unstable inline accessor for value valBinaryAPI1 defined in package object inline-unstable-accessors$package. + | + |Annotate valBinaryAPI1 with `@binaryAPI` to make it accessible. + | + |Adding @binaryAPI may break binary compatibility if a previous version of this + |library was compiled with Scala 3.0-3.3, Binary compatibility should be checked + |using MiMa. To keep binary compatibility you can use @binaryAPIAccessor on + |val valBinaryAPI1. +-- Error: tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.scala:70:4 ------------------------------------ +70 | valBinaryAPI2 + // error + | ^^^^^^^^^^^^^ + |Generated unstable inline accessor for value valBinaryAPI2 defined in package object inline-unstable-accessors$package. + | + |Annotate valBinaryAPI2 with `@binaryAPI` to make it accessible. + | + |Adding @binaryAPI may break binary compatibility if a previous version of this + |library was compiled with Scala 3.0-3.3, Binary compatibility should be checked + |using MiMa. To keep binary compatibility you can use @binaryAPIAccessor on + |val valBinaryAPI2. +-- Error: tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.scala:75:6 ------------------------------------ +75 | I.valBinaryAPI2 + // error + | ^^^^^^^^^^^^^^^ + |Generated unstable inline accessor for value valBinaryAPI2 defined in package object inline-unstable-accessors$package. + | + |Annotate valBinaryAPI2 with `@binaryAPI` to make it accessible. + | + |Adding @binaryAPI may break binary compatibility if a previous version of this + |library was compiled with Scala 3.0-3.3, Binary compatibility should be checked + |using MiMa. To keep binary compatibility you can add the following accessor to package object inline-unstable-accessors$package: + | @binaryAPI private[J] def inline$valBinaryAPI2$i5(x$0: foo.I): Int = x$0.valBinaryAPI2 + | diff --git a/tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.scala b/tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.scala index fd4fd322ad8d..0eb673aa5b81 100644 --- a/tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.scala +++ b/tests/neg-custom-args/fatal-warnings/inline-unstable-accessors.scala @@ -44,3 +44,33 @@ object F: inline def inlined = E.valBinaryAPI2 + // error E.valBinaryAPI4 + +package object G: + private val valBinaryAPI1: Int = 1 + private[foo] val valBinaryAPI2: Int = 1 + @binaryAPIAccessor private val valBinaryAPI3: Int = 1 + @binaryAPI private[foo] val valBinaryAPI4: Int = 1 + inline def inlined = + valBinaryAPI1 + // error + valBinaryAPI2 + // error + valBinaryAPI3 + + valBinaryAPI4 +package object H: + inline def inlined = + G.valBinaryAPI2 + // error + G.valBinaryAPI4 + +package I: + private val valBinaryAPI1: Int = 1 + private[foo] val valBinaryAPI2: Int = 1 + @binaryAPIAccessor private val valBinaryAPI3: Int = 1 + @binaryAPI private[foo] val valBinaryAPI4: Int = 1 + inline def inlined = + valBinaryAPI1 + // error + valBinaryAPI2 + // error + valBinaryAPI3 + + valBinaryAPI4 +package J: + inline def inlined = + I.valBinaryAPI2 + // error + I.valBinaryAPI4