From 86587a3bf2732b233e12c6df5fda6c921af91ad6 Mon Sep 17 00:00:00 2001 From: Sergej Jaskiewicz Date: Fri, 2 Feb 2024 19:18:59 +0100 Subject: [PATCH] [JVM IR] Add KDocs for methods related to synthetic accessor generation --- .../jvm/lower/SyntheticAccessorLowering.kt | 110 +++++++++++++- .../jvm/CachedSyntheticDeclarations.kt | 16 +- .../jvm/ir/IrInlineReferenceLocator.kt | 142 ++++++++++++++---- 3 files changed, 228 insertions(+), 40 deletions(-) diff --git a/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/SyntheticAccessorLowering.kt b/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/SyntheticAccessorLowering.kt index 0f42b2c2952..f64e00c147b 100644 --- a/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/SyntheticAccessorLowering.kt +++ b/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/SyntheticAccessorLowering.kt @@ -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, : Int) { + * $this.myField = + * } + * + * internal inline fun foo(x: Int) { + * access$setMyField$p(this, x) + * } + * } + * ``` + */ private fun modifySetterExpression( oldExpression: IrSetField, accessorSymbol: IrSimpleFunctionSymbol diff --git a/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/CachedSyntheticDeclarations.kt b/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/CachedSyntheticDeclarations.kt index 4b730157d88..2b7e24564c0 100644 --- a/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/CachedSyntheticDeclarations.kt +++ b/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/CachedSyntheticDeclarations.kt @@ -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() fun getSyntheticFunctionAccessor(expression: IrFunctionAccessExpression, scopes: List): 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): IrFunctionSymbol { return createAccessor(reference.symbol, scopes, reference.dispatchReceiver?.type, null) } - private fun createAccessor(expression: IrFunctionAccessExpression, scopes: List): 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, diff --git a/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/ir/IrInlineReferenceLocator.kt b/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/ir/IrInlineReferenceLocator.kt index e40b3adf911..7065e85be93 100644 --- a/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/ir/IrInlineReferenceLocator.kt +++ b/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/ir/IrInlineReferenceLocator.kt @@ -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 { 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() - private val inlineFunctionCallSites = mutableMapOf>() + + /** + * For each private inline function stores the set of innermost scopes in which that private inline function is called. + */ + private val privateInlineFunctionCallSites = mutableMapOf>() 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) }