Open-source multiplayer game server compatible with the RuneScape client https://www.openrs2.org/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
openrs2/deob-bytecode/src/main/kotlin/org/openrs2/deob/bytecode/transform/ConstantArgTransformer.kt

325 lines
12 KiB

package org.openrs2.deob.bytecode.transform
Replace DummyArgTransformer with new ConstantArgTransformer The new transformer uses a different approach to the old one. It starts exploring the call graph from the entry points, recursively analysing method calls. Methods are only re-analysed if their possible argument values change, with the Unknown value being used if we can't identify a single integer constant at a call site. This prevents us from recursing infinitely if the client code does. While this first pass does simplify branches in order to ignore dummy method calls that are never evaluated at runtime, it operates on a copy of the method (as we initially ignore more calls while the argument value sets are smaller, ignoring fewer calls as they build up). A separate second pass simplifies branches on the original method and inlines singleton constants, paving the way for the UnusedArgTransformer to actually remove the newly unused arguments. This new approach has several benefits: - It is much faster than the old approach, as we only re-analyse methods as required by argument value changes, rather than re-analysing every method during every pass. - It doesn't require special cases for dealing with mutually recursive dummy calls. The old approach hard-coded special cases for mutually recursive calls involving groups of 1 and 2 methods. The code for this wasn't clean. Furthermore, while it was just about good enough for the HD client, the SD client contains a mutually recursive group of 3 methods. The new approach is capable of dealing with mutually recursive groups of any size. Finally, the new transformer has a much cleaner implementation. Signed-off-by: Graham <gpe@openrs2.dev>
4 years ago
import com.github.michaelbull.logging.InlineLogger
import org.objectweb.asm.Opcodes.GOTO
import org.objectweb.asm.Opcodes.IFEQ
import org.objectweb.asm.Opcodes.IFGE
import org.objectweb.asm.Opcodes.IFGT
import org.objectweb.asm.Opcodes.IFLE
import org.objectweb.asm.Opcodes.IFLT
import org.objectweb.asm.Opcodes.IFNE
import org.objectweb.asm.Opcodes.IF_ICMPEQ
import org.objectweb.asm.Opcodes.IF_ICMPGE
import org.objectweb.asm.Opcodes.IF_ICMPGT
import org.objectweb.asm.Opcodes.IF_ICMPLE
import org.objectweb.asm.Opcodes.IF_ICMPLT
import org.objectweb.asm.Opcodes.IF_ICMPNE
import org.objectweb.asm.Type
import org.objectweb.asm.tree.AbstractInsnNode
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.JumpInsnNode
import org.objectweb.asm.tree.MethodInsnNode
import org.objectweb.asm.tree.MethodNode
import org.objectweb.asm.tree.analysis.Analyzer
import org.openrs2.asm.MemberRef
import org.openrs2.asm.classpath.ClassPath
import org.openrs2.asm.classpath.Library
import org.openrs2.asm.copy
import org.openrs2.asm.deleteExpression
import org.openrs2.asm.filter.MemberFilter
import org.openrs2.asm.filter.UnionMemberFilter
import org.openrs2.asm.hasCode
import org.openrs2.asm.intConstant
import org.openrs2.asm.isPure
import org.openrs2.asm.nextReal
import org.openrs2.asm.replaceExpression
import org.openrs2.asm.stackMetadata
import org.openrs2.asm.toAbstractInsnNode
import org.openrs2.asm.transform.Transformer
import org.openrs2.deob.bytecode.ArgPartition
import org.openrs2.deob.bytecode.Profile
import org.openrs2.deob.bytecode.analysis.IntBranch
import org.openrs2.deob.bytecode.analysis.IntBranchResult.ALWAYS_TAKEN
import org.openrs2.deob.bytecode.analysis.IntBranchResult.NEVER_TAKEN
import org.openrs2.deob.bytecode.analysis.IntInterpreter
import org.openrs2.deob.bytecode.analysis.IntValueSet
import org.openrs2.deob.bytecode.filter.ReflectedConstructorFilter
import org.openrs2.deob.bytecode.remap.MethodMappingGenerator
import org.openrs2.util.collect.DisjointSet
import org.openrs2.util.collect.removeFirstOrNull
Replace TypedRemapper constants with a more flexible system The new system will make it easier to port the deobfuscator to different revisions. There are two main changes: - The addition of a Profile class, which contains a list of excluded classes, methods and fields, and the maximum obfuscated name length. It is passed to Transformers that require it with dependency injection. - New ClassFilter and MemberFilter infrastructure. The MemberFilter class adds support for filtering fields and methods based on the owner and descriptor, in addition to the name. This makes the filters more precise than the previous system. It also supports globs, which makes it easy to filter whole groups of classes, fields and methods in one go. The Profile class uses a ClassFilter and MemberFilters to represent the list of excluded classes, methods and fields. A separate benefit is the addition of a separate entry points filter to the Profile class. Prior to this commit, many Transformers re-used the excluded method filter to find entry points, which is less precise (many of the excluded methods in 550 are not entry points). Support for filtering methods by owner and descriptor in addition to name allows the DEFAULT_PUBLIC_CTOR_CLASSES Set in VisibilityTransformer to be combined with the entry points filter. In the future it might be desirable to split the excluded method set into three separate sets: - One to represent methods that can't be renamed. - One to represent methods whose signature can't be changed. - One to represent methods that can't be removed. Signed-off-by: Graham <gpe@openrs2.dev>
4 years ago
import javax.inject.Inject
import javax.inject.Singleton
Replace DummyArgTransformer with new ConstantArgTransformer The new transformer uses a different approach to the old one. It starts exploring the call graph from the entry points, recursively analysing method calls. Methods are only re-analysed if their possible argument values change, with the Unknown value being used if we can't identify a single integer constant at a call site. This prevents us from recursing infinitely if the client code does. While this first pass does simplify branches in order to ignore dummy method calls that are never evaluated at runtime, it operates on a copy of the method (as we initially ignore more calls while the argument value sets are smaller, ignoring fewer calls as they build up). A separate second pass simplifies branches on the original method and inlines singleton constants, paving the way for the UnusedArgTransformer to actually remove the newly unused arguments. This new approach has several benefits: - It is much faster than the old approach, as we only re-analyse methods as required by argument value changes, rather than re-analysing every method during every pass. - It doesn't require special cases for dealing with mutually recursive dummy calls. The old approach hard-coded special cases for mutually recursive calls involving groups of 1 and 2 methods. The code for this wasn't clean. Furthermore, while it was just about good enough for the HD client, the SD client contains a mutually recursive group of 3 methods. The new approach is capable of dealing with mutually recursive groups of any size. Finally, the new transformer has a much cleaner implementation. Signed-off-by: Graham <gpe@openrs2.dev>
4 years ago
@Singleton
public class ConstantArgTransformer @Inject constructor(private val profile: Profile) : Transformer() {
Replace DummyArgTransformer with new ConstantArgTransformer The new transformer uses a different approach to the old one. It starts exploring the call graph from the entry points, recursively analysing method calls. Methods are only re-analysed if their possible argument values change, with the Unknown value being used if we can't identify a single integer constant at a call site. This prevents us from recursing infinitely if the client code does. While this first pass does simplify branches in order to ignore dummy method calls that are never evaluated at runtime, it operates on a copy of the method (as we initially ignore more calls while the argument value sets are smaller, ignoring fewer calls as they build up). A separate second pass simplifies branches on the original method and inlines singleton constants, paving the way for the UnusedArgTransformer to actually remove the newly unused arguments. This new approach has several benefits: - It is much faster than the old approach, as we only re-analyse methods as required by argument value changes, rather than re-analysing every method during every pass. - It doesn't require special cases for dealing with mutually recursive dummy calls. The old approach hard-coded special cases for mutually recursive calls involving groups of 1 and 2 methods. The code for this wasn't clean. Furthermore, while it was just about good enough for the HD client, the SD client contains a mutually recursive group of 3 methods. The new approach is capable of dealing with mutually recursive groups of any size. Finally, the new transformer has a much cleaner implementation. Signed-off-by: Graham <gpe@openrs2.dev>
4 years ago
private val pendingMethods = LinkedHashSet<MemberRef>()
private val arglessMethods = mutableSetOf<DisjointSet.Partition<MemberRef>>()
private val argValues = mutableMapOf<ArgPartition, IntValueSet>()
Replace DummyArgTransformer with new ConstantArgTransformer The new transformer uses a different approach to the old one. It starts exploring the call graph from the entry points, recursively analysing method calls. Methods are only re-analysed if their possible argument values change, with the Unknown value being used if we can't identify a single integer constant at a call site. This prevents us from recursing infinitely if the client code does. While this first pass does simplify branches in order to ignore dummy method calls that are never evaluated at runtime, it operates on a copy of the method (as we initially ignore more calls while the argument value sets are smaller, ignoring fewer calls as they build up). A separate second pass simplifies branches on the original method and inlines singleton constants, paving the way for the UnusedArgTransformer to actually remove the newly unused arguments. This new approach has several benefits: - It is much faster than the old approach, as we only re-analyse methods as required by argument value changes, rather than re-analysing every method during every pass. - It doesn't require special cases for dealing with mutually recursive dummy calls. The old approach hard-coded special cases for mutually recursive calls involving groups of 1 and 2 methods. The code for this wasn't clean. Furthermore, while it was just about good enough for the HD client, the SD client contains a mutually recursive group of 3 methods. The new approach is capable of dealing with mutually recursive groups of any size. Finally, the new transformer has a much cleaner implementation. Signed-off-by: Graham <gpe@openrs2.dev>
4 years ago
private lateinit var inheritedMethodSets: DisjointSet<MemberRef>
Replace TypedRemapper constants with a more flexible system The new system will make it easier to port the deobfuscator to different revisions. There are two main changes: - The addition of a Profile class, which contains a list of excluded classes, methods and fields, and the maximum obfuscated name length. It is passed to Transformers that require it with dependency injection. - New ClassFilter and MemberFilter infrastructure. The MemberFilter class adds support for filtering fields and methods based on the owner and descriptor, in addition to the name. This makes the filters more precise than the previous system. It also supports globs, which makes it easy to filter whole groups of classes, fields and methods in one go. The Profile class uses a ClassFilter and MemberFilters to represent the list of excluded classes, methods and fields. A separate benefit is the addition of a separate entry points filter to the Profile class. Prior to this commit, many Transformers re-used the excluded method filter to find entry points, which is less precise (many of the excluded methods in 550 are not entry points). Support for filtering methods by owner and descriptor in addition to name allows the DEFAULT_PUBLIC_CTOR_CLASSES Set in VisibilityTransformer to be combined with the entry points filter. In the future it might be desirable to split the excluded method set into three separate sets: - One to represent methods that can't be renamed. - One to represent methods whose signature can't be changed. - One to represent methods that can't be removed. Signed-off-by: Graham <gpe@openrs2.dev>
4 years ago
private lateinit var entryPoints: MemberFilter
Replace DummyArgTransformer with new ConstantArgTransformer The new transformer uses a different approach to the old one. It starts exploring the call graph from the entry points, recursively analysing method calls. Methods are only re-analysed if their possible argument values change, with the Unknown value being used if we can't identify a single integer constant at a call site. This prevents us from recursing infinitely if the client code does. While this first pass does simplify branches in order to ignore dummy method calls that are never evaluated at runtime, it operates on a copy of the method (as we initially ignore more calls while the argument value sets are smaller, ignoring fewer calls as they build up). A separate second pass simplifies branches on the original method and inlines singleton constants, paving the way for the UnusedArgTransformer to actually remove the newly unused arguments. This new approach has several benefits: - It is much faster than the old approach, as we only re-analyse methods as required by argument value changes, rather than re-analysing every method during every pass. - It doesn't require special cases for dealing with mutually recursive dummy calls. The old approach hard-coded special cases for mutually recursive calls involving groups of 1 and 2 methods. The code for this wasn't clean. Furthermore, while it was just about good enough for the HD client, the SD client contains a mutually recursive group of 3 methods. The new approach is capable of dealing with mutually recursive groups of any size. Finally, the new transformer has a much cleaner implementation. Signed-off-by: Graham <gpe@openrs2.dev>
4 years ago
private var branchesSimplified = 0
private var constantsPropagated = 0
Replace DummyArgTransformer with new ConstantArgTransformer The new transformer uses a different approach to the old one. It starts exploring the call graph from the entry points, recursively analysing method calls. Methods are only re-analysed if their possible argument values change, with the Unknown value being used if we can't identify a single integer constant at a call site. This prevents us from recursing infinitely if the client code does. While this first pass does simplify branches in order to ignore dummy method calls that are never evaluated at runtime, it operates on a copy of the method (as we initially ignore more calls while the argument value sets are smaller, ignoring fewer calls as they build up). A separate second pass simplifies branches on the original method and inlines singleton constants, paving the way for the UnusedArgTransformer to actually remove the newly unused arguments. This new approach has several benefits: - It is much faster than the old approach, as we only re-analyse methods as required by argument value changes, rather than re-analysing every method during every pass. - It doesn't require special cases for dealing with mutually recursive dummy calls. The old approach hard-coded special cases for mutually recursive calls involving groups of 1 and 2 methods. The code for this wasn't clean. Furthermore, while it was just about good enough for the HD client, the SD client contains a mutually recursive group of 3 methods. The new approach is capable of dealing with mutually recursive groups of any size. Finally, the new transformer has a much cleaner implementation. Signed-off-by: Graham <gpe@openrs2.dev>
4 years ago
override fun preTransform(classPath: ClassPath) {
pendingMethods.clear()
arglessMethods.clear()
argValues.clear()
inheritedMethodSets = classPath.createInheritedMethodSets()
Replace TypedRemapper constants with a more flexible system The new system will make it easier to port the deobfuscator to different revisions. There are two main changes: - The addition of a Profile class, which contains a list of excluded classes, methods and fields, and the maximum obfuscated name length. It is passed to Transformers that require it with dependency injection. - New ClassFilter and MemberFilter infrastructure. The MemberFilter class adds support for filtering fields and methods based on the owner and descriptor, in addition to the name. This makes the filters more precise than the previous system. It also supports globs, which makes it easy to filter whole groups of classes, fields and methods in one go. The Profile class uses a ClassFilter and MemberFilters to represent the list of excluded classes, methods and fields. A separate benefit is the addition of a separate entry points filter to the Profile class. Prior to this commit, many Transformers re-used the excluded method filter to find entry points, which is less precise (many of the excluded methods in 550 are not entry points). Support for filtering methods by owner and descriptor in addition to name allows the DEFAULT_PUBLIC_CTOR_CLASSES Set in VisibilityTransformer to be combined with the entry points filter. In the future it might be desirable to split the excluded method set into three separate sets: - One to represent methods that can't be renamed. - One to represent methods whose signature can't be changed. - One to represent methods that can't be removed. Signed-off-by: Graham <gpe@openrs2.dev>
4 years ago
entryPoints = UnionMemberFilter(profile.entryPoints, ReflectedConstructorFilter.create(classPath))
Replace DummyArgTransformer with new ConstantArgTransformer The new transformer uses a different approach to the old one. It starts exploring the call graph from the entry points, recursively analysing method calls. Methods are only re-analysed if their possible argument values change, with the Unknown value being used if we can't identify a single integer constant at a call site. This prevents us from recursing infinitely if the client code does. While this first pass does simplify branches in order to ignore dummy method calls that are never evaluated at runtime, it operates on a copy of the method (as we initially ignore more calls while the argument value sets are smaller, ignoring fewer calls as they build up). A separate second pass simplifies branches on the original method and inlines singleton constants, paving the way for the UnusedArgTransformer to actually remove the newly unused arguments. This new approach has several benefits: - It is much faster than the old approach, as we only re-analyse methods as required by argument value changes, rather than re-analysing every method during every pass. - It doesn't require special cases for dealing with mutually recursive dummy calls. The old approach hard-coded special cases for mutually recursive calls involving groups of 1 and 2 methods. The code for this wasn't clean. Furthermore, while it was just about good enough for the HD client, the SD client contains a mutually recursive group of 3 methods. The new approach is capable of dealing with mutually recursive groups of any size. Finally, the new transformer has a much cleaner implementation. Signed-off-by: Graham <gpe@openrs2.dev>
4 years ago
branchesSimplified = 0
constantsPropagated = 0
Replace DummyArgTransformer with new ConstantArgTransformer The new transformer uses a different approach to the old one. It starts exploring the call graph from the entry points, recursively analysing method calls. Methods are only re-analysed if their possible argument values change, with the Unknown value being used if we can't identify a single integer constant at a call site. This prevents us from recursing infinitely if the client code does. While this first pass does simplify branches in order to ignore dummy method calls that are never evaluated at runtime, it operates on a copy of the method (as we initially ignore more calls while the argument value sets are smaller, ignoring fewer calls as they build up). A separate second pass simplifies branches on the original method and inlines singleton constants, paving the way for the UnusedArgTransformer to actually remove the newly unused arguments. This new approach has several benefits: - It is much faster than the old approach, as we only re-analyse methods as required by argument value changes, rather than re-analysing every method during every pass. - It doesn't require special cases for dealing with mutually recursive dummy calls. The old approach hard-coded special cases for mutually recursive calls involving groups of 1 and 2 methods. The code for this wasn't clean. Furthermore, while it was just about good enough for the HD client, the SD client contains a mutually recursive group of 3 methods. The new approach is capable of dealing with mutually recursive groups of any size. Finally, the new transformer has a much cleaner implementation. Signed-off-by: Graham <gpe@openrs2.dev>
4 years ago
queueEntryPoints(classPath)
while (true) {
val method = pendingMethods.removeFirstOrNull() ?: break
analyzeMethod(classPath, method)
}
}
private fun queueEntryPoints(classPath: ClassPath) {
for (partition in inheritedMethodSets) {
Replace TypedRemapper constants with a more flexible system The new system will make it easier to port the deobfuscator to different revisions. There are two main changes: - The addition of a Profile class, which contains a list of excluded classes, methods and fields, and the maximum obfuscated name length. It is passed to Transformers that require it with dependency injection. - New ClassFilter and MemberFilter infrastructure. The MemberFilter class adds support for filtering fields and methods based on the owner and descriptor, in addition to the name. This makes the filters more precise than the previous system. It also supports globs, which makes it easy to filter whole groups of classes, fields and methods in one go. The Profile class uses a ClassFilter and MemberFilters to represent the list of excluded classes, methods and fields. A separate benefit is the addition of a separate entry points filter to the Profile class. Prior to this commit, many Transformers re-used the excluded method filter to find entry points, which is less precise (many of the excluded methods in 550 are not entry points). Support for filtering methods by owner and descriptor in addition to name allows the DEFAULT_PUBLIC_CTOR_CLASSES Set in VisibilityTransformer to be combined with the entry points filter. In the future it might be desirable to split the excluded method set into three separate sets: - One to represent methods that can't be renamed. - One to represent methods whose signature can't be changed. - One to represent methods that can't be removed. Signed-off-by: Graham <gpe@openrs2.dev>
4 years ago
if (isEntryPoint(classPath, partition)) {
Replace DummyArgTransformer with new ConstantArgTransformer The new transformer uses a different approach to the old one. It starts exploring the call graph from the entry points, recursively analysing method calls. Methods are only re-analysed if their possible argument values change, with the Unknown value being used if we can't identify a single integer constant at a call site. This prevents us from recursing infinitely if the client code does. While this first pass does simplify branches in order to ignore dummy method calls that are never evaluated at runtime, it operates on a copy of the method (as we initially ignore more calls while the argument value sets are smaller, ignoring fewer calls as they build up). A separate second pass simplifies branches on the original method and inlines singleton constants, paving the way for the UnusedArgTransformer to actually remove the newly unused arguments. This new approach has several benefits: - It is much faster than the old approach, as we only re-analyse methods as required by argument value changes, rather than re-analysing every method during every pass. - It doesn't require special cases for dealing with mutually recursive dummy calls. The old approach hard-coded special cases for mutually recursive calls involving groups of 1 and 2 methods. The code for this wasn't clean. Furthermore, while it was just about good enough for the HD client, the SD client contains a mutually recursive group of 3 methods. The new approach is capable of dealing with mutually recursive groups of any size. Finally, the new transformer has a much cleaner implementation. Signed-off-by: Graham <gpe@openrs2.dev>
4 years ago
pendingMethods.addAll(partition)
}
}
}
Replace TypedRemapper constants with a more flexible system The new system will make it easier to port the deobfuscator to different revisions. There are two main changes: - The addition of a Profile class, which contains a list of excluded classes, methods and fields, and the maximum obfuscated name length. It is passed to Transformers that require it with dependency injection. - New ClassFilter and MemberFilter infrastructure. The MemberFilter class adds support for filtering fields and methods based on the owner and descriptor, in addition to the name. This makes the filters more precise than the previous system. It also supports globs, which makes it easy to filter whole groups of classes, fields and methods in one go. The Profile class uses a ClassFilter and MemberFilters to represent the list of excluded classes, methods and fields. A separate benefit is the addition of a separate entry points filter to the Profile class. Prior to this commit, many Transformers re-used the excluded method filter to find entry points, which is less precise (many of the excluded methods in 550 are not entry points). Support for filtering methods by owner and descriptor in addition to name allows the DEFAULT_PUBLIC_CTOR_CLASSES Set in VisibilityTransformer to be combined with the entry points filter. In the future it might be desirable to split the excluded method set into three separate sets: - One to represent methods that can't be renamed. - One to represent methods whose signature can't be changed. - One to represent methods that can't be removed. Signed-off-by: Graham <gpe@openrs2.dev>
4 years ago
private fun isEntryPoint(classPath: ClassPath, partition: DisjointSet.Partition<MemberRef>): Boolean {
for (method in partition) {
val clazz = classPath[method.owner]!!
if (entryPoints.matches(method) || clazz.dependency) {
return true
}
}
return false
}
Replace DummyArgTransformer with new ConstantArgTransformer The new transformer uses a different approach to the old one. It starts exploring the call graph from the entry points, recursively analysing method calls. Methods are only re-analysed if their possible argument values change, with the Unknown value being used if we can't identify a single integer constant at a call site. This prevents us from recursing infinitely if the client code does. While this first pass does simplify branches in order to ignore dummy method calls that are never evaluated at runtime, it operates on a copy of the method (as we initially ignore more calls while the argument value sets are smaller, ignoring fewer calls as they build up). A separate second pass simplifies branches on the original method and inlines singleton constants, paving the way for the UnusedArgTransformer to actually remove the newly unused arguments. This new approach has several benefits: - It is much faster than the old approach, as we only re-analyse methods as required by argument value changes, rather than re-analysing every method during every pass. - It doesn't require special cases for dealing with mutually recursive dummy calls. The old approach hard-coded special cases for mutually recursive calls involving groups of 1 and 2 methods. The code for this wasn't clean. Furthermore, while it was just about good enough for the HD client, the SD client contains a mutually recursive group of 3 methods. The new approach is capable of dealing with mutually recursive groups of any size. Finally, the new transformer has a much cleaner implementation. Signed-off-by: Graham <gpe@openrs2.dev>
4 years ago
private fun analyzeMethod(classPath: ClassPath, ref: MemberRef) {
// find ClassNode/MethodNode
val owner = classPath.getClassNode(ref.owner) ?: return
Replace DummyArgTransformer with new ConstantArgTransformer The new transformer uses a different approach to the old one. It starts exploring the call graph from the entry points, recursively analysing method calls. Methods are only re-analysed if their possible argument values change, with the Unknown value being used if we can't identify a single integer constant at a call site. This prevents us from recursing infinitely if the client code does. While this first pass does simplify branches in order to ignore dummy method calls that are never evaluated at runtime, it operates on a copy of the method (as we initially ignore more calls while the argument value sets are smaller, ignoring fewer calls as they build up). A separate second pass simplifies branches on the original method and inlines singleton constants, paving the way for the UnusedArgTransformer to actually remove the newly unused arguments. This new approach has several benefits: - It is much faster than the old approach, as we only re-analyse methods as required by argument value changes, rather than re-analysing every method during every pass. - It doesn't require special cases for dealing with mutually recursive dummy calls. The old approach hard-coded special cases for mutually recursive calls involving groups of 1 and 2 methods. The code for this wasn't clean. Furthermore, while it was just about good enough for the HD client, the SD client contains a mutually recursive group of 3 methods. The new approach is capable of dealing with mutually recursive groups of any size. Finally, the new transformer has a much cleaner implementation. Signed-off-by: Graham <gpe@openrs2.dev>
4 years ago
val originalMethod = owner.methods.singleOrNull { it.name == ref.name && it.desc == ref.desc } ?: return
if (!originalMethod.hasCode) {
Replace DummyArgTransformer with new ConstantArgTransformer The new transformer uses a different approach to the old one. It starts exploring the call graph from the entry points, recursively analysing method calls. Methods are only re-analysed if their possible argument values change, with the Unknown value being used if we can't identify a single integer constant at a call site. This prevents us from recursing infinitely if the client code does. While this first pass does simplify branches in order to ignore dummy method calls that are never evaluated at runtime, it operates on a copy of the method (as we initially ignore more calls while the argument value sets are smaller, ignoring fewer calls as they build up). A separate second pass simplifies branches on the original method and inlines singleton constants, paving the way for the UnusedArgTransformer to actually remove the newly unused arguments. This new approach has several benefits: - It is much faster than the old approach, as we only re-analyse methods as required by argument value changes, rather than re-analysing every method during every pass. - It doesn't require special cases for dealing with mutually recursive dummy calls. The old approach hard-coded special cases for mutually recursive calls involving groups of 1 and 2 methods. The code for this wasn't clean. Furthermore, while it was just about good enough for the HD client, the SD client contains a mutually recursive group of 3 methods. The new approach is capable of dealing with mutually recursive groups of any size. Finally, the new transformer has a much cleaner implementation. Signed-off-by: Graham <gpe@openrs2.dev>
4 years ago
return
}
/*
* Clone the method - we don't want to mutate it permanently until the
* final pass, as we might discover more routes through the call graph
* later which reduce the number of branches we can simplify.
*/
val method = originalMethod.copy()
Replace DummyArgTransformer with new ConstantArgTransformer The new transformer uses a different approach to the old one. It starts exploring the call graph from the entry points, recursively analysing method calls. Methods are only re-analysed if their possible argument values change, with the Unknown value being used if we can't identify a single integer constant at a call site. This prevents us from recursing infinitely if the client code does. While this first pass does simplify branches in order to ignore dummy method calls that are never evaluated at runtime, it operates on a copy of the method (as we initially ignore more calls while the argument value sets are smaller, ignoring fewer calls as they build up). A separate second pass simplifies branches on the original method and inlines singleton constants, paving the way for the UnusedArgTransformer to actually remove the newly unused arguments. This new approach has several benefits: - It is much faster than the old approach, as we only re-analyse methods as required by argument value changes, rather than re-analysing every method during every pass. - It doesn't require special cases for dealing with mutually recursive dummy calls. The old approach hard-coded special cases for mutually recursive calls involving groups of 1 and 2 methods. The code for this wasn't clean. Furthermore, while it was just about good enough for the HD client, the SD client contains a mutually recursive group of 3 methods. The new approach is capable of dealing with mutually recursive groups of any size. Finally, the new transformer has a much cleaner implementation. Signed-off-by: Graham <gpe@openrs2.dev>
4 years ago
// find existing constant arguments
val args = getArgs(ref)
// simplify branches
simplifyBranches(owner, method, args)
/*
* Record new constant arguments in method calls. This re-runs the
* analyzer rather than re-using the frames from simplifyBranches. This
* ensures we ignore branches that always evaluate to false, preventing
* us from recording constant arguments found in dummy calls (which
* would prevent us from removing further dummy calls/branches).
*/
addArgValues(owner, method, args)
}
private fun getArgs(ref: MemberRef): Array<IntValueSet> {
val partition = inheritedMethodSets[ref]!!
val size = Type.getArgumentTypes(ref.desc).sumOf { it.size }
return Array(size) { i -> argValues[ArgPartition(partition, i)] ?: IntValueSet.Unknown }
Replace DummyArgTransformer with new ConstantArgTransformer The new transformer uses a different approach to the old one. It starts exploring the call graph from the entry points, recursively analysing method calls. Methods are only re-analysed if their possible argument values change, with the Unknown value being used if we can't identify a single integer constant at a call site. This prevents us from recursing infinitely if the client code does. While this first pass does simplify branches in order to ignore dummy method calls that are never evaluated at runtime, it operates on a copy of the method (as we initially ignore more calls while the argument value sets are smaller, ignoring fewer calls as they build up). A separate second pass simplifies branches on the original method and inlines singleton constants, paving the way for the UnusedArgTransformer to actually remove the newly unused arguments. This new approach has several benefits: - It is much faster than the old approach, as we only re-analyse methods as required by argument value changes, rather than re-analysing every method during every pass. - It doesn't require special cases for dealing with mutually recursive dummy calls. The old approach hard-coded special cases for mutually recursive calls involving groups of 1 and 2 methods. The code for this wasn't clean. Furthermore, while it was just about good enough for the HD client, the SD client contains a mutually recursive group of 3 methods. The new approach is capable of dealing with mutually recursive groups of any size. Finally, the new transformer has a much cleaner implementation. Signed-off-by: Graham <gpe@openrs2.dev>
4 years ago
}
private fun addArgValues(owner: ClassNode, method: MethodNode, args: Array<IntValueSet>) {
val analyzer = Analyzer(IntInterpreter(args))
val frames = analyzer.analyze(owner.name, method)
for ((i, frame) in frames.withIndex()) {
if (frame == null) {
continue
}
val insn = method.instructions[i]
if (insn !is MethodInsnNode) {
continue
}
val invokedMethod = inheritedMethodSets[MemberRef(insn)] ?: continue
val size = Type.getArgumentTypes(insn.desc).size
var index = 0
for (j in 0 until size) {
val value = frame.getStack(frame.stackSize - size + j)
if (addArgValues(ArgPartition(invokedMethod, index), value.set)) {
Replace DummyArgTransformer with new ConstantArgTransformer The new transformer uses a different approach to the old one. It starts exploring the call graph from the entry points, recursively analysing method calls. Methods are only re-analysed if their possible argument values change, with the Unknown value being used if we can't identify a single integer constant at a call site. This prevents us from recursing infinitely if the client code does. While this first pass does simplify branches in order to ignore dummy method calls that are never evaluated at runtime, it operates on a copy of the method (as we initially ignore more calls while the argument value sets are smaller, ignoring fewer calls as they build up). A separate second pass simplifies branches on the original method and inlines singleton constants, paving the way for the UnusedArgTransformer to actually remove the newly unused arguments. This new approach has several benefits: - It is much faster than the old approach, as we only re-analyse methods as required by argument value changes, rather than re-analysing every method during every pass. - It doesn't require special cases for dealing with mutually recursive dummy calls. The old approach hard-coded special cases for mutually recursive calls involving groups of 1 and 2 methods. The code for this wasn't clean. Furthermore, while it was just about good enough for the HD client, the SD client contains a mutually recursive group of 3 methods. The new approach is capable of dealing with mutually recursive groups of any size. Finally, the new transformer has a much cleaner implementation. Signed-off-by: Graham <gpe@openrs2.dev>
4 years ago
pendingMethods.addAll(invokedMethod)
}
index += value.size
}
if (size == 0 && arglessMethods.add(invokedMethod)) {
pendingMethods.addAll(invokedMethod)
}
}
}
private fun addArgValues(partition: ArgPartition, value: IntValueSet): Boolean {
val old = argValues[partition]
Replace DummyArgTransformer with new ConstantArgTransformer The new transformer uses a different approach to the old one. It starts exploring the call graph from the entry points, recursively analysing method calls. Methods are only re-analysed if their possible argument values change, with the Unknown value being used if we can't identify a single integer constant at a call site. This prevents us from recursing infinitely if the client code does. While this first pass does simplify branches in order to ignore dummy method calls that are never evaluated at runtime, it operates on a copy of the method (as we initially ignore more calls while the argument value sets are smaller, ignoring fewer calls as they build up). A separate second pass simplifies branches on the original method and inlines singleton constants, paving the way for the UnusedArgTransformer to actually remove the newly unused arguments. This new approach has several benefits: - It is much faster than the old approach, as we only re-analyse methods as required by argument value changes, rather than re-analysing every method during every pass. - It doesn't require special cases for dealing with mutually recursive dummy calls. The old approach hard-coded special cases for mutually recursive calls involving groups of 1 and 2 methods. The code for this wasn't clean. Furthermore, while it was just about good enough for the HD client, the SD client contains a mutually recursive group of 3 methods. The new approach is capable of dealing with mutually recursive groups of any size. Finally, the new transformer has a much cleaner implementation. Signed-off-by: Graham <gpe@openrs2.dev>
4 years ago
val new = if (value.singleton != null) {
if (old != null) {
old union value
} else {
value
}
} else {
IntValueSet.Unknown
}
argValues[partition] = new
Replace DummyArgTransformer with new ConstantArgTransformer The new transformer uses a different approach to the old one. It starts exploring the call graph from the entry points, recursively analysing method calls. Methods are only re-analysed if their possible argument values change, with the Unknown value being used if we can't identify a single integer constant at a call site. This prevents us from recursing infinitely if the client code does. While this first pass does simplify branches in order to ignore dummy method calls that are never evaluated at runtime, it operates on a copy of the method (as we initially ignore more calls while the argument value sets are smaller, ignoring fewer calls as they build up). A separate second pass simplifies branches on the original method and inlines singleton constants, paving the way for the UnusedArgTransformer to actually remove the newly unused arguments. This new approach has several benefits: - It is much faster than the old approach, as we only re-analyse methods as required by argument value changes, rather than re-analysing every method during every pass. - It doesn't require special cases for dealing with mutually recursive dummy calls. The old approach hard-coded special cases for mutually recursive calls involving groups of 1 and 2 methods. The code for this wasn't clean. Furthermore, while it was just about good enough for the HD client, the SD client contains a mutually recursive group of 3 methods. The new approach is capable of dealing with mutually recursive groups of any size. Finally, the new transformer has a much cleaner implementation. Signed-off-by: Graham <gpe@openrs2.dev>
4 years ago
return old != new
}
private fun simplifyBranches(owner: ClassNode, method: MethodNode, args: Array<IntValueSet>): Int {
val analyzer = Analyzer(IntInterpreter(args))
val frames = analyzer.analyze(owner.name, method)
val alwaysTakenBranches = mutableListOf<JumpInsnNode>()
val neverTakenBranches = mutableListOf<JumpInsnNode>()
frame@ for ((i, frame) in frames.withIndex()) {
if (frame == null) {
continue
}
val insn = method.instructions[i]
if (insn !is JumpInsnNode) {
continue
}
when (insn.opcode) {
IFEQ, IFNE, IFLT, IFGE, IFGT, IFLE -> {
val value = frame.getStack(frame.stackSize - 1)
if (value.set !is IntValueSet.Constant) {
continue@frame
}
@Suppress("NON_EXHAUSTIVE_WHEN")
when (IntBranch.evaluateUnary(insn.opcode, value.set.values)) {
ALWAYS_TAKEN -> alwaysTakenBranches += insn
NEVER_TAKEN -> neverTakenBranches += insn
}
}
IF_ICMPEQ, IF_ICMPNE, IF_ICMPLT, IF_ICMPGE, IF_ICMPGT, IF_ICMPLE -> {
val value1 = frame.getStack(frame.stackSize - 2)
val value2 = frame.getStack(frame.stackSize - 1)
if (value1.set !is IntValueSet.Constant || value2.set !is IntValueSet.Constant) {
continue@frame
}
@Suppress("NON_EXHAUSTIVE_WHEN")
when (IntBranch.evaluateBinary(insn.opcode, value1.set.values, value2.set.values)) {
ALWAYS_TAKEN -> alwaysTakenBranches += insn
NEVER_TAKEN -> neverTakenBranches += insn
}
}
}
}
var simplified = 0
for (insn in alwaysTakenBranches) {
val replacement = JumpInsnNode(GOTO, insn.label)
if (method.instructions.replaceExpression(insn, replacement, AbstractInsnNode::isPure)) {
Replace DummyArgTransformer with new ConstantArgTransformer The new transformer uses a different approach to the old one. It starts exploring the call graph from the entry points, recursively analysing method calls. Methods are only re-analysed if their possible argument values change, with the Unknown value being used if we can't identify a single integer constant at a call site. This prevents us from recursing infinitely if the client code does. While this first pass does simplify branches in order to ignore dummy method calls that are never evaluated at runtime, it operates on a copy of the method (as we initially ignore more calls while the argument value sets are smaller, ignoring fewer calls as they build up). A separate second pass simplifies branches on the original method and inlines singleton constants, paving the way for the UnusedArgTransformer to actually remove the newly unused arguments. This new approach has several benefits: - It is much faster than the old approach, as we only re-analyse methods as required by argument value changes, rather than re-analysing every method during every pass. - It doesn't require special cases for dealing with mutually recursive dummy calls. The old approach hard-coded special cases for mutually recursive calls involving groups of 1 and 2 methods. The code for this wasn't clean. Furthermore, while it was just about good enough for the HD client, the SD client contains a mutually recursive group of 3 methods. The new approach is capable of dealing with mutually recursive groups of any size. Finally, the new transformer has a much cleaner implementation. Signed-off-by: Graham <gpe@openrs2.dev>
4 years ago
simplified++
}
}
for (insn in neverTakenBranches) {
if (method.instructions.deleteExpression(insn, AbstractInsnNode::isPure)) {
Replace DummyArgTransformer with new ConstantArgTransformer The new transformer uses a different approach to the old one. It starts exploring the call graph from the entry points, recursively analysing method calls. Methods are only re-analysed if their possible argument values change, with the Unknown value being used if we can't identify a single integer constant at a call site. This prevents us from recursing infinitely if the client code does. While this first pass does simplify branches in order to ignore dummy method calls that are never evaluated at runtime, it operates on a copy of the method (as we initially ignore more calls while the argument value sets are smaller, ignoring fewer calls as they build up). A separate second pass simplifies branches on the original method and inlines singleton constants, paving the way for the UnusedArgTransformer to actually remove the newly unused arguments. This new approach has several benefits: - It is much faster than the old approach, as we only re-analyse methods as required by argument value changes, rather than re-analysing every method during every pass. - It doesn't require special cases for dealing with mutually recursive dummy calls. The old approach hard-coded special cases for mutually recursive calls involving groups of 1 and 2 methods. The code for this wasn't clean. Furthermore, while it was just about good enough for the HD client, the SD client contains a mutually recursive group of 3 methods. The new approach is capable of dealing with mutually recursive groups of any size. Finally, the new transformer has a much cleaner implementation. Signed-off-by: Graham <gpe@openrs2.dev>
4 years ago
simplified++
}
}
return simplified
}
private fun propagateConstantArgs(
classPath: ClassPath,
clazz: ClassNode,
method: MethodNode,
args: Array<IntValueSet>
): Int {
val partition = inheritedMethodSets[MemberRef(clazz, method)]!!
if (!MethodMappingGenerator.isRenamable(classPath, profile.excludedMethods, partition)) {
return 0
}
Replace DummyArgTransformer with new ConstantArgTransformer The new transformer uses a different approach to the old one. It starts exploring the call graph from the entry points, recursively analysing method calls. Methods are only re-analysed if their possible argument values change, with the Unknown value being used if we can't identify a single integer constant at a call site. This prevents us from recursing infinitely if the client code does. While this first pass does simplify branches in order to ignore dummy method calls that are never evaluated at runtime, it operates on a copy of the method (as we initially ignore more calls while the argument value sets are smaller, ignoring fewer calls as they build up). A separate second pass simplifies branches on the original method and inlines singleton constants, paving the way for the UnusedArgTransformer to actually remove the newly unused arguments. This new approach has several benefits: - It is much faster than the old approach, as we only re-analyse methods as required by argument value changes, rather than re-analysing every method during every pass. - It doesn't require special cases for dealing with mutually recursive dummy calls. The old approach hard-coded special cases for mutually recursive calls involving groups of 1 and 2 methods. The code for this wasn't clean. Furthermore, while it was just about good enough for the HD client, the SD client contains a mutually recursive group of 3 methods. The new approach is capable of dealing with mutually recursive groups of any size. Finally, the new transformer has a much cleaner implementation. Signed-off-by: Graham <gpe@openrs2.dev>
4 years ago
val analyzer = Analyzer(IntInterpreter(args))
val frames = analyzer.analyze(clazz.name, method)
val constInsns = mutableMapOf<AbstractInsnNode, Int>()
for ((i, frame) in frames.withIndex()) {
if (frame == null) {
continue
}
val insn = method.instructions[i]
if (insn.intConstant != null) {
// already constant
continue
} else if (!insn.isPure) {
Replace DummyArgTransformer with new ConstantArgTransformer The new transformer uses a different approach to the old one. It starts exploring the call graph from the entry points, recursively analysing method calls. Methods are only re-analysed if their possible argument values change, with the Unknown value being used if we can't identify a single integer constant at a call site. This prevents us from recursing infinitely if the client code does. While this first pass does simplify branches in order to ignore dummy method calls that are never evaluated at runtime, it operates on a copy of the method (as we initially ignore more calls while the argument value sets are smaller, ignoring fewer calls as they build up). A separate second pass simplifies branches on the original method and inlines singleton constants, paving the way for the UnusedArgTransformer to actually remove the newly unused arguments. This new approach has several benefits: - It is much faster than the old approach, as we only re-analyse methods as required by argument value changes, rather than re-analysing every method during every pass. - It doesn't require special cases for dealing with mutually recursive dummy calls. The old approach hard-coded special cases for mutually recursive calls involving groups of 1 and 2 methods. The code for this wasn't clean. Furthermore, while it was just about good enough for the HD client, the SD client contains a mutually recursive group of 3 methods. The new approach is capable of dealing with mutually recursive groups of any size. Finally, the new transformer has a much cleaner implementation. Signed-off-by: Graham <gpe@openrs2.dev>
4 years ago
// can't replace instructions with a side effect
continue
} else if (insn.stackMetadata.pushes != 1) {
Replace DummyArgTransformer with new ConstantArgTransformer The new transformer uses a different approach to the old one. It starts exploring the call graph from the entry points, recursively analysing method calls. Methods are only re-analysed if their possible argument values change, with the Unknown value being used if we can't identify a single integer constant at a call site. This prevents us from recursing infinitely if the client code does. While this first pass does simplify branches in order to ignore dummy method calls that are never evaluated at runtime, it operates on a copy of the method (as we initially ignore more calls while the argument value sets are smaller, ignoring fewer calls as they build up). A separate second pass simplifies branches on the original method and inlines singleton constants, paving the way for the UnusedArgTransformer to actually remove the newly unused arguments. This new approach has several benefits: - It is much faster than the old approach, as we only re-analyse methods as required by argument value changes, rather than re-analysing every method during every pass. - It doesn't require special cases for dealing with mutually recursive dummy calls. The old approach hard-coded special cases for mutually recursive calls involving groups of 1 and 2 methods. The code for this wasn't clean. Furthermore, while it was just about good enough for the HD client, the SD client contains a mutually recursive group of 3 methods. The new approach is capable of dealing with mutually recursive groups of any size. Finally, the new transformer has a much cleaner implementation. Signed-off-by: Graham <gpe@openrs2.dev>
4 years ago
// can't replace instructions pushing more than one result
continue
}
// the value pushed by this instruction is held in the following frame
val nextInsn = insn.nextReal ?: continue
val nextInsnIndex = method.instructions.indexOf(nextInsn)
val nextFrame = frames[nextInsnIndex]
val value = nextFrame.getStack(nextFrame.stackSize - 1)
val singleton = value.set.singleton
if (singleton != null) {
constInsns[insn] = singleton
}
}
var inlined = 0
for ((insn, value) in constInsns) {
if (insn !in method.instructions) {
continue
}
val replacement = value.toAbstractInsnNode()
if (method.instructions.replaceExpression(insn, replacement, AbstractInsnNode::isPure)) {
Replace DummyArgTransformer with new ConstantArgTransformer The new transformer uses a different approach to the old one. It starts exploring the call graph from the entry points, recursively analysing method calls. Methods are only re-analysed if their possible argument values change, with the Unknown value being used if we can't identify a single integer constant at a call site. This prevents us from recursing infinitely if the client code does. While this first pass does simplify branches in order to ignore dummy method calls that are never evaluated at runtime, it operates on a copy of the method (as we initially ignore more calls while the argument value sets are smaller, ignoring fewer calls as they build up). A separate second pass simplifies branches on the original method and inlines singleton constants, paving the way for the UnusedArgTransformer to actually remove the newly unused arguments. This new approach has several benefits: - It is much faster than the old approach, as we only re-analyse methods as required by argument value changes, rather than re-analysing every method during every pass. - It doesn't require special cases for dealing with mutually recursive dummy calls. The old approach hard-coded special cases for mutually recursive calls involving groups of 1 and 2 methods. The code for this wasn't clean. Furthermore, while it was just about good enough for the HD client, the SD client contains a mutually recursive group of 3 methods. The new approach is capable of dealing with mutually recursive groups of any size. Finally, the new transformer has a much cleaner implementation. Signed-off-by: Graham <gpe@openrs2.dev>
4 years ago
inlined++
}
}
return inlined
}
override fun transformCode(classPath: ClassPath, library: Library, clazz: ClassNode, method: MethodNode): Boolean {
val args = getArgs(MemberRef(clazz, method))
branchesSimplified += simplifyBranches(clazz, method, args)
constantsPropagated += propagateConstantArgs(classPath, clazz, method, args)
Replace DummyArgTransformer with new ConstantArgTransformer The new transformer uses a different approach to the old one. It starts exploring the call graph from the entry points, recursively analysing method calls. Methods are only re-analysed if their possible argument values change, with the Unknown value being used if we can't identify a single integer constant at a call site. This prevents us from recursing infinitely if the client code does. While this first pass does simplify branches in order to ignore dummy method calls that are never evaluated at runtime, it operates on a copy of the method (as we initially ignore more calls while the argument value sets are smaller, ignoring fewer calls as they build up). A separate second pass simplifies branches on the original method and inlines singleton constants, paving the way for the UnusedArgTransformer to actually remove the newly unused arguments. This new approach has several benefits: - It is much faster than the old approach, as we only re-analyse methods as required by argument value changes, rather than re-analysing every method during every pass. - It doesn't require special cases for dealing with mutually recursive dummy calls. The old approach hard-coded special cases for mutually recursive calls involving groups of 1 and 2 methods. The code for this wasn't clean. Furthermore, while it was just about good enough for the HD client, the SD client contains a mutually recursive group of 3 methods. The new approach is capable of dealing with mutually recursive groups of any size. Finally, the new transformer has a much cleaner implementation. Signed-off-by: Graham <gpe@openrs2.dev>
4 years ago
return false
}
override fun postTransform(classPath: ClassPath) {
logger.info { "Simplified $branchesSimplified branches and propagated $constantsPropagated constants" }
Replace DummyArgTransformer with new ConstantArgTransformer The new transformer uses a different approach to the old one. It starts exploring the call graph from the entry points, recursively analysing method calls. Methods are only re-analysed if their possible argument values change, with the Unknown value being used if we can't identify a single integer constant at a call site. This prevents us from recursing infinitely if the client code does. While this first pass does simplify branches in order to ignore dummy method calls that are never evaluated at runtime, it operates on a copy of the method (as we initially ignore more calls while the argument value sets are smaller, ignoring fewer calls as they build up). A separate second pass simplifies branches on the original method and inlines singleton constants, paving the way for the UnusedArgTransformer to actually remove the newly unused arguments. This new approach has several benefits: - It is much faster than the old approach, as we only re-analyse methods as required by argument value changes, rather than re-analysing every method during every pass. - It doesn't require special cases for dealing with mutually recursive dummy calls. The old approach hard-coded special cases for mutually recursive calls involving groups of 1 and 2 methods. The code for this wasn't clean. Furthermore, while it was just about good enough for the HD client, the SD client contains a mutually recursive group of 3 methods. The new approach is capable of dealing with mutually recursive groups of any size. Finally, the new transformer has a much cleaner implementation. Signed-off-by: Graham <gpe@openrs2.dev>
4 years ago
}
private companion object {
Replace DummyArgTransformer with new ConstantArgTransformer The new transformer uses a different approach to the old one. It starts exploring the call graph from the entry points, recursively analysing method calls. Methods are only re-analysed if their possible argument values change, with the Unknown value being used if we can't identify a single integer constant at a call site. This prevents us from recursing infinitely if the client code does. While this first pass does simplify branches in order to ignore dummy method calls that are never evaluated at runtime, it operates on a copy of the method (as we initially ignore more calls while the argument value sets are smaller, ignoring fewer calls as they build up). A separate second pass simplifies branches on the original method and inlines singleton constants, paving the way for the UnusedArgTransformer to actually remove the newly unused arguments. This new approach has several benefits: - It is much faster than the old approach, as we only re-analyse methods as required by argument value changes, rather than re-analysing every method during every pass. - It doesn't require special cases for dealing with mutually recursive dummy calls. The old approach hard-coded special cases for mutually recursive calls involving groups of 1 and 2 methods. The code for this wasn't clean. Furthermore, while it was just about good enough for the HD client, the SD client contains a mutually recursive group of 3 methods. The new approach is capable of dealing with mutually recursive groups of any size. Finally, the new transformer has a much cleaner implementation. Signed-off-by: Graham <gpe@openrs2.dev>
4 years ago
private val logger = InlineLogger()
}
}