Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add @binaryAPI and @binaryAPIAccessor #16992

Closed
Closed
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import dotty.tools.dotc.core.Contexts.Context
import dotty.tools.dotc.report
import dotty.tools.dotc.core.Phases

import scala.annotation.binaryAPIAccessor

/**
* Functionality needed in the post-processor whose implementation depends on the compiler
* frontend. All methods are synchronized.
Expand All @@ -20,6 +22,7 @@ sealed abstract class PostProcessorFrontendAccess {
def backendReporting: BackendReporting
def getEntryPoints: List[String]

@binaryAPIAccessor
private val frontendLock: AnyRef = new Object()
inline final def frontendSynch[T](inline x: => T): T = frontendLock.synchronized(x)
}
Expand Down
3 changes: 2 additions & 1 deletion compiler/src/dotty/tools/dotc/Compiler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ class Compiler {
new ElimRepeated, // Rewrite vararg parameters and arguments
new RefChecks) :: // Various checks mostly related to abstract members and overriding
List(new init.Checker) :: // Check initialization of objects
List(new ProtectedAccessors, // Add accessors for protected members
List(new BinaryAPIAnnotations, // Makes @binaryAPI definitions public
new ProtectedAccessors, // Add accessors for protected members
new ExtensionMethods, // Expand methods of value classes with extension methods
new UncacheGivenAliases, // Avoid caching RHS of simple parameterless given aliases
new ElimByName, // Map by-name parameters to functions
Expand Down
9 changes: 6 additions & 3 deletions compiler/src/dotty/tools/dotc/core/ConstraintHandling.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import NameKinds.AvoidNameKind
import util.SimpleIdentitySet
import NullOpsDecorator.stripNull

import scala.annotation.{binaryAPI, binaryAPIAccessor}

/** Methods for adding constraints and solving them.
*
* What goes into a Constraint as opposed to a ConstrainHandler?
Expand All @@ -39,12 +41,12 @@ trait ConstraintHandling {
private var addConstraintInvocations = 0

/** If the constraint is frozen we cannot add new bounds to the constraint. */
protected var frozenConstraint: Boolean = false
@binaryAPI protected var frozenConstraint: Boolean = false

/** Potentially a type lambda that is still instantiatable, even though the constraint
* is generally frozen.
*/
protected var caseLambda: Type = NoType
@binaryAPI protected var caseLambda: Type = NoType

/** If set, align arguments `S1`, `S2`when taking the glb
* `T1 { X = S1 } & T2 { X = S2 }` of a constraint upper bound for some type parameter.
Expand All @@ -56,7 +58,7 @@ trait ConstraintHandling {
/** We are currently comparing type lambdas. Used as a flag for
* optimization: when `false`, no need to do an expensive `pruneLambdaParams`
*/
protected var comparedTypeLambdas: Set[TypeLambda] = Set.empty
@binaryAPI protected var comparedTypeLambdas: Set[TypeLambda] = Set.empty

/** Used for match type reduction: If false, we don't recognize an abstract type
* to be a subtype type of any of its base classes. This is in place only at the
Expand Down Expand Up @@ -110,6 +112,7 @@ trait ConstraintHandling {
* of `1`. So the lower bound is `1 | x.M` and when we level-avoid that we
* get `1 | Int & String`, which simplifies to `Int`.
*/
@binaryAPIAccessor
private var myTrustBounds = true

inline def withUntrustedBounds(op: => Type): Type =
Expand Down
6 changes: 4 additions & 2 deletions compiler/src/dotty/tools/dotc/core/Contexts.scala
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import classfile.ReusableDataReader
import StdNames.nme
import compiletime.uninitialized

import scala.annotation.{binaryAPI, binaryAPIAccessor}
import scala.annotation.internal.sharable

import DenotTransformers.DenotTransformer
Expand Down Expand Up @@ -805,6 +806,7 @@ object Contexts {
* Note: plain TypeComparers always take on the kind of the outer comparer if they are in the same context.
* In other words: tracking or explaining is a sticky property in the same context.
*/
@binaryAPIAccessor
private def comparer(using Context): TypeComparer =
util.Stats.record("comparing")
val base = ctx.base
Expand Down Expand Up @@ -980,7 +982,7 @@ object Contexts {
private[core] var phasesPlan: List[List[Phase]] = uninitialized

/** Phases by id */
private[dotc] var phases: Array[Phase] = uninitialized
@binaryAPI private[dotc] var phases: Array[Phase] = uninitialized

/** Phases with consecutive Transforms grouped into a single phase, Empty array if fusion is disabled */
private[core] var fusedPhases: Array[Phase] = Array.empty[Phase]
Expand Down Expand Up @@ -1019,7 +1021,7 @@ object Contexts {
val generalContextPool = ContextPool()

private[Contexts] val comparers = new mutable.ArrayBuffer[TypeComparer]
private[Contexts] var comparersInUse: Int = 0
@binaryAPI private[Contexts] var comparersInUse: Int = 0

private var charArray = new Array[Char](256)

Expand Down
2 changes: 2 additions & 0 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1039,6 +1039,8 @@ class Definitions {
@tu lazy val RequiresCapabilityAnnot: ClassSymbol = requiredClass("scala.annotation.internal.requiresCapability")
@tu lazy val RetainsAnnot: ClassSymbol = requiredClass("scala.annotation.retains")
@tu lazy val RetainsByNameAnnot: ClassSymbol = requiredClass("scala.annotation.retainsByName")
@tu lazy val BinaryAPIAnnot: ClassSymbol = requiredClass("scala.annotation.binaryAPI")
@tu lazy val BinaryAPIAccessorAnnot: ClassSymbol = requiredClass("scala.annotation.binaryAPIAccessor")

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

Expand Down
11 changes: 11 additions & 0 deletions compiler/src/dotty/tools/dotc/core/SymDenotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1038,6 +1038,17 @@ object SymDenotations {
isOneOf(EffectivelyErased)
|| is(Inline) && !isRetainedInline && !hasAnnotation(defn.ScalaStaticAnnot)

/** Is this a member that will become public in the generated binary */
def isBinaryAPI(using Context): Boolean =
isTerm && (
hasAnnotation(defn.BinaryAPIAnnot) ||
allOverriddenSymbols.exists(sym => sym.hasAnnotation(defn.BinaryAPIAnnot))
)

/** Is this a member that will have an accessor in the generated binary */
def isBinaryAPIAccessor(using Context): Boolean =
isTerm && hasAnnotation(defn.BinaryAPIAccessorAnnot)

/** ()T and => T types should be treated as equivalent for this symbol.
* Note: For the moment, we treat Scala-2 compiled symbols as loose matching,
* because the Scala library does not always follow the right conventions.
Expand Down
9 changes: 6 additions & 3 deletions compiler/src/dotty/tools/dotc/core/TypeComparer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import reporting.trace
import annotation.constructorOnly
import cc.{CapturingType, derivedCapturingType, CaptureSet, stripCapturing, isBoxedCapturing, boxed, boxedUnlessFun, boxedIfTypeParam, isAlwaysPure}

import scala.annotation.{binaryAPI, binaryAPIAccessor}

/** Provides methods to compare types.
*/
class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling, PatternTypeConstrainer {
Expand All @@ -34,7 +36,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
private var myContext: Context = initctx
def comparerContext: Context = myContext

protected given [DummySoItsADef]: Context = myContext
@binaryAPI protected given [DummySoItsADef]: Context = myContext

protected var state: TyperState = compiletime.uninitialized
def constraint: Constraint = state.constraint
Expand Down Expand Up @@ -115,7 +117,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling

private def isBottom(tp: Type) = tp.widen.isRef(NothingClass)

protected def gadtBounds(sym: Symbol)(using Context) = ctx.gadt.bounds(sym)
@binaryAPI protected def gadtBounds(sym: Symbol)(using Context) = ctx.gadt.bounds(sym)
protected def gadtAddBound(sym: Symbol, b: Type, isUpper: Boolean): Boolean = ctx.gadtState.addBound(sym, b, isUpper)

protected def typeVarInstance(tvar: TypeVar)(using Context): Type = tvar.underlying
Expand Down Expand Up @@ -156,6 +158,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
private [this] var leftRoot: Type | Null = null

/** Are we forbidden from recording GADT constraints? */
@binaryAPIAccessor
private var frozenGadt = false
private inline def inFrozenGadt[T](inline op: T): T =
inFrozenGadtIf(true)(op)
Expand Down Expand Up @@ -187,7 +190,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
inFrozenGadtIf(true)(inFrozenConstraint(op))

extension (sym: Symbol)
private inline def onGadtBounds(inline op: TypeBounds => Boolean): Boolean =
@binaryAPIAccessor private inline def onGadtBounds(inline op: TypeBounds => Boolean): Boolean =
val bounds = gadtBounds(sym)
bounds != null && op(bounds)

Expand Down
3 changes: 2 additions & 1 deletion compiler/src/dotty/tools/dotc/core/Types.scala
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import compiletime.uninitialized
import cc.{CapturingType, CaptureSet, derivedCapturingType, isBoxedCapturing, EventuallyCapturingType, boxedUnlessFun}
import CaptureSet.{CompareResult, IdempotentCaptRefMap, IdentityCaptRefMap}

import scala.annotation.binaryAPI
import scala.annotation.internal.sharable
import scala.annotation.threadUnsafe

Expand Down Expand Up @@ -5542,7 +5543,7 @@ object Types {

/** Common base class of TypeMap and TypeAccumulator */
abstract class VariantTraversal:
protected[dotc] var variance: Int = 1
@binaryAPI protected[dotc] var variance: Int = 1

inline protected def atVariance[T](v: Int)(op: => T): T = {
val saved = variance
Expand Down
97 changes: 81 additions & 16 deletions compiler/src/dotty/tools/dotc/inlines/PrepareInlineable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import transform.{AccessProxies, Splicer}
import staging.CrossStageSafety
import transform.SymUtils.*
import config.Printers.inlining
import util.SrcPos
import util.Property
import staging.StagingLevel

Expand All @@ -35,6 +36,9 @@ object PrepareInlineable {
def makeInlineable(tree: Tree)(using Context): Tree =
ctx.property(InlineAccessorsKey).get.makeInlineable(tree)

def makePrivateBinaryAPIAccessor(sym: Symbol)(using Context): Unit =
ctx.property(InlineAccessorsKey).get.makePrivateBinaryAPIAccessor(sym)

def addAccessorDefs(cls: Symbol, body: List[Tree])(using Context): List[Tree] =
ctx.property(InlineAccessorsKey) match
case Some(inlineAccessors) => inlineAccessors.addAccessorDefs(cls, body)
Expand All @@ -51,12 +55,12 @@ object PrepareInlineable {
case _ => false
}

/** A tree map which inserts accessors for non-public term members accessed from inlined code.
*/
abstract class MakeInlineableMap(val inlineSym: Symbol) extends TreeMap with Insert {
def accessorNameOf(name: TermName, site: Symbol)(using Context): TermName =
val accName = InlineAccessorName(name)
if site.isExtensibleClass then accName.expandedName(site) else accName
trait InsertInlineAccessors extends Insert {

def accessorNameOf(accessed: Symbol, site: Symbol)(using Context): TermName =
val accName = InlineAccessorName(accessed.name.asTermName)
if site.isExtensibleClass || (accessed.isBinaryAPIAccessor && !site.is(Module)) then accName.expandedName(site)
else accName

/** A definition needs an accessor if it is private, protected, or qualified private
* and it is not part of the tree that gets inlined. The latter test is implemented
Expand All @@ -71,10 +75,20 @@ object PrepareInlineable {
def needsAccessor(sym: Symbol)(using Context): Boolean =
sym.isTerm &&
(sym.isOneOf(AccessFlags) || sym.privateWithin.exists) &&
!sym.isContainedIn(inlineSym) &&
!sym.isBinaryAPI &&
!(sym.isStableMember && sym.info.widenTermRefExpr.isInstanceOf[ConstantType]) &&
!sym.isInlineMethod &&
(Inlines.inInlineMethod || StagingLevel.level > 0)
}

object InsertPrivateBinaryAPIAccessors extends InsertInlineAccessors

/** A tree map which inserts accessors for non-public term members accessed from inlined code.
*/
abstract class MakeInlineableMap(val inlineSym: Symbol) extends TreeMap with InsertInlineAccessors {

override def needsAccessor(sym: Symbol)(using Context): Boolean =
!sym.isContainedIn(inlineSym) && super.needsAccessor(sym)

def preTransform(tree: Tree)(using Context): Tree

Expand All @@ -87,6 +101,41 @@ object PrepareInlineable {

override def transform(tree: Tree)(using Context): Tree =
postTransform(super.transform(preTransform(tree)))

protected def unstableAccessorWarning(accessor: Symbol, accessed: Symbol, srcPos: SrcPos)(using Context): Unit =
val accessorDefTree = accessorDef(accessor, accessed)
val annot = if accessed.is(Private) then "@binaryAPIAccessor" else "@binaryAPI"
val solution =
if accessed.is(Private) then
s"Annotate ${accessed.name} with `$annot` to generate a stable accessor."
else
s"Annotate ${accessed.name} with `$annot` to make it accessible."

val binaryCompat =
val accessorClass = AccessProxies.hostForAccessorOf(accessed: Symbol)
val inlineAccessorMatches =
def binaryAccessorName =
if accessed.owner.is(Module) then InlineAccessorName(accessed.name.asTermName)
else InlineAccessorName(accessed.name.asTermName).expandedName(accessorClass)
accessor.owner == accessed.owner && accessor.name == binaryAccessorName
if !inlineAccessorMatches then
val within =
if accessor.owner.name.isPackageObjectName then accessor.owner.owner.name.stripModuleClassSuffix
else accessor.owner.name.stripModuleClassSuffix
s"""Adding $annot may break binary compatibility if a previous version of this
|library was compiled with Scala 3.0-3.3, Binary compatibility should be checked
|using MiMa. To keep binary compatibility you can add the following accessor to ${accessor.owner.showKind} ${accessor.owner.name.stripModuleClassSuffix}:
| @binaryAPI private[$within] ${accessorDefTree.show}
|
|""".stripMargin
else if !accessed.is(Private) then
s"""Adding $annot may break binary compatibility if a previous version of this
|library was compiled with Scala 3.0-3.3, Binary compatibility should be checked
|using MiMa. To keep binary compatibility you can use @binaryAPIAccessor on
|$accessed.""".stripMargin
else
""
report.warning(em"Generated unstable inline accessor for $accessed defined in ${accessed.owner}.\n\n$solution\n\n$binaryCompat", srcPos)
}

/** Direct approach: place the accessor with the accessed symbol. This has the
Expand All @@ -101,7 +150,11 @@ object PrepareInlineable {
report.error("Implementation restriction: cannot use private constructors in inline methods", tree.srcPos)
tree // TODO: create a proper accessor for the private constructor
}
else useAccessor(tree)
else
val accessorTree = useAccessor(tree)
if !tree.symbol.isBinaryAPI && !tree.symbol.isBinaryAPIAccessor && tree.symbol != accessorTree.symbol then
unstableAccessorWarning(accessorTree.symbol, tree.symbol, tree.srcPos)
accessorTree
case _ =>
tree
}
Expand All @@ -118,13 +171,14 @@ object PrepareInlineable {
* private[inlines] def next[U](y: U): (T, U) = (x, y)
* }
* class TestPassing {
* inline def foo[A](x: A): (A, Int) = {
* val c = new C[A](x)
* c.next(1)
* }
* inline def bar[A](x: A): (A, String) = {
* val c = new C[A](x)
* c.next("")
* inline def foo[A](x: A): (A, Int) = {
* val c = new C[A](x)
* c.next(1)
* }
* inline def bar[A](x: A): (A, String) = {
* val c = new C[A](x)
* c.next("")
* }
* }
*
* `C` could be compiled separately, so we cannot place the inline accessor in it.
Expand Down Expand Up @@ -185,7 +239,9 @@ object PrepareInlineable {
localRefs.map(TypeTree(_)) ++ leadingTypeArgs, // TODO: pass type parameters in two sections?
(qual :: Nil) :: otherArgss
)
ref(accessor).appliedToArgss(argss1).withSpan(tree.span)
val accessorTree = ref(accessor).appliedToArgss(argss1).withSpan(tree.span)

unstableAccessorWarning(accessorTree.symbol, tree.symbol, tree.srcPos)

// TODO: Handle references to non-public types.
// This is quite tricky, as such types can appear anywhere, including as parts
Expand All @@ -197,6 +253,7 @@ object PrepareInlineable {
// myAccessors += TypeDef(accessor).withPos(tree.pos.focus)
// ref(accessor).withSpan(tree.span)
//
accessorTree
case _: TypeDef if tree.symbol.is(Case) =>
report.error(reporting.CaseClassInInlinedCode(tree), tree)
tree
Expand All @@ -205,6 +262,14 @@ object PrepareInlineable {
}
}

/** Create an inline accessor for this definition. */
def makePrivateBinaryAPIAccessor(sym: Symbol)(using Context): Unit =
if !sym.is(Accessor) && sym.owner.isClass then
val ref = tpd.ref(sym).asInstanceOf[RefTree]
val accessor = InsertPrivateBinaryAPIAccessors.useAccessor(ref)
if sym.is(Mutable) then
InsertPrivateBinaryAPIAccessors.useSetter(accessor)

/** Adds accessors for all non-public term members accessed
* from `tree`. Non-public type members are currently left as they are.
* This means that references to a private type will lead to typing failures
Expand Down
3 changes: 3 additions & 0 deletions compiler/src/dotty/tools/dotc/parsing/Scanners.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import config.Feature.{migrateTo3, fewerBracesEnabled}
import config.SourceVersion.`3.0`
import reporting.{NoProfile, Profile, Message}

import scala.annotation.binaryAPIAccessor

import java.util.Objects

object Scanners {
Expand Down Expand Up @@ -1597,6 +1599,7 @@ object Scanners {
protected def coversIndent(w: IndentWidth): Boolean =
knownWidth != null && w == indentWidth

@binaryAPIAccessor
private var myCommasExpected: Boolean = false

inline def withCommasExpected[T](inline op: => T): T =
Expand Down
Loading