From 0e908b720d08c5cb7bfcec2cee8108e2c01a504a Mon Sep 17 00:00:00 2001 From: Ilmir Usmanov Date: Tue, 19 May 2020 21:25:51 +0200 Subject: [PATCH] Replace SourceInterpreter with specific one in tail-call optimization --- .../CoroutineTransformerMethodVisitor.kt | 211 +------------- ...ndantLocalsEliminationMethodTransformer.kt | 2 +- .../coroutines/TailCallOptimization.kt | 257 ++++++++++++++++++ 3 files changed, 262 insertions(+), 208 deletions(-) create mode 100644 compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/TailCallOptimization.kt diff --git a/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/CoroutineTransformerMethodVisitor.kt b/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/CoroutineTransformerMethodVisitor.kt index 2505bbe74bc..43e0cd30cfe 100644 --- a/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/CoroutineTransformerMethodVisitor.kt +++ b/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/CoroutineTransformerMethodVisitor.kt @@ -10,11 +10,8 @@ import org.jetbrains.kotlin.codegen.ClassBuilder import org.jetbrains.kotlin.codegen.StackValue import org.jetbrains.kotlin.codegen.TransformationMethodVisitor import org.jetbrains.kotlin.codegen.inline.* -import org.jetbrains.kotlin.codegen.optimization.boxing.isUnitInstance import org.jetbrains.kotlin.codegen.optimization.common.* import org.jetbrains.kotlin.codegen.optimization.fixStack.FixStackMethodTransformer -import org.jetbrains.kotlin.codegen.optimization.fixStack.top -import org.jetbrains.kotlin.codegen.optimization.transformer.MethodTransformer import org.jetbrains.kotlin.config.LanguageVersionSettings import org.jetbrains.kotlin.config.isReleaseCoroutines import org.jetbrains.kotlin.resolve.jvm.AsmTypes @@ -26,7 +23,6 @@ import org.jetbrains.org.objectweb.asm.Opcodes import org.jetbrains.org.objectweb.asm.Type import org.jetbrains.org.objectweb.asm.commons.InstructionAdapter import org.jetbrains.org.objectweb.asm.tree.* -import org.jetbrains.org.objectweb.asm.tree.analysis.* import kotlin.math.max private const val COROUTINES_DEBUG_METADATA_VERSION = 1 @@ -110,6 +106,7 @@ class CoroutineTransformerMethodVisitor( languageVersionSettings, containingClassInternalName, methodNode, + suspensionPoints, disableTailCallOptimizationForFunctionReturningUnit ) if (examiner.allSuspensionPointsAreTailCalls(suspensionPoints)) { @@ -892,194 +889,6 @@ class CoroutineTransformerMethodVisitor( private data class SpilledVariableDescriptor(val fieldName: String, val variableName: String) } -// TODO Use this in variable liveness analysis -private class MethodNodeExaminer( - val languageVersionSettings: LanguageVersionSettings, - val containingClassInternalName: String, - val methodNode: MethodNode, - disableTailCallOptimizationForFunctionReturningUnit: Boolean -) { - private val sourceFrames: SourceFrames = - MethodTransformer.analyze(containingClassInternalName, methodNode, IgnoringCopyOperationSourceInterpreter()) - private val controlFlowGraph = ControlFlowGraph.build(methodNode) - - private val safeUnitInstances = mutableSetOf() - private val popsBeforeSafeUnitInstances = mutableSetOf() - private val areturnsAfterSafeUnitInstances = mutableSetOf() - private val meaningfulSuccessorsCache = hashMapOf>() - private val meaningfulPredecessorsCache = hashMapOf>() - - init { - if (!disableTailCallOptimizationForFunctionReturningUnit) { - // retrieve all POP insns - val pops = methodNode.instructions.asSequence().filter { it.opcode == Opcodes.POP } - // for each of them check that all successors are PUSH Unit - val popsBeforeUnitInstances = pops.map { it to it.meaningfulSuccessors() } - .filter { (_, succs) -> succs.all { it.isUnitInstance() } } - .map { it.first }.toList() - for (pop in popsBeforeUnitInstances) { - val units = pop.meaningfulSuccessors() - val allUnitsAreSafe = units.all { unit -> - // check no other predecessor exists - unit.meaningfulPredecessors().all { it in popsBeforeUnitInstances } && - // check they have only returns among successors - unit.meaningfulSuccessors().all { it.opcode == Opcodes.ARETURN } - } - if (!allUnitsAreSafe) continue - // save them all to the properties - popsBeforeSafeUnitInstances += pop - safeUnitInstances += units - units.flatMapTo(areturnsAfterSafeUnitInstances) { it.meaningfulSuccessors() } - } - } - } - - private fun AbstractInsnNode.index() = methodNode.instructions.indexOf(this) - - // GETSTATIC kotlin/Unit.INSTANCE is considered safe iff - // it is part of POP, PUSH Unit, ARETURN sequence. - private fun AbstractInsnNode.isSafeUnitInstance(): Boolean = this in safeUnitInstances - - private fun AbstractInsnNode.isPopBeforeSafeUnitInstance(): Boolean = this in popsBeforeSafeUnitInstances - private fun AbstractInsnNode.isAreturnAfterSafeUnitInstance(): Boolean = this in areturnsAfterSafeUnitInstances - - private fun AbstractInsnNode.meaningfulSuccessors(): List = meaningfulSuccessorsCache.getOrPut(this) { - meaningfulSuccessorsOrPredecessors(true) - } - - private fun AbstractInsnNode.meaningfulPredecessors(): List = meaningfulPredecessorsCache.getOrPut(this) { - meaningfulSuccessorsOrPredecessors(false) - } - - private fun AbstractInsnNode.meaningfulSuccessorsOrPredecessors(isSuccessors: Boolean): List { - fun AbstractInsnNode.isMeaningful() = isMeaningful && opcode != Opcodes.NOP && opcode != Opcodes.GOTO && this !is LineNumberNode - - fun AbstractInsnNode.getIndices() = - if (isSuccessors) controlFlowGraph.getSuccessorsIndices(this) - else controlFlowGraph.getPredecessorsIndices(this) - - val visited = arrayListOf() - fun dfs(insn: AbstractInsnNode) { - if (insn in visited) return - visited += insn - if (!insn.isMeaningful()) { - for (succIndex in insn.getIndices()) { - dfs(methodNode.instructions[succIndex]) - } - } - } - - for (succIndex in getIndices()) { - dfs(methodNode.instructions[succIndex]) - } - return visited.filter { it.isMeaningful() } - } - - fun replacePopsBeforeSafeUnitInstancesWithCoroutineSuspendedChecks() { - val basicAnalyser = Analyzer(BasicInterpreter()) - basicAnalyser.analyze(containingClassInternalName, methodNode) - val typedFrames = basicAnalyser.frames - - val isReferenceMap = popsBeforeSafeUnitInstances - .map { it to (typedFrames[it.index()]?.top()?.isReference == true) } - .toMap() - - for (pop in popsBeforeSafeUnitInstances) { - if (isReferenceMap[pop] == true) { - val label = Label() - methodNode.instructions.insertBefore(pop, withInstructionAdapter { - dup() - loadCoroutineSuspendedMarker(languageVersionSettings) - ifacmpne(label) - areturn(AsmTypes.OBJECT_TYPE) - mark(label) - }) - } - } - } - - fun allSuspensionPointsAreTailCalls(suspensionPoints: List): Boolean { - val safelyReachableReturns = findSafelyReachableReturns() - - val instructions = methodNode.instructions - return suspensionPoints.all { suspensionPoint -> - val beginIndex = instructions.indexOf(suspensionPoint.suspensionCallBegin) - val endIndex = instructions.indexOf(suspensionPoint.suspensionCallEnd) - - val insideTryBlock = methodNode.tryCatchBlocks.any { block -> - val tryBlockStartIndex = instructions.indexOf(block.start) - val tryBlockEndIndex = instructions.indexOf(block.end) - - beginIndex in tryBlockStartIndex..tryBlockEndIndex - } - if (insideTryBlock) return@all false - - safelyReachableReturns[endIndex + 1]?.all { returnIndex -> - sourceFrames[returnIndex]?.top().sure { - "There must be some value on stack to return" - }.insns.any { sourceInsn -> - sourceInsn?.let(instructions::indexOf) in beginIndex..endIndex - } - } ?: false - } - } - - /** - * Let's call an instruction safe if its execution is always invisible: stack modifications, branching, variable insns (invisible in debug) - * - * For some instruction `insn` define the result as following: - * - if there is a path leading to the non-safe instruction then result is `null` - * - Otherwise result contains all the reachable ARETURN indices - * - * @return indices of safely reachable returns for each instruction in the method node - */ - private fun findSafelyReachableReturns(): Array?> { - val insns = methodNode.instructions - val reachableReturnsIndices = Array?>(insns.size()) init@{ index -> - val insn = insns[index] - - if (insn.opcode == Opcodes.ARETURN && !insn.isAreturnAfterSafeUnitInstance()) { - return@init setOf(index) - } - - // Since POP, PUSH Unit, ARETURN behaves like normal return in terms of tail-call optimization, set return index to POP - if (insn.isPopBeforeSafeUnitInstance()) { - return@init setOf(index) - } - - if (!insn.isMeaningful || insn.opcode in SAFE_OPCODES || insn.isInvisibleInDebugVarInsn(methodNode) || isInlineMarker(insn) - || insn.isSafeUnitInstance() || insn.isAreturnAfterSafeUnitInstance() - ) { - setOf() - } else null - } - - var changed: Boolean - do { - changed = false - for (index in 0 until insns.size()) { - if (insns[index].opcode == Opcodes.ARETURN) continue - - @Suppress("RemoveExplicitTypeArguments") - val newResult = - controlFlowGraph - .getSuccessorsIndices(index).plus(index) - .map(reachableReturnsIndices::get) - .fold?, Set?>(mutableSetOf()) { acc, successorsResult -> - if (acc != null && successorsResult != null) acc + successorsResult else null - } - - if (newResult != reachableReturnsIndices[index]) { - reachableReturnsIndices[index] = newResult - changed = true - } - } - } while (changed) - - return reachableReturnsIndices - } -} - internal fun InstructionAdapter.generateContinuationConstructorCall( objectTypeForState: Type?, methodNode: MethodNode, @@ -1176,6 +985,9 @@ internal class SuspensionPoint( } } +internal operator fun List.contains(insn: AbstractInsnNode): Boolean = + any { insn in it } + internal fun getLastParameterIndex(desc: String, access: Int) = Type.getArgumentTypes(desc).dropLast(1).map { it.size }.sum() + (if (!isStatic(access)) 1 else 0) @@ -1206,21 +1018,6 @@ private fun getAllParameterTypes(desc: String, hasDispatchReceiver: Boolean, thi listOfNotNull(if (!hasDispatchReceiver) null else Type.getObjectType(thisName)).toTypedArray() + Type.getArgumentTypes(desc) -internal class IgnoringCopyOperationSourceInterpreter : SourceInterpreter(Opcodes.API_VERSION) { - override fun copyOperation(insn: AbstractInsnNode?, value: SourceValue?) = value -} - -private fun AbstractInsnNode?.isInvisibleInDebugVarInsn(methodNode: MethodNode): Boolean { - val insns = methodNode.instructions - val index = insns.indexOf(this) - return (this is VarInsnNode && methodNode.localVariables.none { - it.index == `var` && index in it.start.let(insns::indexOf)..it.end.let(insns::indexOf) - }) -} - -private val SAFE_OPCODES = - ((Opcodes.DUP..Opcodes.DUP2_X2) + Opcodes.NOP + Opcodes.POP + Opcodes.POP2 + (Opcodes.IFEQ..Opcodes.GOTO)).toSet() - internal fun replaceFakeContinuationsWithRealOnes(methodNode: MethodNode, continuationIndex: Int) { val fakeContinuations = methodNode.instructions.asSequence().filter(::isFakeContinuationMarker).toList() for (fakeContinuation in fakeContinuations) { diff --git a/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/RedundantLocalsEliminationMethodTransformer.kt b/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/RedundantLocalsEliminationMethodTransformer.kt index 564c9ab333a..3e6aa85002b 100644 --- a/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/RedundantLocalsEliminationMethodTransformer.kt +++ b/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/RedundantLocalsEliminationMethodTransformer.kt @@ -115,7 +115,7 @@ internal class RedundantLocalsEliminationMethodTransformer(private val suspensio for (pop in methodNode.instructions.asSequence().filter { it.opcode == Opcodes.POP }) { val value = (frames[methodNode.instructions.indexOf(pop)]?.top() as? PossibleSpilledValue) ?: continue - if (value.usages.isEmpty() && suspensionPoints.none { value.source in it }) { + if (value.usages.isEmpty() && value.source !in suspensionPoints) { toDelete.add(pop) toDelete.add(value.source) } diff --git a/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/TailCallOptimization.kt b/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/TailCallOptimization.kt new file mode 100644 index 00000000000..93b2b1486d9 --- /dev/null +++ b/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/TailCallOptimization.kt @@ -0,0 +1,257 @@ +/* + * 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.codegen.coroutines + +import org.jetbrains.kotlin.codegen.inline.isInlineMarker +import org.jetbrains.kotlin.codegen.optimization.boxing.isUnitInstance +import org.jetbrains.kotlin.codegen.optimization.common.ControlFlowGraph +import org.jetbrains.kotlin.codegen.optimization.common.asSequence +import org.jetbrains.kotlin.codegen.optimization.common.isMeaningful +import org.jetbrains.kotlin.codegen.optimization.fixStack.top +import org.jetbrains.kotlin.codegen.optimization.transformer.MethodTransformer +import org.jetbrains.kotlin.config.LanguageVersionSettings +import org.jetbrains.kotlin.resolve.jvm.AsmTypes +import org.jetbrains.kotlin.utils.sure +import org.jetbrains.org.objectweb.asm.Label +import org.jetbrains.org.objectweb.asm.Opcodes +import org.jetbrains.org.objectweb.asm.Type +import org.jetbrains.org.objectweb.asm.tree.AbstractInsnNode +import org.jetbrains.org.objectweb.asm.tree.LineNumberNode +import org.jetbrains.org.objectweb.asm.tree.MethodNode +import org.jetbrains.org.objectweb.asm.tree.VarInsnNode +import org.jetbrains.org.objectweb.asm.tree.analysis.BasicInterpreter +import org.jetbrains.org.objectweb.asm.tree.analysis.BasicValue +import org.jetbrains.org.objectweb.asm.tree.analysis.Frame + +// TODO Use this in variable liveness analysis +internal class MethodNodeExaminer( + val languageVersionSettings: LanguageVersionSettings, + containingClassInternalName: String, + val methodNode: MethodNode, + suspensionPoints: List, + disableTailCallOptimizationForFunctionReturningUnit: Boolean +) { + private val frames: Array?> = + MethodTransformer.analyze(containingClassInternalName, methodNode, TcoInterpreter(suspensionPoints)) + private val controlFlowGraph = ControlFlowGraph.build(methodNode) + + private val safeUnitInstances = mutableSetOf() + private val popsBeforeSafeUnitInstances = mutableSetOf() + private val areturnsAfterSafeUnitInstances = mutableSetOf() + private val meaningfulSuccessorsCache = hashMapOf>() + private val meaningfulPredecessorsCache = hashMapOf>() + + init { + if (!disableTailCallOptimizationForFunctionReturningUnit) { + // retrieve all POP insns + val pops = methodNode.instructions.asSequence().filter { it.opcode == Opcodes.POP } + // for each of them check that all successors are PUSH Unit + val popsBeforeUnitInstances = pops.map { it to it.meaningfulSuccessors() } + .filter { (_, succs) -> succs.all { it.isUnitInstance() } } + .map { it.first }.toList() + for (pop in popsBeforeUnitInstances) { + val units = pop.meaningfulSuccessors() + val allUnitsAreSafe = units.all { unit -> + // check no other predecessor exists + unit.meaningfulPredecessors().all { it in popsBeforeUnitInstances } && + // check they have only returns among successors + unit.meaningfulSuccessors().all { it.opcode == Opcodes.ARETURN } + } + if (!allUnitsAreSafe) continue + // save them all to the properties + popsBeforeSafeUnitInstances += pop + safeUnitInstances += units + units.flatMapTo(areturnsAfterSafeUnitInstances) { it.meaningfulSuccessors() } + } + } + } + + // GETSTATIC kotlin/Unit.INSTANCE is considered safe iff + // it is part of POP, PUSH Unit, ARETURN sequence. + private fun AbstractInsnNode.isSafeUnitInstance(): Boolean = this in safeUnitInstances + + private fun AbstractInsnNode.isPopBeforeSafeUnitInstance(): Boolean = this in popsBeforeSafeUnitInstances + private fun AbstractInsnNode.isAreturnAfterSafeUnitInstance(): Boolean = this in areturnsAfterSafeUnitInstances + + private fun AbstractInsnNode.meaningfulSuccessors(): List = meaningfulSuccessorsCache.getOrPut(this) { + meaningfulSuccessorsOrPredecessors(true) + } + + private fun AbstractInsnNode.meaningfulPredecessors(): List = meaningfulPredecessorsCache.getOrPut(this) { + meaningfulSuccessorsOrPredecessors(false) + } + + private fun AbstractInsnNode.meaningfulSuccessorsOrPredecessors(isSuccessors: Boolean): List { + fun AbstractInsnNode.isMeaningful() = isMeaningful && opcode != Opcodes.NOP && opcode != Opcodes.GOTO && this !is LineNumberNode + + fun AbstractInsnNode.getIndices() = + if (isSuccessors) controlFlowGraph.getSuccessorsIndices(this) + else controlFlowGraph.getPredecessorsIndices(this) + + val visited = mutableSetOf() + fun dfs(insn: AbstractInsnNode) { + if (insn in visited) return + visited += insn + if (!insn.isMeaningful()) { + for (succIndex in insn.getIndices()) { + dfs(methodNode.instructions[succIndex]) + } + } + } + + for (succIndex in getIndices()) { + dfs(methodNode.instructions[succIndex]) + } + return visited.filter { it.isMeaningful() } + } + + fun replacePopsBeforeSafeUnitInstancesWithCoroutineSuspendedChecks() { + val isReferenceMap = + popsBeforeSafeUnitInstances.associateWith { (frames[methodNode.instructions.indexOf(it)]?.top()?.isReference == true) } + + for (pop in popsBeforeSafeUnitInstances) { + if (isReferenceMap[pop] == true) { + val label = Label() + methodNode.instructions.insertBefore(pop, withInstructionAdapter { + dup() + loadCoroutineSuspendedMarker(languageVersionSettings) + ifacmpne(label) + areturn(AsmTypes.OBJECT_TYPE) + mark(label) + }) + } + } + } + + fun allSuspensionPointsAreTailCalls(suspensionPoints: List): Boolean { + val safelyReachableReturns = findSafelyReachableReturns() + + val instructions = methodNode.instructions + return suspensionPoints.all { suspensionPoint -> + val beginIndex = instructions.indexOf(suspensionPoint.suspensionCallBegin) + val endIndex = instructions.indexOf(suspensionPoint.suspensionCallEnd) + + val insideTryBlock = methodNode.tryCatchBlocks.any { block -> + val tryBlockStartIndex = instructions.indexOf(block.start) + val tryBlockEndIndex = instructions.indexOf(block.end) + + beginIndex in tryBlockStartIndex until tryBlockEndIndex + } + if (insideTryBlock) return@all false + + safelyReachableReturns[endIndex + 1]?.all { returnIndex -> + frames[returnIndex]?.top().sure { + "There must be some value on stack to return" + } is FromSuspensionPointValue + } ?: false + } + } + + /** + * Let's call an instruction safe if its execution is always invisible: stack modifications, branching, variable insns (invisible in debug) + * + * For some instruction `insn` define the result as following: + * - if there is a path leading to the non-safe instruction then result is `null` + * - Otherwise result contains all the reachable ARETURN indices + * + * @return indices of safely reachable returns for each instruction in the method node + */ + private fun findSafelyReachableReturns(): Array?> { + val insns = methodNode.instructions + val reachableReturnsIndices = Array(insns.size()) init@{ index -> + val insn = insns[index] + + if (insn.opcode == Opcodes.ARETURN && !insn.isAreturnAfterSafeUnitInstance()) { + return@init setOf(index) + } + + // Since POP, PUSH Unit, ARETURN behaves like normal return in terms of tail-call optimization, set return index to POP + if (insn.isPopBeforeSafeUnitInstance()) { + return@init setOf(index) + } + + if (!insn.isMeaningful || insn.opcode in SAFE_OPCODES || insn.isInvisibleInDebugVarInsn(methodNode) || isInlineMarker(insn) + || insn.isSafeUnitInstance() || insn.isAreturnAfterSafeUnitInstance() + ) { + setOf() + } else null + } + + var changed: Boolean + do { + changed = false + for (index in 0 until insns.size()) { + if (insns[index].opcode == Opcodes.ARETURN) continue + + @Suppress("RemoveExplicitTypeArguments") + val newResult = + controlFlowGraph + .getSuccessorsIndices(index).plus(index) + .map(reachableReturnsIndices::get) + .fold?, Set?>(mutableSetOf()) { acc, successorsResult -> + if (acc != null && successorsResult != null) acc + successorsResult else null + } + + if (newResult != reachableReturnsIndices[index]) { + reachableReturnsIndices[index] = newResult + changed = true + } + } + } while (changed) + + return reachableReturnsIndices + } +} + +private fun AbstractInsnNode?.isInvisibleInDebugVarInsn(methodNode: MethodNode): Boolean { + val insns = methodNode.instructions + val index = insns.indexOf(this) + return (this is VarInsnNode && methodNode.localVariables.none { + it.index == `var` && index in it.start.let(insns::indexOf)..it.end.let(insns::indexOf) + }) +} + +private val SAFE_OPCODES = + ((Opcodes.DUP..Opcodes.DUP2_X2) + Opcodes.NOP + Opcodes.POP + Opcodes.POP2 + (Opcodes.IFEQ..Opcodes.GOTO)).toSet() + +private object FromSuspensionPointValue : BasicValue(AsmTypes.OBJECT_TYPE) { + override fun equals(other: Any?): Boolean = other is FromSuspensionPointValue +} + +private fun BasicValue?.toFromSuspensionPoint(): BasicValue? = if (this?.type?.sort == Type.OBJECT) FromSuspensionPointValue else this + +private class TcoInterpreter(private val suspensionPoints: List) : BasicInterpreter(Opcodes.API_VERSION) { + override fun copyOperation(insn: AbstractInsnNode, value: BasicValue?): BasicValue? { + return super.copyOperation(insn, value).convert(insn) + } + + private fun BasicValue?.convert(insn: AbstractInsnNode): BasicValue? = if (insn in suspensionPoints) toFromSuspensionPoint() else this + + override fun naryOperation(insn: AbstractInsnNode, values: MutableList?): BasicValue? { + return super.naryOperation(insn, values).convert(insn) + } + + override fun ternaryOperation(insn: AbstractInsnNode, value1: BasicValue?, value2: BasicValue?, value3: BasicValue?): BasicValue? { + return super.ternaryOperation(insn, value1, value2, value3).convert(insn) + } + + override fun merge(value1: BasicValue?, value2: BasicValue?): BasicValue { + return if (value1 is FromSuspensionPointValue || value2 is FromSuspensionPointValue) FromSuspensionPointValue + else super.merge(value1, value2) + } + + override fun unaryOperation(insn: AbstractInsnNode, value: BasicValue?): BasicValue? { + return super.unaryOperation(insn, value).convert(insn) + } + + override fun binaryOperation(insn: AbstractInsnNode, value1: BasicValue?, value2: BasicValue?): BasicValue? { + return super.binaryOperation(insn, value1, value2).convert(insn) + } + + override fun newOperation(insn: AbstractInsnNode): BasicValue? { + return super.newOperation(insn).convert(insn) + } +}