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