99import org .objectweb .asm .tree .ClassNode ;
1010import org .objectweb .asm .tree .InsnList ;
1111import org .objectweb .asm .tree .InsnNode ;
12+ import org .objectweb .asm .tree .JumpInsnNode ;
1213import org .objectweb .asm .tree .LabelNode ;
1314import org .objectweb .asm .tree .MethodNode ;
1415import org .objectweb .asm .tree .MultiANewArrayInsnNode ;
1516import org .objectweb .asm .tree .TryCatchBlockNode ;
1617import org .objectweb .asm .tree .TypeInsnNode ;
1718import org .objectweb .asm .tree .analysis .Frame ;
1819import software .coley .recaf .info .JvmClassInfo ;
19- import software .coley .recaf .services .assembler .ExpressionCompileException ;
2020import software .coley .recaf .services .inheritance .InheritanceGraph ;
2121import software .coley .recaf .services .inheritance .InheritanceGraphService ;
2222import software .coley .recaf .services .inheritance .InheritanceVertex ;
4343import java .util .HashMap ;
4444import java .util .HashSet ;
4545import java .util .IdentityHashMap ;
46+ import java .util .Iterator ;
4647import java .util .List ;
4748import java .util .Map ;
49+ import java .util .Objects ;
4850import java .util .OptionalInt ;
4951import java .util .Set ;
52+ import java .util .stream .Collectors ;
5053
5154import static org .objectweb .asm .Opcodes .*;
5255
@@ -93,14 +96,69 @@ public void transform(@Nonnull JvmTransformerContext context, @Nonnull Workspace
9396 if (method .instructions == null || method .tryCatchBlocks == null || method .tryCatchBlocks .isEmpty ())
9497 continue ;
9598
96- dirty |= pass0PruneNeverThrown (context , workspace , node , method );
97- dirty |= pass1PruneNeverThrowingOrDuplicate (context , node , method );
98- dirty |= pass2ConvertOpaqueThrowToDirectFlow ( );
99+ dirty |= pass0PruneIgnoredHandlers (context , node , method );
100+ dirty |= pass1PruneNeverThrown (context , workspace , node , method );
101+ dirty |= pass2PruneNeverThrowingOrDuplicate ( context , node , method );
99102 }
100103 if (dirty )
101104 context .setNode (bundle , initialClassState , node );
102105 }
103106
107+ /**
108+ * Remove try-catch blocks that cannot possibly be utilized at runtime.
109+ *
110+ * @param context
111+ * Transformer context.
112+ * @param node
113+ * Defining class.
114+ * @param method
115+ * Method to transform.
116+ *
117+ * @return {@code true} when one or more try-catch blocks have been removed.
118+ *
119+ * @throws TransformationException
120+ * Thrown when dead code after transformation could not be pruned.
121+ */
122+ private boolean pass0PruneIgnoredHandlers (@ Nonnull JvmTransformerContext context , @ Nonnull ClassNode node , @ Nonnull MethodNode method ) throws TransformationException {
123+ // Given the following {start, end, handler, ex-type} blocks:
124+ // { R, S, Q, * },
125+ // { R, S, C, * },
126+ // { R, S, S, Ljava/lang/ArrayIndexOutOfBoundsException; }
127+ // Only the first is going to be used.
128+ // - It appears first, so it will be checked first by the JVM
129+ // - Its range covers all possible instructions of the other two try blocks
130+ // - Its handled type is more generic ("*" is catch-all)
131+ // See: https://github.com/openjdk/jdk21u/blob/master/src/hotspot/share/oops/method.cpp#L227
132+ //
133+ // Process:
134+ // 1. Collect try-catch handlers keyed by their range
135+ // 2. Prune handlers of narrower types in the collection
136+ // 3. Retain only remaining handlers in the collection
137+ List <TryCatchBlockNode > blocks = new ArrayList <>(method .tryCatchBlocks );
138+ Map <ThrowingRange , Handlers > handlersMap = new HashMap <>();
139+ for (TryCatchBlockNode block : blocks ) {
140+ int start = AsmInsnUtil .indexOf (block .start );
141+ int end = AsmInsnUtil .indexOf (block .end );
142+ if (start < end ) {
143+ ThrowingRange range = new ThrowingRange (start , end );
144+ handlersMap .computeIfAbsent (range , r -> new Handlers ()).addBlock (block );
145+ }
146+ }
147+ for (Handlers handlers : handlersMap .values ())
148+ handlers .prune (inheritanceGraph );
149+ Set <TryCatchBlockNode > allHandlers = handlersMap .values ()
150+ .stream ()
151+ .flatMap (handlers -> handlers .blocks .stream ())
152+ .collect (Collectors .toSet ());
153+ if (method .tryCatchBlocks .retainAll (allHandlers )) {
154+ // Removing handlers can mean blocks starting with an expected 'Throwable' on the stack are now invalid.
155+ // These should be dead code though, so if we prune code that isn't visitable these should go away.
156+ context .pruneDeadCode (node , method );
157+ return true ;
158+ }
159+ return false ;
160+ }
161+
104162 /**
105163 * Remove try-catch blocks that have handle exception types that are defined in the workspace
106164 * but never actually constructed and thrown.
@@ -117,9 +175,10 @@ public void transform(@Nonnull JvmTransformerContext context, @Nonnull Workspace
117175 * @return {@code true} when one or more try-catch blocks have been removed.
118176 *
119177 * @throws TransformationException
120- * Thrown when the {@link ExpressionCompileException} cannot be found in the transformer context.
178+ * Thrown when the {@link ExceptionCollectionTransformer} cannot be found in the transformer context,
179+ * or when dead code couldn't be pruned.
121180 */
122- private boolean pass0PruneNeverThrown (@ Nonnull JvmTransformerContext context , @ Nonnull Workspace workspace , @ Nonnull ClassNode node , @ Nonnull MethodNode method ) throws TransformationException {
181+ private boolean pass1PruneNeverThrown (@ Nonnull JvmTransformerContext context , @ Nonnull Workspace workspace , @ Nonnull ClassNode node , @ Nonnull MethodNode method ) throws TransformationException {
123182 ExceptionCollectionTransformer exceptions = context .getJvmTransformer (ExceptionCollectionTransformer .class );
124183
125184 // Collect which blocks are candidates for removal.
@@ -228,7 +287,7 @@ private boolean pass0PruneNeverThrown(@Nonnull JvmTransformerContext context, @N
228287 * @throws TransformationException
229288 * Thrown when code cannot be analyzed <i>(Needed for certain checks)</i>.
230289 */
231- private boolean pass1PruneNeverThrowingOrDuplicate (@ Nonnull JvmTransformerContext context , @ Nonnull ClassNode node , @ Nonnull MethodNode method ) throws TransformationException {
290+ private boolean pass2PruneNeverThrowingOrDuplicate (@ Nonnull JvmTransformerContext context , @ Nonnull ClassNode node , @ Nonnull MethodNode method ) throws TransformationException {
232291 InsnList instructions = method .instructions ;
233292 List <TryCatchBlockNode > tryCatchBlocks = method .tryCatchBlocks ;
234293 Frame <ReValue >[] frames = context .analyze (inheritanceGraph , node , method );
@@ -260,6 +319,7 @@ private boolean pass1PruneNeverThrowingOrDuplicate(@Nonnull JvmTransformerContex
260319 case FALOAD :
261320 case LALOAD :
262321 case SALOAD :
322+ case BALOAD :
263323 case AALOAD : {
264324 ReValue indexValue = frame .getStack (frame .getStackSize () - 1 );
265325 ReValue arrayValue = frame .getStack (frame .getStackSize () - 2 );
@@ -298,6 +358,7 @@ private boolean pass1PruneNeverThrowingOrDuplicate(@Nonnull JvmTransformerContex
298358 }
299359 case IASTORE :
300360 case DASTORE :
361+ case BASTORE :
301362 case FASTORE :
302363 case LASTORE :
303364 case SASTORE :
@@ -633,7 +694,10 @@ && doesHandleException(tryCatch, EX_CCE)) {
633694 return false ;
634695 }
635696
636- private boolean pass2ConvertOpaqueThrowToDirectFlow () {
697+ private boolean pass3ConvertOpaqueThrowToDirectFlow (@ Nonnull JvmTransformerContext context , @ Nonnull ClassNode node , @ Nonnull MethodNode method ) {
698+ // TODO: Rather than make this a separate pass, I think we can work the intended behavior outlined here into the prior pass
699+ if (true ) return false ;
700+
637701 // TODO: Look for try blocks that end in 'throw T' with a 'catch T' handler
638702 // - Must always take the path
639703 // - Not always direct, can be '1 / 0' with a 'catch MathError' handler
@@ -642,6 +706,30 @@ private boolean pass2ConvertOpaqueThrowToDirectFlow() {
642706 // - Worst case, it relies on stack info from the thrown exception
643707 // (we can keep the 'new T' or replace the throwing '1 / 0' with 'new T')
644708 // - If found, replace the throwing code with a 'goto handler'
709+ InsnList instructions = method .instructions ;
710+ for (TryCatchBlockNode tryCatch : new ArrayList <>(method .tryCatchBlocks )) {
711+ int start = instructions .indexOf (tryCatch .start );
712+ int end = instructions .indexOf (tryCatch .end );
713+
714+ // TODO: Validate that the block WILL flow into a 'throw' case
715+ // - Same code as prior pass?
716+ boolean willThrow = true ;
717+ Set <AbstractInsnNode > throwingInstructions = Collections .newSetFromMap (new IdentityHashMap <>());
718+ for (int i = start ; i < end ; i ++) {
719+ // TODO: Mark willThow false if control flow leads to path where no-100% throwing behavior is observed
720+ }
721+
722+ // If the try range of the block WILL throw, we can replace the offending instructions with jumps to the handler block
723+ if (willThrow && !throwingInstructions .isEmpty ()) {
724+ for (AbstractInsnNode thrower : throwingInstructions ) {
725+ // TODO: If exception is not already on stack top, replace with junk exception (null is not a good replacement)
726+ instructions .insertBefore (thrower , new InsnNode (ACONST_NULL ));
727+ instructions .insertBefore (thrower , new JumpInsnNode (GOTO , tryCatch .handler ));
728+ }
729+ method .tryCatchBlocks .remove (tryCatch );
730+ }
731+ }
732+
645733 return false ;
646734 }
647735
@@ -737,6 +825,16 @@ boolean canThrow() {
737825 */
738826 private record Block (@ Nullable String type , int start , int end , int handler ) {}
739827
828+ /**
829+ * Range of some code.
830+ *
831+ * @param start
832+ * Start label.
833+ * @param end
834+ * End label.
835+ */
836+ private record Range (@ Nonnull LabelNode start , @ Nonnull LabelNode end ) {}
837+
740838 /**
741839 * Range of instructions that can possibly throw exceptions.
742840 *
@@ -751,4 +849,50 @@ public ThrowingRange merge(@Nonnull ThrowingRange other) {
751849 return new ThrowingRange (Math .min (first , other .first ), Math .max (last , other .last ));
752850 }
753851 }
852+
853+ /**
854+ * Collection of try catch blocks.
855+ *
856+ * @param blocks
857+ * Wrapped list of blocks.
858+ * @param seenTypes
859+ * Observed types handled by the blocks.
860+ */
861+ private record Handlers (@ Nonnull List <TryCatchBlockNode > blocks , @ Nonnull Set <String > seenTypes ) {
862+ private Handlers () {
863+ this (new ArrayList <>(), new HashSet <>());
864+ }
865+
866+ /**
867+ * @param block
868+ * Block to add.
869+ */
870+ public void addBlock (@ Nonnull TryCatchBlockNode block ) {
871+ blocks .add (block );
872+ }
873+
874+ /**
875+ * Remove entries from {@link #blocks} that are redundant.
876+ *
877+ * @param graph
878+ * Inheritance graph for classes in the workspace.
879+ */
880+ public void prune (@ Nonnull InheritanceGraph graph ) {
881+ Iterator <TryCatchBlockNode > it = blocks .iterator ();
882+ while (it .hasNext ()) {
883+ TryCatchBlockNode block = it .next ();
884+ String handledType = Objects .requireNonNullElse (block .type , "java/lang/Object" );
885+ inner :
886+ {
887+ for (String seenType : seenTypes ) {
888+ if (graph .isAssignableFrom (seenType , handledType )) {
889+ it .remove ();
890+ break inner ;
891+ }
892+ }
893+ seenTypes .add (handledType );
894+ }
895+ }
896+ }
897+ }
754898}
0 commit comments