Skip to content

Instantly share code, notes, and snippets.

@jaskarth
Last active March 11, 2024 18:03
Show Gist options
  • Save jaskarth/c7171b0e34b6eccf0b9f1c37030867dc to your computer and use it in GitHub Desktop.
Save jaskarth/c7171b0e34b6eccf0b9f1c37030867dc to your computer and use it in GitHub Desktop.

Float precision

Source:

double x = 0.2F;

Fernflower:

double x = 0.20000000298023224D;

Quiltflower

double x = 0.2F;

Fernflower isn't able to resugar the float->double inaccuracy correctly.

Switch Expressions

Source:

public void test(String directionStr) {
    String axis = switch (directionStr.toLowerCase()) {
      case "north":
      case "south":
        yield "y";
      case "east":
      case "west":
        yield "x";
      case "up":
      case "down":
        yield "z";
      default:
        throw new IllegalStateException("Unexpected value: " + directionStr);
    };
    System.out.println(axis);
  }

Fernflower:

  public void test(String directionStr) {
        String var10000;
        switch (directionStr.toLowerCase()) {
            case "north":
            case "south":
                var10000 = "y";
                break;
            case "east":
            case "west":
                var10000 = "x";
                break;
            case "up":
            case "down":
                var10000 = "z";
                break;
            default:
                throw new IllegalStateException("Unexpected value: " + directionStr);
        }

        String axis = var10000;
        System.out.println(axis);
    }

Quiltflower:

public void test(String directionStr) {
  String var3 = directionStr.toLowerCase();

  String axis = switch(var3) {
     case "north", "south" -> "y";
     case "east", "west" -> "x";
     case "up", "down" -> "z";
     default -> throw new IllegalStateException("Unexpected value: " + directionStr);
  };
  System.out.println(axis);
}

Fernflower is unable to resugar the switch expression at all, and instead makes a standard switch statement.

While loops

Source:

while (i > 10) {
    i++;

    if (i == 15) {
        break;
    }

    System.out.println(0);
}

Fernflower:

while(true) {
    if (i > 10) {
        ++i;
        if (i != 15) {
            System.out.println(0);
            continue;
        }
    }

    return;
}

Quiltflower:

while(i > 10 && ++i != 15) {
   System.out.println(0);
}

Fernflower extends the infinite loop to the return, which produces much more complex code. While Quiltflower is a bit aggressive on the loop merging, it's also significantly more concise.

Empty infinite loops

Source:

public void test() {
    while (true) {

    }
}

Fernflower:

public void test() {
    // $FF: Couldn't be decompiled
}

Quiltflower:

public void test() {
    while(true) {
    }
 }

Fernflower is entirely unable to decompile the empty loop, which is due to a minor bug in the goto instruction resolver.

Try-with-resources

Source:

try (Scanner scanner = new Scanner(file)) {
  scanner.next();
}

Fernflower:

Scanner scanner = new Scanner(file);

try {
    scanner.next();
} catch (Throwable var6) {
    try {
        scanner.close();
    } catch (Throwable var5) {
        var6.addSuppressed(var5);
    }

    throw var6;
}

scanner.close();

Quiltflower:

try (Scanner scanner = new Scanner(file)) {
   scanner.next();
}

Fernflower is not able to resugar the output, and is left to display the try-with-resources directly.

For-each over array

Source:

int[] array = new int[]{1, 2, 3, 4};
for (int i : array) {
    System.out.println(i);
}

Fernflower:

int[] array = new int[]{1, 2, 3, 4};
int[] var2 = array;
int var3 = array.length;

for(int var4 = 0; var4 < var3; ++var4) {
    int i = var2[var4];
    System.out.println(i);
}

Quiltflower:

int[] array = new int[]{1, 2, 3, 4};

for(int i : array) {
    System.out.println(i);
}

Fernflower doesn't recognize the foreach loop and represents the iteration with a for loop instead.

Generics

Source:

public class Inner<T> {
    public Class<? super T> get() {
      return null;
    }
}

  public <T> Class<T> test(Inner<T> inner) {
    Class<T> t = (Class<T>) inner.get();
    return (Class<T>) inner.get();
}

Fernflower:

public class Inner<T> {
    public Inner() {
    }

    public Class<? super T> get() {
        return null;
    }
}

public <T> Class<T> test(Inner<T> inner) {
    Class<T> t = inner.get();
    return inner.get();
}

Quiltflower:

public class Inner<T> {
  public Class<? super T> get() {
     return null;
  }
}

public <T> Class<T> test(Inner<T> inner) {
  Class<T> t = (Class<T>)inner.get();
  return (Class<T>)inner.get();
}

Fernflower doesn't cast the contravariant type to the regular type, which would fail to compile.

My favorite example

Source:

https://github.com/gradle/gradle/blob/master/subprojects/model-core/src/main/java/org/gradle/api/internal/tasks/DefaultTaskDependency.java#L75

Fernflower:

Quiltflower:

@Override
public void visitDependencies(TaskDependencyResolveContext context) {
   Set<Object> mutableValues = this.getMutableValues();
   if (!mutableValues.isEmpty() || !this.immutableValues.isEmpty()) {
      Deque<Object> queue = new ArrayDeque(mutableValues.size() + this.immutableValues.size());
      queue.addAll(this.immutableValues);
      queue.addAll(mutableValues);

      while(!queue.isEmpty()) {
         Object dependency = queue.removeFirst();
         if (dependency instanceof Buildable) {
            context.add(dependency);
         } else if (dependency instanceof Task) {
            context.add(dependency);
         } else if (dependency instanceof TaskDependency) {
            context.add(dependency);
         } else if (dependency instanceof ProviderInternal) {
            ProviderInternal<?> provider = (ProviderInternal)dependency;
            ValueSupplier.ValueProducer producer = provider.getProducer();
            if (producer.isKnown()) {
               producer.visitProducerTasks(context);
            } else {
               queue.addFirst(provider.get());
            }
         } else if (dependency instanceof TaskDependencyContainer) {
            ((TaskDependencyContainer)dependency).visitDependencies(context);
         } else if (dependency instanceof Closure) {
            Closure closure = (Closure)dependency;
            Object closureResult = closure.call(context.getTask());
            if (closureResult != null) {
               queue.addFirst(closureResult);
            }
         } else if (dependency instanceof List) {
            List<?> list = (List)dependency;
            if (list instanceof RandomAccess) {
               for(int i = list.size() - 1; i >= 0; --i) {
                  queue.addFirst(list.get(i));
               }
            } else {
               ListIterator<?> iterator = list.listIterator(list.size());

               while(iterator.hasPrevious()) {
                  Object item = iterator.previous();
                  queue.addFirst(item);
               }
            }
         } else if (dependency instanceof Iterable && !(dependency instanceof Path)) {
            Iterable<?> iterable = Cast.uncheckedNonnullCast(dependency);
            addAllFirst(queue, Iterables.toArray(iterable, Object.class));
         } else if (dependency instanceof Map) {
            Map<?, ?> map = Cast.uncheckedNonnullCast(dependency);
            addAllFirst(queue, map.values().toArray());
         } else if (dependency instanceof Object[]) {
            Object[] array = dependency;
            addAllFirst(queue, array);
         } else if (!(dependency instanceof Callable)) {
            if (this.resolver == null || !(dependency instanceof CharSequence)) {
               List<String> formats = new ArrayList();
               if (this.resolver != null) {
                  formats.add("A String or CharSequence task name or path");
               }

               formats.add("A Task instance");
               formats.add("A TaskReference instance");
               formats.add("A Buildable instance");
               formats.add("A TaskDependency instance");
               formats.add("A Provider that represents a task output");
               formats.add("A Provider instance that returns any of these types");
               formats.add("A Closure instance that returns any of these types");
               formats.add("A Callable instance that returns any of these types");
               formats.add("An Iterable, Collection, Map or array instance that contains any of these types");
               throw new UnsupportedNotationException(dependency, String.format("Cannot convert %s to a task.", dependency), null, formats);
            }

            context.add(this.resolver.resolveTask(dependency.toString()));
         } else {
            Callable<?> callable = Cast.uncheckedNonnullCast(dependency);
            Object callableResult = GUtil.uncheckedCall(Cast.uncheckedNonnullCast(callable));
            if (callableResult != null) {
               queue.addFirst(callableResult);
            }
         }
      }

   }
}

..... yeah.

@NebelNidas
Copy link

NebelNidas commented Oct 29, 2022

Could you do a comparison where you throw CFR and Procyon into the mix? Would be nice to see each decompiler's strengths and weaknesses and how they compare to one another :)

@jaskarth
Copy link
Author

That'll likely be a separate gist, but yes definitely! Will do when I have time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment