eclipse-jdt / eclipse.jdt.core

Eclipse Public License 2.0
164 stars 130 forks source link

ECJ infers different reified type than javac #1961

Open Madjosz opened 9 months ago

Madjosz commented 9 months ago

Update: See an even more reduced example below.

Consider the following example:

import java.io.IOException;

public class ReifiedException {

  public static void main(String... args) {
    assertThrows(() -> CheckedSupplier.supplyNullOnExc(ReifiedException::read));
  }

  private interface CheckedSupplier<T, X extends Exception> {
    T getChecked() throws X;

    static <T, X extends Exception> T supplyNullOnExc(CheckedSupplier<T, X> checkedSupplier, X... reified) {
      System.out.println(reified.getClass().getComponentType());
      return null;
    }
  }

  // behaviour differs when <T> is present
  static <T> Object assertThrows(Executable executable) {
    try {
      executable.execute();
    } catch (Throwable t) {}
    return null;
  }
  private interface Executable {
    void execute() throws Throwable;
  }
  private static int read() throws IOException {
    throw new RuntimeException();
  }
}

The CheckedSupplier should be generic over the type of the supplied value T and the type of the checked exception X which can be thrown. supplyNullOnExc should capture the actual type of the Exception at compile time via the reified varargs array. For demonstration purposes I printed the type in the method.

The above code resembles a minified version an actual JUnit5 test case. We obsereved different behaviours of the code compiled by ECJ vs. javac: ECJ (and javac 8) inferes X as IOException (which is what we expect as read() throws IOException) while javac (starting at JDK 9) infers it just as Exception.

Removing the type parameter <T> from assertThrows() leads to javac being able to also infer IOException so this might as well be a bug in javac but I am not well-versed in the JLS to determine which of the compilers actually follows the specs here.

Tested versions

ECJ builds with target -21, execution environment version did not matter

srikanth-sankaran commented 9 months ago

@stephan-herrmann - may we hear from the master of type inference please ?? 😊

Madjosz commented 9 months ago

A colleague was able to reduce the example even more:

public class Generic {

    public static void main(String... args) {
        run(() -> supplyNull(new RuntimeException()));
        runT(() -> supplyNull(new RuntimeException()));
    }

    static <R, X extends Exception> R supplyNull(X... excs) {
        System.out.println(excs.getClass().getComponentType());
        return null;
    }

    // behaviour differs when <T> is present
    static void run(Runnable runnable) { runnable.run(); }
    static <T> void runT(Runnable runnable) { runnable.run(); }
}
Madjosz commented 9 months ago

To clarify the behaviour I also opened a bug at Oracle/OpenJDK: https://bugs.openjdk.org/browse/JDK-8325859

stephan-herrmann commented 8 months ago

A colleague was able to reduce the example even more:

Thanks. This looks great.

Here the lambda is being inferred against target type Runnable in both cases. I see no indication what so ever how the (unused!) type parameter <T> could possibly change inferred types for the lambda. True, that <T> must now be inferred, and without any constraints I'm sure it will get inferred as j.l.Object. But this doesn't change the Runnable parameter.

To clarify the behaviour I also opened a bug at Oracle/OpenJDK: https://bugs.openjdk.org/browse/JDK-8325859

Cool, I'm curious to see if you'll get an answer there.