Skip to content

Commit a696e00

Browse files
committed
Add @binaryAPI 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. It cannot be used on `private`/`private[this]` definitions. 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. If the linting option `-WunstableInlineAccessors` is enabled, then a warning will be emitted if an inline accessor is generated. The warning will guide the user to the use of `@binaryAPI`. For SIP: scala/improvement-proposals#58
1 parent ca6a80e commit a696e00

27 files changed

+1178
-13
lines changed

compiler/src/dotty/tools/dotc/Compiler.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ class Compiler {
7272
new ElimRepeated, // Rewrite vararg parameters and arguments
7373
new RefChecks) :: // Various checks mostly related to abstract members and overriding
7474
List(new init.Checker) :: // Check initialization of objects
75-
List(new ProtectedAccessors, // Add accessors for protected members
75+
List(new BinaryAPIAnnotations, // Makes @binaryAPI definitions public
76+
new ProtectedAccessors, // Add accessors for protected members
7677
new ExtensionMethods, // Expand methods of value classes with extension methods
7778
new UncacheGivenAliases, // Avoid caching RHS of simple parameterless given aliases
7879
new ElimByName, // Map by-name parameters to functions

compiler/src/dotty/tools/dotc/config/ScalaSettings.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ private sealed trait WarningSettings:
163163
val WvalueDiscard: Setting[Boolean] = BooleanSetting("-Wvalue-discard", "Warn when non-Unit expression results are unused.")
164164
val WNonUnitStatement = BooleanSetting("-Wnonunit-statement", "Warn when block statements are non-Unit expressions.")
165165
val WimplausiblePatterns = BooleanSetting("-Wimplausible-patterns", "Warn if comparison with a pattern value looks like it might always fail.")
166+
val WunstableInlineAccessors = BooleanSetting("-WunstableInlineAccessors", "Warn an inline methods has references to non-stable binary APIs.")
166167
val Wunused: Setting[List[ChoiceWithHelp[String]]] = MultiChoiceHelpSetting(
167168
name = "-Wunused",
168169
helpArg = "warning",

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,6 +1047,7 @@ class Definitions {
10471047
@tu lazy val RequiresCapabilityAnnot: ClassSymbol = requiredClass("scala.annotation.internal.requiresCapability")
10481048
@tu lazy val RetainsAnnot: ClassSymbol = requiredClass("scala.annotation.retains")
10491049
@tu lazy val RetainsByNameAnnot: ClassSymbol = requiredClass("scala.annotation.retainsByName")
1050+
@tu lazy val BinaryAPIAnnot: ClassSymbol = requiredClass("scala.annotation.binaryAPI")
10501051

10511052
@tu lazy val JavaRepeatableAnnot: ClassSymbol = requiredClass("java.lang.annotation.Repeatable")
10521053

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,6 +1041,13 @@ object SymDenotations {
10411041
isOneOf(EffectivelyErased)
10421042
|| is(Inline) && !isRetainedInline && !hasAnnotation(defn.ScalaStaticAnnot)
10431043

1044+
/** Is this a member that will become public in the generated binary */
1045+
def isBinaryAPI(using Context): Boolean =
1046+
isTerm && (
1047+
hasAnnotation(defn.BinaryAPIAnnot) ||
1048+
allOverriddenSymbols.exists(sym => sym.hasAnnotation(defn.BinaryAPIAnnot))
1049+
)
1050+
10441051
/** ()T and => T types should be treated as equivalent for this symbol.
10451052
* Note: For the moment, we treat Scala-2 compiled symbols as loose matching,
10461053
* because the Scala library does not always follow the right conventions.

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import transform.SymUtils.*
2323
import config.Printers.inlining
2424
import util.Property
2525
import staging.StagingLevel
26+
import dotty.tools.dotc.reporting.Message
27+
import dotty.tools.dotc.util.SrcPos
2628

2729
object PrepareInlineable {
2830
import tpd._
@@ -72,6 +74,7 @@ object PrepareInlineable {
7274
sym.isTerm &&
7375
(sym.isOneOf(AccessFlags) || sym.privateWithin.exists) &&
7476
!sym.isContainedIn(inlineSym) &&
77+
!sym.isBinaryAPI &&
7578
!(sym.isStableMember && sym.info.widenTermRefExpr.isInstanceOf[ConstantType]) &&
7679
!sym.isInlineMethod &&
7780
(Inlines.inInlineMethod || StagingLevel.level > 0)
@@ -87,6 +90,11 @@ object PrepareInlineable {
8790

8891
override def transform(tree: Tree)(using Context): Tree =
8992
postTransform(super.transform(preTransform(tree)))
93+
94+
protected def checkUnstableAccessor(accessedTree: Tree, accessor: Symbol)(using Context): Unit =
95+
if ctx.settings.WunstableInlineAccessors.value then
96+
val accessorTree = accessorDef(accessor, accessedTree.symbol)
97+
report.warning(reporting.UnstableInlineAccessor(accessedTree.symbol, accessorTree), accessedTree)
9098
}
9199

92100
/** Direct approach: place the accessor with the accessed symbol. This has the
@@ -101,7 +109,11 @@ object PrepareInlineable {
101109
report.error("Implementation restriction: cannot use private constructors in inline methods", tree.srcPos)
102110
tree // TODO: create a proper accessor for the private constructor
103111
}
104-
else useAccessor(tree)
112+
else
113+
val accessor = useAccessor(tree)
114+
if tree != accessor then
115+
checkUnstableAccessor(tree, accessor.symbol)
116+
accessor
105117
case _ =>
106118
tree
107119
}
@@ -180,6 +192,8 @@ object PrepareInlineable {
180192
accessorInfo = abstractQualType(addQualType(dealiasMap(accessedType))),
181193
accessed = accessed)
182194

195+
checkUnstableAccessor(tree, accessor)
196+
183197
val (leadingTypeArgs, otherArgss) = splitArgs(argss)
184198
val argss1 = joinArgs(
185199
localRefs.map(TypeTree(_)) ++ leadingTypeArgs, // TODO: pass type parameters in two sections?

compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,9 +199,10 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe
199199
case ClosureCannotHaveInternalParameterDependenciesID // errorNumber: 183
200200
case MatchTypeNoCasesID // errorNumber: 184
201201
case UnimportedAndImportedID // errorNumber: 185
202-
case ImplausiblePatternWarningID // erorNumber: 186
202+
case ImplausiblePatternWarningID // errorNumber: 186
203203
case SynchronizedCallOnBoxedClassID // errorNumber: 187
204-
case VarArgsParamCannotBeGivenID // erorNumber: 188
204+
case VarArgsParamCannotBeGivenID // errorNumber: 188
205+
case UnstableInlineAccessorID // errorNumber: 189
205206

206207
def errorNumber = ordinal - 1
207208

compiler/src/dotty/tools/dotc/reporting/messages.scala

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -876,7 +876,7 @@ extends Message(PatternMatchExhaustivityID) {
876876

877877
val pathes = List(
878878
ActionPatch(
879-
srcPos = endPos,
879+
srcPos = endPos,
880880
replacement = uncoveredCases.map(c => indent(s"case $c => ???", startColumn))
881881
.mkString("\n", "\n", "")
882882
),
@@ -3038,3 +3038,43 @@ class ImplausiblePatternWarning(pat: tpd.Tree, selType: Type)(using Context)
30383038
|$pat could match selector of type $selType
30393039
|only if there is an `equals` method identifying elements of the two types."""
30403040
def explain(using Context) = ""
3041+
3042+
class UnstableInlineAccessor(accessed: Symbol, accessorTree: tpd.Tree)(using Context)
3043+
extends Message(UnstableInlineAccessorID) {
3044+
def kind = MessageKind.Compatibility
3045+
3046+
def msg(using Context) =
3047+
i"""Unstable inline accessor ${accessor.name} was generated in $where."""
3048+
3049+
def explain(using Context) =
3050+
i"""Access to non-public $accessed causes the automatic generation of an accessor.
3051+
|This accessor is not stable, its name may change or it may disappear
3052+
|if not needed in a future version.
3053+
|
3054+
|To make sure that the inlined code is binary compatible you must make sure that
3055+
|$accessed is public in the binary API.
3056+
| * Option 1: Annotate $accessed with @binaryAPI
3057+
| * Option 2: Make $accessed public
3058+
|
3059+
|This change may break binary compatibility if a previous version of this
3060+
|library was compiled with generated accessors. Binary compatibility should
3061+
|be checked using MiMa. If binary compatibility is broken, you should add the
3062+
|old accessor explicitly in the source code. The following code should be
3063+
|added to $where:
3064+
| @binaryAPI private[$within] ${accessorTree.showIndented(2)}
3065+
|"""
3066+
// FIXME accessorTree is shown with package classes, see tests/neg/inline-unstable-accessors.check:
3067+
// - @binaryAPI private[G] def inline$valBinaryAPI1: Int = foo.G.package.valBinaryAPI1
3068+
// - @binaryAPI private[I] def inline$valBinaryAPI1: Int = foo.I.inline-unstable-accessors$package.valBinaryAPI1
3069+
3070+
private def accessor = accessorTree.symbol
3071+
3072+
private def where =
3073+
if accessor.owner.name.isPackageObjectName then s"package ${within}"
3074+
else if accessor.owner.is(Module) then s"object $within"
3075+
else s"class $within"
3076+
3077+
private def within =
3078+
if accessor.owner.name.isPackageObjectName then accessor.owner.owner.name.stripModuleClassSuffix
3079+
else accessor.owner.name.stripModuleClassSuffix
3080+
}

compiler/src/dotty/tools/dotc/transform/AccessProxies.scala

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@ abstract class AccessProxies {
3232
/** The accessor definitions that need to be added to class `cls` */
3333
private def accessorDefs(cls: Symbol)(using Context): Iterator[DefDef] =
3434
for accessor <- cls.info.decls.iterator; accessed <- accessedBy.get(accessor) yield
35-
DefDef(accessor.asTerm, prefss => {
35+
accessorDef(accessor, accessed)
36+
37+
protected def accessorDef(accessor: Symbol, accessed: Symbol)(using Context): DefDef =
38+
DefDef(accessor.asTerm,
39+
prefss => {
3640
def numTypeParams = accessed.info match {
3741
case info: PolyType => info.paramNames.length
3842
case _ => 0
@@ -42,7 +46,7 @@ abstract class AccessProxies {
4246
if (passReceiverAsArg(accessor.name))
4347
(argss.head.head.select(accessed), targs.takeRight(numTypeParams), argss.tail)
4448
else
45-
(if (accessed.isStatic) ref(accessed) else ref(TermRef(cls.thisType, accessed)),
49+
(if (accessed.isStatic) ref(accessed) else ref(TermRef(accessor.owner.thisType, accessed)),
4650
targs, argss)
4751
val rhs =
4852
if (accessor.name.isSetterName &&
@@ -54,7 +58,8 @@ abstract class AccessProxies {
5458
.appliedToArgss(forwardedArgss)
5559
.etaExpandCFT(using ctx.withOwner(accessor))
5660
rhs.withSpan(accessed.span)
57-
})
61+
}
62+
)
5863

5964
/** Add all needed accessors to the `body` of class `cls` */
6065
def addAccessorDefs(cls: Symbol, body: List[Tree])(using Context): List[Tree] = {
@@ -149,7 +154,7 @@ abstract class AccessProxies {
149154
def accessorIfNeeded(tree: Tree)(using Context): Tree = tree match {
150155
case tree: RefTree if needsAccessor(tree.symbol) =>
151156
if (tree.symbol.isConstructor) {
152-
report.error("Implementation restriction: cannot use private constructors in inlineable methods", tree.srcPos)
157+
report.error("Cannot use private constructors in inline methods. You can use @binaryAPI to make constructor accessible in inline methods.", tree.srcPos)
153158
tree // TODO: create a proper accessor for the private constructor
154159
}
155160
else useAccessor(tree)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package dotty.tools.dotc.transform
2+
3+
import dotty.tools.dotc.core.Contexts.*
4+
import dotty.tools.dotc.core.DenotTransformers.SymTransformer
5+
import dotty.tools.dotc.core.Flags.*
6+
import dotty.tools.dotc.core.Symbols.NoSymbol
7+
import dotty.tools.dotc.core.SymDenotations.SymDenotation
8+
import dotty.tools.dotc.transform.MegaPhase.MiniPhase
9+
import dotty.tools.dotc.typer.RefChecks
10+
11+
/** Makes @binaryAPI definitions public.
12+
*
13+
* This makes it possible to elide the generations of some unnecessary accessors.
14+
*/
15+
class BinaryAPIAnnotations extends MiniPhase with SymTransformer:
16+
17+
override def runsAfterGroupsOf: Set[String] = Set(RefChecks.name)
18+
19+
override def phaseName: String = BinaryAPIAnnotations.name
20+
override def description: String = BinaryAPIAnnotations.description
21+
22+
def transformSym(d: SymDenotation)(using Context): SymDenotation = {
23+
if d.isBinaryAPI then
24+
d.resetFlag(Protected)
25+
d.setPrivateWithin(NoSymbol)
26+
if d.is(Module) then
27+
val moduleClass = d.moduleClass
28+
moduleClass.resetFlag(Protected)
29+
moduleClass.setPrivateWithin(NoSymbol)
30+
d
31+
}
32+
33+
object BinaryAPIAnnotations:
34+
val name: String = "binaryAPIAnnotations"
35+
val description: String = "makes @binaryAPI definitions public"

compiler/src/dotty/tools/dotc/transform/PostTyper.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,10 +169,13 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase =>
169169
atPhase(thisPhase)(cls.annotationsCarrying(Set(defn.CompanionMethodMetaAnnot)))
170170
++ sym.annotations)
171171
else
172+
val binaryAPIAnnotOpt = sym.getAnnotation(defn.BinaryAPIAnnot)
172173
if sym.is(Param) then
173174
sym.keepAnnotationsCarrying(thisPhase, Set(defn.ParamMetaAnnot), orNoneOf = defn.NonBeanMetaAnnots)
174175
else if sym.is(ParamAccessor) then
175-
sym.keepAnnotationsCarrying(thisPhase, Set(defn.GetterMetaAnnot, defn.FieldMetaAnnot))
176+
// FIXME: copyAndKeepAnnotationsCarrying is dropping defn.BinaryAPIAnnot
177+
sym.keepAnnotationsCarrying(thisPhase, Set(defn.GetterMetaAnnot, defn.FieldMetaAnnot, defn.BinaryAPIAnnot, defn.BinaryAPIAnnot))
178+
for binaryAPIAnnot <- binaryAPIAnnotOpt do sym.addAnnotation(binaryAPIAnnot)
176179
else
177180
sym.keepAnnotationsCarrying(thisPhase, Set(defn.GetterMetaAnnot, defn.FieldMetaAnnot), orNoneOf = defn.NonBeanMetaAnnots)
178181
if sym.isScala2Macro && !ctx.settings.XignoreScala2Macros.value then

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,11 @@ object Checking {
542542
fail(em"Inline methods cannot be @tailrec")
543543
if sym.hasAnnotation(defn.TargetNameAnnot) && sym.isClass && sym.isTopLevelClass then
544544
fail(TargetNameOnTopLevelClass(sym))
545+
if sym.hasAnnotation(defn.BinaryAPIAnnot) then
546+
if sym.is(Enum) then fail(em"@binaryAPI cannot be used on enum definitions")
547+
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")
548+
else if !sym.owner.isClass && !(sym.is(Param) && sym.owner.isConstructor) then fail(em"@binaryAPI cannot be used on local definitions")
549+
else if sym.is(Private) && !sym.privateWithin.exists && !sym.isConstructor then fail(em"@binaryAPI cannot be used on private definitions\n\nCould the definition `private[${sym.owner.name}]` or `protected` instead")
545550
if (sym.hasAnnotation(defn.NativeAnnot)) {
546551
if (!sym.is(Deferred))
547552
fail(NativeMembersMayNotHaveImplementation(sym))

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,9 @@ trait TypeAssigner {
107107
val tpe1 = accessibleType(tpe, superAccess)
108108
if tpe1.exists then tpe1
109109
else tpe match
110-
case tpe: NamedType => inaccessibleErrorType(tpe, superAccess, pos)
111-
case NoType => tpe
110+
case tpe: NamedType if !tpe.termSymbol.isBinaryAPI =>
111+
inaccessibleErrorType(tpe, superAccess, pos)
112+
case _ => tpe
112113

113114
/** Return a potentially skolemized version of `qualTpe` to be used
114115
* as a prefix when selecting `name`.

0 commit comments

Comments
 (0)