Skip to content

Commit 53c771a

Browse files
committed
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.
1 parent 830230f commit 53c771a

22 files changed

+640
-15
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/core/Definitions.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1039,6 +1039,7 @@ class Definitions {
10391039
@tu lazy val RequiresCapabilityAnnot: ClassSymbol = requiredClass("scala.annotation.internal.requiresCapability")
10401040
@tu lazy val RetainsAnnot: ClassSymbol = requiredClass("scala.annotation.retains")
10411041
@tu lazy val RetainsByNameAnnot: ClassSymbol = requiredClass("scala.annotation.retainsByName")
1042+
@tu lazy val BinaryAPIAnnot: ClassSymbol = requiredClass("scala.annotation.binaryAPI")
10421043

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

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

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

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

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

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ object PrepareInlineable {
5454
/** A tree map which inserts accessors for non-public term members accessed from inlined code.
5555
*/
5656
abstract class MakeInlineableMap(val inlineSym: Symbol) extends TreeMap with Insert {
57+
58+
def useBinaryAPI: Boolean
59+
5760
def accessorNameOf(name: TermName, site: Symbol)(using Context): TermName =
5861
val accName = InlineAccessorName(name)
5962
if site.isExtensibleClass then accName.expandedName(site) else accName
@@ -71,6 +74,7 @@ object PrepareInlineable {
7174
def needsAccessor(sym: Symbol)(using Context): Boolean =
7275
sym.isTerm &&
7376
(sym.isOneOf(AccessFlags) || sym.privateWithin.exists) &&
77+
(!useBinaryAPI || !sym.isBinaryAPI) &&
7478
!sym.isContainedIn(inlineSym) &&
7579
!(sym.isStableMember && sym.info.widenTermRefExpr.isInstanceOf[ConstantType]) &&
7680
!sym.isInlineMethod &&
@@ -94,7 +98,7 @@ object PrepareInlineable {
9498
* possible if the receiver is essentially this or an outer this, which is indicated
9599
* by the test that we can find a host for the accessor.
96100
*/
97-
class MakeInlineableDirect(inlineSym: Symbol) extends MakeInlineableMap(inlineSym) {
101+
class MakeInlineableDirect(inlineSym: Symbol, val useBinaryAPI: Boolean) extends MakeInlineableMap(inlineSym) {
98102
def preTransform(tree: Tree)(using Context): Tree = tree match {
99103
case tree: RefTree if needsAccessor(tree.symbol) =>
100104
if (tree.symbol.isConstructor) {
@@ -118,13 +122,14 @@ object PrepareInlineable {
118122
* private[inlines] def next[U](y: U): (T, U) = (x, y)
119123
* }
120124
* class TestPassing {
121-
* inline def foo[A](x: A): (A, Int) = {
122-
* val c = new C[A](x)
123-
* c.next(1)
124-
* }
125-
* inline def bar[A](x: A): (A, String) = {
126-
* val c = new C[A](x)
127-
* c.next("")
125+
* inline def foo[A](x: A): (A, Int) = {
126+
* val c = new C[A](x)
127+
* c.next(1)
128+
* }
129+
* inline def bar[A](x: A): (A, String) = {
130+
* val c = new C[A](x)
131+
* c.next("")
132+
* }
128133
* }
129134
*
130135
* `C` could be compiled separately, so we cannot place the inline accessor in it.
@@ -137,7 +142,7 @@ object PrepareInlineable {
137142
* Since different calls might have different receiver types, we need to generate one
138143
* such accessor per call, so they need to have unique names.
139144
*/
140-
class MakeInlineablePassing(inlineSym: Symbol) extends MakeInlineableMap(inlineSym) {
145+
class MakeInlineablePassing(inlineSym: Symbol, val useBinaryAPI: Boolean) extends MakeInlineableMap(inlineSym) {
141146

142147
def preTransform(tree: Tree)(using Context): Tree = tree match {
143148
case _: Apply | _: TypeApply | _: RefTree
@@ -220,8 +225,13 @@ object PrepareInlineable {
220225
// so no accessors are needed for them.
221226
tree
222227
else
223-
new MakeInlineablePassing(inlineSym).transform(
224-
new MakeInlineableDirect(inlineSym).transform(tree))
228+
// Make sure the old accessors are generated for binary compatibility
229+
new MakeInlineablePassing(inlineSym, useBinaryAPI = false).transform(
230+
new MakeInlineableDirect(inlineSym, useBinaryAPI = false).transform(tree))
231+
232+
// TODO: warn if MakeInlineablePassing or MakeInlineableDirect generate accessors when useBinaryAPI in enabled
233+
new MakeInlineablePassing(inlineSym, useBinaryAPI = true).transform(
234+
new MakeInlineableDirect(inlineSym, useBinaryAPI = true).transform(tree))
225235
}
226236
}
227237

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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+
class BinaryAPIAnnotations extends MiniPhase with SymTransformer:
13+
14+
override def runsAfterGroupsOf: Set[String] = Set(RefChecks.name)
15+
16+
override def phaseName: String = BinaryAPIAnnotations.name
17+
override def description: String = BinaryAPIAnnotations.description
18+
19+
def transformSym(d: SymDenotation)(using Context): SymDenotation = {
20+
if d.isBinaryAPI then
21+
d.resetFlag(Protected)
22+
d.setPrivateWithin(NoSymbol)
23+
if d.is(Module) then
24+
val moduleClass = d.moduleClass
25+
moduleClass.resetFlag(Protected)
26+
moduleClass.setPrivateWithin(NoSymbol)
27+
d
28+
}
29+
30+
object BinaryAPIAnnotations:
31+
val name: String = "binaryAPIAnnotations"
32+
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
@@ -165,10 +165,13 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase
165165
atPhase(thisPhase)(cls.annotationsCarrying(Set(defn.CompanionMethodMetaAnnot)))
166166
++ sym.annotations)
167167
else
168+
val binaryAPIAnnotOpt = sym.getAnnotation(defn.BinaryAPIAnnot)
168169
if sym.is(Param) then
169170
sym.keepAnnotationsCarrying(thisPhase, Set(defn.ParamMetaAnnot), orNoneOf = defn.NonBeanMetaAnnots)
170171
else if sym.is(ParamAccessor) then
171-
sym.keepAnnotationsCarrying(thisPhase, Set(defn.GetterMetaAnnot, defn.FieldMetaAnnot))
172+
// FIXME: copyAndKeepAnnotationsCarrying is dropping defn.BinaryAPIAnnot
173+
sym.keepAnnotationsCarrying(thisPhase, Set(defn.GetterMetaAnnot, defn.FieldMetaAnnot, defn.BinaryAPIAnnot))
174+
for binaryAPIAnnot <- binaryAPIAnnotOpt do sym.addAnnotation(binaryAPIAnnot)
172175
else
173176
sym.keepAnnotationsCarrying(thisPhase, Set(defn.GetterMetaAnnot, defn.FieldMetaAnnot), orNoneOf = defn.NonBeanMetaAnnots)
174177
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
@@ -526,6 +526,11 @@ object Checking {
526526
fail(em"Inline methods cannot be @tailrec")
527527
if sym.hasAnnotation(defn.TargetNameAnnot) && sym.isClass && sym.isTopLevelClass then
528528
fail(TargetNameOnTopLevelClass(sym))
529+
if sym.hasAnnotation(defn.BinaryAPIAnnot) then
530+
if sym.is(Enum) then fail(em"@binaryAPI cannot be used on enum definitions.")
531+
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")
532+
else if !sym.owner.isClass && !(sym.is(Param) && sym.owner.isConstructor) then fail(em"@binaryAPI cannot be used on local definitions.")
533+
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.")
529534
if (sym.hasAnnotation(defn.NativeAnnot)) {
530535
if (!sym.is(Deferred))
531536
fail(NativeMembersMayNotHaveImplementation(sym))

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,9 @@ trait TypeAssigner {
9999
val tpe1 = accessibleType(tpe, superAccess)
100100
if tpe1.exists then tpe1
101101
else tpe match
102-
case tpe: NamedType => inaccessibleErrorType(tpe, superAccess, pos)
102+
case tpe: NamedType =>
103+
if tpe.termSymbol.isBinaryAPI then tpe
104+
else inaccessibleErrorType(tpe, superAccess, pos)
103105
case NoType => tpe
104106

105107
/** Return a potentially skolemized version of `qualTpe` to be used

0 commit comments

Comments
 (0)