diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index 22150afad1e8..f1b6f41aa28e 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -73,7 +73,8 @@ class Compiler { new RefChecks) :: // Various checks mostly related to abstract members and overriding List(new semanticdb.ExtractSemanticDB.AppendDiagnostics) :: // Attach warnings to extracted SemanticDB and write to .semanticdb file List(new init.Checker) :: // Check initialization of objects - List(new ProtectedAccessors, // Add accessors for protected members + List(new PublicInBinary, // Makes @publicInBinary 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/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index b10c9859e4d0..e3a85e2d220c 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -165,6 +165,7 @@ private sealed trait WarningSettings: val WvalueDiscard: Setting[Boolean] = BooleanSetting("-Wvalue-discard", "Warn when non-Unit expression results are unused.") val WNonUnitStatement = BooleanSetting("-Wnonunit-statement", "Warn when block statements are non-Unit expressions.") val WimplausiblePatterns = BooleanSetting("-Wimplausible-patterns", "Warn if comparison with a pattern value looks like it might always fail.") + val WunstableInlineAccessors = BooleanSetting("-WunstableInlineAccessors", "Warn an inline methods has references to non-stable binary APIs.") val Wunused: Setting[List[ChoiceWithHelp[String]]] = MultiChoiceHelpSetting( name = "-Wunused", helpArg = "warning", diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 97b1b7db1b21..06d0497683f8 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1056,6 +1056,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 PublicInBinaryAnnot: ClassSymbol = requiredClass("scala.annotation.publicInBinary") @tu lazy val JavaRepeatableAnnot: ClassSymbol = requiredClass("java.lang.annotation.Repeatable") @@ -1067,6 +1068,8 @@ class Definitions { // A list of meta-annotations that are relevant for fields and accessors @tu lazy val NonBeanMetaAnnots: Set[Symbol] = Set(FieldMetaAnnot, GetterMetaAnnot, ParamMetaAnnot, SetterMetaAnnot, CompanionClassMetaAnnot, CompanionMethodMetaAnnot) + @tu lazy val NonBeanParamAccessorAnnots: Set[Symbol] = + Set(PublicInBinaryAnnot) @tu lazy val MetaAnnots: Set[Symbol] = NonBeanMetaAnnots + BeanGetterMetaAnnot + BeanSetterMetaAnnot diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 395164b1dd48..bc5ff4f2fd55 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -1031,6 +1031,13 @@ object SymDenotations { isOneOf(EffectivelyErased) || is(Inline) && !isRetainedInline && !hasAnnotation(defn.ScalaStaticAnnot) + /** Is this a member that will become public in the generated binary */ + def hasPublicInBinary(using Context): Boolean = + isTerm && ( + hasAnnotation(defn.PublicInBinaryAnnot) || + allOverriddenSymbols.exists(sym => sym.hasAnnotation(defn.PublicInBinaryAnnot)) + ) + /** ()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..3cebce52f8d2 100644 --- a/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala +++ b/compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala @@ -23,6 +23,8 @@ import transform.SymUtils.* import config.Printers.inlining import util.Property import staging.StagingLevel +import dotty.tools.dotc.reporting.Message +import dotty.tools.dotc.util.SrcPos object PrepareInlineable { import tpd._ @@ -72,6 +74,7 @@ object PrepareInlineable { sym.isTerm && (sym.isOneOf(AccessFlags) || sym.privateWithin.exists) && !sym.isContainedIn(inlineSym) && + !sym.hasPublicInBinary && !(sym.isStableMember && sym.info.widenTermRefExpr.isInstanceOf[ConstantType]) && !sym.isInlineMethod && (Inlines.inInlineMethod || StagingLevel.level > 0) @@ -87,6 +90,11 @@ object PrepareInlineable { override def transform(tree: Tree)(using Context): Tree = postTransform(super.transform(preTransform(tree))) + + protected def checkUnstableAccessor(accessedTree: Tree, accessor: Symbol)(using Context): Unit = + if ctx.settings.WunstableInlineAccessors.value then + val accessorTree = accessorDef(accessor, accessedTree.symbol) + report.warning(reporting.UnstableInlineAccessor(accessedTree.symbol, accessorTree), accessedTree) } /** Direct approach: place the accessor with the accessed symbol. This has the @@ -101,7 +109,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 accessor = useAccessor(tree) + if tree != accessor then + checkUnstableAccessor(tree, accessor.symbol) + accessor case _ => tree } @@ -180,6 +192,8 @@ object PrepareInlineable { accessorInfo = abstractQualType(addQualType(dealiasMap(accessedType))), accessed = accessed) + checkUnstableAccessor(tree, accessor) + val (leadingTypeArgs, otherArgss) = splitArgs(argss) val argss1 = joinArgs( localRefs.map(TypeTree(_)) ++ leadingTypeArgs, // TODO: pass type parameters in two sections? diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 4f63bfb38691..055805e4c3b1 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -123,14 +123,19 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { else if homogenizedView then isEmptyPrefix(sym) // drop and anonymous classes, but not scala, Predef. else if sym.isPackageObject then isOmittablePrefix(sym.owner) else isOmittablePrefix(sym) + def isSkippedPackageObject(sym: Symbol) = + sym.isPackageObject && !homogenizedView && !printDebug tp.prefix match { - case thisType: ThisType if isOmittable(thisType.cls) => - "" - case termRef @ TermRef(pre, _) => + case thisType: ThisType => + val sym = thisType.cls + if isSkippedPackageObject(sym) then toTextPrefixOf(sym.typeRef) + else if isOmittable(sym) then "" + else super.toTextPrefixOf(tp) + case termRef: TermRef => val sym = termRef.symbol - if sym.isPackageObject && !homogenizedView && !printDebug then toTextPrefixOf(termRef) - else if (isOmittable(sym)) "" + if isSkippedPackageObject(sym) then toTextPrefixOf(termRef) + else if isOmittable(sym) then "" else super.toTextPrefixOf(tp) case _ => super.toTextPrefixOf(tp) } diff --git a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala index 8e583509cd98..2c934664d475 100644 --- a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala +++ b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala @@ -204,6 +204,7 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe case VarArgsParamCannotBeGivenID // errorNumber: 188 case ExtractorNotFoundID // errorNumber: 189 case PureUnitExpressionID // errorNumber: 190 + case UnstableInlineAccessorID // errorNumber: 191 def errorNumber = ordinal - 1 diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index fa5a16ee2245..25af61e01075 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -3085,3 +3085,40 @@ class ImplausiblePatternWarning(pat: tpd.Tree, selType: Type)(using Context) |$pat could match selector of type $selType |only if there is an `equals` method identifying elements of the two types.""" def explain(using Context) = "" + +class UnstableInlineAccessor(accessed: Symbol, accessorTree: tpd.Tree)(using Context) + extends Message(UnstableInlineAccessorID) { + def kind = MessageKind.Compatibility + + def msg(using Context) = + i"""Unstable inline accessor ${accessor.name} was generated in $where.""" + + def explain(using Context) = + i"""Access to non-public $accessed causes the automatic generation of an accessor. + |This accessor is not stable, its name may change or it may disappear + |if not needed in a future version. + | + |To make sure that the inlined code is binary compatible you must make sure that + |$accessed is public in the binary API. + | * Option 1: Annotate $accessed with @publicInBinary + | * Option 2: Make $accessed public + | + |This change may break binary compatibility if a previous version of this + |library was compiled with generated accessors. Binary compatibility should + |be checked using MiMa. If binary compatibility is broken, you should add the + |old accessor explicitly in the source code. The following code should be + |added to $where: + | @publicInBinary private[$within] ${accessorTree.show} + |""" + + private def accessor = accessorTree.symbol + + private def where = + if accessor.owner.name.isPackageObjectName then s"package ${within}" + else if accessor.owner.is(Module) then s"object $within" + else s"class $within" + + private def within = + if accessor.owner.name.isPackageObjectName then accessor.owner.owner.name.stripModuleClassSuffix + else accessor.owner.name.stripModuleClassSuffix +} diff --git a/compiler/src/dotty/tools/dotc/transform/AccessProxies.scala b/compiler/src/dotty/tools/dotc/transform/AccessProxies.scala index 3175ffceae49..921ff0a926a2 100644 --- a/compiler/src/dotty/tools/dotc/transform/AccessProxies.scala +++ b/compiler/src/dotty/tools/dotc/transform/AccessProxies.scala @@ -32,7 +32,11 @@ 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 => { + accessorDef(accessor, accessed) + + 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 @@ -42,7 +46,7 @@ abstract class AccessProxies { 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)), + (if (accessed.isStatic) ref(accessed) else ref(TermRef(accessor.owner.thisType, accessed)), targs, argss) val rhs = if (accessor.name.isSetterName && @@ -54,7 +58,8 @@ abstract class AccessProxies { .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] = { @@ -149,7 +154,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 @publicInBinary 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/transform/PostTyper.scala b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala index ff322dad9ab6..ccf4e1d110e0 100644 --- a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala +++ b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala @@ -172,7 +172,7 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase => 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)) + sym.keepAnnotationsCarrying(thisPhase, Set(defn.GetterMetaAnnot, defn.FieldMetaAnnot), orNoneOf = defn.NonBeanParamAccessorAnnots) 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/PublicInBinaryAnnotations.scala b/compiler/src/dotty/tools/dotc/transform/PublicInBinaryAnnotations.scala new file mode 100644 index 000000000000..201bc11488bc --- /dev/null +++ b/compiler/src/dotty/tools/dotc/transform/PublicInBinaryAnnotations.scala @@ -0,0 +1,35 @@ +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 @publicInBinary definitions public. + * + * This makes it possible to elide the generations of some unnecessary accessors. + */ +class PublicInBinary extends MiniPhase with SymTransformer: + + override def runsAfterGroupsOf: Set[String] = Set(RefChecks.name) + + override def phaseName: String = PublicInBinary.name + override def description: String = PublicInBinary.description + + def transformSym(d: SymDenotation)(using Context): SymDenotation = { + if d.hasPublicInBinary then + d.resetFlag(Protected) + d.setPrivateWithin(NoSymbol) + if d.is(Module) then + val moduleClass = d.moduleClass + moduleClass.resetFlag(Protected) + moduleClass.setPrivateWithin(NoSymbol) + d + } + +object PublicInBinary: + val name: String = "publicInBinary" + val description: String = "makes @publicInBinary definitions public" diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index c8026ad5784b..17a3e95a429a 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -543,6 +543,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.PublicInBinaryAnnot) then + if sym.is(Enum) then fail(em"@publicInBinary cannot be used on enum definitions") + else if sym.isType && !sym.is(Module) && !(sym.is(Given) || sym.companionModule.is(Given)) then fail(em"@publicInBinary cannot be used on ${sym.showKind} definitions") + else if !sym.owner.isClass && !(sym.is(Param) && sym.owner.isConstructor) then fail(em"@publicInBinary cannot be used on local definitions") + else if sym.is(Private) && !sym.privateWithin.exists && !sym.isConstructor then fail(em"@publicInBinary cannot be used on private definitions\n\nCould the definition `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 8ded39030a1e..d32bc20ccfb9 100644 --- a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala +++ b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala @@ -107,8 +107,9 @@ trait TypeAssigner { val tpe1 = accessibleType(tpe, superAccess) if tpe1.exists then tpe1 else tpe match - case tpe: NamedType => inaccessibleErrorType(tpe, superAccess, pos) - case NoType => tpe + case tpe: NamedType if !tpe.termSymbol.hasPublicInBinary => + inaccessibleErrorType(tpe, superAccess, pos) + case _ => tpe /** Return a potentially skolemized version of `qualTpe` to be used * as a prefix when selecting `name`. diff --git a/compiler/test/dotty/tools/backend/jvm/PublicInBinaryTests.scala b/compiler/test/dotty/tools/backend/jvm/PublicInBinaryTests.scala new file mode 100644 index 000000000000..9bfef45049d2 --- /dev/null +++ b/compiler/test/dotty/tools/backend/jvm/PublicInBinaryTests.scala @@ -0,0 +1,342 @@ +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 PublicInBinaryTests 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 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 publicInBinaryDef(): Unit = { + val code = + """import scala.annotation.publicInBinary + |class C: + | @publicInBinary private[C] def packagePrivateMethod: Int = 1 + | @publicInBinary protected def protectedMethod: Int = 1 + | inline def inlined = packagePrivateMethod + protectedMethod + | def testInlined = inlined + """.stripMargin + checkBCode(code) { dir => + val cClass = loadClassNode(dir.lookupName("C.class", directory = false).input, skipDebugInfo = false) + + checkPublicMethod(cClass, "packagePrivateMethod", "()I") + checkPublicMethod(cClass, "protectedMethod", "()I") + + // Check that the @publicInBinary annotated method is called + val testInlined = getMethod(cClass, "testInlined") + val testInlinedInstructions = instructionsFromMethod(testInlined).filter(_.isInstanceOf[Invoke]) + assertSameCode(testInlinedInstructions, List( + Invoke(INVOKEVIRTUAL, "C", "packagePrivateMethod", "()I", false), + Invoke(INVOKEVIRTUAL, "C", "protectedMethod", "()I", false), + )) + } + } + + @Test + def publicInBinaryVal(): Unit = { + val code = + """import scala.annotation.publicInBinary + |class C: + | @publicInBinary private[C] val packagePrivateVal: Int = 1 + | @publicInBinary protected val protectedVal: Int = 1 + | @publicInBinary private[C] lazy val lazyPackagePrivateVal: Int = 1 + | @publicInBinary protected lazy val lazyProtectedVal: Int = 1 + | inline def inlined = packagePrivateVal + protectedVal + lazyPackagePrivateVal + lazyProtectedVal + | def testInlined = inlined + """.stripMargin + checkBCode(code) { dir => + val cClass = loadClassNode(dir.lookupName("C.class", directory = false).input, skipDebugInfo = false) + + checkPublicMethod(cClass, "packagePrivateVal", "()I") + checkPublicMethod(cClass, "protectedVal", "()I") + + checkPublicMethod(cClass, "lazyPackagePrivateVal", "()I") + checkPublicMethod(cClass, "lazyProtectedVal", "()I") + + // Check that the @publicInBinaryinaryinaryinaryinary annotated method is called + val testInlined = getMethod(cClass, "testInlined") + val testInlinedInstructions = instructionsFromMethod(testInlined).filter(_.isInstanceOf[Invoke]) + assertSameCode(testInlinedInstructions, List( + Invoke(INVOKEVIRTUAL, "C", "packagePrivateVal", "()I", false), + Invoke(INVOKEVIRTUAL, "C", "protectedVal", "()I", false), + Invoke(INVOKEVIRTUAL, "C", "lazyPackagePrivateVal", "()I", false), + Invoke(INVOKEVIRTUAL, "C", "lazyProtectedVal", "()I", false), + )) + } + } + + @Test + def publicInBinaryVar(): Unit = { + val code = + """import scala.annotation.publicInBinary + |class C: + | @publicInBinary private[C] var packagePrivateVar: Int = 1 + | @publicInBinary protected var protectedVar: Int = 1 + | inline def inlined = + | packagePrivateVar = 1 + | protectedVar = 1 + | packagePrivateVar + protectedVar + | def testInlined = inlined + """.stripMargin + checkBCode(code) { dir => + val cClass = loadClassNode(dir.lookupName("C.class", directory = false).input, skipDebugInfo = false) + + checkPublicMethod(cClass, "packagePrivateVar", "()I") + checkPublicMethod(cClass, "packagePrivateVar_$eq", "(I)V") + checkPublicMethod(cClass, "protectedVar", "()I") + checkPublicMethod(cClass, "protectedVar_$eq", "(I)V") + + // Check that the @publicInBinaryinaryinary annotated method is called + val testInlined = getMethod(cClass, "testInlined") + val testInlinedInstructions = instructionsFromMethod(testInlined).filter(_.isInstanceOf[Invoke]) + assertSameCode(testInlinedInstructions, List( + Invoke(INVOKEVIRTUAL, "C", "packagePrivateVar_$eq", "(I)V", false), + Invoke(INVOKEVIRTUAL, "C", "protectedVar_$eq", "(I)V", false), + Invoke(INVOKEVIRTUAL, "C", "packagePrivateVar", "()I", false), + Invoke(INVOKEVIRTUAL, "C", "protectedVar", "()I", false), + )) + } + } + + @Test + def publicInBinaryGiven(): Unit = { + val code = + """import scala.annotation.publicInBinary + |class C: + | @publicInBinary private[C] given packagePrivateGiven1: Int = 1 + | @publicInBinary protected given protectedGiven1: Int = 1 + | @publicInBinary private[C] given packagePrivateGiven2(using Int): Int = 1 + | @publicInBinary protected given protectedGiven2(using Int): Int = 1 + | inline def inlined = + | packagePrivateGiven1 + protectedGiven1 + packagePrivateGiven2(using 1) + protectedGiven2(using 1) + | def testInlined = inlined + """.stripMargin + checkBCode(code) { dir => + val cClass = loadClassNode(dir.lookupName("C.class", directory = false).input, skipDebugInfo = false) + checkPublicMethod(cClass, "packagePrivateGiven1", "()I") + checkPublicMethod(cClass, "protectedGiven1", "()I") + + checkPublicMethod(cClass, "packagePrivateGiven2", "(I)I") + checkPublicMethod(cClass, "protectedGiven2", "(I)I") + + // Check that the @publicInBinary annotated method is called + val testInlined = getMethod(cClass, "testInlined") + val testInlinedInstructions = instructionsFromMethod(testInlined).filter(_.isInstanceOf[Invoke]) + assertSameCode(testInlinedInstructions, List( + Invoke(INVOKEVIRTUAL, "C", "packagePrivateGiven1", "()I", false), + Invoke(INVOKEVIRTUAL, "C", "protectedGiven1", "()I", false), + Invoke(INVOKEVIRTUAL, "C", "packagePrivateGiven2", "(I)I", false), + Invoke(INVOKEVIRTUAL, "C", "protectedGiven2", "(I)I", false), + )) + } + } + + @Test + def publicInBinaryClassParam(): Unit = { + val code = + """import scala.annotation.publicInBinary + |class C( + | @publicInBinary private[C] val packagePrivateVal: Int = 1, + | @publicInBinary protected val protectedVal: Int = 1, + |) { + | inline def inlined = + | packagePrivateVal + protectedVal + | def testInlined = inlined + |} + """.stripMargin + checkBCode(code) { dir => + val cClass = loadClassNode(dir.lookupName("C.class", directory = false).input, skipDebugInfo = false) + checkPublicMethod(cClass, "packagePrivateVal", "()I") + checkPublicMethod(cClass, "protectedVal", "()I") + + // Check that the @publicInBinary annotated method is called + val testInlined = getMethod(cClass, "testInlined") + val testInlinedInstructions = instructionsFromMethod(testInlined).filter(_.isInstanceOf[Invoke]) + assertSameCode(testInlinedInstructions, List( + Invoke(INVOKEVIRTUAL, "C", "packagePrivateVal", "()I", false), + Invoke(INVOKEVIRTUAL, "C", "protectedVal", "()I", false), + )) + } + } + + @Test + def publicInBinaryObject(): Unit = { + val code = + """package foo + |import scala.annotation.publicInBinary + |@publicInBinary private object PrivateObject + |@publicInBinary private[foo] object PackagePrivateObject + |@publicInBinary protected object ProtectedObject + """.stripMargin + checkBCode(code) { dir => + val privateObject = loadClassNode(dir.subdirectoryNamed("foo").lookupName("PrivateObject$.class", directory = false).input, skipDebugInfo = false) + checkPublicField(privateObject, "MODULE$") + + val packagePrivateObject = loadClassNode(dir.subdirectoryNamed("foo").lookupName("PackagePrivateObject$.class", directory = false).input, skipDebugInfo = false) + checkPublicField(packagePrivateObject, "MODULE$") + + val protectedObject = loadClassNode(dir.subdirectoryNamed("foo").lookupName("ProtectedObject$.class", directory = false).input, skipDebugInfo = false) + checkPublicField(protectedObject, "MODULE$") + } + } + + @Test + def publicInBinaryTraitDefs(): Unit = { + val code = + """import scala.annotation.publicInBinary + |trait C: + | @publicInBinary private[C] val packagePrivateVal: Int = 1 + | @publicInBinary protected val protectedVal: Int = 1 + | @publicInBinary private[C] lazy val packagePrivateLazyVal: Int = 1 + | @publicInBinary protected lazy val protectedLazyVal: Int = 1 + | @publicInBinary private[C] var packagePrivateVar: Int = 1 + | @publicInBinary protected var protectedVar: Int = 1 + | @publicInBinary private[C] def packagePrivateDef: Int = 1 + | @publicInBinary protected def protectedDef: Int = 1 + | inline def inlined = + | packagePrivateVar = 1 + | protectedVar = 1 + | packagePrivateVal + + | protectedVal + + | packagePrivateLazyVal + + | protectedLazyVal + + | packagePrivateVar + + | protectedVar + + | packagePrivateDef + + | protectedDef + | def testInlined = inlined + """.stripMargin + checkBCode(code) { dir => + val cTrait = loadClassNode(dir.lookupName("C.class", directory = false).input, skipDebugInfo = false) + + checkPublicMethod(cTrait, "packagePrivateVal", "()I") + checkPublicMethod(cTrait, "protectedVal", "()I") + checkPublicMethod(cTrait, "packagePrivateLazyVal", "()I") + checkPublicMethod(cTrait, "protectedLazyVal", "()I") + checkPublicMethod(cTrait, "packagePrivateVar", "()I") + checkPublicMethod(cTrait, "packagePrivateVar_$eq", "(I)V") + checkPublicMethod(cTrait, "protectedVar", "()I") + checkPublicMethod(cTrait, "protectedVar_$eq", "(I)V") + checkPublicMethod(cTrait, "packagePrivateDef", "()I") + checkPublicMethod(cTrait, "protectedDef", "()I") + + // Check that the @publicInBinary annotated method is called + val testInlined = getMethod(cTrait, "testInlined") + val testInlinedInstructions = instructionsFromMethod(testInlined).filter(_.isInstanceOf[Invoke]) + assertSameCode(testInlinedInstructions, List( + Invoke(INVOKEINTERFACE, "C", "packagePrivateVar_$eq", "(I)V", true), + Invoke(INVOKEINTERFACE, "C", "protectedVar_$eq", "(I)V", true), + Invoke(INVOKEINTERFACE, "C", "packagePrivateVal", "()I", true), + Invoke(INVOKEINTERFACE, "C", "protectedVal", "()I", true), + Invoke(INVOKEINTERFACE, "C", "packagePrivateLazyVal", "()I", true), + Invoke(INVOKEINTERFACE, "C", "protectedLazyVal", "()I", true), + Invoke(INVOKEINTERFACE, "C", "packagePrivateVar", "()I", true), + Invoke(INVOKEINTERFACE, "C", "protectedVar", "()I", true), + Invoke(INVOKEINTERFACE, "C", "packagePrivateDef", "()I", true), + Invoke(INVOKEINTERFACE, "C", "protectedDef", "()I", true) + )) + } + } + + @Test + def i13215(): Unit = { + val code = + """import scala.annotation.publicInBinary + |package foo: + | trait Bar: + | inline def baz = Baz + | def testInlined = baz + | @publicInBinary private[foo] object Baz + """.stripMargin + checkBCode(code) { dir => + val barClass = loadClassNode(dir.subdirectoryNamed("foo").lookupName("Bar.class", directory = false).input, skipDebugInfo = false) + checkPublicMethod(barClass, "testInlined", "()Lfoo/Baz$;") + } + } + + @Test + def i13215b(): Unit = { + val code = + """import scala.annotation.publicInBinary + |package foo: + | trait Bar: + | inline def baz = Baz + | def testInlined = baz + | @publicInBinary private object Baz + """.stripMargin + checkBCode(code) { dir => + val barClass = loadClassNode(dir.subdirectoryNamed("foo").lookupName("Bar.class", directory = false).input, skipDebugInfo = false) + checkPublicMethod(barClass, "testInlined", "()Lfoo/Baz$;") + } + } + + @Test + def i15413(): Unit = { + val code = + """import scala.quoted.* + |import scala.annotation.publicInBinary + |class Macro: + | inline def foo = Macro.fooImpl + | def test = foo + |object Macro: + | @publicInBinary private[Macro] def fooImpl = {} + """.stripMargin + checkBCode(code) { dir => + val macroClass = loadClassNode(dir.lookupName("Macro.class", directory = false).input, skipDebugInfo = false) + 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.publicInBinary + |class C: + | inline def baz = D.bazImpl + | def test = baz + |object D: + | @publicInBinary private[foo] def bazImpl = {} + """.stripMargin + checkBCode(code) { dir => + val barClass = loadClassNode(dir.subdirectoryNamed("foo").lookupName("C.class", directory = false).input, skipDebugInfo = false) + 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/publicInBinary.scala b/library/src/scala/annotation/publicInBinary.scala new file mode 100644 index 000000000000..badb27ae55bb --- /dev/null +++ b/library/src/scala/annotation/publicInBinary.scala @@ -0,0 +1,14 @@ +package scala.annotation + +/** A binary API is a definition that is annotated with `@publicInBinary` or overrides a definition annotated with `@publicInBinary`. + * 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. + * + * This annotation cannot be used on `private`/`private[this]` definitions. + * + * This can be used to access private[T]/protected definitions within inline definitions. + * + * Removing this annotation from a non-public definition is a binary incompatible change. + */ +@experimental +final class publicInBinary extends scala.annotation.StaticAnnotation diff --git a/tests/neg/inline-unstable-accessors.check b/tests/neg/inline-unstable-accessors.check new file mode 100644 index 000000000000..6ad3d4832d90 --- /dev/null +++ b/tests/neg/inline-unstable-accessors.check @@ -0,0 +1,345 @@ +-- [E191] Compatibility Error: tests/neg/inline-unstable-accessors.scala:10:4 ------------------------------------------ +10 | valBinaryAPI1 + // error + | ^^^^^^^^^^^^^ + | Unstable inline accessor foo$A$$inline$valBinaryAPI1 was generated in class A. + |-------------------------------------------------------------------------------------------------------------------- + | Explanation (enabled by `-explain`) + |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Access to non-public value valBinaryAPI1 causes the automatic generation of an accessor. + | This accessor is not stable, its name may change or it may disappear + | if not needed in a future version. + | + | To make sure that the inlined code is binary compatible you must make sure that + | value valBinaryAPI1 is public in the binary API. + | * Option 1: Annotate value valBinaryAPI1 with @publicInBinary + | * Option 2: Make value valBinaryAPI1 public + | + | This change may break binary compatibility if a previous version of this + | library was compiled with generated accessors. Binary compatibility should + | be checked using MiMa. If binary compatibility is broken, you should add the + | old accessor explicitly in the source code. The following code should be + | added to class A: + | @publicInBinary private[A] final def foo$A$$inline$valBinaryAPI1: Int = this.valBinaryAPI1 + -------------------------------------------------------------------------------------------------------------------- +-- [E191] Compatibility Error: tests/neg/inline-unstable-accessors.scala:11:4 ------------------------------------------ +11 | valBinaryAPI2 + // error + | ^^^^^^^^^^^^^ + | Unstable inline accessor foo$A$$inline$valBinaryAPI2 was generated in class A. + |-------------------------------------------------------------------------------------------------------------------- + | Explanation (enabled by `-explain`) + |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Access to non-public value valBinaryAPI2 causes the automatic generation of an accessor. + | This accessor is not stable, its name may change or it may disappear + | if not needed in a future version. + | + | To make sure that the inlined code is binary compatible you must make sure that + | value valBinaryAPI2 is public in the binary API. + | * Option 1: Annotate value valBinaryAPI2 with @publicInBinary + | * Option 2: Make value valBinaryAPI2 public + | + | This change may break binary compatibility if a previous version of this + | library was compiled with generated accessors. Binary compatibility should + | be checked using MiMa. If binary compatibility is broken, you should add the + | old accessor explicitly in the source code. The following code should be + | added to class A: + | @publicInBinary private[A] def foo$A$$inline$valBinaryAPI2: Int = this.valBinaryAPI2 + -------------------------------------------------------------------------------------------------------------------- +-- [E191] Compatibility Error: tests/neg/inline-unstable-accessors.scala:15:6 ------------------------------------------ +15 | a.valBinaryAPI2 + // error + | ^^^^^^^^^^^^^^^ + | Unstable inline accessor inline$valBinaryAPI2$i1 was generated in class B. + |-------------------------------------------------------------------------------------------------------------------- + | Explanation (enabled by `-explain`) + |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Access to non-public value valBinaryAPI2 causes the automatic generation of an accessor. + | This accessor is not stable, its name may change or it may disappear + | if not needed in a future version. + | + | To make sure that the inlined code is binary compatible you must make sure that + | value valBinaryAPI2 is public in the binary API. + | * Option 1: Annotate value valBinaryAPI2 with @publicInBinary + | * Option 2: Make value valBinaryAPI2 public + | + | This change may break binary compatibility if a previous version of this + | library was compiled with generated accessors. Binary compatibility should + | be checked using MiMa. If binary compatibility is broken, you should add the + | old accessor explicitly in the source code. The following code should be + | added to class B: + | @publicInBinary private[B] def inline$valBinaryAPI2$i1(x$0: foo.A): Int = x$0.valBinaryAPI2 + -------------------------------------------------------------------------------------------------------------------- +-- [E191] Compatibility Error: tests/neg/inline-unstable-accessors.scala:23:4 ------------------------------------------ +23 | valBinaryAPI1 + // error + | ^^^^^^^^^^^^^ + | Unstable inline accessor inline$valBinaryAPI1 was generated in class C. + |-------------------------------------------------------------------------------------------------------------------- + | Explanation (enabled by `-explain`) + |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Access to non-public value valBinaryAPI1 causes the automatic generation of an accessor. + | This accessor is not stable, its name may change or it may disappear + | if not needed in a future version. + | + | To make sure that the inlined code is binary compatible you must make sure that + | value valBinaryAPI1 is public in the binary API. + | * Option 1: Annotate value valBinaryAPI1 with @publicInBinary + | * Option 2: Make value valBinaryAPI1 public + | + | This change may break binary compatibility if a previous version of this + | library was compiled with generated accessors. Binary compatibility should + | be checked using MiMa. If binary compatibility is broken, you should add the + | old accessor explicitly in the source code. The following code should be + | added to class C: + | @publicInBinary private[C] final def inline$valBinaryAPI1: Int = this.valBinaryAPI1 + -------------------------------------------------------------------------------------------------------------------- +-- [E191] Compatibility Error: tests/neg/inline-unstable-accessors.scala:24:4 ------------------------------------------ +24 | valBinaryAPI2 + // error + | ^^^^^^^^^^^^^ + | Unstable inline accessor inline$valBinaryAPI2 was generated in class C. + |-------------------------------------------------------------------------------------------------------------------- + | Explanation (enabled by `-explain`) + |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Access to non-public value valBinaryAPI2 causes the automatic generation of an accessor. + | This accessor is not stable, its name may change or it may disappear + | if not needed in a future version. + | + | To make sure that the inlined code is binary compatible you must make sure that + | value valBinaryAPI2 is public in the binary API. + | * Option 1: Annotate value valBinaryAPI2 with @publicInBinary + | * Option 2: Make value valBinaryAPI2 public + | + | This change may break binary compatibility if a previous version of this + | library was compiled with generated accessors. Binary compatibility should + | be checked using MiMa. If binary compatibility is broken, you should add the + | old accessor explicitly in the source code. The following code should be + | added to class C: + | @publicInBinary private[C] def inline$valBinaryAPI2: Int = this.valBinaryAPI2 + -------------------------------------------------------------------------------------------------------------------- +-- [E191] Compatibility Error: tests/neg/inline-unstable-accessors.scala:28:6 ------------------------------------------ +28 | c.valBinaryAPI2 + // error + | ^^^^^^^^^^^^^^^ + | Unstable inline accessor inline$valBinaryAPI2$i2 was generated in class D. + |-------------------------------------------------------------------------------------------------------------------- + | Explanation (enabled by `-explain`) + |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Access to non-public value valBinaryAPI2 causes the automatic generation of an accessor. + | This accessor is not stable, its name may change or it may disappear + | if not needed in a future version. + | + | To make sure that the inlined code is binary compatible you must make sure that + | value valBinaryAPI2 is public in the binary API. + | * Option 1: Annotate value valBinaryAPI2 with @publicInBinary + | * Option 2: Make value valBinaryAPI2 public + | + | This change may break binary compatibility if a previous version of this + | library was compiled with generated accessors. Binary compatibility should + | be checked using MiMa. If binary compatibility is broken, you should add the + | old accessor explicitly in the source code. The following code should be + | added to class D: + | @publicInBinary private[D] def inline$valBinaryAPI2$i2(x$0: foo.C): Int = x$0.valBinaryAPI2 + -------------------------------------------------------------------------------------------------------------------- +-- [E191] Compatibility Error: tests/neg/inline-unstable-accessors.scala:36:4 ------------------------------------------ +36 | valBinaryAPI1 + // error + | ^^^^^^^^^^^^^ + | Unstable inline accessor inline$valBinaryAPI1 was generated in object E. + |-------------------------------------------------------------------------------------------------------------------- + | Explanation (enabled by `-explain`) + |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Access to non-public value valBinaryAPI1 causes the automatic generation of an accessor. + | This accessor is not stable, its name may change or it may disappear + | if not needed in a future version. + | + | To make sure that the inlined code is binary compatible you must make sure that + | value valBinaryAPI1 is public in the binary API. + | * Option 1: Annotate value valBinaryAPI1 with @publicInBinary + | * Option 2: Make value valBinaryAPI1 public + | + | This change may break binary compatibility if a previous version of this + | library was compiled with generated accessors. Binary compatibility should + | be checked using MiMa. If binary compatibility is broken, you should add the + | old accessor explicitly in the source code. The following code should be + | added to object E: + | @publicInBinary private[E] final def inline$valBinaryAPI1: Int = foo.E.valBinaryAPI1 + -------------------------------------------------------------------------------------------------------------------- +-- [E191] Compatibility Error: tests/neg/inline-unstable-accessors.scala:37:4 ------------------------------------------ +37 | valBinaryAPI2 + // error + | ^^^^^^^^^^^^^ + | Unstable inline accessor inline$valBinaryAPI2 was generated in object E. + |-------------------------------------------------------------------------------------------------------------------- + | Explanation (enabled by `-explain`) + |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Access to non-public value valBinaryAPI2 causes the automatic generation of an accessor. + | This accessor is not stable, its name may change or it may disappear + | if not needed in a future version. + | + | To make sure that the inlined code is binary compatible you must make sure that + | value valBinaryAPI2 is public in the binary API. + | * Option 1: Annotate value valBinaryAPI2 with @publicInBinary + | * Option 2: Make value valBinaryAPI2 public + | + | This change may break binary compatibility if a previous version of this + | library was compiled with generated accessors. Binary compatibility should + | be checked using MiMa. If binary compatibility is broken, you should add the + | old accessor explicitly in the source code. The following code should be + | added to object E: + | @publicInBinary private[E] def inline$valBinaryAPI2: Int = foo.E.valBinaryAPI2 + -------------------------------------------------------------------------------------------------------------------- +-- [E191] Compatibility Error: tests/neg/inline-unstable-accessors.scala:41:6 ------------------------------------------ +41 | E.valBinaryAPI2 + // error + | ^^^^^^^^^^^^^^^ + | Unstable inline accessor inline$valBinaryAPI2$i3 was generated in object F. + |-------------------------------------------------------------------------------------------------------------------- + | Explanation (enabled by `-explain`) + |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Access to non-public value valBinaryAPI2 causes the automatic generation of an accessor. + | This accessor is not stable, its name may change or it may disappear + | if not needed in a future version. + | + | To make sure that the inlined code is binary compatible you must make sure that + | value valBinaryAPI2 is public in the binary API. + | * Option 1: Annotate value valBinaryAPI2 with @publicInBinary + | * Option 2: Make value valBinaryAPI2 public + | + | This change may break binary compatibility if a previous version of this + | library was compiled with generated accessors. Binary compatibility should + | be checked using MiMa. If binary compatibility is broken, you should add the + | old accessor explicitly in the source code. The following code should be + | added to object F: + | @publicInBinary private[F] def inline$valBinaryAPI2$i3(x$0: object foo.E): Int = x$0.valBinaryAPI2 + -------------------------------------------------------------------------------------------------------------------- +-- [E191] Compatibility Error: tests/neg/inline-unstable-accessors.scala:49:4 ------------------------------------------ +49 | valBinaryAPI1 + // error + | ^^^^^^^^^^^^^ + | Unstable inline accessor inline$valBinaryAPI1 was generated in package G. + |-------------------------------------------------------------------------------------------------------------------- + | Explanation (enabled by `-explain`) + |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Access to non-public value valBinaryAPI1 causes the automatic generation of an accessor. + | This accessor is not stable, its name may change or it may disappear + | if not needed in a future version. + | + | To make sure that the inlined code is binary compatible you must make sure that + | value valBinaryAPI1 is public in the binary API. + | * Option 1: Annotate value valBinaryAPI1 with @publicInBinary + | * Option 2: Make value valBinaryAPI1 public + | + | This change may break binary compatibility if a previous version of this + | library was compiled with generated accessors. Binary compatibility should + | be checked using MiMa. If binary compatibility is broken, you should add the + | old accessor explicitly in the source code. The following code should be + | added to package G: + | @publicInBinary private[G] def inline$valBinaryAPI1: Int = foo.G.valBinaryAPI1 + -------------------------------------------------------------------------------------------------------------------- +-- [E191] Compatibility Error: tests/neg/inline-unstable-accessors.scala:50:4 ------------------------------------------ +50 | valBinaryAPI2 + // error + | ^^^^^^^^^^^^^ + | Unstable inline accessor inline$valBinaryAPI2 was generated in package G. + |-------------------------------------------------------------------------------------------------------------------- + | Explanation (enabled by `-explain`) + |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Access to non-public value valBinaryAPI2 causes the automatic generation of an accessor. + | This accessor is not stable, its name may change or it may disappear + | if not needed in a future version. + | + | To make sure that the inlined code is binary compatible you must make sure that + | value valBinaryAPI2 is public in the binary API. + | * Option 1: Annotate value valBinaryAPI2 with @publicInBinary + | * Option 2: Make value valBinaryAPI2 public + | + | This change may break binary compatibility if a previous version of this + | library was compiled with generated accessors. Binary compatibility should + | be checked using MiMa. If binary compatibility is broken, you should add the + | old accessor explicitly in the source code. The following code should be + | added to package G: + | @publicInBinary private[G] def inline$valBinaryAPI2: Int = foo.G.valBinaryAPI2 + -------------------------------------------------------------------------------------------------------------------- +-- [E191] Compatibility Error: tests/neg/inline-unstable-accessors.scala:54:6 ------------------------------------------ +54 | G.valBinaryAPI2 + // error + | ^^^^^^^^^^^^^^^ + | Unstable inline accessor inline$valBinaryAPI2$i4 was generated in package H. + |-------------------------------------------------------------------------------------------------------------------- + | Explanation (enabled by `-explain`) + |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Access to non-public value valBinaryAPI2 causes the automatic generation of an accessor. + | This accessor is not stable, its name may change or it may disappear + | if not needed in a future version. + | + | To make sure that the inlined code is binary compatible you must make sure that + | value valBinaryAPI2 is public in the binary API. + | * Option 1: Annotate value valBinaryAPI2 with @publicInBinary + | * Option 2: Make value valBinaryAPI2 public + | + | This change may break binary compatibility if a previous version of this + | library was compiled with generated accessors. Binary compatibility should + | be checked using MiMa. If binary compatibility is broken, you should add the + | old accessor explicitly in the source code. The following code should be + | added to package H: + | @publicInBinary private[H] def inline$valBinaryAPI2$i4(x$0: foo.G): Int = x$0.valBinaryAPI2 + -------------------------------------------------------------------------------------------------------------------- +-- [E191] Compatibility Error: tests/neg/inline-unstable-accessors.scala:62:4 ------------------------------------------ +62 | valBinaryAPI1 + // error + | ^^^^^^^^^^^^^ + | Unstable inline accessor inline$valBinaryAPI1 was generated in package I. + |-------------------------------------------------------------------------------------------------------------------- + | Explanation (enabled by `-explain`) + |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Access to non-public value valBinaryAPI1 causes the automatic generation of an accessor. + | This accessor is not stable, its name may change or it may disappear + | if not needed in a future version. + | + | To make sure that the inlined code is binary compatible you must make sure that + | value valBinaryAPI1 is public in the binary API. + | * Option 1: Annotate value valBinaryAPI1 with @publicInBinary + | * Option 2: Make value valBinaryAPI1 public + | + | This change may break binary compatibility if a previous version of this + | library was compiled with generated accessors. Binary compatibility should + | be checked using MiMa. If binary compatibility is broken, you should add the + | old accessor explicitly in the source code. The following code should be + | added to package I: + | @publicInBinary private[I] def inline$valBinaryAPI1: Int = foo.I.valBinaryAPI1 + -------------------------------------------------------------------------------------------------------------------- +-- [E191] Compatibility Error: tests/neg/inline-unstable-accessors.scala:63:4 ------------------------------------------ +63 | valBinaryAPI2 + // error + | ^^^^^^^^^^^^^ + | Unstable inline accessor inline$valBinaryAPI2 was generated in package I. + |-------------------------------------------------------------------------------------------------------------------- + | Explanation (enabled by `-explain`) + |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Access to non-public value valBinaryAPI2 causes the automatic generation of an accessor. + | This accessor is not stable, its name may change or it may disappear + | if not needed in a future version. + | + | To make sure that the inlined code is binary compatible you must make sure that + | value valBinaryAPI2 is public in the binary API. + | * Option 1: Annotate value valBinaryAPI2 with @publicInBinary + | * Option 2: Make value valBinaryAPI2 public + | + | This change may break binary compatibility if a previous version of this + | library was compiled with generated accessors. Binary compatibility should + | be checked using MiMa. If binary compatibility is broken, you should add the + | old accessor explicitly in the source code. The following code should be + | added to package I: + | @publicInBinary private[I] def inline$valBinaryAPI2: Int = foo.I.valBinaryAPI2 + -------------------------------------------------------------------------------------------------------------------- +-- [E191] Compatibility Error: tests/neg/inline-unstable-accessors.scala:67:6 ------------------------------------------ +67 | I.valBinaryAPI2 + // error + | ^^^^^^^^^^^^^^^ + | Unstable inline accessor inline$valBinaryAPI2$i5 was generated in package J. + |-------------------------------------------------------------------------------------------------------------------- + | Explanation (enabled by `-explain`) + |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Access to non-public value valBinaryAPI2 causes the automatic generation of an accessor. + | This accessor is not stable, its name may change or it may disappear + | if not needed in a future version. + | + | To make sure that the inlined code is binary compatible you must make sure that + | value valBinaryAPI2 is public in the binary API. + | * Option 1: Annotate value valBinaryAPI2 with @publicInBinary + | * Option 2: Make value valBinaryAPI2 public + | + | This change may break binary compatibility if a previous version of this + | library was compiled with generated accessors. Binary compatibility should + | be checked using MiMa. If binary compatibility is broken, you should add the + | old accessor explicitly in the source code. The following code should be + | added to package J: + | @publicInBinary private[J] def inline$valBinaryAPI2$i5(x$0: foo.I): Int = x$0.valBinaryAPI2 + -------------------------------------------------------------------------------------------------------------------- diff --git a/tests/neg/inline-unstable-accessors.scala b/tests/neg/inline-unstable-accessors.scala new file mode 100644 index 000000000000..19dfc694ae64 --- /dev/null +++ b/tests/neg/inline-unstable-accessors.scala @@ -0,0 +1,68 @@ +//> using options -Werror -WunstableInlineAccessors -explain + +package foo +import scala.annotation.publicInBinary +class A: + private val valBinaryAPI1: Int = 1 + private[foo] val valBinaryAPI2: Int = 1 + @publicInBinary private[foo] val valBinaryAPI3: Int = 1 + inline def inlined = + valBinaryAPI1 + // error + valBinaryAPI2 + // error + valBinaryAPI3 +class B(val a: A): + inline def inlined = + a.valBinaryAPI2 + // error + a.valBinaryAPI3 + +final class C: + private val valBinaryAPI1: Int = 1 + private[foo] val valBinaryAPI2: Int = 1 + @publicInBinary private[foo] val valBinaryAPI3: Int = 1 + inline def inlined = + valBinaryAPI1 + // error + valBinaryAPI2 + // error + valBinaryAPI3 +final class D(val c: C): + inline def inlined = + c.valBinaryAPI2 + // error + c.valBinaryAPI3 + +object E: + private val valBinaryAPI1: Int = 1 + private[foo] val valBinaryAPI2: Int = 1 + @publicInBinary private[foo] val valBinaryAPI3: Int = 1 + inline def inlined = + valBinaryAPI1 + // error + valBinaryAPI2 + // error + valBinaryAPI3 +object F: + inline def inlined = + E.valBinaryAPI2 + // error + E.valBinaryAPI3 + +package object G: + private val valBinaryAPI1: Int = 1 + private[foo] val valBinaryAPI2: Int = 1 + @publicInBinary private[foo] val valBinaryAPI3: Int = 1 + inline def inlined = + valBinaryAPI1 + // error + valBinaryAPI2 + // error + valBinaryAPI3 +package object H: + inline def inlined = + G.valBinaryAPI2 + // error + G.valBinaryAPI3 + +package I: + private val valBinaryAPI1: Int = 1 + private[foo] val valBinaryAPI2: Int = 1 + @publicInBinary private[foo] val valBinaryAPI3: Int = 1 + inline def inlined = + valBinaryAPI1 + // error + valBinaryAPI2 + // error + valBinaryAPI3 +package J: + inline def inlined = + I.valBinaryAPI2 + // error + I.valBinaryAPI3 diff --git a/tests/neg/publicInBinary-not-accessible.check b/tests/neg/publicInBinary-not-accessible.check new file mode 100644 index 000000000000..5f02d66351c1 --- /dev/null +++ b/tests/neg/publicInBinary-not-accessible.check @@ -0,0 +1,38 @@ +-- [E173] Reference Error: tests/neg/publicInBinary-not-accessible.scala:16:4 ------------------------------------------ +16 | a.p // error + | ^^^ + | value p cannot be accessed as a member of (a : foo.A) from the top-level definitions in package foo. + | private[A] value p can only be accessed from class A in package foo. +-- [E173] Reference Error: tests/neg/publicInBinary-not-accessible.scala:17:4 ------------------------------------------ +17 | a.a // error + | ^^^ + | value a cannot be accessed as a member of (a² : foo.A) from the top-level definitions in package foo. + | private[A] value a can only be accessed from class A in package foo. + | + | where: a is a value in class A + | a² is a parameter in method test +-- [E173] Reference Error: tests/neg/publicInBinary-not-accessible.scala:18:4 ------------------------------------------ +18 | a.b // error + | ^^^ + | lazy value b cannot be accessed as a member of (a : foo.A) from the top-level definitions in package foo. + | private[A] lazy value b can only be accessed from class A in package foo. +-- [E173] Reference Error: tests/neg/publicInBinary-not-accessible.scala:19:4 ------------------------------------------ +19 | a.c // error + | ^^^ + | variable c cannot be accessed as a member of (a : foo.A) from the top-level definitions in package foo. + | private[A] variable c can only be accessed from class A in package foo. +-- [E173] Reference Error: tests/neg/publicInBinary-not-accessible.scala:20:4 ------------------------------------------ +20 | a.d // error + | ^^^ + | method d cannot be accessed as a member of (a : foo.A) from the top-level definitions in package foo. + | private[A] method d can only be accessed from class A in package foo. +-- [E173] Reference Error: tests/neg/publicInBinary-not-accessible.scala:21:4 ------------------------------------------ +21 | a.e // error + | ^^^ + | given instance e cannot be accessed as a member of (a : foo.A) from the top-level definitions in package foo. + | private[A] given instance e can only be accessed from class A in package foo. +-- [E173] Reference Error: tests/neg/publicInBinary-not-accessible.scala:22:4 ------------------------------------------ +22 | a.f(using 1.0) // error + | ^^^ + | given instance f cannot be accessed as a member of (a : foo.A) from the top-level definitions in package foo. + | private[A] given instance f can only be accessed from class A in package foo. diff --git a/tests/neg/publicInBinary-not-accessible.scala b/tests/neg/publicInBinary-not-accessible.scala new file mode 100644 index 000000000000..23ddbf2db7d0 --- /dev/null +++ b/tests/neg/publicInBinary-not-accessible.scala @@ -0,0 +1,22 @@ +//> using options -Werror -WunstableInlineAccessors + +package foo + +import scala.annotation.publicInBinary + +class A(@publicInBinary private[A] val p: Int): + @publicInBinary private[A] val a: Int = 1 + @publicInBinary private[A] lazy val b: Int = 1 + @publicInBinary private[A] var c: Int = 1 + @publicInBinary private[A] def d: Int = 1 + @publicInBinary private[A] given e: Int = 1 + @publicInBinary 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/publicInBinary.check b/tests/neg/publicInBinary.check new file mode 100644 index 000000000000..8ce8acd5b50f --- /dev/null +++ b/tests/neg/publicInBinary.check @@ -0,0 +1,32 @@ +-- Error: tests/neg/publicInBinary.scala:8:22 -------------------------------------------------------------------------- +8 |@publicInBinary class C: // error + | ^ + | @publicInBinary cannot be used on class definitions +-- Error: tests/neg/publicInBinary.scala:10:24 ------------------------------------------------------------------------- +10 | @publicInBinary def g = () // error + | ^ + | @publicInBinary cannot be used on local definitions +-- Error: tests/neg/publicInBinary.scala:12:24 ------------------------------------------------------------------------- +12 |class D[@publicInBinary T] // error + | ^ + | @publicInBinary cannot be used on type definitions +-- Error: tests/neg/publicInBinary.scala:16:21 ------------------------------------------------------------------------- +16 |@publicInBinary enum Enum1: // error + | ^ + | @publicInBinary cannot be used on enum definitions +-- Error: tests/neg/publicInBinary.scala:20:23 ------------------------------------------------------------------------- +20 | @publicInBinary case A // error + | ^ + | @publicInBinary cannot be used on enum definitions +-- Error: tests/neg/publicInBinary.scala:21:23 ------------------------------------------------------------------------- +21 | @publicInBinary case B(a: Int) // error + | ^ + | @publicInBinary cannot be used on enum definitions +-- Error: tests/neg/publicInBinary.scala:7:21 -------------------------------------------------------------------------- +7 |@publicInBinary type A // error + | ^ + | @publicInBinary cannot be used on type definitions +-- Error: tests/neg/publicInBinary.scala:14:22 ------------------------------------------------------------------------- +14 |def f(@publicInBinary x: Int) = 3 // error + | ^ + | @publicInBinary cannot be used on local definitions diff --git a/tests/neg/publicInBinary.scala b/tests/neg/publicInBinary.scala new file mode 100644 index 000000000000..2bf342f43ee7 --- /dev/null +++ b/tests/neg/publicInBinary.scala @@ -0,0 +1,21 @@ +//> using optionsoptions -Werror -WunstableInlineAccessors + +package foo + +import scala.annotation.publicInBinary + +@publicInBinary type A // error +@publicInBinary class C: // error + def f: Unit = + @publicInBinary def g = () // error + () +class D[@publicInBinary T] // error + +def f(@publicInBinary x: Int) = 3 // error + +@publicInBinary enum Enum1: // error + case A + +enum Enum2: + @publicInBinary case A // error + @publicInBinary 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..15c2a2568530 --- /dev/null +++ b/tests/pos-macros/i15413/Macro_1.scala @@ -0,0 +1,10 @@ +//> using optionsoptionsoptions -Werror -WunstableInlineAccessors + +import scala.quoted.* +import scala.annotation.publicInBinary + +class Macro: + inline def foo = ${ Macro.fooImpl } + +object Macro: + @publicInBinary 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..c1e9bab422f8 --- /dev/null +++ b/tests/pos-macros/i15413b/Macro_1.scala @@ -0,0 +1,10 @@ +//> using options -Werror -WunstableInlineAccessors + +package bar + +import scala.quoted.* +import scala.annotation.publicInBinary + +inline def foo = ${ fooImpl } + +@publicInBinary 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-special/fatal-warnings/i17735.scala b/tests/pos-special/fatal-warnings/i17735.scala index 56050d8fd5fd..f171d4a028f7 100644 --- a/tests/pos-special/fatal-warnings/i17735.scala +++ b/tests/pos-special/fatal-warnings/i17735.scala @@ -1,4 +1,4 @@ -// scalac: -Wvalue-discard +//> using options -Wvalue-discard import scala.collection.mutable import scala.annotation.nowarn diff --git a/tests/pos-special/fatal-warnings/i17735a.scala b/tests/pos-special/fatal-warnings/i17735a.scala index d089763295e6..fe0ea7e6bc45 100644 --- a/tests/pos-special/fatal-warnings/i17735a.scala +++ b/tests/pos-special/fatal-warnings/i17735a.scala @@ -1,4 +1,4 @@ -// scalac: -Wvalue-discard -Wconf:msg=non-Unit:s +//> using options -Wvalue-discard -Wconf:msg=non-Unit:s import scala.collection.mutable import scala.annotation.nowarn diff --git a/tests/pos-special/fatal-warnings/i17741.scala b/tests/pos-special/fatal-warnings/i17741.scala index 07af8b1abd3d..7171aab83e4b 100644 --- a/tests/pos-special/fatal-warnings/i17741.scala +++ b/tests/pos-special/fatal-warnings/i17741.scala @@ -1,4 +1,4 @@ -// scalac: -Wnonunit-statement +//> using options -Wnonunit-statement class Node() class Elem( diff --git a/tests/pos/publicInBinary.scala b/tests/pos/publicInBinary.scala new file mode 100644 index 000000000000..55b3cb413efc --- /dev/null +++ b/tests/pos/publicInBinary.scala @@ -0,0 +1,140 @@ +//> using options -Werror -WunstableInlineAccessors + +package foo + +import scala.annotation.publicInBinary + +class Foo(@publicInBinary private[Foo] val paramVal: Int, @publicInBinary private[Foo] var paramVar: Int): + @publicInBinary + protected val protectedVal: Int = 2 + @publicInBinary + private[foo] val packagePrivateVal: Int = 2 + @publicInBinary + protected var protectedVar: Int = 2 + @publicInBinary + private[foo] var packagePrivateVar: Int = 2 + + inline def foo: Int = + paramVar = 3 + protectedVar = 3 + packagePrivateVar = 3 + paramVal + paramVar + 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): + @publicInBinary // TODO warn? Not needed because Foo.protectedVal is already @publicInBinary + override protected val protectedVal: Int = 2 + + @publicInBinary + 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 + +@publicInBinary given Int = 1 +@publicInBinary given (using Double): Int = 1 + +trait A[T]: + def f: T +@publicInBinary given A[Int] with + def f: Int = 1 +@publicInBinary 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) { + @publicInBinary 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 { + @publicInBinary private object Foo: + @publicInBinary private[foo] def x: Int = 1 + inline def f: Int = Foo.x +} +def testFoo = foo.f + +def localTest = + class Foo: + @publicInBinary private[Foo] val a: Int = 1 + @publicInBinary protected val b: Int = 1 + +package traits { + trait Trait: + @publicInBinary private[Trait] val myVal = 1 + @publicInBinary private[Trait] lazy val myLazyVl = 2 + @publicInBinary private[Trait] var myVar = 2 + @publicInBinary private[Trait] def myDef = 3 + @publicInBinary private[Trait] given myGiven: Int = 4 + + @publicInBinary protected val myVal2 = 1 + @publicInBinary protected lazy val myLazyVl2 = 2 + @publicInBinary protected var myVar2 = 2 + @publicInBinary protected def myDef2 = 3 + @publicInBinary 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 + @publicInBinary private[Foo] def bar: Any = ??? + end Foo + + def test = + Baz.foo + (new Baz).foo + val baz = new Baz + baz.foo +} + +package constructors { + class Foo @publicInBinary private[constructors] (x: Int): + @publicInBinary private[constructors] def this(x: Int, y: Int) = this(x + y) + + class Bar @publicInBinary(@publicInBinary private[Bar] x: Int): + @publicInBinary 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 = + val f = constructors.newFoo(1) + val g = constructors.newFoo(1, 2) + val h = new constructors.Bar(1).bar diff --git a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala index a01c71724b0e..086e6f123553 100644 --- a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala @@ -58,6 +58,9 @@ val experimentalDefinitionInLibrary = Set( //// New feature: into "scala.annotation.allowConversions", + //// New feature: @publicInBinary + "scala.annotation.publicInBinary", + //// New feature: Macro annotations "scala.annotation.MacroAnnotation", diff --git a/tests/run/i13215.scala b/tests/run/i13215.scala new file mode 100644 index 000000000000..738eb25d598a --- /dev/null +++ b/tests/run/i13215.scala @@ -0,0 +1,14 @@ +//> using options -Werror -WunstableInlineAccessors + +import scala.annotation.publicInBinary + +package foo { + trait Bar: + inline def baz = Baz + + @publicInBinary private[foo] object Baz +} + +@main def Test: Unit = + val bar = new foo.Bar {} + bar.baz