RFR: 8367530: The exhaustiveness errors could be improved [v8]

Jan Lahoda jlahoda at openjdk.org
Thu Dec 4 14:44:08 UTC 2025


On Wed, 19 Nov 2025 16:15:17 GMT, Jan Lahoda <jlahoda at openjdk.org> wrote:

>> src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java line 832:
>> 
>>> 830:                                                     Set.of(defaultPattern));
>>> 831:         } catch (TimeoutException ex) {
>>> 832:             return ex.missingPatterns != null ? ex.missingPatterns : Set.of();
>> 
>> Instead of a timeout, I wonder if you could instead cut the recursion at a specific threshold. It seems to me that recursing more will provide more precision _at the nested level_, so it's a trade off between when do we want to stop. 
>> 
>> Overload resolution provides some kind of precedent:
>> 
>> 
>> error: incompatible types: String cannot be converted to int
>>       m("Hello");
>>         ^
>> Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
>> 1 error
>> 
>> 
>> (We "compress" the diagnostic whenever we can somehow figure out if an overload is "better" than the others). Then if you provide the option, you get the full thing:
>> 
>> 
>> error: no suitable method found for m(String)
>>       m("Hello");
>>       ^
>>     method Test.m() is not applicable
>>       (actual and formal argument lists differ in length)
>>     method Test.m(int) is not applicable
>>       (argument mismatch; String cannot be converted to int)
>>     method Test.m(int,int) is not applicable
>>       (actual and formal argument lists differ in length)
>>     method Test.m(int,int,int) is not applicable
>>       (actual and formal argument lists differ in length)
>>     method Test.m(int,int,int,int) is not applicable
>>       (actual and formal argument lists differ in length)
>>     method Test.m(int,int,int,int,int) is not applicable
>>       (actual and formal argument lists differ in length)
>>     method Test.m(int,int,int,int,int,int) is not applicable
>>       (actual and formal argument lists differ in length)
>> 
>> 
>> But, also, maybe putting an upper bound on the recursion, no matter what, might be a good idea?
>
> While I agree that having a timeout in weird/non-standard in javac, I believe it is the first time when we have a process that a) can run for a very long time; b) is not required for correctness. In all other cases I can recall, if some process is slow (including e.g. verifying exhaustiveness), then it is required for correctness. And so we cannot skip it based on criteria like time.
> 
> The timeout here provides a way to say how much real-world time is the user willing to invest to get the outcome - if more time is invested, more detailed missing patterns may possibly be computed. It is a somewhat weird approach for javac, but it is a limit that (I think) the user and javac can agree on.
> 
> We could introduce a limit on e.g. the depth to which we expand, but adding one level of nesting may affect performance significantly or almost not at all, depending on e.g. the record type form/shape.
> 
> E.g. having a record with many components, where each component is a sealed type with many permitted subtypes, one level of nesting may lead to a high number of newly generated patterns possibly taking a lot of time to go through them. But having a record that has a single component that is not a sealed type should only generate one pattern, and so have minimal impact.

As an example, if I have:

package exhaustiveness.test;

public class Deep {

    public record Box0(Box1 v) {}
    public record Box1(Box2 v) {}
    public record Box2(Box3 v) {}
    public record Box3(Box4 v) {}
    public record Box4(Box5 v) {}
    public record Box5(Box6 v) {}
    public record Box6(Box7 v) {}
    public record Box7(Box8 v) {}
    public record Box8(Box9 v) {}
    public record Box9(I v) {}
    public sealed interface I {}
    public final class C1 implements I {}
    public final class C2 implements I {}

    public int test(Box0 box0) {
        return switch (box0) {
            case Box0(Box1(Box2(Box3(Box4(Box5(Box6(Box7(Box8(Box9(C1 _)))))))))) -> 0;
        };
    }
}


this runs very quickly on my laptop <1s in total, despite recursing quite deep (and it finds the one missing pattern). If I have something like:

package exhaustiveness.test;

public class Shallow {

    public sealed interface Base {}

    public sealed interface I0 extends Base {}
    public non-sealed interface L0 extends Base {}

    public non-sealed interface L1 extends I0 {}
    public non-sealed interface L2 extends I0 {}
    public non-sealed interface L3 extends I0 {}
//    public non-sealed interface L4 extends I0 {}
//    public non-sealed interface L5 extends I0 {}
//    public non-sealed interface L6 extends I0 {}
//    public non-sealed interface L7 extends I0 {}
//    public non-sealed interface L8 extends I0 {}
//    public non-sealed interface L9 extends I0 {}
    public record Data(Base b1, Base b2, Base b3, Base b4) {}
    public int test(Data d) {
        return switch (d) {
            case Data(I0 _, I0 _, I0 _, I0 _) -> 0;
            case Data(L0 _, L0 _, L0 _, L0 _) -> 0;
            case Data(I0 _,    _,    _,    _) -> 0;
        };
    }
}


the current run time on my laptop is ~25s; if I un-comment L4, the time is ~2m15s. Also, in this case it is relatively difficult to reduce details, as if the record pattern is not expanded, the less-detailed version is simply `Data _`.

So, while there may be a way to estimate complexity, it depends on many factors - the depth, the number of sealed types, but also the user-written patterns (as those may make exhaustiveness computation faster or slower). And, to me, it feels like a heuristic to the real factor, which is what duration can be reasonably spent by this computation.

-------------

PR Review Comment: https://git.openjdk.org/jdk/pull/27256#discussion_r2589353598


More information about the compiler-dev mailing list