JVM_IR: read inline fun bytecode to detect unused property references

I.e. optimize `$$delegatedProperties` in the same way as the old
backend. This is more reliable than trying to match bytecode.
This commit is contained in:
pyos
2021-06-21 13:43:19 +02:00
committed by Dmitry Petrov
parent bd6d96114b
commit ee4d9a89ea
6 changed files with 71 additions and 378 deletions
@@ -7,7 +7,7 @@ package org.jetbrains.kotlin.codegen
import org.jetbrains.kotlin.codegen.binding.CodegenBinding
import org.jetbrains.kotlin.codegen.inline.loadCompiledInlineFunction
import org.jetbrains.kotlin.codegen.optimization.nullCheck.isCheckParameterIsNotNull
import org.jetbrains.kotlin.codegen.optimization.nullCheck.usesLocalExceptParameterNullCheck
import org.jetbrains.kotlin.codegen.state.GenerationState
import org.jetbrains.kotlin.codegen.state.KotlinTypeMapper
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
@@ -17,10 +17,6 @@ import org.jetbrains.kotlin.resolve.FunctionImportedFromObject
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall
import org.jetbrains.kotlin.resolve.jvm.requiresFunctionNameManglingForReturnType
import org.jetbrains.kotlin.serialization.deserialization.descriptors.DescriptorWithContainerSource
import org.jetbrains.org.objectweb.asm.Opcodes
import org.jetbrains.org.objectweb.asm.tree.AbstractInsnNode
import org.jetbrains.org.objectweb.asm.tree.MethodNode
import org.jetbrains.org.objectweb.asm.tree.VarInsnNode
class DelegatedPropertiesCodegenHelper(private val state: GenerationState) {
@@ -75,20 +71,7 @@ class DelegatedPropertiesCodegenHelper(private val state: GenerationState) {
val asmMethod = state.typeMapper.mapAsmMethod(calleeDescriptor)
val isMangled = requiresFunctionNameManglingForReturnType(calleeDescriptor)
val methodNode = loadCompiledInlineFunction(containerId, asmMethod, calleeDescriptor.isSuspend, isMangled, state).node
return isMetadataParameterUsedInCompiledMethodBody(metadataParameterIndex, methodNode)
}
private fun isMetadataParameterUsedInCompiledMethodBody(metadataParameterIndex: Int, methodNode: MethodNode): Boolean =
methodNode.instructions.toArray().any { insn ->
insn is VarInsnNode && insn.opcode == Opcodes.ALOAD && insn.`var` == metadataParameterIndex &&
!isParameterNullCheckArgument(insn)
}
private fun isParameterNullCheckArgument(insn: AbstractInsnNode): Boolean {
val next1 = insn.next
val next2 = next1.next
return next1 != null && next2 != null &&
next1.opcode == Opcodes.LDC && next2.isCheckParameterIsNotNull()
return methodNode.usesLocalExceptParameterNullCheck(metadataParameterIndex)
}
private fun getMetadataParameterIndex(calleeDescriptor: FunctionDescriptor): Int {
@@ -436,6 +436,14 @@ internal fun AbstractInsnNode.isCheckNotNull() =
desc == "(Ljava/lang/Object;)V"
}
fun MethodNode.usesLocalExceptParameterNullCheck(index: Int): Boolean =
instructions.toArray().any {
it is VarInsnNode && it.opcode == Opcodes.ALOAD && it.`var` == index && !it.isParameterCheckedForNull()
}
internal fun AbstractInsnNode.isParameterCheckedForNull(): Boolean =
next?.takeIf { it.opcode == Opcodes.LDC }?.next?.isCheckParameterIsNotNull() == true
internal fun AbstractInsnNode.isCheckParameterIsNotNull() =
isInsn<MethodInsnNode>(Opcodes.INVOKESTATIC) {
owner == IntrinsicMethods.INTRINSICS_CLASS_NAME &&
@@ -120,9 +120,6 @@ class ClassCodegen private constructor(
if (generated) return
generated = true
// We remove reads of `$$delegatedProperties` (and the field itself) if they are not in fact used for anything.
val delegatedProperties = irClass.fields.singleOrNull { it.origin == JvmLoweredDeclarationOrigin.GENERATED_PROPERTY_REFERENCE }
val delegatedPropertyOptimizer = if (delegatedProperties != null) DelegatedPropertyOptimizer() else null
// Generating a method node may cause the addition of a field with an initializer if an inline function
// call uses `assert` and the JVM assertions mode is enabled. To avoid concurrent modification errors,
// there is a very specific generation order.
@@ -130,19 +127,14 @@ class ClassCodegen private constructor(
// 1. Any method other than `<clinit>` can add a field and a `<clinit>` statement:
for (method in irClass.declarations.filterIsInstance<IrFunction>()) {
if (method.name.asString() != "<clinit>") {
generateMethod(method, smap, delegatedPropertyOptimizer)
generateMethod(method, smap)
}
}
// 2. `<clinit>` itself can add a field, but the statement is generated via the `return init` hack:
irClass.functions.find { it.name.asString() == "<clinit>" }?.let { generateMethod(it, smap, delegatedPropertyOptimizer) }
// 3. Now we have all the fields (`$$delegatedProperties` might be redundant if all reads were optimized out):
irClass.functions.find { it.name.asString() == "<clinit>" }?.let { generateMethod(it, smap) }
// 3. Now we have all the fields, including `$assertionsDisabled` if needed:
for (field in irClass.fields) {
if (field !== delegatedProperties ||
delegatedPropertyOptimizer?.needsDelegatedProperties == true ||
irClass.isCompanion
) {
generateField(field)
}
generateField(field)
}
// 4. Generate nested classes at the end, to ensure that when the companion's metadata is serialized
// everything moved to the outer class has already been recorded in `globalSerializationBindings`.
@@ -357,19 +349,13 @@ class ClassCodegen private constructor(
return SMAPAndMethodNode(cloneMethodNode(node), smap)
}
private fun generateMethod(method: IrFunction, classSMAP: SourceMapper, delegatedPropertyOptimizer: DelegatedPropertyOptimizer?) {
private fun generateMethod(method: IrFunction, classSMAP: SourceMapper) {
if (method.isFakeOverride) {
jvmSignatureClashDetector.trackFakeOverrideMethod(method)
return
}
val (node, smap) = generateMethodNode(method)
if (delegatedPropertyOptimizer != null) {
delegatedPropertyOptimizer.transform(node)
if (method.name.asString() == "<clinit>" && !irClass.isCompanion) {
delegatedPropertyOptimizer.transformClassInitializer(node)
}
}
node.preprocessSuspendMarkers(
method.origin == JvmLoweredDeclarationOrigin.FOR_INLINE_STATE_MACHINE_TEMPLATE || method.isEffectivelyInlineOnly(),
method.origin == JvmLoweredDeclarationOrigin.FOR_INLINE_STATE_MACHINE_TEMPLATE_CAPTURES_CROSSINLINE
@@ -1,338 +0,0 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.backend.jvm.codegen
import org.jetbrains.kotlin.codegen.optimization.common.asSequence
import org.jetbrains.kotlin.codegen.optimization.common.isMeaningful
import org.jetbrains.kotlin.load.java.JvmAbi
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
import org.jetbrains.org.objectweb.asm.Opcodes
import org.jetbrains.org.objectweb.asm.tree.*
class DelegatedPropertyOptimizer {
private var usedDelegatedProperties = mutableSetOf<Int>()
private var useOfDelegatesPropertiesArray = false
val needsDelegatedProperties: Boolean
get() = useOfDelegatesPropertiesArray || usedDelegatedProperties.isNotEmpty()
// Remove unused loads of cached KProperties from the given method and record uses of cached KProperties.
fun transform(methodNode: MethodNode) {
class KPropertyRanges(val propertyIndex: Int, val ranges: MutableList<Pair<AbstractInsnNode, AbstractInsnNode>> = mutableListOf())
val variableToDelegatedPropertyIndex = mutableMapOf<Int, KPropertyRanges>()
// We look for bytecode patterns of the form
//
// getstatic $$delegatedProperties:[Lkotlin/reflect/KProperty;
// iconst <n>
// aaload
// (checkcast ...)
// astore <x>
//
// If the variable `x` is used, we mark the delegated property at index `n` as used,
// otherwise we delete the code in question. This is the pattern that results from an
// inline call with a cached KProperty argument. If the `astore` instruction is missing
// then that's a different use of the KProperty at index `n` and we mark it as used.
// Finally, if there is any other code which loads the `$$delegatedProperties` array,
// we mark it as generally unsafe to remove.
for (insn in methodNode.instructions) {
if (!insn.isDelegatedPropertiesArray)
continue
var index = 0
var current = matchRange(insn) {
index = iconst()
aaload()
}
if (current == null) {
// There's an unknown use of the $$delegatedProperties array, we need to ensure that we don't remove it.
useOfDelegatesPropertiesArray = true
return
}
var slot = 0
current = matchRange(current) {
slot = astore()
}
if (current == null) {
// There's an actual use of the delegated property at `index`, mark it.
usedDelegatedProperties.add(index)
} else {
variableToDelegatedPropertyIndex.getOrPut(slot) {
KPropertyRanges(index)
}.ranges += insn to current
}
}
if (variableToDelegatedPropertyIndex.isEmpty())
return
// Mark all of the KProperty variables which are used in the current method.
methodNode.instructions.asSequence().filterIsInstance<VarInsnNode>().filter { it.opcode == Opcodes.ALOAD }.forEach {
variableToDelegatedPropertyIndex[it.`var`]?.let { kPropertyRange ->
usedDelegatedProperties.add(kPropertyRange.propertyIndex)
variableToDelegatedPropertyIndex.remove(it.`var`)
}
}
// Remove the unused KProperty loads.
val locals = methodNode.localVariables.mapTo(mutableSetOf()) { it.index }
for (kPropertyRanges in variableToDelegatedPropertyIndex.values) {
for ((from, to) in kPropertyRanges.ranges) {
// Stores to variables with LVT entries need to be preserved. We store `null` for
// unused KProperties, for compatibility with the JVM BE.
val storeInsn = to as VarInsnNode
if (storeInsn.`var` !in locals) {
methodNode.instructions.removeRange(from, to)
} else {
methodNode.instructions.removeRange(from, to.previous)
methodNode.instructions.insertBefore(to, InsnNode(Opcodes.ACONST_NULL))
}
}
}
}
// Remove the initialization of unused cached KProperties, or remove the $$delegatedProperties field initialization
// completely, if it is not used.
fun transformClassInitializer(methodNode: MethodNode) {
if (useOfDelegatesPropertiesArray)
return
for (insn in methodNode.instructions) {
// Parse the array allocation
//
// LDC <length>
// ANEWARRAY kotlin/reflect/KProperty
// ASTORE <x>
//
val size = insn.intValue ?: continue
var slot = 0
var current = matchRange(insn) {
anewarray("kotlin/reflect/KProperty")
slot = astore()
} ?: continue
// Note that there can be cases where some property references are used, in which case we
// can only remove the initialization code for some of the references, not the whole
// $$delegatedProperties array.
val rangesToRemove = if (usedDelegatedProperties.isNotEmpty()) {
mutableListOf<Pair<AbstractInsnNode, AbstractInsnNode>>()
} else {
null
}
// Parse the array element initializers
for (i in 0 until size) {
var index = 0
val start = current.next
// The code to initialize a single array element looks like this
//
// ALOAD <x>
//
// LDC <index>
//
// NEW kotlin/jvm/internal/(Mutable)PropertyReference[012]Impl
// DUP
// LDC <current>.class
// LDC "<module>"
// INVOKESTATIC kotlin/jvm/internal/Reflection.getOrCreateKotlinPackage (Ljava/lang/Class;Ljava/lang/String;)Lkotlin/reflect/KDeclarationContainer;
// LDC "<property name>"
// LDC "<property getter signature>"
// INVOKESPECIAL kotlin/jvm/internal/(Mutable)PropertyReference[012]Impl.<init> (Lkotlin/reflect/KDeclarationContainer;Ljava/lang/String;Ljava/lang/String;)V
// INVOKESTATIC kotlin/jvm/internal/Reflection.(mutable)property[012] (Lkotlin/jvm/internal/PropertyReference[012];)Lkotlin/reflect/KProperty[012];
//
// AASTORE
//
// With the caveat that we might create a class instead of a package as the `KDeclarationContainer`,
// and with optimized references the Java class is passed to the constructor without wrapping.
current = matchRange(current) {
aload(slot)
index = iconst()
new(PROPERTY_REFERENCE_CLASSES)
dup()
ldc() // java class
either(
{
optional { ldc() } // module name, if package
invokestatic("kotlin/jvm/internal/Reflection") // getOrCreateKotlinPackage or getOrCreateKotlinClass
ldc() // name
ldc() // signature
invokespecial("<init>", "(Lkotlin/reflect/KDeclarationContainer;Ljava/lang/String;Ljava/lang/String;)V")
}, {
ldc() // name
ldc() // signature
iconst() // flags
invokespecial("<init>", "(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;I)V")
}
)
invokestatic("kotlin/jvm/internal/Reflection") // (property|mutableProperty)[012]
aastore()
} ?: break
if (rangesToRemove != null && index !in usedDelegatedProperties) {
rangesToRemove.add(start to current)
}
}
// At this point the code sets the $$delegatedProperties field to the constructed array.
//
// ALOAD <x>
// PUTSTATIC <current>.$$delegatedProperties : [Lkotlin/reflect/KProperty;
current = matchRange(current) {
aload(slot)
putstatic(JvmAbi.DELEGATED_PROPERTIES_ARRAY_NAME, "[Lkotlin/reflect/KProperty;")
} ?: continue
if (rangesToRemove == null) {
// Remove the whole array initialization
methodNode.instructions.removeRange(insn, current)
} else {
for ((from, to) in rangesToRemove) {
methodNode.instructions.removeRange(from, to)
}
}
// There is only one initializer for the $$delegatedProperties array.
return
}
}
// Match a half-open range of instructions from start (exclusive!) and returns either null if the instructions
// did not match or the last instruction which matched.
private fun matchRange(start: AbstractInsnNode, body: InstructionMatcher.() -> Unit): AbstractInsnNode? =
InstructionMatcher(start).apply(body).current
companion object {
val PROPERTY_REFERENCE_CLASSES = setOf(
"kotlin/jvm/internal/PropertyReference0Impl",
"kotlin/jvm/internal/PropertyReference1Impl",
"kotlin/jvm/internal/PropertyReference2Impl",
"kotlin/jvm/internal/MutablePropertyReference0Impl",
"kotlin/jvm/internal/MutablePropertyReference1Impl",
"kotlin/jvm/internal/MutablePropertyReference2Impl",
)
}
}
// Match a straight-line segment of instructions, while ignoring irrelevant instructions such as labels, linenumbers,
// frames, nops, and casts.
private class InstructionMatcher(var current: AbstractInsnNode?) {
fun peek(): AbstractInsnNode? {
var insn = current?.next ?: return null
while (!insn.isMeaningful || insn.opcode == Opcodes.NOP || insn.opcode == Opcodes.CHECKCAST) {
insn = insn.next ?: return null
}
return insn
}
inline fun match(predicate: (AbstractInsnNode) -> Boolean) {
current = peek()?.takeIf(predicate)
}
inline fun <T> parse(default: T, op: (AbstractInsnNode) -> T?): T {
var value = default
match {
op(it)?.let {
value = it
true
} ?: false
}
return value
}
inline fun optional(block: () -> Unit) {
either(block) {}
}
inline fun either(first: () -> Unit, second: () -> Unit) {
val start = current
first()
if (current == null) {
current = start
second()
}
}
fun iconst(): Int = parse(0) { it.intValue }
fun aaload() {
match { it.opcode == Opcodes.AALOAD }
}
fun aastore() {
match { it.opcode == Opcodes.AASTORE }
}
fun anewarray(desc: String) {
match { it.opcode == Opcodes.ANEWARRAY && it is TypeInsnNode && it.desc == desc }
}
fun aload(slot: Int) {
match { it.opcode == Opcodes.ALOAD && it is VarInsnNode && it.`var` == slot }
}
fun astore(): Int =
parse(0) { insn -> insn.safeAs<VarInsnNode>()?.takeIf { it.opcode == Opcodes.ASTORE }?.`var` }
fun putstatic(name: String, desc: String) {
match { it.opcode == Opcodes.PUTSTATIC && it is FieldInsnNode && it.name == name && it.desc == desc }
}
fun dup() {
match { it.opcode == Opcodes.DUP }
}
fun ldc() {
match { it.opcode == Opcodes.LDC }
}
fun invokestatic(owner: String) {
match { it.opcode == Opcodes.INVOKESTATIC && it is MethodInsnNode && it.owner == owner }
}
fun invokespecial(name: String, desc: String) {
match { it.opcode == Opcodes.INVOKESPECIAL && it is MethodInsnNode && it.name == name && it.desc == desc }
}
fun new(classNames: Set<String>) {
match { it.opcode == Opcodes.NEW && it is TypeInsnNode && it.desc in classNames }
}
}
private fun InsnList.safeRemove(insn: AbstractInsnNode) {
if (insn !is LabelNode && insn !is LineNumberNode)
remove(insn)
}
private fun InsnList.removeRange(from: AbstractInsnNode, to: AbstractInsnNode) {
var current = from
do {
val next = current.next
safeRemove(current)
current = next
} while (current != to)
safeRemove(to)
}
private val AbstractInsnNode.isDelegatedPropertiesArray: Boolean
get() = opcode == Opcodes.GETSTATIC && this is FieldInsnNode
&& name == JvmAbi.DELEGATED_PROPERTIES_ARRAY_NAME && desc == "[Lkotlin/reflect/KProperty;"
private val AbstractInsnNode.intValue: Int?
get() = when (opcode) {
Opcodes.ICONST_0 -> 0
Opcodes.ICONST_1 -> 1
Opcodes.ICONST_2 -> 2
Opcodes.ICONST_3 -> 3
Opcodes.ICONST_4 -> 4
Opcodes.ICONST_5 -> 5
Opcodes.ICONST_M1 -> -1
Opcodes.BIPUSH, Opcodes.SIPUSH -> safeAs<IntInsnNode>()?.operand
Opcodes.LDC -> safeAs<LdcInsnNode>()?.cst as? Int
else -> null
}
@@ -14,6 +14,7 @@ import org.jetbrains.kotlin.backend.common.lower.createIrBuilder
import org.jetbrains.kotlin.backend.common.phaser.makeIrFilePhase
import org.jetbrains.kotlin.backend.jvm.JvmBackendContext
import org.jetbrains.kotlin.backend.jvm.JvmLoweredDeclarationOrigin
import org.jetbrains.kotlin.backend.jvm.codegen.parentClassId
import org.jetbrains.kotlin.backend.jvm.ir.createJvmIrBuilder
import org.jetbrains.kotlin.backend.jvm.ir.irArrayOf
import org.jetbrains.kotlin.backend.jvm.ir.needsAccessor
@@ -21,15 +22,20 @@ import org.jetbrains.kotlin.backend.jvm.lower.FunctionReferenceLowering.Companio
import org.jetbrains.kotlin.backend.jvm.lower.FunctionReferenceLowering.Companion.calculateOwnerKClass
import org.jetbrains.kotlin.backend.jvm.lower.FunctionReferenceLowering.Companion.kClassToJavaClass
import org.jetbrains.kotlin.backend.jvm.lower.inlineclasses.InlineClassAbi
import org.jetbrains.kotlin.backend.jvm.lower.inlineclasses.hasMangledReturnType
import org.jetbrains.kotlin.backend.jvm.lower.inlineclasses.isInlineClassFieldGetter
import org.jetbrains.kotlin.codegen.inline.loadCompiledInlineFunction
import org.jetbrains.kotlin.codegen.optimization.nullCheck.usesLocalExceptParameterNullCheck
import org.jetbrains.kotlin.descriptors.DescriptorVisibilities
import org.jetbrains.kotlin.descriptors.Modality
import org.jetbrains.kotlin.ir.IrElement
import org.jetbrains.kotlin.ir.IrStatement
import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET
import org.jetbrains.kotlin.ir.builders.*
import org.jetbrains.kotlin.ir.builders.declarations.*
import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.ir.expressions.*
import org.jetbrains.kotlin.ir.expressions.impl.IrConstImpl
import org.jetbrains.kotlin.ir.expressions.impl.IrFunctionReferenceImpl
import org.jetbrains.kotlin.ir.expressions.impl.IrGetObjectValueImpl
import org.jetbrains.kotlin.ir.expressions.impl.IrInstanceInitializerCallImpl
@@ -41,11 +47,13 @@ import org.jetbrains.kotlin.ir.types.impl.IrSimpleTypeImpl
import org.jetbrains.kotlin.ir.types.impl.makeTypeProjection
import org.jetbrains.kotlin.ir.types.typeWith
import org.jetbrains.kotlin.ir.util.*
import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid
import org.jetbrains.kotlin.load.java.JavaDescriptorVisibilities
import org.jetbrains.kotlin.load.java.JvmAbi
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.name.SpecialNames
import org.jetbrains.kotlin.types.Variance
import java.util.concurrent.ConcurrentHashMap
internal val propertyReferencePhase = makeIrFilePhase(
::PropertyReferenceLowering,
@@ -255,6 +263,54 @@ private class PropertyReferenceLowering(val context: JvmBackendContext) : IrElem
override fun visitLocalDelegatedPropertyReference(expression: IrLocalDelegatedPropertyReference): IrExpression =
cachedKProperty(expression)
private fun IrSimpleFunction.usesParameter(index: Int): Boolean {
parentClassId?.let { containerId ->
// This function was imported from a jar. Didn't run the inline class lowering yet though - have to map manually.
val replaced = context.inlineClassReplacements.getReplacementFunction(this) ?: this
val signature = context.methodSignatureMapper.mapSignatureSkipGeneric(replaced)
val localIndex = signature.valueParameters.take(index + if (replaced.extensionReceiverParameter != null) 1 else 0)
.sumOf { it.asmType.size } + (if (replaced.dispatchReceiverParameter != null) 1 else 0)
// Null checks are removed during inlining, so we can ignore them.
return loadCompiledInlineFunction(containerId, signature.asmMethod, isSuspend, hasMangledReturnType, context.state)
.node.usesLocalExceptParameterNullCheck(localIndex)
}
var result = false
accept(object : IrElementVisitorVoid {
override fun visitElement(element: IrElement) =
if (result) Unit else element.acceptChildren(this, null)
override fun visitGetValue(expression: IrGetValue) {
if (expression.symbol == valueParameters[index].symbol) result = true
}
}, null)
return result
}
// Assuming that the only functions that take PROPERTY_REFERENCE_FOR_DELEGATE-kind references are getValue,
// setValue, and provideDelegate, there is only one valid index for each symbol, so we don't need it in the key.
private val usesPropertyParameterCache = ConcurrentHashMap<IrSymbol, Boolean>()
override fun visitCall(expression: IrCall): IrExpression {
if (!expression.symbol.owner.isInline) {
// Can't optimize if the function can start using the reference later.
// TODO: optimize if callee is in the same file? this requires removing the null check from it,
// or at least providing *some* non-null fake property value.
return super.visitCall(expression)
}
for (index in expression.symbol.owner.valueParameters.indices) {
val value = expression.getValueArgument(index)
if (value is IrCallableReference<*> && value.origin == IrStatementOrigin.PROPERTY_REFERENCE_FOR_DELEGATE) {
if (!usesPropertyParameterCache.getOrPut(expression.symbol) { expression.symbol.owner.usesParameter(index) }) {
// Don't generate an entry in `$$delegatedProperties`, it won't be used anyway.
expression.putValueArgument(index, IrConstImpl.constNull(value.startOffset, value.endOffset, value.type))
}
}
}
return super.visitCall(expression)
}
private fun cachedKProperty(expression: IrCallableReference<*>): IrExpression {
expression.transformChildrenVoid()
if (expression.origin != IrStatementOrigin.PROPERTY_REFERENCE_FOR_DELEGATE)
@@ -55,8 +55,6 @@ final class H2$Companion$property$2 {
@kotlin.Metadata
public final class H2$Companion {
// source: 'delegatedPropertiesInCompanionObject.kt'
synthetic final static field $$delegatedProperties: kotlin.reflect.KProperty[]
static method <clinit>(): void
private method <init>(): void
public synthetic method <init>(p0: kotlin.jvm.internal.DefaultConstructorMarker): void
public final @org.jetbrains.annotations.NotNull method getProperty(): java.lang.String