-
Notifications
You must be signed in to change notification settings - Fork 135
Convert lambdas to method references when javac compiles a method reference to a lambda #532
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop/1.12.0
Are you sure you want to change the base?
Changes from 3 commits
d6436aa
e3b28ed
762f0fb
552ab6f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,260 @@ | ||
| package org.jetbrains.java.decompiler.modules.decompiler; | ||
|
|
||
| import org.jetbrains.java.decompiler.code.CodeConstants; | ||
| import org.jetbrains.java.decompiler.code.FullInstructionSequence; | ||
| import org.jetbrains.java.decompiler.code.Instruction; | ||
| import org.jetbrains.java.decompiler.main.ClassesProcessor; | ||
| import org.jetbrains.java.decompiler.main.ClassesProcessor.ClassNode; | ||
| import org.jetbrains.java.decompiler.main.ClassesProcessor.ClassNode.LambdaInformation; | ||
| import org.jetbrains.java.decompiler.main.DecompilerContext; | ||
| import org.jetbrains.java.decompiler.modules.decompiler.exps.*; | ||
| import org.jetbrains.java.decompiler.modules.decompiler.stats.RootStatement; | ||
| import org.jetbrains.java.decompiler.modules.decompiler.stats.Statement; | ||
| import org.jetbrains.java.decompiler.struct.StructClass; | ||
| import org.jetbrains.java.decompiler.struct.StructMethod; | ||
| import org.jetbrains.java.decompiler.struct.consts.LinkConstant; | ||
| import org.jetbrains.java.decompiler.util.InterpreterUtil; | ||
|
|
||
| import java.io.IOException; | ||
| import java.util.Iterator; | ||
|
|
||
| public class MethodReferenceHelper { | ||
| public static boolean convertToMethodReference(RootStatement root) throws IOException { | ||
| return convertToMethodReferenceRec(root); | ||
| } | ||
|
|
||
| public static boolean convertToMethodReferenceRec(Statement stat) throws IOException { | ||
| boolean result = false; | ||
| for (Statement subStat : stat.getStats()) { | ||
| result |= convertToMethodReferenceRec(subStat); | ||
| } | ||
| if (stat.getExprents() != null) { | ||
| for (int i = 0; i < stat.getExprents().size(); i++) { | ||
| Exprent exp = stat.getExprents().get(i); | ||
| for (Exprent subExp : exp.getAllExprents(true)) { | ||
| if (convertToMethodReference(stat, i, subExp)) { | ||
| result |= true; | ||
| } | ||
| for (int j = 0; j < stat.getExprents().size(); j++) { | ||
| if (stat.getExprents().get(j) == exp) { | ||
| i = j; | ||
| break; | ||
| } | ||
| } | ||
| if (removeInstanceAssignment(stat, i, subExp)) { | ||
| result |= true; | ||
| } | ||
| for (int j = 0; j < stat.getExprents().size(); j++) { | ||
| if (stat.getExprents().get(j) == exp) { | ||
| i = j; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| private static boolean convertToMethodReference(Statement stat, int i, Exprent exp) throws IOException { | ||
| if (!(exp instanceof NewExprent newExprent) | ||
| || !newExprent.isLambda() | ||
| || newExprent.isMethodReference()) | ||
| return false; | ||
| String className = newExprent.getNewType().value; | ||
| ClassNode node = DecompilerContext.getClassProcessor().getMapRootClasses().get(className); | ||
| LambdaInformation info = node.lambdaInformation; | ||
| StructClass struct = DecompilerContext.getStructContext().getClass(info.content_class_name); | ||
| StructMethod method = struct.getMethod(info.content_method_key); | ||
| if (!method.hasModifier(CodeConstants.ACC_STATIC)) | ||
| return false; | ||
| method.expandData(struct); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, does this need to manually parse the instructions or would going through the exprents suffice?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The I did consider doing it when the lambda is written to the output like with array constructor references but that would prevent inlining the variable since if it happened when the lambda is written nothing in |
||
| FullInstructionSequence seq = method.getInstructionSequence(); | ||
| Iterator<Instruction> iterator = seq.iterator(); | ||
| int argument = 0; | ||
|
|
||
| if (!iterator.hasNext()) | ||
| return false; | ||
| Instruction next = iterator.next(); | ||
|
|
||
| // new instance | ||
| if (next.opcode == CodeConstants.opc_new) { | ||
| if (!iterator.hasNext()) | ||
| return false; | ||
| next = iterator.next(); | ||
| if (next.opcode != CodeConstants.opc_dup) | ||
| return false; | ||
|
|
||
| if (!iterator.hasNext()) | ||
| return false; | ||
| next = iterator.next(); | ||
| } | ||
|
|
||
| // Load arguments | ||
| while (iterator.hasNext() | ||
| && next.opcode >= CodeConstants.opc_iload && next.opcode <= CodeConstants.opc_aload | ||
| && next.operand(0) == argument) { | ||
| next = iterator.next(); | ||
| argument++; | ||
| } | ||
|
|
||
| boolean varargs = false; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is this code doing?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
| int varargsCount = 0; | ||
| // varargs array length | ||
| if (next.opcode >= CodeConstants.opc_bipush && next.opcode <= CodeConstants.opc_sipush) { | ||
| varargsCount = next.operand(0); | ||
| varargs = true; | ||
|
|
||
| // varargs array creation | ||
| if (!iterator.hasNext()) | ||
| return false; | ||
| next = iterator.next(); | ||
| if (next.opcode < CodeConstants.opc_newarray || next.opcode > CodeConstants.opc_anewarray) | ||
| return false; | ||
| for (int j = 0; j < varargsCount; j++) { | ||
|
|
||
| // duplicate varargs array | ||
| if (!iterator.hasNext()) | ||
| return false; | ||
| next = iterator.next(); | ||
| if (next.opcode != CodeConstants.opc_dup) | ||
| return false; | ||
|
|
||
| // varargs array index | ||
| if (!iterator.hasNext()) | ||
| return false; | ||
| next = iterator.next(); | ||
| if (next.opcode < CodeConstants.opc_bipush || next.opcode > CodeConstants.opc_sipush | ||
| || next.operand(0) != j) | ||
| return false; | ||
|
|
||
| // load argument for varargs array | ||
| if (!iterator.hasNext()) | ||
| return false; | ||
| next = iterator.next(); | ||
| if (next.opcode < CodeConstants.opc_iload || next.opcode > CodeConstants.opc_aload | ||
| || next.operand(0) != argument + j) | ||
| return false; | ||
|
|
||
| // store argument in varargs array | ||
| if (!iterator.hasNext()) | ||
| return false; | ||
| next = iterator.next(); | ||
| if (next.opcode < CodeConstants.opc_iastore || next.opcode > CodeConstants.opc_sastore) | ||
| return false; | ||
| } | ||
|
|
||
| if (!iterator.hasNext()) | ||
| return false; | ||
| next = iterator.next(); | ||
| } | ||
| // invoke method | ||
| if (next.opcode < CodeConstants.opc_invokevirtual || next.opcode > CodeConstants.opc_invokeinterface) | ||
| return false; | ||
|
|
||
| Instruction invoke = next; | ||
|
|
||
| if (!iterator.hasNext()) | ||
| return false; | ||
| next = iterator.next(); | ||
|
|
||
| if (next.opcode == CodeConstants.opc_pop) { | ||
| if (!iterator.hasNext()) | ||
| return false; | ||
| next = iterator.next(); | ||
| } | ||
|
|
||
| // return | ||
| if (next.opcode < CodeConstants.opc_ireturn || next.opcode > CodeConstants.opc_return) | ||
| return false; | ||
|
|
||
| if (iterator.hasNext()) | ||
| return false; | ||
|
|
||
| LinkConstant link = struct.getPool().getLinkConstant(invoke.operand(0)); | ||
| boolean instance = invoke.opcode != CodeConstants.opc_invokestatic | ||
| && !link.elementname.equals(CodeConstants.INIT_NAME); | ||
|
|
||
| String newClass = link.classname; | ||
| String newMethod = link.elementname; | ||
| String newDescriptor = link.descriptor; | ||
| String newKey = InterpreterUtil.makeUniqueKey(newMethod, newDescriptor); | ||
|
|
||
| StructClass newStructClass = DecompilerContext.getStructContext().getClass(newClass); | ||
| if (newStructClass == null) | ||
| return false; | ||
| StructMethod newStructMethod = newStructClass.getMethodRecursive(newMethod, newDescriptor); | ||
| if (newStructMethod == null) | ||
| return false; | ||
|
|
||
| if (newStructMethod.hasModifier(CodeConstants.ACC_SYNTHETIC)) | ||
| return false; | ||
|
|
||
| ClassNode newNode = DecompilerContext.getClassProcessor().getMapRootClasses().get(newClass); | ||
| if (newNode != null && newNode.type == ClassesProcessor.ClassNode.Type.ANONYMOUS) | ||
| return false; | ||
|
|
||
| if (method.methodDescriptor().params.length - (varargs ? varargsCount - 1 : 0) != newStructMethod.methodDescriptor().params.length + (instance ? 1 : 0)) | ||
| return false; | ||
| if (newExprent.getConstructor().getLstParameters().size() > (instance ? 1 : 0)) | ||
| return false; | ||
| info.content_class_name = newClass; | ||
| info.content_method_name = newMethod; | ||
| info.content_method_descriptor = newDescriptor; | ||
| info.content_method_invocation_type = switch (invoke.opcode) { | ||
| case CodeConstants.opc_invokevirtual -> CodeConstants.CONSTANT_MethodHandle_REF_invokeVirtual; | ||
| case CodeConstants.opc_invokespecial -> instance ? CodeConstants.CONSTANT_MethodHandle_REF_newInvokeSpecial : CodeConstants.CONSTANT_MethodHandle_REF_invokeSpecial; | ||
| case CodeConstants.opc_invokestatic -> CodeConstants.CONSTANT_MethodHandle_REF_invokeStatic; | ||
| case CodeConstants.opc_invokeinterface -> CodeConstants.CONSTANT_MethodHandle_REF_invokeInterface; | ||
| default -> -1; | ||
| }; | ||
| info.content_method_key = newKey; | ||
| info.is_method_reference = true; | ||
| info.is_content_method_static = invoke.opcode == CodeConstants.opc_invokestatic; | ||
| newExprent.setMethodReference(true); | ||
| InvocationExprent constructor = newExprent.getConstructor(); | ||
| if (instance && constructor.getLstParameters().size() == 1) { | ||
| constructor.setInstance(constructor.getLstParameters().get(0)); | ||
| } | ||
|
|
||
| if (i > 0) { | ||
| Exprent instanceExp = newExprent.getConstructor().getInstance(); | ||
| Exprent previous = stat.getExprents().get(i - 1); | ||
| if (instanceExp instanceof VarExprent varExp | ||
| && previous instanceof AssignmentExprent assignment | ||
| && varExp.equalsVersions(assignment.getLeft()) | ||
| && !varExp.isVarReferenced(stat.getTopParent(), (VarExprent) assignment.getLeft())) { | ||
| newExprent.getConstructor().setInstance(assignment.getRight()); | ||
| newExprent.getConstructor().getLstParameters().set(0, assignment.getRight()); | ||
| stat.getExprents().remove(i - 1); | ||
| } | ||
| } | ||
| return true; | ||
| } | ||
|
|
||
| private static boolean removeInstanceAssignment(Statement stat, int i, Exprent exp) { | ||
| if (exp instanceof NewExprent newExp | ||
| && newExp.isLambda() | ||
| && newExp.isMethodReference() | ||
| && i >= 2 | ||
| && newExp.getConstructor().getInstance() instanceof VarExprent stackVar | ||
| && stackVar.isStack()) { | ||
| Exprent nonNull = stat.getExprents().get(i - 1); | ||
| Exprent assign = stat.getExprents().get(i - 2); | ||
| if (nonNull instanceof InvocationExprent inv | ||
| && inv.getClassname().equals("java/util/Objects") | ||
| && inv.getName().equals("requireNonNull") | ||
| && inv.getStringDescriptor().equals("(Ljava/lang/Object;)Ljava/lang/Object;") | ||
| && stackVar.equalsVersions(inv.getLstParameters().get(0)) | ||
| && assign instanceof AssignmentExprent stackAssign | ||
| && stackVar.equalsVersions(stackAssign.getLeft())) { | ||
| newExp.getConstructor().setInstance(stackAssign.getRight()); | ||
| newExp.getConstructor().getLstParameters().set(0, stackAssign.getRight()); | ||
| stat.getExprents().remove(i - 1); | ||
| stat.getExprents().remove(i - 2); | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,11 +6,11 @@ import java.util.concurrent.CompletableFuture; | |
| import java.util.stream.Stream; | ||
|
|
||
| public class TestGenericsQualified { | ||
| public Comparator<String> field = Comparator.<String, Integer>comparing(s -> s.length()).thenComparing(i -> i.toString());// 10 | ||
| public Comparator<String> field = Comparator.<String, Integer>comparing(s -> s.length()).thenComparing(String::toString);// 10 | ||
| public CompletableFuture<String> field2 = CompletableFuture.<String>supplyAsync(() -> "").thenCompose(s -> CompletableFuture.supplyAsync(() -> s + "2"));// 11 | ||
| public Optional<String> field3 = Optional.of("").map(s -> s + "3");// 12 | ||
| public Stream<String> field4 = Stream.of("1", "2").sorted(Comparator.<String, Integer>comparing(s -> s.length()).thenComparing(i -> i.toString()));// 13 14 | ||
| public Comparator<String> field5 = Comparator.comparing(String::length).thenComparing(i -> i.toString());// 15 | ||
| public Stream<String> field4 = Stream.of("1", "2").sorted(Comparator.<String, Integer>comparing(s -> s.length()).thenComparing(String::toString));// 13 14 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe this output is wrong. In the original source, these are regular lambdas and not references.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. They would have to match the lambda that javac generates when converting a method reference to a lambda. I could require the variable to exist but then it would only work for java 25. |
||
| public Comparator<String> field5 = Comparator.comparing(String::length).thenComparing(String::toString);// 15 | ||
| public Comparator<TestGenericsQualified> field6 = Comparator.<TestGenericsQualified, Integer>comparing(TestGenericsQualified::get).reversed();// 16 | ||
|
|
||
| public int get() { | ||
|
|
@@ -22,7 +22,7 @@ public class TestGenericsQualified { | |
| } | ||
|
|
||
| public Comparator<String> method() { | ||
| return Comparator.<String, Integer>comparing(s -> s.length()).thenComparing(i -> i.toString());// 27 | ||
| return Comparator.<String, Integer>comparing(s -> s.length()).thenComparing(String::toString);// 27 | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -99,14 +99,6 @@ class 'pkg/TestGenericsQualified' { | |
| 7 8 | ||
| } | ||
|
|
||
| method 'lambda$new$1 (Ljava/lang/String;)Ljava/lang/String;' { | ||
| 0 8 | ||
| 1 8 | ||
| 2 8 | ||
| 3 8 | ||
| 4 8 | ||
| } | ||
|
|
||
| method 'lambda$new$2 ()Ljava/lang/String;' { | ||
| 0 9 | ||
| 1 9 | ||
|
|
@@ -151,22 +143,6 @@ class 'pkg/TestGenericsQualified' { | |
| 7 11 | ||
| } | ||
|
|
||
| method 'lambda$new$7 (Ljava/lang/String;)Ljava/lang/String;' { | ||
| 0 11 | ||
| 1 11 | ||
| 2 11 | ||
| 3 11 | ||
| 4 11 | ||
| } | ||
|
|
||
| method 'lambda$new$8 (Ljava/lang/String;)Ljava/lang/String;' { | ||
| 0 12 | ||
| 1 12 | ||
| 2 12 | ||
| 3 12 | ||
| 4 12 | ||
| } | ||
|
|
||
| method 'get ()I' { | ||
| 0 16 | ||
| 1 16 | ||
|
|
@@ -199,14 +175,6 @@ class 'pkg/TestGenericsQualified' { | |
| 6 24 | ||
| 7 24 | ||
| } | ||
|
|
||
| method 'lambda$method$10 (Ljava/lang/String;)Ljava/lang/String;' { | ||
| 0 24 | ||
| 1 24 | ||
| 2 24 | ||
| 3 24 | ||
| 4 24 | ||
| } | ||
| } | ||
|
|
||
| Lines mapping: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This appears to also modify existing lambdas which could be represented with a method reference. Perhaps it should be behind a decompiler option