-
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 2 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,267 @@ | ||
| 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 | ||
|
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. It's probably reasonable to flip this |
||
| && newExprent.isLambda() | ||
| && !newExprent.isMethodReference()) { | ||
| if (stat.getTopParent().mt.getName().contains("<init>")) { | ||
| System.out.println(); | ||
|
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. Debug line |
||
| } | ||
| 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); | ||
| FullInstructionSequence seq = method.getInstructionSequence(); | ||
| Iterator<Instruction> iterator = seq.iterator(); | ||
| Instruction next = null; | ||
|
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. Unnecessary initializer |
||
| int argument = 0; | ||
|
|
||
| if (!iterator.hasNext()) | ||
| return false; | ||
| 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++; | ||
| } | ||
|
|
||
| if (next == null) | ||
|
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. This should never be true ( |
||
| return false; | ||
| boolean varargs = false; | ||
| 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; | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| 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