[JVM IR] Add KDocs for methods related to synthetic accessor generation

This commit is contained in:
Sergej Jaskiewicz
2024-02-02 19:18:59 +01:00
committed by Space Team
parent 524dd6794a
commit 86587a3bf2
3 changed files with 228 additions and 40 deletions
@@ -41,6 +41,19 @@ internal class SyntheticAccessorLowering(val context: JvmBackendContext) : FileL
}
companion object {
/**
* Whether `this` is accessible in [currentScope], according to the platform rules, and with respect to function inlining.
*
* @param context The backend context.
* @param currentScope The scope in which `this` is to be accessed.
* @param inlineScopeResolver The helper that allows to find the places from which private inline functions are called (useful if
* `this` is accessed from a private inline function).
* @param withSuper If an access to this symbol (like [IrCall]) has a `super` qualifier, the access rules will be stricter.
* @param thisObjReference If this is a member access, the class symbol of the receiver.
* @param fromOtherClassLoader If `this` is a protected declaration being accessed from the same package but not from a subclass,
* setting this parameter to `true` marks this declaration as inaccessible, since JVM `protected`, unlike Kotlin `protected`,
* permits accesses from the same package, _provided the call is not across class loader boundaries_.
*/
fun IrSymbol.isAccessible(
context: JvmBackendContext,
currentScope: ScopeWithIr?,
@@ -84,8 +97,6 @@ internal class SyntheticAccessorLowering(val context: JvmBackendContext) : FileL
return when {
jvmVisibility == Opcodes.ACC_PRIVATE -> ownerClass == scopeClassOrPackage
!withSuper && samePackage && jvmVisibility == 0 /* package only */ -> true
// JVM `protected`, unlike Kotlin `protected`, permits accesses from the same package,
// provided the call is not across class loader boundaries.
!withSuper && samePackage && !fromOtherClassLoader -> true
// Super calls and cross-package protected accesses are both only possible from a subclass of the declaration
// owner. Also, the target of a non-static call must be assignable to the current class. This is a verification
@@ -220,7 +231,7 @@ private class SyntheticAccessorTransformer(
override fun visitGetField(expression: IrGetField): IrExpression {
val dispatchReceiverType = expression.receiver?.type
val dispatchReceiverClassSymbol = dispatchReceiverType?.classifierOrNull as? IrClassSymbol
if (expression.symbol.isAccessible(false, dispatchReceiverClassSymbol)) {
if (expression.symbol.isAccessible(withSuper = false, dispatchReceiverClassSymbol)) {
return super.visitExpression(expression)
}
@@ -303,6 +314,45 @@ private class SyntheticAccessorTransformer(
return super.visitBlock(expression)
}
/**
* Produces a call to the synthetic accessor [accessorSymbol] to replace the call expression [oldExpression].
*
* Before:
* ```kotlin
* class C protected constructor(val value: Int) {
*
* protected fun protectedFun(a: Int): String = a.toString()
*
* internal inline fun foo(x: Int) {
* println(protectedFun(x))
* }
*
* internal inline fun copy(): C = C(value)
* }
* ```
*
* After:
* ```kotlin
* class C protected constructor(val value: Int) {
*
* public constructor(
* value: Int,
* constructor_marker: kotlin.jvm.internal.DefaultConstructorMarker?
* ) : this(value)
*
* protected fun protectedFun(a: Int): String = a.toString()
*
* public static fun access$protectedFun($this: C, a: Int): String =
* $this.protectedFun(a)
*
* internal inline fun foo(x: Int) {
* println(C.access$protectedFun(this, x))
* }
*
* internal inline fun copy(): C = C(value, null)
* }
* ```
*/
private fun modifyFunctionAccessExpression(
oldExpression: IrFunctionAccessExpression,
accessorSymbol: IrFunctionSymbol
@@ -342,6 +392,31 @@ private class SyntheticAccessorTransformer(
private fun createAccessorMarkerArgument() =
IrConstImpl.constNull(UNDEFINED_OFFSET, UNDEFINED_OFFSET, context.ir.symbols.defaultConstructorMarker.defaultType.makeNullable())
/**
* Produces a call to the synthetic accessor [accessorSymbol] to replace the field _read_ expression [oldExpression].
*
* Before:
* ```kotlin
* class C {
* protected /*field*/ val myField: Int
*
* internal inline fun foo(): Int = myField + 1
* }
* ```
*
* After:
* ```kotlin
* class C {
* protected /*field*/ val myField: Int
*
* public static fun access$getMyField$p($this: C): Int =
* $this.myField
*
* internal inline fun foo(): Int =
* C.access$getMyField$p(this) + 1
* }
* ```
*/
private fun modifyGetterExpression(
oldExpression: IrGetField,
accessorSymbol: IrSimpleFunctionSymbol
@@ -358,6 +433,35 @@ private class SyntheticAccessorTransformer(
return call
}
/**
* Produces a call to the synthetic accessor [accessorSymbol] to replace the field _write_ expression [oldExpression].
*
* Before:
* ```kotlin
* class C {
* protected var myField: Int = 0
*
* internal inline fun foo(x: Int) {
* myField = x
* }
* }
* ```
*
* After:
* ```kotlin
* class C {
* protected var myField: Int = 0
*
* public static fun access$setMyField$p($this: C, <set-?>: Int) {
* $this.myField = <set-?>
* }
*
* internal inline fun foo(x: Int) {
* access$setMyField$p(this, x)
* }
* }
* ```
*/
private fun modifySetterExpression(
oldExpression: IrSetField,
accessorSymbol: IrSimpleFunctionSymbol
@@ -28,6 +28,11 @@ import org.jetbrains.kotlin.name.Name
import org.jetbrains.org.objectweb.asm.Opcodes
import java.util.concurrent.ConcurrentHashMap
/**
* Generates visible synthetic accessor functions for symbols that are otherwise inaccessible, for example,
* when inlining a function that references a private method of a class outside of that class, or generating a class for a lambda
* expression that uses a `super` qualifier in its body.
*/
class CachedSyntheticDeclarations(private val context: JvmBackendContext) {
private data class FieldKey(val fieldSymbol: IrFieldSymbol, val parent: IrDeclarationParent, val superQualifierSymbol: IrClassSymbol?)
@@ -42,19 +47,16 @@ class CachedSyntheticDeclarations(private val context: JvmBackendContext) {
private val setterMap = ConcurrentHashMap<FieldKey, IrSimpleFunctionSymbol>()
fun getSyntheticFunctionAccessor(expression: IrFunctionAccessExpression, scopes: List<ScopeWithIr>): IrFunctionSymbol {
return createAccessor(expression, scopes)
return if (expression is IrCall)
createAccessor(expression.symbol, scopes, expression.dispatchReceiver?.type, expression.superQualifierSymbol)
else
createAccessor(expression.symbol, scopes, null, null)
}
fun getSyntheticFunctionAccessor(reference: IrFunctionReference, scopes: List<ScopeWithIr>): IrFunctionSymbol {
return createAccessor(reference.symbol, scopes, reference.dispatchReceiver?.type, null)
}
private fun createAccessor(expression: IrFunctionAccessExpression, scopes: List<ScopeWithIr>): IrFunctionSymbol =
if (expression is IrCall)
createAccessor(expression.symbol, scopes, expression.dispatchReceiver?.type, expression.superQualifierSymbol)
else
createAccessor(expression.symbol, scopes, null, null)
private fun createAccessor(
symbol: IrFunctionSymbol,
scopes: List<ScopeWithIr>,
@@ -14,10 +14,11 @@ import org.jetbrains.kotlin.descriptors.DescriptorVisibilities
import org.jetbrains.kotlin.ir.IrElement
import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.ir.expressions.*
import org.jetbrains.kotlin.ir.util.getPackageFragment
import org.jetbrains.kotlin.ir.visitors.IrElementVisitor
import org.jetbrains.kotlin.name.FqName
/**
* An IR visitor that also has a customization point for handling lambdas passed to inline functions (see [visitInlineLambda]).
*/
abstract class IrInlineReferenceLocator(private val context: JvmBackendContext) : IrElementVisitor<Unit, IrDeclaration?> {
override fun visitElement(element: IrElement, data: IrDeclaration?) =
element.acceptChildren(this, if (element is IrDeclaration && element !is IrVariable) element else data)
@@ -33,24 +34,69 @@ abstract class IrInlineReferenceLocator(private val context: JvmBackendContext)
return super.visitFunctionAccess(expression, data)
}
/**
* Called by this visitor whenever a lambda is passed to an inline function.
*
* @param argument The lambda expression passed as an argument to [callee].
* @param callee The inline function.
* @param parameter The parameter of [callee] to which the lambda is passed.
* @param scope The declaration in scope of which [callee] is being called.
*/
abstract fun visitInlineLambda(argument: IrFunctionReference, callee: IrFunction, parameter: IrValueParameter, scope: IrDeclaration)
}
/**
* An IR visitor that collects scopes in which inline functions/lambdas are called.
*
* Usage: call [findInlineCallSites] on an [IrFile], and then use [findContainer] to determine the class or package from which an inline
* function is accessible.
*/
class IrInlineScopeResolver(context: JvmBackendContext) : IrInlineReferenceLocator(context) {
private class CallSite(val parent: IrElement?, val approximateToPackage: Boolean)
/**
* Describes a scope in which an inline function is called.
*
* @property scope [IrDeclaration] or [IrDeclarationParent] in which a function is called
* @property approximateToPackage Whether the inline function being called can be inlined into some other class in the same package
* (for exmaple, if it's a crossinline lambda), thus forcing the [findContainer] to return that package instead of a class.
*/
private class CallSite(val scope: IrElement?, val approximateToPackage: Boolean)
/**
* For every private inline function/lambda stores the innermost _common_ scope in which it is called.
* For example, for function `foo` in the following code, the innermost common scope would be the class `Nested`.
* ```kotlin
* package com.example
*
* class C {
* private inline fun foo() = 42
*
* class Nested {
* private inline fun bar() = C().foo()
* private inline fun baz() = C().foo()
* }
* }
* ```
*
* Populated by [findContainer].
*/
private val inlineCallSites = mutableMapOf<IrFunction, CallSite>()
private val inlineFunctionCallSites = mutableMapOf<IrFunction, Set<IrElement>>()
/**
* For each private inline function stores the set of innermost scopes in which that private inline function is called.
*/
private val privateInlineFunctionCallSites = mutableMapOf<IrFunction, Set<IrDeclaration>>()
override fun visitInlineLambda(argument: IrFunctionReference, callee: IrFunction, parameter: IrValueParameter, scope: IrDeclaration) {
// suspendCoroutine and suspendCoroutineUninterceptedOrReturn accept crossinline lambdas to disallow non-local returns,
// but these lambdas are effectively inline
inlineCallSites[argument.symbol.owner] = CallSite(scope, parameter.isCrossinline && !callee.isCoroutineIntrinsic())
inlineCallSites[argument.symbol.owner] =
CallSite(scope, approximateToPackage = parameter.isCrossinline && !callee.isCoroutineIntrinsic())
}
override fun visitSimpleFunction(declaration: IrSimpleFunction, data: IrDeclaration?) {
if (declaration.isPrivateInline) {
inlineFunctionCallSites.putIfAbsent(declaration, mutableSetOf())
privateInlineFunctionCallSites.putIfAbsent(declaration, mutableSetOf())
}
super.visitSimpleFunction(declaration, data)
}
@@ -58,7 +104,7 @@ class IrInlineScopeResolver(context: JvmBackendContext) : IrInlineReferenceLocat
override fun visitCall(expression: IrCall, data: IrDeclaration?) {
val callee = expression.symbol.owner
if (callee.isPrivateInline && data != null) {
(inlineFunctionCallSites.getOrPut(callee) { mutableSetOf() } as MutableSet).add(data)
(privateInlineFunctionCallSites.getOrPut(callee) { mutableSetOf() } as MutableSet).add(data)
}
super.visitCall(expression, data)
}
@@ -67,7 +113,7 @@ class IrInlineScopeResolver(context: JvmBackendContext) : IrInlineReferenceLocat
if (expression is IrInlinedFunctionBlock && expression.isFunctionInlining()) {
val callee = expression.inlineDeclaration
if (callee is IrSimpleFunction && callee.isPrivateInline && data != null) {
(inlineFunctionCallSites.getOrPut(callee) { mutableSetOf() } as MutableSet).add(data)
(privateInlineFunctionCallSites.getOrPut(callee) { mutableSetOf() } as MutableSet).add(data)
}
}
@@ -80,14 +126,33 @@ class IrInlineScopeResolver(context: JvmBackendContext) : IrInlineReferenceLocat
private fun IrFunction.isCoroutineIntrinsic(): Boolean =
isBuiltInSuspendCoroutine() || isBuiltInSuspendCoroutineUninterceptedOrReturn()
/**
* The class from which all accesses in [scope] will be done after bytecode generation.
* If [scope] is a crossinline lambda, this is not possible, as the lambda may be inlined
* into some other class; in that case, return at least the package fragment.
*
* @param scope [IrDeclaration] or [IrDeclarationParent].
* @return [IrClass] or at least [IrPackageFragment] from which all accesses in the current scope will be done after bytecode
* generation, or `null` if the space of potential call sites is unconstrained, e.g. if [scope] is referenced from an internal inline
* function.
*/
fun findContainer(scope: IrElement): IrDeclarationContainer? =
findContainer(scope, approximateToPackage = false)
// Get the class from which all accesses in the current scope will be done after bytecode generation.
// If the current scope is a crossinline lambda, this is not possible, as the lambda maybe inlined
// into some other class; in that case, get at least the package.
private tailrec fun findContainer(context: IrElement?, approximateToPackage: Boolean): IrDeclarationContainer? {
val callSite = inlineCallSites[context]
/**
* The class from which all accesses in [scope] will be done after bytecode generation.
* If [scope] is a crossinline lambda, this is not possible, as the lambda may be inlined
* into some other class; in that case, return at least the package fragment.
*
* @param scope [IrDeclaration] or [IrDeclarationParent]
* @param approximateToPackage Whether the inline function being called can be inlined into some other class in the same package,
* for example, if it's a crossinline lambda.
* @return [IrClass] or at least [IrPackageFragment] from which all accesses in the current scope will be done after bytecode
* generation, or `null` if the space of potential call sites is unconstrained, e.g. if [scope] is referenced from an internal inline
* function.
*/
private tailrec fun findContainer(scope: IrElement?, approximateToPackage: Boolean): IrDeclarationContainer? {
val callSite = inlineCallSites[scope]
return when {
// Crossinline lambdas can be inlined into some other class in the same package. However,
// classes within crossinline lambdas should not be regenerated, so if we've already found
@@ -99,7 +164,7 @@ class IrInlineScopeResolver(context: JvmBackendContext) : IrInlineReferenceLocat
// object { val x = f() } // this call is done in C$g$1$1
// }
// }
callSite != null -> findContainer(callSite.parent, approximateToPackage || callSite.approximateToPackage)
callSite != null -> findContainer(callSite.scope, approximateToPackage || callSite.approximateToPackage)
// Inline functions can be inlined into anywhere. Not even private inline functions are safe:
// class C {
// fun f() {}
@@ -113,25 +178,25 @@ class IrInlineScopeResolver(context: JvmBackendContext) : IrInlineReferenceLocat
// TODO: this has some weird effects for inline functions in local classes, e.g. they
// access the capture fields (package-private) through accessors; this may or may not
// be necessary - local types should in theory not be usable outside the current file.
context is IrFunction && context.isInline -> {
val callSites = inlineFunctionCallSites[context] ?: return null
scope is IrFunction && scope.isInline -> {
val callSites = privateInlineFunctionCallSites[scope] ?: return null
// Mark to avoid infinite recursion on self-recursive inline functions (those are only
// detected reliably by codegen; frontend only filters out simple cases).
inlineCallSites[context] = CallSite(null, approximateToPackage = false)
inlineCallSites[scope] = CallSite(scope = null, approximateToPackage = false)
val commonCallSite = when {
callSites.isEmpty() -> CallSite(context.parent, false)
callSites.size == 1 -> CallSite(callSites.single(), false)
callSites.isEmpty() -> CallSite(scope.parent, approximateToPackage = false)
callSites.size == 1 -> CallSite(callSites.single(), approximateToPackage = false)
else -> {
@Suppress("NON_TAIL_RECURSIVE_CALL")
val results = callSites.map { findContainer(it, approximateToPackage = false) ?: return null }
// If all call sites are within a single class, use it. Otherwise, all scopes must be within
// the current file's package.
val single = results.first().takeIf { results.all { other -> it === other } }
CallSite(single ?: context.parent, single == null)
CallSite(single ?: scope.parent, approximateToPackage = single == null)
}
}
inlineCallSites[context] = commonCallSite
findContainer(commonCallSite.parent, approximateToPackage || commonCallSite.approximateToPackage)
inlineCallSites[scope] = commonCallSite
findContainer(commonCallSite.scope, approximateToPackage || commonCallSite.approximateToPackage)
}
// TODO: if this class is an object local to an inline function, it could be regenerated,
// so the scope depends on the declaration accessed (see KT-48508):
@@ -147,22 +212,39 @@ class IrInlineScopeResolver(context: JvmBackendContext) : IrInlineReferenceLocat
// }
// Further complicating things, the accessor for `f1` cannot be in `C$inlineFun$1`, as otherwise
// the accessor itself will be regenerated (and thus not work) at `inlineFun` call sites.
context is IrClass && !approximateToPackage -> context
scope is IrClass && !approximateToPackage -> scope
// Inline lambdas have already been moved out to the containing class, but we still need to check
// the containing function (again, see above), so navigate there instead.
context is IrDeclaration -> findContainer(context.parent, approximateToPackage)
scope is IrDeclaration -> findContainer(scope.parent, approximateToPackage)
// The only non-declaration parent should be the package.
else -> context as? IrPackageFragment
else -> scope as? IrPackageFragment
}
}
}
/**
* Calls [onLambda] for each place in the IR subtree where a lambda is passed to an inline function.
*
* @param context The backend context
* @param onLambda The closure to execute for each such lambda. Accepts the lambda expression, the inline function being called,
* the parameter of that inline function to which the lambda is passed, and the scope in which the inline function is called.
*/
inline fun IrFile.findInlineLambdas(
context: JvmBackendContext, crossinline onLambda: (IrFunctionReference, IrFunction, IrValueParameter, IrDeclaration) -> Unit
) = accept(object : IrInlineReferenceLocator(context) {
override fun visitInlineLambda(argument: IrFunctionReference, callee: IrFunction, parameter: IrValueParameter, scope: IrDeclaration) =
onLambda(argument, callee, parameter, scope)
}, null)
context: JvmBackendContext, crossinline onLambda: (IrFunctionReference, IrFunction, IrValueParameter, IrDeclaration) -> Unit,
) = accept(
object : IrInlineReferenceLocator(context) {
override fun visitInlineLambda(
argument: IrFunctionReference,
callee: IrFunction,
parameter: IrValueParameter,
scope: IrDeclaration,
) = onLambda(argument, callee, parameter, scope)
},
null,
)
/**
* Runs [IrInlineScopeResolver] on this [IrFile] and returns the scope resolver instance.
*/
fun IrFile.findInlineCallSites(context: JvmBackendContext) =
IrInlineScopeResolver(context).apply { accept(this, null) }