On Wed, 19 Nov 2025 16:15:17 GMT, Jan Lahoda <[email protected]> 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

Reply via email to