Skip to content

Commit 6ce6ab4

Browse files
committed
Add @publicInBinary annotation and -WunstableInlineAccessors
Introduces [SIP-52](scala/improvement-proposals#58) as experimental feature. 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. 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 `@publicInBinary` 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 `@publicInBinary`.
1 parent c88c0fe commit 6ce6ab4

33 files changed

+1283
-17
lines changed

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

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

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1060,6 +1060,7 @@ class Definitions {
10601060
@tu lazy val RequiresCapabilityAnnot: ClassSymbol = requiredClass("scala.annotation.internal.requiresCapability")
10611061
@tu lazy val RetainsAnnot: ClassSymbol = requiredClass("scala.annotation.retains")
10621062
@tu lazy val RetainsByNameAnnot: ClassSymbol = requiredClass("scala.annotation.retainsByName")
1063+
@tu lazy val PublicInBinaryAnnot: ClassSymbol = requiredClass("scala.annotation.publicInBinary")
10631064

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

@@ -1071,6 +1072,8 @@ class Definitions {
10711072
// A list of meta-annotations that are relevant for fields and accessors
10721073
@tu lazy val NonBeanMetaAnnots: Set[Symbol] =
10731074
Set(FieldMetaAnnot, GetterMetaAnnot, ParamMetaAnnot, SetterMetaAnnot, CompanionClassMetaAnnot, CompanionMethodMetaAnnot)
1075+
@tu lazy val NonBeanParamAccessorAnnots: Set[Symbol] =
1076+
Set(PublicInBinaryAnnot)
10741077
@tu lazy val MetaAnnots: Set[Symbol] =
10751078
NonBeanMetaAnnots + BeanGetterMetaAnnot + BeanSetterMetaAnnot
10761079

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

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

1034+
/** Is this a member that will become public in the generated binary */
1035+
def hasPublicInBinary(using Context): Boolean =
1036+
isTerm && (
1037+
hasAnnotation(defn.PublicInBinaryAnnot) ||
1038+
allOverriddenSymbols.exists(sym => sym.hasAnnotation(defn.PublicInBinaryAnnot))
1039+
)
1040+
10341041
/** ()T and => T types should be treated as equivalent for this symbol.
10351042
* Note: For the moment, we treat Scala-2 compiled symbols as loose matching,
10361043
* 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
@@ -22,6 +22,8 @@ import staging.CrossStageSafety
2222
import config.Printers.inlining
2323
import util.Property
2424
import staging.StagingLevel
25+
import dotty.tools.dotc.reporting.Message
26+
import dotty.tools.dotc.util.SrcPos
2527

2628
object PrepareInlineable {
2729
import tpd.*
@@ -71,6 +73,7 @@ object PrepareInlineable {
7173
sym.isTerm &&
7274
(sym.isOneOf(AccessFlags) || sym.privateWithin.exists) &&
7375
!sym.isContainedIn(inlineSym) &&
76+
!sym.hasPublicInBinary &&
7477
!(sym.isStableMember && sym.info.widenTermRefExpr.isInstanceOf[ConstantType]) &&
7578
!sym.isInlineMethod &&
7679
(Inlines.inInlineMethod || StagingLevel.level > 0)
@@ -86,6 +89,11 @@ object PrepareInlineable {
8689

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

9199
/** Direct approach: place the accessor with the accessed symbol. This has the
@@ -100,7 +108,11 @@ object PrepareInlineable {
100108
report.error("Implementation restriction: cannot use private constructors in inline methods", tree.srcPos)
101109
tree // TODO: create a proper accessor for the private constructor
102110
}
103-
else useAccessor(tree)
111+
else
112+
val accessor = useAccessor(tree)
113+
if tree != accessor then
114+
checkUnstableAccessor(tree, accessor.symbol)
115+
accessor
104116
case _ =>
105117
tree
106118
}
@@ -179,6 +191,8 @@ object PrepareInlineable {
179191
accessorInfo = abstractQualType(addQualType(dealiasMap(accessedType))),
180192
accessed = accessed)
181193

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

compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,14 +121,19 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
121121
else if homogenizedView then isEmptyPrefix(sym) // drop <root> and anonymous classes, but not scala, Predef.
122122
else if sym.isPackageObject then isOmittablePrefix(sym.owner)
123123
else isOmittablePrefix(sym)
124+
def isSkippedPackageObject(sym: Symbol) =
125+
sym.isPackageObject && !homogenizedView && !printDebug
124126

125127
tp.prefix match {
126-
case thisType: ThisType if isOmittable(thisType.cls) =>
127-
""
128-
case termRef @ TermRef(pre, _) =>
128+
case thisType: ThisType =>
129+
val sym = thisType.cls
130+
if isSkippedPackageObject(sym) then toTextPrefixOf(sym.typeRef)
131+
else if isOmittable(sym) then ""
132+
else super.toTextPrefixOf(tp)
133+
case termRef: TermRef =>
129134
val sym = termRef.symbol
130-
if sym.isPackageObject && !homogenizedView && !printDebug then toTextPrefixOf(termRef)
131-
else if (isOmittable(sym)) ""
135+
if isSkippedPackageObject(sym) then toTextPrefixOf(termRef)
136+
else if isOmittable(sym) then ""
132137
else super.toTextPrefixOf(tp)
133138
case _ => super.toTextPrefixOf(tp)
134139
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe
204204
case VarArgsParamCannotBeGivenID // errorNumber: 188
205205
case ExtractorNotFoundID // errorNumber: 189
206206
case PureUnitExpressionID // errorNumber: 190
207+
case UnstableInlineAccessorID // errorNumber: 191
207208

208209
def errorNumber = ordinal - 1
209210

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3087,3 +3087,40 @@ class ImplausiblePatternWarning(pat: tpd.Tree, selType: Type)(using Context)
30873087
|$pat could match selector of type $selType
30883088
|only if there is an `equals` method identifying elements of the two types."""
30893089
def explain(using Context) = ""
3090+
3091+
class UnstableInlineAccessor(accessed: Symbol, accessorTree: tpd.Tree)(using Context)
3092+
extends Message(UnstableInlineAccessorID) {
3093+
def kind = MessageKind.Compatibility
3094+
3095+
def msg(using Context) =
3096+
i"""Unstable inline accessor ${accessor.name} was generated in $where."""
3097+
3098+
def explain(using Context) =
3099+
i"""Access to non-public $accessed causes the automatic generation of an accessor.
3100+
|This accessor is not stable, its name may change or it may disappear
3101+
|if not needed in a future version.
3102+
|
3103+
|To make sure that the inlined code is binary compatible you must make sure that
3104+
|$accessed is public in the binary API.
3105+
| * Option 1: Annotate $accessed with @publicInBinary
3106+
| * Option 2: Make $accessed public
3107+
|
3108+
|This change may break binary compatibility if a previous version of this
3109+
|library was compiled with generated accessors. Binary compatibility should
3110+
|be checked using MiMa. If binary compatibility is broken, you should add the
3111+
|old accessor explicitly in the source code. The following code should be
3112+
|added to $where:
3113+
| @publicInBinary private[$within] ${accessorTree.show}
3114+
|"""
3115+
3116+
private def accessor = accessorTree.symbol
3117+
3118+
private def where =
3119+
if accessor.owner.name.isPackageObjectName then s"package ${within}"
3120+
else if accessor.owner.is(Module) then s"object $within"
3121+
else s"class $within"
3122+
3123+
private def within =
3124+
if accessor.owner.name.isPackageObjectName then accessor.owner.owner.name.stripModuleClassSuffix
3125+
else accessor.owner.name.stripModuleClassSuffix
3126+
}

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

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

5863
/** Add all needed accessors to the `body` of class `cls` */
5964
def addAccessorDefs(cls: Symbol, body: List[Tree])(using Context): List[Tree] = {
@@ -148,7 +153,7 @@ abstract class AccessProxies {
148153
def accessorIfNeeded(tree: Tree)(using Context): Tree = tree match {
149154
case tree: RefTree if needsAccessor(tree.symbol) =>
150155
if (tree.symbol.isConstructor) {
151-
report.error("Implementation restriction: cannot use private constructors in inlineable methods", tree.srcPos)
156+
report.error("Cannot use private constructors in inline methods. You can use @publicInBinary to make constructor accessible in inline methods.", tree.srcPos)
152157
tree // TODO: create a proper accessor for the private constructor
153158
}
154159
else useAccessor(tree)

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,10 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase =>
172172
if sym.is(Param) then
173173
sym.keepAnnotationsCarrying(thisPhase, Set(defn.ParamMetaAnnot), orNoneOf = defn.NonBeanMetaAnnots)
174174
else if sym.is(ParamAccessor) then
175+
// @publicInBinary is not a meta-annotation and therefore not kept by `keepAnnotationsCarrying`
176+
val publicInBinaryAnnotOpt = sym.getAnnotation(defn.PublicInBinaryAnnot)
175177
sym.keepAnnotationsCarrying(thisPhase, Set(defn.GetterMetaAnnot, defn.FieldMetaAnnot))
178+
for publicInBinaryAnnot <- publicInBinaryAnnotOpt do sym.addAnnotation(publicInBinaryAnnot)
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/transform/ProtectedAccessors.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ object ProtectedAccessors {
3737
* is not in a subclass or subtrait of `sym`?
3838
*/
3939
def needsAccessorIfNotInSubclass(sym: Symbol)(using Context): Boolean =
40-
sym.isTerm && sym.is(Protected) &&
40+
sym.isTerm && sym.is(Protected) && !sym.hasPublicInBinary &&
4141
!sym.owner.is(Trait) && // trait methods need to be handled specially, are currently always public
4242
!insideBoundaryOf(sym)
4343

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,12 @@ object Checking {
538538
fail(em"Inline methods cannot be @tailrec")
539539
if sym.hasAnnotation(defn.TargetNameAnnot) && sym.isClass && sym.isTopLevelClass then
540540
fail(TargetNameOnTopLevelClass(sym))
541+
if sym.hasAnnotation(defn.PublicInBinaryAnnot) then
542+
if sym.is(Enum) then fail(em"@publicInBinary cannot be used on enum definitions")
543+
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")
544+
else if !sym.owner.isClass && !(sym.is(Param) && sym.owner.isConstructor) then fail(em"@publicInBinary cannot be used on local definitions")
545+
else if sym.is(ParamAccessor) && sym.is(Private) then fail(em"@publicInBinary cannot be non `val` constructor parameters")
546+
else if sym.is(Private) && !sym.privateWithin.exists && !sym.isConstructor then fail(em"@publicInBinary cannot be used on private definitions\n\nConsider using `private[${sym.owner.name}]` or `protected` instead")
541547
if (sym.hasAnnotation(defn.NativeAnnot)) {
542548
if (!sym.is(Deferred))
543549
fail(NativeMembersMayNotHaveImplementation(sym))

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,10 @@ 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 =>
111+
if tpe.termSymbol.hasPublicInBinary && tpd.enclosingInlineds.nonEmpty then tpe
112+
else inaccessibleErrorType(tpe, superAccess, pos)
113+
case _ => tpe
112114

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

compiler/test-resources/repl/i15493

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,4 +146,4 @@ scala> Vector.unapplySeq(Vector(2))
146146
val res35: scala.collection.SeqFactory.UnapplySeqWrapper[Int] = scala.collection.SeqFactory$UnapplySeqWrapper@df507bfd
147147

148148
scala> new scala.concurrent.duration.DurationInt(5)
149-
val res36: scala.concurrent.duration.package.DurationInt = scala.concurrent.duration.package$DurationInt@5
149+
val res36: scala.concurrent.duration.DurationInt = scala.concurrent.duration.package$DurationInt@5

0 commit comments

Comments
 (0)