From 3d6728cec9073e7fa3c7ab5217b187ada8b0a619 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Mon, 4 Aug 2025 17:42:42 +0200 Subject: [PATCH 01/33] A attempt to create more user-friendly errors for switch exaustiveness. --- .../com/sun/tools/javac/comp/Flow.java | 479 ++++++++++++++++-- .../tools/javac/resources/compiler.properties | 10 + .../tools/javac/patterns/Exhaustiveness.java | 41 +- .../ExhaustivenessConvenientErrors.java | 430 ++++++++++++++++ 4 files changed, 913 insertions(+), 47 deletions(-) create mode 100644 test/langtools/tools/javac/patterns/ExhaustivenessConvenientErrors.java diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java index e685f139b680c..04280096f556c 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java @@ -59,12 +59,18 @@ import com.sun.tools.javac.resources.CompilerProperties.Fragments; import static com.sun.tools.javac.tree.JCTree.Tag.*; import com.sun.tools.javac.util.JCDiagnostic.Fragment; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.SequencedSet; +import java.util.TreeSet; import java.util.function.Predicate; import java.util.stream.Collectors; import static java.util.stream.Collectors.groupingBy; +import java.util.stream.Stream; /** This pass implements dataflow analysis for Java programs though * different AST visitor steps. Liveness analysis (see AliveAnalyzer) checks that @@ -216,6 +222,7 @@ public class Flow { private Env attrEnv; private Lint lint; private final Infer infer; + private final int missingExhaustivenessTimeout; public static Flow instance(Context context) { Flow instance = context.get(flowKey); @@ -342,6 +349,17 @@ protected Flow(Context context) { rs = Resolve.instance(context); diags = JCDiagnostic.Factory.instance(context); Source source = Source.instance(context); + Options options = Options.instance(context); + String timeout = options.get("exhaustivityTimeout"); + int computedTimeout = 1000; + if (timeout != null) { + try { + computedTimeout = Integer.parseInt(timeout); + } catch (NumberFormatException ex) { + //TODO: notify? + } + } + missingExhaustivenessTimeout = computedTimeout; } /** @@ -730,9 +748,14 @@ public void visitSwitch(JCSwitch tree) { tree.isExhaustive = tree.hasUnconditionalPattern || TreeInfo.isErrorEnumSwitch(tree.selector, tree.cases); if (exhaustiveSwitch) { - tree.isExhaustive |= exhausts(tree.selector, tree.cases); + Set pendingNotExhaustiveDetails = new TreeSet<>(); + tree.isExhaustive |= exhausts(tree.selector, tree.cases, pendingNotExhaustiveDetails); if (!tree.isExhaustive) { - log.error(tree, Errors.NotExhaustiveStatement); + if (pendingNotExhaustiveDetails.isEmpty()) { + log.error(tree, Errors.NotExhaustiveStatement); + } else { + log.error(tree, Errors.NotExhaustiveStatementDetails(pendingNotExhaustiveDetails.stream().collect(Collectors.joining("\n")))); + } } } if (!tree.hasUnconditionalPattern && !exhaustiveSwitch) { @@ -765,21 +788,26 @@ public void visitSwitchExpression(JCSwitchExpression tree) { } } + Set pendingNotExhaustiveDetails = new TreeSet<>(); if (tree.hasUnconditionalPattern || TreeInfo.isErrorEnumSwitch(tree.selector, tree.cases)) { tree.isExhaustive = true; } else { - tree.isExhaustive = exhausts(tree.selector, tree.cases); + tree.isExhaustive = exhausts(tree.selector, tree.cases, pendingNotExhaustiveDetails); } if (!tree.isExhaustive) { - log.error(tree, Errors.NotExhaustive); + if (pendingNotExhaustiveDetails.isEmpty()) { + log.error(tree, Errors.NotExhaustive); + } else { + log.error(tree, Errors.NotExhaustiveDetails(pendingNotExhaustiveDetails.stream().collect(Collectors.joining("\n")))); + } } alive = prevAlive; alive = alive.or(resolveYields(tree, prevPendingExits)); } - private boolean exhausts(JCExpression selector, List cases) { + private boolean exhausts(JCExpression selector, List cases, Set pendingNotExhaustiveDetails) { Set patternSet = new HashSet<>(); Map> enum2Constants = new HashMap<>(); Set booleanLiterals = new HashSet<>(Set.of(0, 1)); @@ -821,44 +849,353 @@ private boolean exhausts(JCExpression selector, List cases) { patternSet.add(new BindingPattern(e.getKey().type)); } } - Set patterns = patternSet; - boolean useHashes = true; try { - boolean repeat = true; - while (repeat) { - Set updatedPatterns; - updatedPatterns = reduceBindingPatterns(selector.type, patterns); - updatedPatterns = reduceNestedPatterns(updatedPatterns, useHashes); - updatedPatterns = reduceRecordPatterns(updatedPatterns); - updatedPatterns = removeCoveredRecordPatterns(updatedPatterns); - repeat = !updatedPatterns.equals(patterns); - if (checkCovered(selector.type, patterns)) { - return true; - } - if (!repeat) { - //there may be situation like: - //class B permits S1, S2 - //patterns: R(S1, B), R(S2, S2) - //this might be joined to R(B, S2), as B could be rewritten to S2 - //but hashing in reduceNestedPatterns will not allow that - //disable the use of hashing, and use subtyping in - //reduceNestedPatterns to handle situations like this: - repeat = useHashes; - useHashes = false; - } else { - //if a reduction happened, make sure hashing in reduceNestedPatterns - //is enabled, as the hashing speeds up the process significantly: - useHashes = true; - } - patterns = updatedPatterns; + Pair> coveredResult = isCovered(selector.type, patternSet); + if (coveredResult.fst) { + return true; } - return checkCovered(selector.type, patterns); + if (missingExhaustivenessTimeout == 0) { + return false; + } + + PatternDescription defaultPattern = new BindingPattern(selector.type); + Set missingPatterns = expandMissingPatternDescriptions(selector.type, selector.type, defaultPattern, coveredResult.snd, Set.of(defaultPattern)); + + for (PatternDescription missing : missingPatterns) { + pendingNotExhaustiveDetails.add(missing.toString()); + } + return false; } catch (CompletionFailure cf) { chk.completionError(selector.pos(), cf); return true; //error recovery } } + private Set expandMissingPatternDescriptions(Type selectorType, Type targetType, PatternDescription toExpand, Set basePatterns, Set inMissingPatterns) { + if (toExpand instanceof BindingPattern bp) { + if (bp.type.tsym.isSealed()) { + List permitted = ((ClassSymbol) bp.type.tsym).getPermittedSubclasses(); + Set viablePermittedPatterns = permitted.stream().map(type -> type.tsym).filter(csym -> { + Type instantiated; + if (csym.type.allparams().isEmpty()) { + instantiated = csym.type; + } else { + instantiated = infer.instantiatePatternType(targetType, csym); + } + + return instantiated != null && types.isCastable(targetType, instantiated); + }).map(csym -> new BindingPattern(types.erasure(csym.type))).collect(Collectors.toSet()); + + boolean reduced = false; + + for (Iterator it = viablePermittedPatterns.iterator(); it.hasNext(); ) { + BindingPattern current = it.next(); + Set reducedPermittedPatterns = new HashSet<>(viablePermittedPatterns); + + reducedPermittedPatterns.remove(current); + + Set replaced = replace(inMissingPatterns, toExpand, reducedPermittedPatterns); + + if (isCovered(selectorType, joinSets(basePatterns, replaced)).fst) { + it.remove(); + reduced = true; + } + } + + if (!reduced) { + return inMissingPatterns; + } + + Set currentMissingPatterns = replace(inMissingPatterns, toExpand, viablePermittedPatterns); + + //try to recursively work on each viable pattern: + for (PatternDescription viable : viablePermittedPatterns) { + currentMissingPatterns = expandMissingPatternDescriptions(selectorType, targetType, viable, basePatterns, currentMissingPatterns); + } + + return currentMissingPatterns; + } else if ((bp.type.tsym.flags_field & Flags.RECORD) != 0 && + basePatternsHaveRecordPatternOnThisSpot(basePatterns, findRootContaining(inMissingPatterns, toExpand), toExpand)) { //only expand record types into record patterns if there's a chance it may change the outcome + Type[] componentTypes = ((ClassSymbol) bp.type.tsym).getRecordComponents() + .map(r -> types.memberType(bp.type, r)) + .toArray(s -> new Type[s]); + List> combinatorialNestedTypes = List.of(List.nil()); + for (Type componentType : componentTypes) { + List variants; + if (componentType.tsym.isSealed()) { + variants = leafPermittedSubTypes(componentType.tsym, csym -> { + Type instantiated; + if (csym.type.allparams().isEmpty()) { + instantiated = csym.type; + } else { + instantiated = infer.instantiatePatternType(componentType, csym); + } + + return instantiated != null && types.isCastable(componentType, instantiated); + }).stream().map(csym -> csym.type).collect(List.collector()); //XXX: csym.type => instantiate + } else { + variants = List.of(componentType); + } + List> newCombinatorialNestedTypes = List.nil(); + for (List existing : combinatorialNestedTypes) { + for (Type nue : variants) { + newCombinatorialNestedTypes = newCombinatorialNestedTypes.prepend(existing.append(nue)); + } + } + combinatorialNestedTypes = newCombinatorialNestedTypes; + } + + Set combinatorialPatterns = combinatorialNestedTypes.stream().map(combination -> new RecordPattern(bp.type, componentTypes, combination.map(BindingPattern::new).toArray(PatternDescription[]::new))).collect(Collectors.toSet()); + + //remove unnecessary: + //preserve the most specific: + combinatorialPatterns = new LinkedHashSet<>(sortPattern(combinatorialPatterns, basePatterns, inMissingPatterns).reversed()); + + for (Iterator it = combinatorialPatterns.iterator(); it.hasNext(); ) { + PatternDescription current = it.next(); + Set reducedAdded = new HashSet<>(combinatorialPatterns); + + reducedAdded.remove(current); + + if (isCovered(selectorType, joinSets(basePatterns, replace(inMissingPatterns, bp, reducedAdded))).fst) { + it.remove(); + } + } + + Pair> combinatorial = isCovered(targetType, combinatorialPatterns); + + if (!combinatorial.fst) { + //nothing better can be done(?) + combinatorialPatterns = combinatorial.snd; + } + + //combine sealed subtypes into the supertype, if all is covered. + //but preserve more specific record patterns in positions where there are record patterns + //this is particularly for the case where the sealed supertype only has one permitted type, the record: + Set sortedCandidates = sortPattern(combinatorialPatterns, basePatterns, combinatorialPatterns); + + //remove unnecessary: + OUTER: for (Iterator it = sortedCandidates.iterator(); it.hasNext(); ) { + PatternDescription current = it.next(); + Set reducedAdded = new HashSet<>(sortedCandidates); + + reducedAdded.remove(current); + + if (isCovered(selectorType, joinSets(basePatterns, replace(inMissingPatterns, bp, reducedAdded))).fst) { + it.remove(); + } + } + + Set currentMissingPatterns = replace(inMissingPatterns, toExpand, sortedCandidates); + + for (PatternDescription addedPattern : sortedCandidates) { + if (addedPattern instanceof RecordPattern addedRP) { + for (int c = 0; c < addedRP.nested.length; c++) { + currentMissingPatterns = expandMissingPatternDescriptions(selectorType, addedRP.fullComponentTypes[c], addedRP.nested[c], basePatterns, currentMissingPatterns); + } + } + } + + return currentMissingPatterns; + } + } + return inMissingPatterns; + } + + private PatternDescription findRootContaining(Set rootPatterns, PatternDescription added) { + for (PatternDescription pd : rootPatterns) { + if (isUnderRoot(pd, added)) { + return pd; + } + } + + //assert? + return null; + } + + private boolean basePatternsHaveRecordPatternOnThisSpot(Set basePatterns, PatternDescription rootPattern, PatternDescription added) { + if (rootPattern == added) { + return basePatterns.stream().anyMatch(pd -> pd instanceof RecordPattern); + } + if (!(rootPattern instanceof RecordPattern rootPatternRecord)) { + return false; + } + int index = -1; + for (int c = 0; c < rootPatternRecord.nested.length; c++) { + if (isUnderRoot(rootPatternRecord.nested[c], added)) { + index = c; + break; + } + } + Assert.check(index != (-1)); + //TODO: isSameType erasure? + //TODO: indexing into the nested array - error recovery(!) + int indexFin = index; + Set filteredBasePatterns = basePatterns.stream().filter(pd -> pd instanceof RecordPattern).map(rp -> (RecordPattern) rp).filter(rp -> types.isSameType(rp.recordType(), rootPatternRecord.recordType())).map(rp -> rp.nested[indexFin]).collect(Collectors.toSet()); + return basePatternsHaveRecordPatternOnThisSpot(filteredBasePatterns, rootPatternRecord.nested[index], added); + } + + private boolean isUnderRoot(PatternDescription root, PatternDescription searchFor) { + if (root == searchFor) { + return true; + } else if (root instanceof RecordPattern rp) { + for (int c = 0; c < rp.nested.length; c++) { + if (isUnderRoot(rp.nested[c], searchFor)) { + return true; + } + } + } + return false; + } + + private SequencedSet sortPattern(Set candidates, Set basePatterns, Set missingPatterns) { + SequencedSet sortedCandidates = new LinkedHashSet<>(); + + while (!candidates.isEmpty()) { + PatternDescription mostSpecific = null; + for (PatternDescription current : candidates) { + if (mostSpecific == null || isMoreImportant(current, mostSpecific, basePatterns, missingPatterns)) { + mostSpecific = current; + } + } + sortedCandidates.add(mostSpecific); + candidates.remove(mostSpecific); + } + return sortedCandidates; + } + + private Set replace(Iterable in, PatternDescription what, Collection to) { + Set result = new HashSet<>(); + + for (PatternDescription pd : in) { + Collection replaced = replace(pd, what, to); + if (replaced != null) { + result.addAll(replaced); + } else { + result.add(pd); + } + } + + return result; + } + + //null: no change + private Collection replace(PatternDescription in, PatternDescription what, Collection to) { + if (in == what) { + return to; + } else if (in instanceof RecordPattern rp) { + for (int c = 0; c < rp.nested.length; c++) { + Collection replaced = replace(rp.nested[c], what, to); + if (replaced != null) { + Set withReplaced = new HashSet<>(); + + generatePatternsWithReplacedNestedPattern(rp, c, replaced, withReplaced::add); + + return replace(withReplaced, what, to); + } + } + return null; + } else { + return null; //binding patterns have no children + } + } + + //true iff pd1 is more important than pd2 + //false otherwise + //TODO: there may be a better name for this method: + private boolean isMoreImportant(PatternDescription pd1, PatternDescription pd2, Set basePatterns, Set missingPatterns) { + if (!(pd1 instanceof RecordPattern rp1)) { + throw new AssertionError(); + } + if (!(pd2 instanceof RecordPattern rp2)) { + throw new AssertionError(); + } + for (int c = 0; c < rp1.nested.length; c++) { + BindingPattern bp1 = (BindingPattern) rp1.nested[c]; + Type t1 = bp1.type(); + BindingPattern bp2 = (BindingPattern) rp2.nested[c]; + Type t2 = bp2.type(); + boolean t1IsImportantRecord = (t1.tsym.flags_field & RECORD) != 0 && hasMatchingRecordPattern(basePatterns, missingPatterns, bp1); + boolean t2IsImportantRecord = (t2.tsym.flags_field & RECORD) != 0 && hasMatchingRecordPattern(basePatterns, missingPatterns, bp2); + if (t1IsImportantRecord && !t2IsImportantRecord) { + return false; + } + if (!t1IsImportantRecord && t2IsImportantRecord) { + return true; + } + if (!types.isSameType(t1, t2) && types.isSubtype(t1, t2)) { + return true; + } + } + return false; + } + + private boolean hasMatchingRecordPattern(Set basePatterns, Set missingPatterns, PatternDescription query) { + PatternDescription root = findRootContaining(missingPatterns, query); + + if (root == null) { + return false; + } + return basePatternsHaveRecordPatternOnThisSpot(basePatterns, root, query); + } + + private Set joinSets(Collection s1, Collection s2) { + Set result = new HashSet<>(); + + result.addAll(s1); + result.addAll(s2); + return result; + } + + //TODO: unify with the similar code below: + private void generatePatternsWithReplacedNestedPattern(RecordPattern basePattern, int replaceComponent, Iterable updatedNestedPatterns, Consumer sink) { + for (PatternDescription nested : updatedNestedPatterns) { + PatternDescription[] newNested = + Arrays.copyOf(basePattern.nested, basePattern.nested.length); + newNested[replaceComponent] = nested; + sink.accept(new RecordPattern(basePattern.recordType(), + basePattern.backtrackOnly(), + basePattern.fullComponentTypes(), + newNested)); + } + } + + private Pair> isCovered(Type selectorType, Set patterns) { + boolean backtrack = true; //XXX: this should be inverted, right? + boolean repeat = true; + Set updatedPatterns; + do { + updatedPatterns = reduceBindingPatterns(selectorType, patterns); + updatedPatterns = reduceNestedPatterns(updatedPatterns, backtrack); + updatedPatterns = reduceRecordPatterns(updatedPatterns); + updatedPatterns = removeCoveredRecordPatterns(updatedPatterns); + repeat = !updatedPatterns.equals(patterns); + if (checkCovered(selectorType, patterns)) { + return Pair.of(true, null); + } + if (!repeat) { + //there may be situation like: + //class B permits S1, S2 + //patterns: R(S1, B), R(S2, S2) + //this might be joined to R(B, S2), as B could be rewritten to S2 + //but hashing in reduceNestedPatterns will not allow that + //disable the use of hashing, and use subtyping in + //reduceNestedPatterns to handle situations like this: + repeat = backtrack; + backtrack = false; + } else { + //if a reduction happened, make sure hashing in reduceNestedPatterns + //is enabled, as the hashing speeds up the process significantly: + backtrack = true; + } + patterns = updatedPatterns; + } while (repeat); + if (checkCovered(selectorType, patterns)) { + return Pair.of(true, null); + } + return Pair.of(false, updatedPatterns); + } + private boolean checkCovered(Type seltype, Iterable patterns) { for (Type seltypeComponent : components(seltype)) { for (PatternDescription pd : patterns) { @@ -931,6 +1268,7 @@ private Set reduceBindingPatterns(Type selectorType, Set reduceBindingPatterns(Type selectorType, Set allPermittedSubTypes(TypeSymbol root, Predicate return permitted; } + private Set leafPermittedSubTypes(TypeSymbol root, Predicate accept) { + Set permitted = new HashSet<>(); + List permittedSubtypesClosure = baseClasses(root); + + while (permittedSubtypesClosure.nonEmpty()) { + ClassSymbol current = permittedSubtypesClosure.head; + + permittedSubtypesClosure = permittedSubtypesClosure.tail; + + current.complete(); + + if (current.isSealed() && current.isAbstract()) { + for (Type t : current.getPermittedSubclasses()) { + ClassSymbol csym = (ClassSymbol) t.tsym; + + if (accept.test(csym)) { + permittedSubtypesClosure = permittedSubtypesClosure.prepend(csym); + } + } + } else { + permitted.add(current); + } + } + + return permitted; + } + private List baseClasses(TypeSymbol root) { if (root instanceof ClassSymbol clazz) { return List.of(clazz); @@ -1029,7 +1394,7 @@ private List baseClasses(TypeSymbol root) { * as pattern hashes cannot be used to speed up the matching process */ private Set reduceNestedPatterns(Set patterns, - boolean useHashes) { + boolean backtrack) { /* implementation note: * finding a sub-set of patterns that only differ in a single * column is time-consuming task, so this method speeds it up by: @@ -1059,7 +1424,8 @@ private Set reduceNestedPatterns(Set pat .stream() //error recovery, ignore patterns with incorrect number of nested patterns: .filter(pd -> pd.nested.length == nestedPatternsCount) - .collect(groupingBy(pd -> useHashes ? pd.hashCode(mismatchingCandidateFin) : 0)); + .filter(pd -> !backtrack || !pd.backtrackOnly()) + .collect(groupingBy(pd -> backtrack ? pd.hashCode(mismatchingCandidateFin) : 0)); for (var candidates : groupEquivalenceCandidates.values()) { var candidatesArr = candidates.toArray(RecordPattern[]::new); @@ -1083,7 +1449,7 @@ private Set reduceNestedPatterns(Set pat for (int i = 0; i < rpOne.nested.length; i++) { if (i != mismatchingCandidate) { if (!rpOne.nested[i].equals(rpOther.nested[i])) { - if (useHashes || + if (backtrack || //when not using hashes, //check if rpOne.nested[i] is //a subtype of rpOther.nested[i]: @@ -1100,15 +1466,16 @@ private Set reduceNestedPatterns(Set pat } var nestedPatterns = join.stream().map(rp -> rp.nested[mismatchingCandidateFin]).collect(Collectors.toSet()); - var updatedPatterns = reduceNestedPatterns(nestedPatterns, useHashes); + var updatedPatterns = reduceNestedPatterns(nestedPatterns, backtrack); updatedPatterns = reduceRecordPatterns(updatedPatterns); updatedPatterns = removeCoveredRecordPatterns(updatedPatterns); updatedPatterns = reduceBindingPatterns(rpOne.fullComponentTypes()[mismatchingCandidateFin], updatedPatterns); if (!nestedPatterns.equals(updatedPatterns)) { - if (useHashes) { - current.removeAll(join); + for (RecordPattern rp : join) { + current.remove(rp); + current.add(new RecordPattern(rp.recordType(), true, rp.fullComponentTypes(), rp.nested())); } for (PatternDescription nested : updatedPatterns) { @@ -1116,6 +1483,7 @@ private Set reduceNestedPatterns(Set pat Arrays.copyOf(rpOne.nested, rpOne.nested.length); newNested[mismatchingCandidateFin] = nested; current.add(new RecordPattern(rpOne.recordType(), + rpOne.backtrackOnly(), rpOne.fullComponentTypes(), newNested)); } @@ -3528,7 +3896,9 @@ public static Liveness from(boolean value) { } } - sealed interface PatternDescription { } + sealed interface PatternDescription { + public Type type(); + } public PatternDescription makePatternDescription(Type selectorType, JCPattern pattern) { if (pattern instanceof JCBindingPattern binding) { Type type = !selectorType.isPrimitive() && types.isSubtype(selectorType, binding.type) @@ -3563,7 +3933,12 @@ public PatternDescription makePatternDescription(Type selectorType, JCPattern pa throw Assert.error(); } } - record BindingPattern(Type type) implements PatternDescription { + record BindingPattern(Type type, int permittedSubtypes) implements PatternDescription { + + public BindingPattern(Type type) { + this(type, -1); + } + @Override public int hashCode() { return type.tsym.hashCode(); @@ -3578,10 +3953,17 @@ public String toString() { return type.tsym + " _"; } } - record RecordPattern(Type recordType, int _hashCode, Type[] fullComponentTypes, PatternDescription... nested) implements PatternDescription { + record RecordPattern(Type recordType, int _hashCode, boolean backtrackOnly, Type[] fullComponentTypes, PatternDescription... nested) implements PatternDescription { public RecordPattern(Type recordType, Type[] fullComponentTypes, PatternDescription[] nested) { - this(recordType, hashCode(-1, recordType, nested), fullComponentTypes, nested); + this(recordType, false, fullComponentTypes, nested); + } + + public RecordPattern(Type recordType, boolean backtrackOnly, Type[] fullComponentTypes, PatternDescription[] nested) { + this(recordType, hashCode(-1, recordType, nested), backtrackOnly, fullComponentTypes, nested); + if ("PatternSwitchPerformance.Path(PatternSwitchPerformance.Player _, PatternSwitchPerformance.BasicCell(PatternSwitchPerformance.Underneath _, PatternSwitchPerformance.Block _), PatternSwitchPerformance.BasicCell(PatternSwitchPerformance.Underneath _, PatternSwitchPerformance.Enemy _))".equals(toString())) { + System.err.println("foobar"); + } } @Override @@ -3616,5 +3998,10 @@ public String toString() { .map(pd -> pd.toString()) .collect(Collectors.joining(", ")) + ")"; } + + @Override + public Type type() { + return recordType; + } } } diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/compiler.properties b/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/compiler.properties index 7dde6cc963f82..a21b3e7acd2eb 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/compiler.properties +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/compiler.properties @@ -1473,6 +1473,16 @@ compiler.err.not.exhaustive=\ compiler.err.not.exhaustive.statement=\ the switch statement does not cover all possible input values +# 0: string +compiler.err.not.exhaustive.details=\ + the switch expression does not cover all possible input values\n\ + missing patterns: {0} + +# 0: string +compiler.err.not.exhaustive.statement.details=\ + the switch statement does not cover all possible input values\n\ + missing patterns: {0} + compiler.err.initializer.must.be.able.to.complete.normally=\ initializer must be able to complete normally diff --git a/test/langtools/tools/javac/patterns/Exhaustiveness.java b/test/langtools/tools/javac/patterns/Exhaustiveness.java index 3f86ada6e7ebc..29947d6b6cef7 100644 --- a/test/langtools/tools/javac/patterns/Exhaustiveness.java +++ b/test/langtools/tools/javac/patterns/Exhaustiveness.java @@ -1633,7 +1633,7 @@ public void testDeeplyNestedExhaustive(Path base) throws Exception { true); } - @Test +// @Test public void testDeeplyNestedNotExhaustive(Path base) throws Exception { List variants = createDeeplyNestedVariants().stream().collect(Collectors.toCollection(ArrayList::new)); variants.remove((int) (Math.random() * variants.size())); @@ -2182,6 +2182,44 @@ static int r(R r) { """); } + @Test + public void testX(Path base) throws Exception { + doTest(base, + new String[0], + """ + package test; + public class Test { + private int test(Root r) { + return switch (r) { + case Root(R1 _, _, _) -> 0; + case Root(R2 _, R1 _, _) -> 0; + case Root(R2 _, R2 _, R1 _) -> 0; + case Root(R2 _, R2(R1 _, R1 _), R2(R1 _, R1 _)) -> 0; + case Root(R2 _, R2(R1 _, R1 _), R2(R1 _, R2 _)) -> 0; + case Root(R2 _, R2(R1 _, R1 _), R2(R2 _, R1 _)) -> 0; + case Root(R2 _, R2(R1 _, R1 _), R2(R2 _, R2 _)) -> 0; + case Root(R2 _, R2(R1 _, R2 _), R2(R1 _, R1 _)) -> 0; + case Root(R2 _, R2(R1 _, R2 _), R2(R1 _, R2 _)) -> 0; + case Root(R2 _, R2(R1 _, R2 _), R2(R2 _, R1 _)) -> 0; + case Root(R2 _, R2(R1 _, R2 _), R2(R2 _, R2 _)) -> 0; + case Root(R2 _, R2(R2 _, R1 _), R2(R1 _, R1 _)) -> 0; + case Root(R2 _, R2(R2 _, R1 _), R2(R1 _, R2 _)) -> 0; + case Root(R2 _, R2(R2 _, R1 _), R2(R2 _, R1 _)) -> 0; + case Root(R2 _, R2(R2 _, R1 _), R2(R2 _, R2 _)) -> 0; + case Root(R2 _, R2(R2 _, R2 _), R2(R1 _, R1 _)) -> 0; + case Root(R2 _, R2(R2 _, R2 _), R2(R1 _, R2 _)) -> 0; + case Root(R2 _, R2(R2 _, R2 _), R2(R2 _, R1 _)) -> 0; + case Root(R2 _, R2(R2 _, R2 _), R2 _) -> 0; //functionally equivalent to: Root(R2 _, R2(R2 _, R2 _), R2(R2 _, R2 _)) + }; + } + sealed interface Base {} + record R1() implements Base {} + record R2(Base b1, Base b2) implements Base {} + record Root(Base b1, Base b2, Base b3) {} + } + """); + } + private void doTest(Path base, String[] libraryCode, String testCode, String... expectedErrors) throws IOException { doTest(base, libraryCode, testCode, false, expectedErrors); } @@ -2218,6 +2256,7 @@ private void doTest(Path base, String[] libraryCode, String testCode, boolean st "-Xlint:-preview", "--class-path", libClasses.toString(), "-XDshould-stop.at=FLOW", + "-XDexhaustivityTimeout=0", stopAtFlow ? "-XDshould-stop.ifNoError=FLOW" : "-XDnoop") .outdir(classes) diff --git a/test/langtools/tools/javac/patterns/ExhaustivenessConvenientErrors.java b/test/langtools/tools/javac/patterns/ExhaustivenessConvenientErrors.java new file mode 100644 index 0000000000000..1b34e109b3a00 --- /dev/null +++ b/test/langtools/tools/javac/patterns/ExhaustivenessConvenientErrors.java @@ -0,0 +1,430 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/** + * @test + * @bug 9999999 + * @summary Check exhaustiveness of switches over sealed types. + * @library /tools/lib + * @modules jdk.compiler/com.sun.tools.javac.api + * jdk.compiler/com.sun.tools.javac.main + * jdk.compiler/com.sun.tools.javac.util + * @build toolbox.ToolBox toolbox.JavacTask + * @run main ExhaustivenessConvenientErrors +*/ + +import com.sun.tools.javac.api.ClientCodeWrapper.DiagnosticSourceUnwrapper; +import com.sun.tools.javac.util.JCDiagnostic; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import toolbox.JavacTask; +import toolbox.Task; +import toolbox.TestRunner; +import toolbox.ToolBox; + +public class ExhaustivenessConvenientErrors extends TestRunner { + + ToolBox tb; + + public static void main(String... args) throws Exception { + new ExhaustivenessConvenientErrors().runTests(); + } + + ExhaustivenessConvenientErrors() { + super(System.err); + tb = new ToolBox(); + } + + public void runTests() throws Exception { + runTests(m -> new Object[] { Paths.get(m.getName()) }); + } + + @Test + public void testExhaustiveSealedClasses(Path base) throws Exception { + doTest(base, + new String[]{""" + package lib; + public sealed interface S permits A, B {} + """, + """ + package lib; + public final class A implements S {} + """, + """ + package lib; + public final class B implements S {} + """}, + """ + package test; + import lib.*; + public class Test { + private int test(S obj) { + return switch (obj) { + case A a -> 0; + }; + } + } + """, + "lib.B _"); + } + + @Test + public void testExhaustiveSealedClassesTransitive(Path base) throws Exception { + doTest(base, + new String[]{""" + package lib; + public sealed interface S1 permits S2, A {} + """, + """ + package lib; + public sealed interface S2 extends S1 permits S3, B {} + """, + """ + package lib; + public sealed interface S3 extends S2 permits C, D {} + """, + """ + package lib; + public final class A implements S1 {} + """, + """ + package lib; + public final class B implements S2 {} + """, + """ + package lib; + public final class C implements S3 {} + """, + """ + package lib; + public final class D implements S3 {} + """}, + """ + package test; + import lib.*; + public class Test { + private int test(S1 obj) { + return switch (obj) { + case A a -> 0; + case B a -> 0; + case D a -> 0; + }; + } + } + """, + "lib.C _"); + } + + @Test + public void testTrivialRecord(Path base) throws Exception { + doTest(base, + new String[]{""" + package lib; + public sealed interface S permits A, B {} + """, + """ + package lib; + public final class A implements S {} + """, + """ + package lib; + public final class B implements S {} + """, + """ + package lib; + public record R(S s) {} + """}, + """ + package test; + import lib.*; + public class Test { + private int test(R r) { + return switch (r) { + case R(A a) -> 0; + }; + } + } + """, + "lib.R(lib.B _)"); + } + + @Test + public void testNonNestedRecord(Path base) throws Exception { + doTest(base, + new String[]{""" + package lib; + public sealed interface S permits A, B {} + """, + """ + package lib; + public final class A implements S {} + """, + """ + package lib; + public final class B implements S {} + """, + """ + package lib; + public record R(S s1, S s2) {} + """}, + """ + package test; + import lib.*; + public class Test { + private int test(R r) { + return switch (r) { + case R(A a, B b) -> 0; + case R(B b, A a) -> 0; + }; + } + } + """, + "lib.R(lib.A _, lib.A _)", + "lib.R(lib.B _, lib.B _)"); + } + + @Test + public void testComplex1(Path base) throws Exception { + doTest(base, + new String[0], + """ + package test; + import lib.*; + public class Test { + private int test(Root r) { + return switch (r) { + case Root(R1 _, _, _) -> 0; + }; + } + sealed interface Base {} + record R1() implements Base {} + record R2() implements Base {} + record R3(Base b1, Base b2) implements Base {} + record Root(Base b1, Base b2, Base b3) {} + } + """, + "test.Test.Root(test.Test.R2 _, test.Test.Base _, test.Test.Base _)", + "test.Test.Root(test.Test.R3 _, test.Test.Base _, test.Test.Base _)"); + } + + @Test + public void testComplex2(Path base) throws Exception { + doTest(base, + new String[0], + """ + package test; + import lib.*; + public class Test { + private int test(Root r) { + return switch (r) { + case Root(R1 _, _, _) -> 0; + case Root(R2 _, R1 _, _) -> 0; + case Root(R2 _, R2 _, R1 _) -> 0; + case Root(R2 _, R2(R1 _, R1 _), R2(R1 _, R1 _)) -> 0; + case Root(R2 _, R2(R1 _, R1 _), R2(R1 _, R2 _)) -> 0; + case Root(R2 _, R2(R1 _, R1 _), R2(R2 _, R1 _)) -> 0; + case Root(R2 _, R2(R1 _, R1 _), R2(R2 _, R2 _)) -> 0; + case Root(R2 _, R2(R1 _, R2 _), R2(R1 _, R1 _)) -> 0; + case Root(R2 _, R2(R1 _, R2 _), R2(R1 _, R2 _)) -> 0; + case Root(R2 _, R2(R1 _, R2 _), R2(R2 _, R1 _)) -> 0; + case Root(R2 _, R2(R1 _, R2 _), R2(R2 _, R2 _)) -> 0; + case Root(R2 _, R2(R2 _, R1 _), R2(R1 _, R1 _)) -> 0; + case Root(R2 _, R2(R2 _, R1 _), R2(R1 _, R2 _)) -> 0; + case Root(R2 _, R2(R2 _, R1 _), R2(R2 _, R1 _)) -> 0; + case Root(R2 _, R2(R2 _, R1 _), R2(R2 _, R2 _)) -> 0; + case Root(R2 _, R2(R2 _, R2 _), R2(R1 _, R1 _)) -> 0; + case Root(R2 _, R2(R2 _, R2 _), R2(R1 _, R2 _)) -> 0; + case Root(R2 _, R2(R2 _, R2 _), R2(R2 _, R1 _)) -> 0; +// case Root(R2 _, R2(R2 _, R2 _), R2(R2 _, R2 _)) -> 0; + }; + } + sealed interface Base {} + record R1() implements Base {} + record R2(Base b1, Base b2) implements Base {} + record Root(Base b1, Base b2, Base b3) {} + } + """, + "test.Test.Root(test.Test.R2 _, test.Test.R2(test.Test.R2 _, test.Test.R2 _), test.Test.R2(test.Test.R2 _, test.Test.R2 _))"); + } + + @Test + public void testComplex3(Path base) throws Exception { + doTest(base, + new String[0], + """ + package test; + public class Test { + private int test(Triple p) { + return switch (p) { + case Triple(B _, _, _) -> 0; + case Triple(_, A _, _) -> 0; + case Triple(_, _, A _) -> 0; + case Triple(A p, C(Nested _, NestedBaseA _), _) -> 0; + case Triple(A p, C(Nested _, NestedBaseB _), C(Underneath _, NestedBaseA _)) -> 0; + case Triple(A p, C(Nested _, NestedBaseB _), C(Underneath _, NestedBaseB _)) -> 0; + case Triple(A p, C(Nested _, NestedBaseB _), C(Underneath _, NestedBaseC _)) -> 0; + case Triple(A p, C(Nested _, NestedBaseC _), C(Underneath _, NestedBaseA _)) -> 0; + case Triple(A p, C(Nested _, NestedBaseC _), C(Underneath _, NestedBaseB _)) -> 0; +// case Path(A p, C(Nested _, NestedBaseC _), C(Underneath _, NestedBaseC _)) -> 0; + }; + } + record Triple(Base c1, Base c2, Base c3) {} + sealed interface Base permits A, B {} + record A(boolean key) implements Base { + } + sealed interface B extends Base {} + record C(Nested n, NestedBase b) implements B {} + record Nested() {} + sealed interface NestedBase {} + record NestedBaseA() implements NestedBase {} + record NestedBaseB() implements NestedBase {} + record NestedBaseC() implements NestedBase {} + } + """, + "test.Test.Triple(test.Test.A _, test.Test.C(test.Test.Nested _, test.Test.NestedBaseC _), test.Test.C(test.Test.Nested _, test.Test.NestedBaseC _))"); + } + +// @Test TODO: this still produced sub-optimal results: + public void testComplex4(Path base) throws Exception { + doTest(base, + new String[0], + """ + package test; + import lib.*; + public class Test { + private int test(Root r) { + return switch (r) { + case Root(R1 _, _, _) -> 0; + case Root(R2 _, R1 _, _) -> 0; + case Root(R2 _, R2 _, R1 _) -> 0; + case Root(R2 _, R2(R1 _, R1 _), R2(R1 _, R1 _)) -> 0; + case Root(R2 _, R2(R1 _, R1 _), R2(R1 _, R2 _)) -> 0; + case Root(R2 _, R2(R1 _, R1 _), R2(R2 _, R1 _)) -> 0; + case Root(R2 _, R2(R1 _, R1 _), R2(R2 _, R2 _)) -> 0; + case Root(R2 _, R2(R1 _, R2 _), R2(R1 _, R1 _)) -> 0; + case Root(R2 _, R2(R1 _, R2 _), R2(R1 _, R2 _)) -> 0; +// case Root(R2 _, R2(R1 _, R2 _), R2(R2 _, R1 _)) -> 0; + case Root(R2 _, R2(R1 _, R2 _), R2(R2 _, R2 _)) -> 0; + case Root(R2 _, R2(R2 _, R1 _), R2(R1 _, R1 _)) -> 0; + case Root(R2 _, R2(R2 _, R1 _), R2(R1 _, R2 _)) -> 0; + case Root(R2 _, R2(R2 _, R1 _), R2(R2 _, R1 _)) -> 0; + case Root(R2 _, R2(R2 _, R1 _), R2(R2 _, R2 _)) -> 0; + case Root(R2 _, R2(R2 _, R2 _), R2(R1 _, R1 _)) -> 0; + case Root(R2 _, R2(R2 _, R2 _), R2(R1 _, R2 _)) -> 0; + case Root(R2 _, R2(R2 _, R2 _), R2(R2 _, R1 _)) -> 0; +// case Root(R2 _, R2(R2 _, R2 _), R2(R2 _, R2 _)) -> 0; + }; + } + sealed interface Base {} + record R1() implements Base {} + record R2(Base b1, Base b2) implements Base {} + record Root(Base b1, Base b2, Base b3) {} + } + """, + "test.Test.Root(test.Test.R2 _, test.Test.R2(test.Test.R2 _, test.Test.R2 _), test.Test.R2(test.Test.R2 _, test.Test.R2 _))"); + } + + @Test + public void testInfiniteRecursion(Path base) throws Exception { + doTest(base, + new String[0], + """ + package test; + public class Test { + private int test(R r) { + return switch (r) { + case R(_, _, R(_, _, _, _), String s) -> 0; + case R(_, _, R(_, _, _, String str), _) -> 0; + }; + } + } + public record R(R r1, R r2, R r3, Object o) {} + """, + "test.R(test.R _, test.R _, test.R(test.R _, test.R _, test.R _, java.lang.Object _), java.lang.Object _)"); + } + + private void doTest(Path base, String[] libraryCode, String testCode, String... expectedMissingPatterns) throws IOException { + Path current = base.resolve("."); + Path libClasses = current.resolve("libClasses"); + + Files.createDirectories(libClasses); + + if (libraryCode.length != 0) { + Path libSrc = current.resolve("lib-src"); + + for (String code : libraryCode) { + tb.writeJavaFiles(libSrc, code); + } + + new JavacTask(tb) + .outdir(libClasses) + .files(tb.findJavaFiles(libSrc)) + .run(); + } + + Path src = current.resolve("src"); + tb.writeJavaFiles(src, testCode); + + Path classes = current.resolve("libClasses"); + + Files.createDirectories(libClasses); + Set missingPatterns = new HashSet<>(); + + new JavacTask(tb) + .options("-XDrawDiagnostics", + "-XDdev", + "-Xlint:-preview", + "--class-path", libClasses.toString(), + "-XDshould-stop.at=FLOW", + "-XDshould-stop.ifNoError=FLOW") + .outdir(classes) + .files(tb.findJavaFiles(src)) + .diagnosticListener(d -> { + if ("compiler.err.not.exhaustive.details".equals(d.getCode())) { + if (d instanceof DiagnosticSourceUnwrapper uw) { + d = uw.d; + } + if (d instanceof JCDiagnostic diag) { + missingPatterns.addAll(List.of(((String) diag.getArgs()[0]).split("\n"))); + } + } + }) + .run(Task.Expect.FAIL) + .writeAll() + .getOutputLines(Task.OutputKind.DIRECT); + + Set expectedPatterns = new HashSet<>(List.of(expectedMissingPatterns)); + + if (!expectedPatterns.equals(missingPatterns)) { + throw new AssertionError("Incorrect errors, expected: " + expectedPatterns + + ", actual: " + missingPatterns); + } + } + +} From cad665ea407a4de15ebad5c4fcb62fd1feda15b3 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Wed, 6 Aug 2025 13:28:07 +0200 Subject: [PATCH 02/33] Improvements. --- .../com/sun/tools/javac/comp/Flow.java | 44 ++++++++--------- .../ExhaustivenessConvenientErrors.java | 47 ++++++++++++++++++- 2 files changed, 67 insertions(+), 24 deletions(-) diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java index 04280096f556c..ce739a5da0434 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java @@ -850,8 +850,8 @@ private boolean exhausts(JCExpression selector, List cases, Set } } try { - Pair> coveredResult = isCovered(selector.type, patternSet); - if (coveredResult.fst) { + CoverageResult coveredResult = isCovered(selector.type, patternSet); + if (coveredResult.covered()) { return true; } if (missingExhaustivenessTimeout == 0) { @@ -859,7 +859,7 @@ private boolean exhausts(JCExpression selector, List cases, Set } PatternDescription defaultPattern = new BindingPattern(selector.type); - Set missingPatterns = expandMissingPatternDescriptions(selector.type, selector.type, defaultPattern, coveredResult.snd, Set.of(defaultPattern)); + Set missingPatterns = expandMissingPatternDescriptions(selector.type, selector.type, defaultPattern, coveredResult.incompletePatterns(), Set.of(defaultPattern)); for (PatternDescription missing : missingPatterns) { pendingNotExhaustiveDetails.add(missing.toString()); @@ -896,7 +896,7 @@ private Set expandMissingPatternDescriptions(Type selectorTy Set replaced = replace(inMissingPatterns, toExpand, reducedPermittedPatterns); - if (isCovered(selectorType, joinSets(basePatterns, replaced)).fst) { + if (isCovered(selectorType, joinSets(basePatterns, replaced)).covered()) { it.remove(); reduced = true; } @@ -957,16 +957,16 @@ private Set expandMissingPatternDescriptions(Type selectorTy reducedAdded.remove(current); - if (isCovered(selectorType, joinSets(basePatterns, replace(inMissingPatterns, bp, reducedAdded))).fst) { + if (isCovered(selectorType, joinSets(basePatterns, replace(inMissingPatterns, bp, reducedAdded))).covered()) { it.remove(); } } - Pair> combinatorial = isCovered(targetType, combinatorialPatterns); + CoverageResult coverageResult = isCovered(targetType, combinatorialPatterns); - if (!combinatorial.fst) { + if (!coverageResult.covered()) { //nothing better can be done(?) - combinatorialPatterns = combinatorial.snd; + combinatorialPatterns = coverageResult.incompletePatterns(); } //combine sealed subtypes into the supertype, if all is covered. @@ -981,7 +981,7 @@ private Set expandMissingPatternDescriptions(Type selectorTy reducedAdded.remove(current); - if (isCovered(selectorType, joinSets(basePatterns, replace(inMissingPatterns, bp, reducedAdded))).fst) { + if (isCovered(selectorType, joinSets(basePatterns, replace(inMissingPatterns, bp, reducedAdded))).covered()) { it.remove(); } } @@ -1104,16 +1104,14 @@ private Collection replace(PatternDescription in, //false otherwise //TODO: there may be a better name for this method: private boolean isMoreImportant(PatternDescription pd1, PatternDescription pd2, Set basePatterns, Set missingPatterns) { - if (!(pd1 instanceof RecordPattern rp1)) { - throw new AssertionError(); - } - if (!(pd2 instanceof RecordPattern rp2)) { - throw new AssertionError(); - } - for (int c = 0; c < rp1.nested.length; c++) { - BindingPattern bp1 = (BindingPattern) rp1.nested[c]; + if (pd1 instanceof RecordPattern rp1 && pd2 instanceof RecordPattern rp2) { + for (int c = 0; c < rp1.nested.length; c++) { + if (isMoreImportant((BindingPattern) rp1.nested[c], (BindingPattern) rp2.nested[c], basePatterns, missingPatterns)) { + return true; + } + } + } else if (pd1 instanceof BindingPattern bp1 && pd2 instanceof BindingPattern bp2) { Type t1 = bp1.type(); - BindingPattern bp2 = (BindingPattern) rp2.nested[c]; Type t2 = bp2.type(); boolean t1IsImportantRecord = (t1.tsym.flags_field & RECORD) != 0 && hasMatchingRecordPattern(basePatterns, missingPatterns, bp1); boolean t2IsImportantRecord = (t2.tsym.flags_field & RECORD) != 0 && hasMatchingRecordPattern(basePatterns, missingPatterns, bp2); @@ -1160,7 +1158,7 @@ private void generatePatternsWithReplacedNestedPattern(RecordPattern basePattern } } - private Pair> isCovered(Type selectorType, Set patterns) { + private CoverageResult isCovered(Type selectorType, Set patterns) { boolean backtrack = true; //XXX: this should be inverted, right? boolean repeat = true; Set updatedPatterns; @@ -1171,7 +1169,7 @@ private Pair> isCovered(Type selectorType, Set< updatedPatterns = removeCoveredRecordPatterns(updatedPatterns); repeat = !updatedPatterns.equals(patterns); if (checkCovered(selectorType, patterns)) { - return Pair.of(true, null); + return new CoverageResult(true, null); } if (!repeat) { //there may be situation like: @@ -1191,11 +1189,13 @@ private Pair> isCovered(Type selectorType, Set< patterns = updatedPatterns; } while (repeat); if (checkCovered(selectorType, patterns)) { - return Pair.of(true, null); + return new CoverageResult(true, null); } - return Pair.of(false, updatedPatterns); + return new CoverageResult(false, updatedPatterns); } + private record CoverageResult(boolean covered, Set incompletePatterns) {} + private boolean checkCovered(Type seltype, Iterable patterns) { for (Type seltypeComponent : components(seltype)) { for (PatternDescription pd : patterns) { diff --git a/test/langtools/tools/javac/patterns/ExhaustivenessConvenientErrors.java b/test/langtools/tools/javac/patterns/ExhaustivenessConvenientErrors.java index 1b34e109b3a00..96e3f6f39a31a 100644 --- a/test/langtools/tools/javac/patterns/ExhaustivenessConvenientErrors.java +++ b/test/langtools/tools/javac/patterns/ExhaustivenessConvenientErrors.java @@ -310,7 +310,7 @@ record NestedBaseC() implements NestedBase {} "test.Test.Triple(test.Test.A _, test.Test.C(test.Test.Nested _, test.Test.NestedBaseC _), test.Test.C(test.Test.Nested _, test.Test.NestedBaseC _))"); } -// @Test TODO: this still produced sub-optimal results: + @Test public void testComplex4(Path base) throws Exception { doTest(base, new String[0], @@ -347,7 +347,50 @@ record R2(Base b1, Base b2) implements Base {} record Root(Base b1, Base b2, Base b3) {} } """, - "test.Test.Root(test.Test.R2 _, test.Test.R2(test.Test.R2 _, test.Test.R2 _), test.Test.R2(test.Test.R2 _, test.Test.R2 _))"); + "test.Test.Root(test.Test.R2 _, test.Test.R2(test.Test.Base _, test.Test.R2 _), test.Test.R2(test.Test.R2 _, test.Test.Base _))"); + //ideally, the result would be as follow, but it is difficult to split Base on two distinct places: +// "test.Test.Root(test.Test.R2 _, test.Test.R2(test.Test.R1 _, test.Test.R2 _), test.Test.R2(test.Test.R2 _, test.Test.R1 _))", +// "test.Test.Root(test.Test.R2 _, test.Test.R2(test.Test.R2 _, test.Test.R2 _), test.Test.R2(test.Test.R2 _, test.Test.R2 _))"); + } + + @Test + public void testComplex5(Path base) throws Exception { + doTest(base, + new String[0], + """ + package test; + public class Test { + private int test(Triple p) { + return switch (p) { + case Triple(B _, _, _) -> 0; + case Triple(_, A _, _) -> 0; + case Triple(_, _, A _) -> 0; +// case Triple(A p, C(Nested _, NestedBaseA _), _) -> 0; + case Triple(A p, C(Nested _, NestedBaseB _), C(Nested _, NestedBaseA _)) -> 0; + case Triple(A p, C(Nested _, NestedBaseB _), C(Nested _, NestedBaseB _)) -> 0; + case Triple(A p, C(Nested _, NestedBaseB _), C(Nested _, NestedBaseC _)) -> 0; + case Triple(A p, C(Nested _, NestedBaseC _), C(Nested _, NestedBaseA _)) -> 0; + case Triple(A p, C(Nested _, NestedBaseC _), C(Nested _, NestedBaseB _)) -> 0; +// case Path(A p, C(Nested _, NestedBaseC _), C(Nested _, NestedBaseC _)) -> 0; + }; + } + record Triple(Base c1, Base c2, Base c3) {} + sealed interface Base permits A, B {} + record A(boolean key) implements Base { + } + sealed interface B extends Base {} + record C(Nested n, NestedBase b) implements B {} + record Nested() {} + sealed interface NestedBase {} + record NestedBaseA() implements NestedBase {} + record NestedBaseB() implements NestedBase {} + record NestedBaseC() implements NestedBase {} + } + """, + "test.Test.Triple(test.Test.A _, test.Test.C(test.Test.Nested _, test.Test.NestedBaseA _), test.Test.C _)", + //the following could be: + //test.Test.Triple(test.Test.A _, test.Test.C(test.Test.Nested _, test.Test.NestedBaseC _), test.Test.C(test.Test.Nested _, test.Test.NestedBaseC _)) + "test.Test.Triple(test.Test.A _, test.Test.C(test.Test.Nested _, test.Test.NestedBaseC _), test.Test.C _)"); } @Test From 2cfbc715c064607f5cf997fda7eb41c0c59f99d4 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Thu, 7 Aug 2025 18:27:35 +0200 Subject: [PATCH 03/33] 8364991: Incorrect not-exhaustive error --- .../com/sun/tools/javac/comp/Flow.java | 26 +++++--- .../tools/javac/patterns/Exhaustiveness.java | 64 ++++++++++++++++++- 2 files changed, 80 insertions(+), 10 deletions(-) diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java index e685f139b680c..001192cf625be 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java @@ -822,6 +822,7 @@ private boolean exhausts(JCExpression selector, List cases) { } } Set patterns = patternSet; + Set> seenFallback = new HashSet<>(); boolean useHashes = true; try { boolean repeat = true; @@ -843,7 +844,7 @@ private boolean exhausts(JCExpression selector, List cases) { //but hashing in reduceNestedPatterns will not allow that //disable the use of hashing, and use subtyping in //reduceNestedPatterns to handle situations like this: - repeat = useHashes; + repeat = useHashes && seenFallback.add(updatedPatterns); useHashes = false; } else { //if a reduction happened, make sure hashing in reduceNestedPatterns @@ -1083,15 +1084,24 @@ private Set reduceNestedPatterns(Set pat for (int i = 0; i < rpOne.nested.length; i++) { if (i != mismatchingCandidate) { if (!rpOne.nested[i].equals(rpOther.nested[i])) { - if (useHashes || - //when not using hashes, - //check if rpOne.nested[i] is - //a subtype of rpOther.nested[i]: - !(rpOne.nested[i] instanceof BindingPattern bpOne) || - !(rpOther.nested[i] instanceof BindingPattern bpOther) || - !types.isSubtype(types.erasure(bpOne.type), types.erasure(bpOther.type))) { + if (useHashes) { continue NEXT_PATTERN; } + //when not using hashes, + //check if rpOne.nested[i] is + //a subtype of rpOther.nested[i]: + if (!(rpOther.nested[i] instanceof BindingPattern bpOther)) { + continue NEXT_PATTERN; + } + if (rpOne.nested[i] instanceof BindingPattern bpOne) { + if (!types.isSubtype(types.erasure(bpOne.type), types.erasure(bpOther.type))) { + continue NEXT_PATTERN; + } + } else if (rpOne.nested[i] instanceof RecordPattern nestedRPOne) { + if (!types.isSubtype(types.erasure(nestedRPOne.recordType()), types.erasure(bpOther.type))) { + continue NEXT_PATTERN; + } + } } } } diff --git a/test/langtools/tools/javac/patterns/Exhaustiveness.java b/test/langtools/tools/javac/patterns/Exhaustiveness.java index 3f86ada6e7ebc..53c4bf8231760 100644 --- a/test/langtools/tools/javac/patterns/Exhaustiveness.java +++ b/test/langtools/tools/javac/patterns/Exhaustiveness.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -23,7 +23,7 @@ /** * @test - * @bug 8262891 8268871 8274363 8281100 8294670 8311038 8311815 8325215 8333169 8327368 + * @bug 8262891 8268871 8274363 8281100 8294670 8311038 8311815 8325215 8333169 8327368 8364991 * @summary Check exhaustiveness of switches over sealed types. * @library /tools/lib * @modules jdk.compiler/com.sun.tools.javac.api @@ -2182,6 +2182,66 @@ static int r(R r) { """); } + @Test //JDK-8364991 + public void testDifferentReductionPaths(Path base) throws Exception { + doTest(base, + new String[0], + """ + package test; + public class Test { + private int test(Root r) { + return switch (r) { + case Root(R1 _, _, _) -> 0; + case Root(R2 _, R1 _, _) -> 0; + case Root(R2 _, R2 _, R1 _) -> 0; + case Root(R2 _, R2(R1 _, R1 _), R2(R1 _, R1 _)) -> 0; + case Root(R2 _, R2(R1 _, R1 _), R2(R1 _, R2 _)) -> 0; + case Root(R2 _, R2(R1 _, R1 _), R2(R2 _, R1 _)) -> 0; + case Root(R2 _, R2(R1 _, R1 _), R2(R2 _, R2 _)) -> 0; + case Root(R2 _, R2(R1 _, R2 _), R2(R1 _, R1 _)) -> 0; + case Root(R2 _, R2(R1 _, R2 _), R2(R1 _, R2 _)) -> 0; + case Root(R2 _, R2(R1 _, R2 _), R2(R2 _, R1 _)) -> 0; + case Root(R2 _, R2(R1 _, R2 _), R2(R2 _, R2 _)) -> 0; + case Root(R2 _, R2(R2 _, R1 _), R2(R1 _, R1 _)) -> 0; + case Root(R2 _, R2(R2 _, R1 _), R2(R1 _, R2 _)) -> 0; + case Root(R2 _, R2(R2 _, R1 _), R2(R2 _, R1 _)) -> 0; + case Root(R2 _, R2(R2 _, R1 _), R2(R2 _, R2 _)) -> 0; + case Root(R2 _, R2(R2 _, R2 _), R2(R1 _, R1 _)) -> 0; + case Root(R2 _, R2(R2 _, R2 _), R2(R1 _, R2 _)) -> 0; + case Root(R2 _, R2(R2 _, R2 _), R2(R2 _, R1 _)) -> 0; + case Root(R2 _, R2(R2 _, R2 _), R2 _) -> 0; //functionally equivalent to: Root(R2 _, R2(R2 _, R2 _), R2(R2 _, R2 _)) + }; + } + sealed interface Base {} + record R1() implements Base {} + record R2(Base b1, Base b2) implements Base {} + record Root(Base b1, Base b2, Base b3) {} + } + """); + } + + @Test //JDK-8364991 + public void testX(Path base) throws Exception { + doTest(base, + new String[0], + """ + package test; + public class Test { + private int test(Root r) { + return switch (r) { + case Root(R2 _, R2(R1 _)) -> 0; + case Root(R2(R1 _), R2(R2 _)) -> 0; + case Root(R2(R2 _), R2 _) -> 0; //the above is functionally equivalent to: Root(R2(R2 _), R2(R2 _)) -> 0; + }; + } + sealed interface Base {} + record R1() implements Base {} + record R2(Base b) implements Base {} + record Root(R2 b2, R2 b3) {} + } + """); + } + private void doTest(Path base, String[] libraryCode, String testCode, String... expectedErrors) throws IOException { doTest(base, libraryCode, testCode, false, expectedErrors); } From e4f655d70f38975823b60e4498c72dcf4188e28e Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Thu, 7 Aug 2025 19:13:38 +0200 Subject: [PATCH 04/33] Improving test debuggability. --- test/langtools/tools/javac/patterns/Exhaustiveness.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/langtools/tools/javac/patterns/Exhaustiveness.java b/test/langtools/tools/javac/patterns/Exhaustiveness.java index 53c4bf8231760..a0236f323e26d 100644 --- a/test/langtools/tools/javac/patterns/Exhaustiveness.java +++ b/test/langtools/tools/javac/patterns/Exhaustiveness.java @@ -1636,10 +1636,11 @@ public void testDeeplyNestedExhaustive(Path base) throws Exception { @Test public void testDeeplyNestedNotExhaustive(Path base) throws Exception { List variants = createDeeplyNestedVariants().stream().collect(Collectors.toCollection(ArrayList::new)); - variants.remove((int) (Math.random() * variants.size())); + int removed = (int) (Math.random() * variants.size()); + variants.remove(removed); String code = testCodeForVariants(variants); - System.err.println("analyzing:"); + System.err.println("analyzing (removed: " + removed + "):"); System.err.println(code); doTest(base, new String[0], From 8647a7459bf139e334f56af31eb745ac3e632565 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Thu, 7 Aug 2025 19:14:17 +0200 Subject: [PATCH 05/33] Better name. --- .../share/classes/com/sun/tools/javac/comp/Flow.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java index c797b7a347c10..ba0233eadbef7 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java @@ -850,7 +850,7 @@ private boolean exhausts(JCExpression selector, List cases, Set } } try { - CoverageResult coveredResult = isCovered(selector.type, patternSet); + CoverageResult coveredResult = computeCoverage(selector.type, patternSet); if (coveredResult.covered()) { return true; } @@ -896,7 +896,7 @@ private Set expandMissingPatternDescriptions(Type selectorTy Set replaced = replace(inMissingPatterns, toExpand, reducedPermittedPatterns); - if (isCovered(selectorType, joinSets(basePatterns, replaced)).covered()) { + if (computeCoverage(selectorType, joinSets(basePatterns, replaced)).covered()) { it.remove(); reduced = true; } @@ -957,12 +957,12 @@ private Set expandMissingPatternDescriptions(Type selectorTy reducedAdded.remove(current); - if (isCovered(selectorType, joinSets(basePatterns, replace(inMissingPatterns, bp, reducedAdded))).covered()) { + if (computeCoverage(selectorType, joinSets(basePatterns, replace(inMissingPatterns, bp, reducedAdded))).covered()) { it.remove(); } } - CoverageResult coverageResult = isCovered(targetType, combinatorialPatterns); + CoverageResult coverageResult = computeCoverage(targetType, combinatorialPatterns); if (!coverageResult.covered()) { //nothing better can be done(?) @@ -981,7 +981,7 @@ private Set expandMissingPatternDescriptions(Type selectorTy reducedAdded.remove(current); - if (isCovered(selectorType, joinSets(basePatterns, replace(inMissingPatterns, bp, reducedAdded))).covered()) { + if (computeCoverage(selectorType, joinSets(basePatterns, replace(inMissingPatterns, bp, reducedAdded))).covered()) { it.remove(); } } @@ -1157,7 +1157,7 @@ private void generatePatternsWithReplacedNestedPattern(RecordPattern basePattern } } - private CoverageResult isCovered(Type selectorType, Set patterns) { + private CoverageResult computeCoverage(Type selectorType, Set patterns) { Set updatedPatterns; Set> seenPatterns = new HashSet<>(); boolean useHashes = true; From 5f70d8bfb33303c4cbb66281487019bb17d4f772 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Tue, 12 Aug 2025 15:36:56 +0200 Subject: [PATCH 06/33] Avoid computing exhaustiveness unnecessarily, cleanup. --- .../share/classes/com/sun/tools/javac/comp/Flow.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java index ba0233eadbef7..fa063be3b68e6 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java @@ -749,7 +749,7 @@ public void visitSwitch(JCSwitch tree) { TreeInfo.isErrorEnumSwitch(tree.selector, tree.cases); if (exhaustiveSwitch) { Set pendingNotExhaustiveDetails = new TreeSet<>(); - tree.isExhaustive |= exhausts(tree.selector, tree.cases, pendingNotExhaustiveDetails); + tree.isExhaustive = tree.isExhaustive || exhausts(tree.selector, tree.cases, pendingNotExhaustiveDetails); if (!tree.isExhaustive) { if (pendingNotExhaustiveDetails.isEmpty()) { log.error(tree, Errors.NotExhaustiveStatement); @@ -857,7 +857,7 @@ private boolean exhausts(JCExpression selector, List cases, Set if (missingExhaustivenessTimeout == 0) { return false; } - + //TODO: should stop computation when time runs out: PatternDescription defaultPattern = new BindingPattern(selector.type); Set missingPatterns = expandMissingPatternDescriptions(selector.type, selector.type, defaultPattern, coveredResult.incompletePatterns(), Set.of(defaultPattern)); @@ -1191,7 +1191,7 @@ private CoverageResult computeCoverage(Type selectorType, Set incompletePatterns) {} From 8735a3d7c0cc38676b8d7f74ce187d29019333c0 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Thu, 11 Sep 2025 16:41:34 +0200 Subject: [PATCH 07/33] Adjusting to spec. --- .../com/sun/tools/javac/comp/Flow.java | 54 ++++++++++++++----- .../tools/javac/patterns/Exhaustiveness.java | 8 +-- 2 files changed, 45 insertions(+), 17 deletions(-) diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java index 1bf45308fdf4a..c2ad9deb57dcb 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java @@ -60,6 +60,7 @@ import static com.sun.tools.javac.tree.JCTree.Tag.*; import com.sun.tools.javac.util.JCDiagnostic.Fragment; import java.util.Arrays; +import java.util.IdentityHashMap; import java.util.Iterator; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -801,6 +802,7 @@ private boolean exhausts(JCExpression selector, List cases) { } } Set patterns = patternSet; + Map> replaces = new IdentityHashMap<>(); Set> seenFallback = new HashSet<>(); boolean useHashes = true; try { @@ -808,8 +810,8 @@ private boolean exhausts(JCExpression selector, List cases) { while (repeat) { Set updatedPatterns; updatedPatterns = reduceBindingPatterns(selector.type, patterns); - updatedPatterns = reduceNestedPatterns(updatedPatterns, useHashes); - updatedPatterns = reduceRecordPatterns(updatedPatterns); + updatedPatterns = reduceNestedPatterns(updatedPatterns, replaces, useHashes); + updatedPatterns = reduceRecordPatterns(updatedPatterns, replaces); updatedPatterns = removeCoveredRecordPatterns(updatedPatterns); repeat = !updatedPatterns.equals(patterns); if (checkCovered(selector.type, patterns)) { @@ -1009,6 +1011,7 @@ private List baseClasses(TypeSymbol root) { * as pattern hashes cannot be used to speed up the matching process */ private Set reduceNestedPatterns(Set patterns, + Map> replaces, boolean useHashes) { /* implementation note: * finding a sub-set of patterns that only differ in a single @@ -1060,7 +1063,7 @@ private Set reduceNestedPatterns(Set pat RecordPattern rpOther = candidatesArr[nextCandidate]; if (rpOne.recordType.tsym == rpOther.recordType.tsym) { - for (int i = 0; i < rpOne.nested.length; i++) { + ACCEPT: for (int i = 0; i < rpOne.nested.length; i++) { if (i != mismatchingCandidate) { if (!rpOne.nested[i].equals(rpOther.nested[i])) { if (useHashes) { @@ -1077,9 +1080,26 @@ private Set reduceNestedPatterns(Set pat continue NEXT_PATTERN; } } else if (rpOne.nested[i] instanceof RecordPattern nestedRPOne) { - if (!types.isSubtype(types.erasure(nestedRPOne.recordType()), types.erasure(bpOther.type))) { + boolean foundMatchingReplaced = false; + Set pendingReplacedPatterns = new HashSet<>(replaces.getOrDefault(rpOther.nested[i], Set.of())); + + while (!pendingReplacedPatterns.isEmpty()) { + PatternDescription currentReplaced = pendingReplacedPatterns.iterator().next(); + + pendingReplacedPatterns.remove(currentReplaced); + + if (nestedRPOne.equals(currentReplaced)) { + foundMatchingReplaced = true; + break; + } + + pendingReplacedPatterns.addAll(replaces.getOrDefault(currentReplaced, Set.of())); + } + if (!foundMatchingReplaced) { continue NEXT_PATTERN; } + } else { + continue NEXT_PATTERN; } } } @@ -1089,9 +1109,9 @@ private Set reduceNestedPatterns(Set pat } var nestedPatterns = join.stream().map(rp -> rp.nested[mismatchingCandidateFin]).collect(Collectors.toSet()); - var updatedPatterns = reduceNestedPatterns(nestedPatterns, useHashes); + var updatedPatterns = reduceNestedPatterns(nestedPatterns, replaces, useHashes); - updatedPatterns = reduceRecordPatterns(updatedPatterns); + updatedPatterns = reduceRecordPatterns(updatedPatterns, replaces); updatedPatterns = removeCoveredRecordPatterns(updatedPatterns); updatedPatterns = reduceBindingPatterns(rpOne.fullComponentTypes()[mismatchingCandidateFin], updatedPatterns); @@ -1104,9 +1124,11 @@ private Set reduceNestedPatterns(Set pat PatternDescription[] newNested = Arrays.copyOf(rpOne.nested, rpOne.nested.length); newNested[mismatchingCandidateFin] = nested; - current.add(new RecordPattern(rpOne.recordType(), + RecordPattern nue = new RecordPattern(rpOne.recordType(), rpOne.fullComponentTypes(), - newNested)); + newNested); + current.add(nue); + replaces.put(nue, new HashSet<>(join)); } } } @@ -1128,12 +1150,12 @@ private Set reduceNestedPatterns(Set pat * all the $nestedX pattern cover the given record component, * and replace those with a simple binding pattern over $record. */ - private Set reduceRecordPatterns(Set patterns) { + private Set reduceRecordPatterns(Set patterns, Map> replaces) { var newPatterns = new HashSet(); boolean modified = false; for (PatternDescription pd : patterns) { if (pd instanceof RecordPattern rpOne) { - PatternDescription reducedPattern = reduceRecordPattern(rpOne); + PatternDescription reducedPattern = reduceRecordPattern(rpOne, replaces); if (reducedPattern != rpOne) { newPatterns.add(reducedPattern); modified = true; @@ -1145,7 +1167,7 @@ private Set reduceRecordPatterns(Set pat return modified ? newPatterns : patterns; } - private PatternDescription reduceRecordPattern(PatternDescription pattern) { + private PatternDescription reduceRecordPattern(PatternDescription pattern, Map> replaces) { if (pattern instanceof RecordPattern rpOne) { Type[] componentType = rpOne.fullComponentTypes(); //error recovery, ignore patterns with incorrect number of nested patterns: @@ -1155,7 +1177,7 @@ private PatternDescription reduceRecordPattern(PatternDescription pattern) { PatternDescription[] reducedNestedPatterns = null; boolean covered = true; for (int i = 0; i < componentType.length; i++) { - PatternDescription newNested = reduceRecordPattern(rpOne.nested[i]); + PatternDescription newNested = reduceRecordPattern(rpOne.nested[i], replaces); if (newNested != rpOne.nested[i]) { if (reducedNestedPatterns == null) { reducedNestedPatterns = Arrays.copyOf(rpOne.nested, rpOne.nested.length); @@ -1166,9 +1188,13 @@ private PatternDescription reduceRecordPattern(PatternDescription pattern) { covered &= checkCovered(componentType[i], List.of(newNested)); } if (covered) { - return new BindingPattern(rpOne.recordType); + PatternDescription pd = new BindingPattern(rpOne.recordType); + replaces.put(pd, Set.of(pattern)); + return pd; } else if (reducedNestedPatterns != null) { - return new RecordPattern(rpOne.recordType, rpOne.fullComponentTypes(), reducedNestedPatterns); + PatternDescription pd = new RecordPattern(rpOne.recordType, rpOne.fullComponentTypes(), reducedNestedPatterns); + replaces.put(pd, Set.of(pattern)); + return pd; } } return pattern; diff --git a/test/langtools/tools/javac/patterns/Exhaustiveness.java b/test/langtools/tools/javac/patterns/Exhaustiveness.java index a0236f323e26d..6600f14a07a1c 100644 --- a/test/langtools/tools/javac/patterns/Exhaustiveness.java +++ b/test/langtools/tools/javac/patterns/Exhaustiveness.java @@ -2222,7 +2222,7 @@ record Root(Base b1, Base b2, Base b3) {} } @Test //JDK-8364991 - public void testX(Path base) throws Exception { + public void testBindingPatternDoesNotStandInPlaceOfRecordPatterns(Path base) throws Exception { doTest(base, new String[0], """ @@ -2232,7 +2232,7 @@ private int test(Root r) { return switch (r) { case Root(R2 _, R2(R1 _)) -> 0; case Root(R2(R1 _), R2(R2 _)) -> 0; - case Root(R2(R2 _), R2 _) -> 0; //the above is functionally equivalent to: Root(R2(R2 _), R2(R2 _)) -> 0; + case Root(R2(R2 _), R2 _) -> 0; }; } sealed interface Base {} @@ -2240,7 +2240,9 @@ record R1() implements Base {} record R2(Base b) implements Base {} record Root(R2 b2, R2 b3) {} } - """); + """, + "Test.java:4:16: compiler.err.not.exhaustive", + "1 error"); } private void doTest(Path base, String[] libraryCode, String testCode, String... expectedErrors) throws IOException { From 29428b34ae194f5aecea2ebe39fbd6cdc7ed345a Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Thu, 11 Sep 2025 16:56:19 +0200 Subject: [PATCH 08/33] Fixing the exhaustiveness search. --- .../com/sun/tools/javac/comp/Flow.java | 51 +++++++++++-------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java index e9d3acd5c59c8..816ad3472e56b 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java @@ -830,7 +830,7 @@ private boolean exhausts(JCExpression selector, List cases, Set } } try { - CoverageResult coveredResult = computeCoverage(selector.type, patternSet); + CoverageResult coveredResult = computeCoverage(selector.type, patternSet, false); if (coveredResult.covered()) { return true; } @@ -876,7 +876,7 @@ private Set expandMissingPatternDescriptions(Type selectorTy Set replaced = replace(inMissingPatterns, toExpand, reducedPermittedPatterns); - if (computeCoverage(selectorType, joinSets(basePatterns, replaced)).covered()) { + if (computeCoverage(selectorType, joinSets(basePatterns, replaced), true).covered()) { it.remove(); reduced = true; } @@ -937,12 +937,12 @@ private Set expandMissingPatternDescriptions(Type selectorTy reducedAdded.remove(current); - if (computeCoverage(selectorType, joinSets(basePatterns, replace(inMissingPatterns, bp, reducedAdded))).covered()) { + if (computeCoverage(selectorType, joinSets(basePatterns, replace(inMissingPatterns, bp, reducedAdded)), true).covered()) { it.remove(); } } - CoverageResult coverageResult = computeCoverage(targetType, combinatorialPatterns); + CoverageResult coverageResult = computeCoverage(targetType, combinatorialPatterns, true); if (!coverageResult.covered()) { //nothing better can be done(?) @@ -961,7 +961,7 @@ private Set expandMissingPatternDescriptions(Type selectorTy reducedAdded.remove(current); - if (computeCoverage(selectorType, joinSets(basePatterns, replace(inMissingPatterns, bp, reducedAdded))).covered()) { + if (computeCoverage(selectorType, joinSets(basePatterns, replace(inMissingPatterns, bp, reducedAdded)), true).covered()) { it.remove(); } } @@ -1137,7 +1137,7 @@ private void generatePatternsWithReplacedNestedPattern(RecordPattern basePattern } } - private CoverageResult computeCoverage(Type selectorType, Set patterns) { + private CoverageResult computeCoverage(Type selectorType, Set patterns, boolean search) { Set updatedPatterns; Map> replaces = new IdentityHashMap<>(); Set> seenPatterns = new HashSet<>(); @@ -1145,7 +1145,7 @@ private CoverageResult computeCoverage(Type selectorType, Set baseClasses(TypeSymbol root) { */ private Set reduceNestedPatterns(Set patterns, Map> replaces, - boolean useHashes) { + boolean useHashes, + boolean search) { /* implementation note: * finding a sub-set of patterns that only differ in a single * column is time-consuming task, so this method speeds it up by: @@ -1444,23 +1445,29 @@ private Set reduceNestedPatterns(Set pat continue NEXT_PATTERN; } } else if (rpOne.nested[i] instanceof RecordPattern nestedRPOne) { - boolean foundMatchingReplaced = false; - Set pendingReplacedPatterns = new HashSet<>(replaces.getOrDefault(rpOther.nested[i], Set.of())); + if (search) { + if (!types.isSubtype(types.erasure(nestedRPOne.recordType()), types.erasure(bpOther.type))) { + continue NEXT_PATTERN; + } + } else { + boolean foundMatchingReplaced = false; + Set pendingReplacedPatterns = new HashSet<>(replaces.getOrDefault(rpOther.nested[i], Set.of())); - while (!pendingReplacedPatterns.isEmpty()) { - PatternDescription currentReplaced = pendingReplacedPatterns.iterator().next(); + while (!pendingReplacedPatterns.isEmpty()) { + PatternDescription currentReplaced = pendingReplacedPatterns.iterator().next(); - pendingReplacedPatterns.remove(currentReplaced); + pendingReplacedPatterns.remove(currentReplaced); - if (nestedRPOne.equals(currentReplaced)) { - foundMatchingReplaced = true; - break; - } + if (nestedRPOne.equals(currentReplaced)) { + foundMatchingReplaced = true; + break; + } - pendingReplacedPatterns.addAll(replaces.getOrDefault(currentReplaced, Set.of())); - } - if (!foundMatchingReplaced) { - continue NEXT_PATTERN; + pendingReplacedPatterns.addAll(replaces.getOrDefault(currentReplaced, Set.of())); + } + if (!foundMatchingReplaced) { + continue NEXT_PATTERN; + } } } else { continue NEXT_PATTERN; @@ -1473,7 +1480,7 @@ private Set reduceNestedPatterns(Set pat } var nestedPatterns = join.stream().map(rp -> rp.nested[mismatchingCandidateFin]).collect(Collectors.toSet()); - var updatedPatterns = reduceNestedPatterns(nestedPatterns, replaces, useHashes); + var updatedPatterns = reduceNestedPatterns(nestedPatterns, replaces, useHashes, search); updatedPatterns = reduceRecordPatterns(updatedPatterns, replaces); updatedPatterns = removeCoveredRecordPatterns(updatedPatterns); From bed37c4f1048a525c9e5d4396625a43b2e1d770e Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Fri, 12 Sep 2025 09:12:37 +0200 Subject: [PATCH 09/33] Adding test. --- .../tools/javac/patterns/Exhaustiveness.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/test/langtools/tools/javac/patterns/Exhaustiveness.java b/test/langtools/tools/javac/patterns/Exhaustiveness.java index 6600f14a07a1c..5a252c7569542 100644 --- a/test/langtools/tools/javac/patterns/Exhaustiveness.java +++ b/test/langtools/tools/javac/patterns/Exhaustiveness.java @@ -2221,6 +2221,37 @@ record Root(Base b1, Base b2, Base b3) {} """); } + @Test //JDK-8364991 + public void testDifferentReductionPathsSimplified(Path base) throws Exception { + doTest(base, + new String[0], + """ + package test; + public class Test { + private int test1(Root r) { + return switch (r) { + case Root(R2(R1 _), R2(R1 _)) -> 0; + case Root(R2(R1 _), R2(R2 _)) -> 0; + case Root(R2(R2 _), R2(R1 _)) -> 0; + case Root(R2(R2 _), R2 _) -> 0; + }; + } + private int test2(Root r) { + return switch (r) { + case Root(R2(R1 _), R2(R1 _)) -> 0; + case Root(R2(R2 _), R2(R1 _)) -> 0; + case Root(R2(R1 _), R2(R2 _)) -> 0; + case Root(R2 _, R2(R2 _)) -> 0; + }; + } + sealed interface Base {} + record R1() implements Base {} + record R2(Base b1) implements Base {} + record Root(R2 b2, R2 b3) {} + } + """); + } + @Test //JDK-8364991 public void testBindingPatternDoesNotStandInPlaceOfRecordPatterns(Path base) throws Exception { doTest(base, From 0d778871a41aa47f151bf223204989bc441edd14 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Fri, 12 Sep 2025 11:15:09 +0200 Subject: [PATCH 10/33] 8367499: Refactor exhaustiveness computation from Flow into a separate class --- .../javac/comp/ExhaustivenessComputer.java | 639 ++++++++++++++++++ .../com/sun/tools/javac/comp/Flow.java | 581 +--------------- 2 files changed, 643 insertions(+), 577 deletions(-) create mode 100644 src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java new file mode 100644 index 0000000000000..7683450ac2bdc --- /dev/null +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java @@ -0,0 +1,639 @@ +/* + * Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.sun.tools.javac.comp; + +import java.util.Map; +import java.util.Map.Entry; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; +import com.sun.tools.javac.code.*; +import com.sun.tools.javac.tree.*; +import com.sun.tools.javac.util.*; + +import com.sun.tools.javac.code.Symbol.*; +import com.sun.tools.javac.tree.JCTree.*; + +import com.sun.tools.javac.code.Kinds.Kind; +import com.sun.tools.javac.code.Type.TypeVar; +import java.util.Arrays; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static java.util.stream.Collectors.groupingBy; + +/** + *

This is NOT part of any supported API. + * If you write code that depends on this, you do so at your own risk. + * This code and its internal interfaces are subject to change or + * deletion without notice. + */ +public class ExhaustivenessComputer { + protected static final Context.Key exhaustivenessKey = new Context.Key<>(); + + private final Symtab syms; + private final Types types; + private final Check chk; + private final Infer infer; + + public static ExhaustivenessComputer instance(Context context) { + ExhaustivenessComputer instance = context.get(exhaustivenessKey); + if (instance == null) + instance = new ExhaustivenessComputer(context); + return instance; + } + + @SuppressWarnings("this-escape") + protected ExhaustivenessComputer(Context context) { + context.put(exhaustivenessKey, this); + syms = Symtab.instance(context); + types = Types.instance(context); + chk = Check.instance(context); + infer = Infer.instance(context); + } + + public boolean exhausts(JCExpression selector, List cases) { + Set patternSet = new HashSet<>(); + Map> enum2Constants = new HashMap<>(); + Set booleanLiterals = new HashSet<>(Set.of(0, 1)); + for (JCCase c : cases) { + if (!TreeInfo.unguardedCase(c)) + continue; + + for (var l : c.labels) { + if (l instanceof JCPatternCaseLabel patternLabel) { + for (Type component : components(selector.type)) { + patternSet.add(makePatternDescription(component, patternLabel.pat)); + } + } else if (l instanceof JCConstantCaseLabel constantLabel) { + if (types.unboxedTypeOrType(selector.type).hasTag(TypeTag.BOOLEAN)) { + Object value = ((JCLiteral) constantLabel.expr).value; + booleanLiterals.remove(value); + } else { + Symbol s = TreeInfo.symbol(constantLabel.expr); + if (s != null && s.isEnum()) { + enum2Constants.computeIfAbsent(s.owner, x -> { + Set result = new HashSet<>(); + s.owner.members() + .getSymbols(sym -> sym.kind == Kind.VAR && sym.isEnum()) + .forEach(result::add); + return result; + }).remove(s); + } + } + } + } + } + + if (types.unboxedTypeOrType(selector.type).hasTag(TypeTag.BOOLEAN) && booleanLiterals.isEmpty()) { + return true; + } + + for (Entry> e : enum2Constants.entrySet()) { + if (e.getValue().isEmpty()) { + patternSet.add(new BindingPattern(e.getKey().type)); + } + } + Set patterns = patternSet; + Map> replaces = new IdentityHashMap<>(); + Set> seenFallback = new HashSet<>(); + boolean useHashes = true; + try { + boolean repeat = true; + while (repeat) { + Set updatedPatterns; + updatedPatterns = reduceBindingPatterns(selector.type, patterns); + updatedPatterns = reduceNestedPatterns(updatedPatterns, replaces, useHashes); + updatedPatterns = reduceRecordPatterns(updatedPatterns, replaces); + updatedPatterns = removeCoveredRecordPatterns(updatedPatterns); + repeat = !updatedPatterns.equals(patterns); + if (checkCovered(selector.type, patterns)) { + return true; + } + if (!repeat) { + //there may be situation like: + //class B permits S1, S2 + //patterns: R(S1, B), R(S2, S2) + //this might be joined to R(B, S2), as B could be rewritten to S2 + //but hashing in reduceNestedPatterns will not allow that + //disable the use of hashing, and use subtyping in + //reduceNestedPatterns to handle situations like this: + repeat = useHashes && seenFallback.add(updatedPatterns); + useHashes = false; + } else { + //if a reduction happened, make sure hashing in reduceNestedPatterns + //is enabled, as the hashing speeds up the process significantly: + useHashes = true; + } + patterns = updatedPatterns; + } + return checkCovered(selector.type, patterns); + } catch (CompletionFailure cf) { + chk.completionError(selector.pos(), cf); + return true; //error recovery + } + } + + private boolean checkCovered(Type seltype, Iterable patterns) { + for (Type seltypeComponent : components(seltype)) { + for (PatternDescription pd : patterns) { + if(isBpCovered(seltypeComponent, pd)) { + return true; + } + } + } + return false; + } + + private List components(Type seltype) { + return switch (seltype.getTag()) { + case CLASS -> { + if (seltype.isCompound()) { + if (seltype.isIntersection()) { + yield ((Type.IntersectionClassType) seltype).getComponents() + .stream() + .flatMap(t -> components(t).stream()) + .collect(List.collector()); + } + yield List.nil(); + } + yield List.of(types.erasure(seltype)); + } + case TYPEVAR -> components(((TypeVar) seltype).getUpperBound()); + default -> List.of(types.erasure(seltype)); + }; + } + + /* In a set of patterns, search for a sub-set of binding patterns that + * in combination exhaust their sealed supertype. If such a sub-set + * is found, it is removed, and replaced with a binding pattern + * for the sealed supertype. + */ + private Set reduceBindingPatterns(Type selectorType, Set patterns) { + Set existingBindings = patterns.stream() + .filter(pd -> pd instanceof BindingPattern) + .map(pd -> ((BindingPattern) pd).type.tsym) + .collect(Collectors.toSet()); + + for (PatternDescription pdOne : patterns) { + if (pdOne instanceof BindingPattern bpOne) { + Set toAdd = new HashSet<>(); + + for (Type sup : types.directSupertypes(bpOne.type)) { + ClassSymbol clazz = (ClassSymbol) types.erasure(sup).tsym; + + clazz.complete(); + + if (clazz.isSealed() && clazz.isAbstract() && + //if a binding pattern for clazz already exists, no need to analyze it again: + !existingBindings.contains(clazz)) { + ListBuffer bindings = new ListBuffer<>(); + //do not reduce to types unrelated to the selector type: + Type clazzErasure = types.erasure(clazz.type); + if (components(selectorType).stream() + .map(types::erasure) + .noneMatch(c -> types.isSubtype(clazzErasure, c))) { + continue; + } + + Set permitted = allPermittedSubTypes(clazz, csym -> { + Type instantiated; + if (csym.type.allparams().isEmpty()) { + instantiated = csym.type; + } else { + instantiated = infer.instantiatePatternType(selectorType, csym); + } + + return instantiated != null && types.isCastable(selectorType, instantiated); + }); + + for (PatternDescription pdOther : patterns) { + if (pdOther instanceof BindingPattern bpOther) { + Set currentPermittedSubTypes = + allPermittedSubTypes(bpOther.type.tsym, s -> true); + + PERMITTED: for (Iterator it = permitted.iterator(); it.hasNext();) { + Symbol perm = it.next(); + + for (Symbol currentPermitted : currentPermittedSubTypes) { + if (types.isSubtype(types.erasure(currentPermitted.type), + types.erasure(perm.type))) { + it.remove(); + continue PERMITTED; + } + } + if (types.isSubtype(types.erasure(perm.type), + types.erasure(bpOther.type))) { + it.remove(); + } + } + } + } + + if (permitted.isEmpty()) { + toAdd.add(new BindingPattern(clazz.type)); + } + } + } + + if (!toAdd.isEmpty()) { + Set newPatterns = new HashSet<>(patterns); + newPatterns.addAll(toAdd); + return newPatterns; + } + } + } + return patterns; + } + + private Set allPermittedSubTypes(TypeSymbol root, Predicate accept) { + Set permitted = new HashSet<>(); + List permittedSubtypesClosure = baseClasses(root); + + while (permittedSubtypesClosure.nonEmpty()) { + ClassSymbol current = permittedSubtypesClosure.head; + + permittedSubtypesClosure = permittedSubtypesClosure.tail; + + current.complete(); + + if (current.isSealed() && current.isAbstract()) { + for (Type t : current.getPermittedSubclasses()) { + ClassSymbol csym = (ClassSymbol) t.tsym; + + if (accept.test(csym)) { + permittedSubtypesClosure = permittedSubtypesClosure.prepend(csym); + permitted.add(csym); + } + } + } + } + + return permitted; + } + + private List baseClasses(TypeSymbol root) { + if (root instanceof ClassSymbol clazz) { + return List.of(clazz); + } else if (root instanceof TypeVariableSymbol tvar) { + ListBuffer result = new ListBuffer<>(); + for (Type bound : tvar.getBounds()) { + result.appendList(baseClasses(bound.tsym)); + } + return result.toList(); + } else { + return List.nil(); + } + } + + /* Among the set of patterns, find sub-set of patterns such: + * $record($prefix$, $nested, $suffix$) + * Where $record, $prefix$ and $suffix$ is the same for each pattern + * in the set, and the patterns only differ in one "column" in + * the $nested pattern. + * Then, the set of $nested patterns is taken, and passed recursively + * to reduceNestedPatterns and to reduceBindingPatterns, to + * simplify the pattern. If that succeeds, the original found sub-set + * of patterns is replaced with a new set of patterns of the form: + * $record($prefix$, $resultOfReduction, $suffix$) + * + * useHashes: when true, patterns will be subject to exact equivalence; + * when false, two binding patterns will be considered equivalent + * if one of them is more generic than the other one; + * when false, the processing will be significantly slower, + * as pattern hashes cannot be used to speed up the matching process + */ + private Set reduceNestedPatterns(Set patterns, + Map> replaces, + boolean useHashes) { + /* implementation note: + * finding a sub-set of patterns that only differ in a single + * column is time-consuming task, so this method speeds it up by: + * - group the patterns by their record class + * - for each column (nested pattern) do: + * -- group patterns by their hash + * -- in each such by-hash group, find sub-sets that only differ in + * the chosen column, and then call reduceBindingPatterns and reduceNestedPatterns + * on patterns in the chosen column, as described above + */ + var groupByRecordClass = + patterns.stream() + .filter(pd -> pd instanceof RecordPattern) + .map(pd -> (RecordPattern) pd) + .collect(groupingBy(pd -> (ClassSymbol) pd.recordType.tsym)); + + for (var e : groupByRecordClass.entrySet()) { + int nestedPatternsCount = e.getKey().getRecordComponents().size(); + Set current = new HashSet<>(e.getValue()); + + for (int mismatchingCandidate = 0; + mismatchingCandidate < nestedPatternsCount; + mismatchingCandidate++) { + int mismatchingCandidateFin = mismatchingCandidate; + var groupEquivalenceCandidates = + current + .stream() + //error recovery, ignore patterns with incorrect number of nested patterns: + .filter(pd -> pd.nested.length == nestedPatternsCount) + .collect(groupingBy(pd -> useHashes ? pd.hashCode(mismatchingCandidateFin) : 0)); + for (var candidates : groupEquivalenceCandidates.values()) { + var candidatesArr = candidates.toArray(RecordPattern[]::new); + + for (int firstCandidate = 0; + firstCandidate < candidatesArr.length; + firstCandidate++) { + RecordPattern rpOne = candidatesArr[firstCandidate]; + ListBuffer join = new ListBuffer<>(); + + join.append(rpOne); + + NEXT_PATTERN: for (int nextCandidate = 0; + nextCandidate < candidatesArr.length; + nextCandidate++) { + if (firstCandidate == nextCandidate) { + continue; + } + + RecordPattern rpOther = candidatesArr[nextCandidate]; + if (rpOne.recordType.tsym == rpOther.recordType.tsym) { + ACCEPT: for (int i = 0; i < rpOne.nested.length; i++) { + if (i != mismatchingCandidate) { + if (!rpOne.nested[i].equals(rpOther.nested[i])) { + if (useHashes) { + continue NEXT_PATTERN; + } + //when not using hashes, + //check if rpOne.nested[i] is + //a subtype of rpOther.nested[i]: + if (!(rpOther.nested[i] instanceof BindingPattern bpOther)) { + continue NEXT_PATTERN; + } + if (rpOne.nested[i] instanceof BindingPattern bpOne) { + if (!types.isSubtype(types.erasure(bpOne.type), types.erasure(bpOther.type))) { + continue NEXT_PATTERN; + } + } else if (rpOne.nested[i] instanceof RecordPattern nestedRPOne) { + boolean foundMatchingReplaced = false; + Set pendingReplacedPatterns = new HashSet<>(replaces.getOrDefault(rpOther.nested[i], Set.of())); + + while (!pendingReplacedPatterns.isEmpty()) { + PatternDescription currentReplaced = pendingReplacedPatterns.iterator().next(); + + pendingReplacedPatterns.remove(currentReplaced); + + if (nestedRPOne.equals(currentReplaced)) { + foundMatchingReplaced = true; + break; + } + + pendingReplacedPatterns.addAll(replaces.getOrDefault(currentReplaced, Set.of())); + } + if (!foundMatchingReplaced) { + continue NEXT_PATTERN; + } + } else { + continue NEXT_PATTERN; + } + } + } + } + join.append(rpOther); + } + } + + var nestedPatterns = join.stream().map(rp -> rp.nested[mismatchingCandidateFin]).collect(Collectors.toSet()); + var updatedPatterns = reduceNestedPatterns(nestedPatterns, replaces, useHashes); + + updatedPatterns = reduceRecordPatterns(updatedPatterns, replaces); + updatedPatterns = removeCoveredRecordPatterns(updatedPatterns); + updatedPatterns = reduceBindingPatterns(rpOne.fullComponentTypes()[mismatchingCandidateFin], updatedPatterns); + + if (!nestedPatterns.equals(updatedPatterns)) { + if (useHashes) { + current.removeAll(join); + } + + for (PatternDescription nested : updatedPatterns) { + PatternDescription[] newNested = + Arrays.copyOf(rpOne.nested, rpOne.nested.length); + newNested[mismatchingCandidateFin] = nested; + RecordPattern nue = new RecordPattern(rpOne.recordType(), + rpOne.fullComponentTypes(), + newNested); + current.add(nue); + replaces.put(nue, new HashSet<>(join)); + } + } + } + } + } + + if (!current.equals(new HashSet<>(e.getValue()))) { + Set result = new HashSet<>(patterns); + result.removeAll(e.getValue()); + result.addAll(current); + return result; + } + } + return patterns; + } + + /* In the set of patterns, find those for which, given: + * $record($nested1, $nested2, ...) + * all the $nestedX pattern cover the given record component, + * and replace those with a simple binding pattern over $record. + */ + private Set reduceRecordPatterns(Set patterns, Map> replaces) { + var newPatterns = new HashSet(); + boolean modified = false; + for (PatternDescription pd : patterns) { + if (pd instanceof RecordPattern rpOne) { + PatternDescription reducedPattern = reduceRecordPattern(rpOne, replaces); + if (reducedPattern != rpOne) { + newPatterns.add(reducedPattern); + modified = true; + continue; + } + } + newPatterns.add(pd); + } + return modified ? newPatterns : patterns; + } + + private PatternDescription reduceRecordPattern(PatternDescription pattern, Map> replaces) { + if (pattern instanceof RecordPattern rpOne) { + Type[] componentType = rpOne.fullComponentTypes(); + //error recovery, ignore patterns with incorrect number of nested patterns: + if (componentType.length != rpOne.nested.length) { + return pattern; + } + PatternDescription[] reducedNestedPatterns = null; + boolean covered = true; + for (int i = 0; i < componentType.length; i++) { + PatternDescription newNested = reduceRecordPattern(rpOne.nested[i], replaces); + if (newNested != rpOne.nested[i]) { + if (reducedNestedPatterns == null) { + reducedNestedPatterns = Arrays.copyOf(rpOne.nested, rpOne.nested.length); + } + reducedNestedPatterns[i] = newNested; + } + + covered &= checkCovered(componentType[i], List.of(newNested)); + } + if (covered) { + PatternDescription pd = new BindingPattern(rpOne.recordType); + replaces.put(pd, Set.of(pattern)); + return pd; + } else if (reducedNestedPatterns != null) { + PatternDescription pd = new RecordPattern(rpOne.recordType, rpOne.fullComponentTypes(), reducedNestedPatterns); + replaces.put(pd, Set.of(pattern)); + return pd; + } + } + return pattern; + } + + private Set removeCoveredRecordPatterns(Set patterns) { + Set existingBindings = patterns.stream() + .filter(pd -> pd instanceof BindingPattern) + .map(pd -> ((BindingPattern) pd).type.tsym) + .collect(Collectors.toSet()); + Set result = new HashSet<>(patterns); + + for (Iterator it = result.iterator(); it.hasNext();) { + PatternDescription pd = it.next(); + if (pd instanceof RecordPattern rp && existingBindings.contains(rp.recordType.tsym)) { + it.remove(); + } + } + + return result; + } + + private boolean isBpCovered(Type componentType, PatternDescription newNested) { + if (newNested instanceof BindingPattern bp) { + Type seltype = types.erasure(componentType); + Type pattype = types.erasure(bp.type); + + return seltype.isPrimitive() ? + types.isUnconditionallyExact(seltype, pattype) : + (bp.type.isPrimitive() && types.isUnconditionallyExact(types.unboxedType(seltype), bp.type)) || types.isSubtype(seltype, pattype); + } + return false; + } + + sealed interface PatternDescription { } + public PatternDescription makePatternDescription(Type selectorType, JCPattern pattern) { + if (pattern instanceof JCBindingPattern binding) { + Type type = !selectorType.isPrimitive() && types.isSubtype(selectorType, binding.type) + ? selectorType : binding.type; + return new BindingPattern(type); + } else if (pattern instanceof JCRecordPattern record) { + Type[] componentTypes; + + if (!record.type.isErroneous()) { + componentTypes = ((ClassSymbol) record.type.tsym).getRecordComponents() + .map(r -> types.memberType(record.type, r)) + .toArray(s -> new Type[s]); + } + else { + componentTypes = record.nested.map(t -> types.createErrorType(t.type)).toArray(s -> new Type[s]);; + } + + PatternDescription[] nestedDescriptions = + new PatternDescription[record.nested.size()]; + int i = 0; + for (List it = record.nested; + it.nonEmpty(); + it = it.tail, i++) { + Type componentType = i < componentTypes.length ? componentTypes[i] + : syms.errType; + nestedDescriptions[i] = makePatternDescription(types.erasure(componentType), it.head); + } + return new RecordPattern(record.type, componentTypes, nestedDescriptions); + } else if (pattern instanceof JCAnyPattern) { + return new BindingPattern(selectorType); + } else { + throw Assert.error(); + } + } + record BindingPattern(Type type) implements PatternDescription { + @Override + public int hashCode() { + return type.tsym.hashCode(); + } + @Override + public boolean equals(Object o) { + return o instanceof BindingPattern other && + type.tsym == other.type.tsym; + } + @Override + public String toString() { + return type.tsym + " _"; + } + } + record RecordPattern(Type recordType, int _hashCode, Type[] fullComponentTypes, PatternDescription... nested) implements PatternDescription { + + public RecordPattern(Type recordType, Type[] fullComponentTypes, PatternDescription[] nested) { + this(recordType, hashCode(-1, recordType, nested), fullComponentTypes, nested); + } + + @Override + public int hashCode() { + return _hashCode; + } + + @Override + public boolean equals(Object o) { + return o instanceof RecordPattern other && + recordType.tsym == other.recordType.tsym && + Arrays.equals(nested, other.nested); + } + + public int hashCode(int excludeComponent) { + return hashCode(excludeComponent, recordType, nested); + } + + public static int hashCode(int excludeComponent, Type recordType, PatternDescription... nested) { + int hash = 5; + hash = 41 * hash + recordType.tsym.hashCode(); + for (int i = 0; i < nested.length; i++) { + if (i != excludeComponent) { + hash = 41 * hash + nested[i].hashCode(); + } + } + return hash; + } + @Override + public String toString() { + return recordType.tsym + "(" + Arrays.stream(nested) + .map(pd -> pd.toString()) + .collect(Collectors.joining(", ")) + ")"; + } + } +} diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java index c2ad9deb57dcb..e74aed6a35703 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java @@ -27,11 +27,7 @@ package com.sun.tools.javac.comp; -import java.util.Map; -import java.util.Map.Entry; import java.util.HashMap; -import java.util.HashSet; -import java.util.Set; import java.util.function.Consumer; import com.sun.source.tree.LambdaExpressionTree.BodyKind; @@ -51,21 +47,12 @@ import static com.sun.tools.javac.code.Flags.*; import static com.sun.tools.javac.code.Flags.BLOCK; -import com.sun.tools.javac.code.Kinds.Kind; import static com.sun.tools.javac.code.Kinds.Kind.*; -import com.sun.tools.javac.code.Type.TypeVar; import static com.sun.tools.javac.code.TypeTag.BOOLEAN; import static com.sun.tools.javac.code.TypeTag.VOID; import com.sun.tools.javac.resources.CompilerProperties.Fragments; import static com.sun.tools.javac.tree.JCTree.Tag.*; import com.sun.tools.javac.util.JCDiagnostic.Fragment; -import java.util.Arrays; -import java.util.IdentityHashMap; -import java.util.Iterator; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -import static java.util.stream.Collectors.groupingBy; /** This pass implements dataflow analysis for Java programs though * different AST visitor steps. Liveness analysis (see AliveAnalyzer) checks that @@ -214,8 +201,8 @@ public class Flow { private TreeMaker make; private final Resolve rs; private final JCDiagnostic.Factory diags; + private final ExhaustivenessComputer exhaustiveness; private Env attrEnv; - private final Infer infer; public static Flow instance(Context context) { Flow instance = context.get(flowKey); @@ -337,10 +324,9 @@ protected Flow(Context context) { syms = Symtab.instance(context); types = Types.instance(context); chk = Check.instance(context); - infer = Infer.instance(context); rs = Resolve.instance(context); diags = JCDiagnostic.Factory.instance(context); - Source source = Source.instance(context); + exhaustiveness = ExhaustivenessComputer.instance(context); } /** @@ -710,7 +696,7 @@ public void visitSwitch(JCSwitch tree) { tree.isExhaustive = tree.hasUnconditionalPattern || TreeInfo.isErrorEnumSwitch(tree.selector, tree.cases); if (exhaustiveSwitch) { - tree.isExhaustive |= exhausts(tree.selector, tree.cases); + tree.isExhaustive |= exhaustiveness.exhausts(tree.selector, tree.cases); if (!tree.isExhaustive) { log.error(tree, Errors.NotExhaustiveStatement); } @@ -749,7 +735,7 @@ public void visitSwitchExpression(JCSwitchExpression tree) { TreeInfo.isErrorEnumSwitch(tree.selector, tree.cases)) { tree.isExhaustive = true; } else { - tree.isExhaustive = exhausts(tree.selector, tree.cases); + tree.isExhaustive = exhaustiveness.exhausts(tree.selector, tree.cases); } if (!tree.isExhaustive) { @@ -759,464 +745,6 @@ public void visitSwitchExpression(JCSwitchExpression tree) { alive = alive.or(resolveYields(tree, prevPendingExits)); } - private boolean exhausts(JCExpression selector, List cases) { - Set patternSet = new HashSet<>(); - Map> enum2Constants = new HashMap<>(); - Set booleanLiterals = new HashSet<>(Set.of(0, 1)); - for (JCCase c : cases) { - if (!TreeInfo.unguardedCase(c)) - continue; - - for (var l : c.labels) { - if (l instanceof JCPatternCaseLabel patternLabel) { - for (Type component : components(selector.type)) { - patternSet.add(makePatternDescription(component, patternLabel.pat)); - } - } else if (l instanceof JCConstantCaseLabel constantLabel) { - if (types.unboxedTypeOrType(selector.type).hasTag(TypeTag.BOOLEAN)) { - Object value = ((JCLiteral) constantLabel.expr).value; - booleanLiterals.remove(value); - } else { - Symbol s = TreeInfo.symbol(constantLabel.expr); - if (s != null && s.isEnum()) { - enum2Constants.computeIfAbsent(s.owner, x -> { - Set result = new HashSet<>(); - s.owner.members() - .getSymbols(sym -> sym.kind == Kind.VAR && sym.isEnum()) - .forEach(result::add); - return result; - }).remove(s); - } - } - } - } - } - - if (types.unboxedTypeOrType(selector.type).hasTag(TypeTag.BOOLEAN) && booleanLiterals.isEmpty()) { - return true; - } - - for (Entry> e : enum2Constants.entrySet()) { - if (e.getValue().isEmpty()) { - patternSet.add(new BindingPattern(e.getKey().type)); - } - } - Set patterns = patternSet; - Map> replaces = new IdentityHashMap<>(); - Set> seenFallback = new HashSet<>(); - boolean useHashes = true; - try { - boolean repeat = true; - while (repeat) { - Set updatedPatterns; - updatedPatterns = reduceBindingPatterns(selector.type, patterns); - updatedPatterns = reduceNestedPatterns(updatedPatterns, replaces, useHashes); - updatedPatterns = reduceRecordPatterns(updatedPatterns, replaces); - updatedPatterns = removeCoveredRecordPatterns(updatedPatterns); - repeat = !updatedPatterns.equals(patterns); - if (checkCovered(selector.type, patterns)) { - return true; - } - if (!repeat) { - //there may be situation like: - //class B permits S1, S2 - //patterns: R(S1, B), R(S2, S2) - //this might be joined to R(B, S2), as B could be rewritten to S2 - //but hashing in reduceNestedPatterns will not allow that - //disable the use of hashing, and use subtyping in - //reduceNestedPatterns to handle situations like this: - repeat = useHashes && seenFallback.add(updatedPatterns); - useHashes = false; - } else { - //if a reduction happened, make sure hashing in reduceNestedPatterns - //is enabled, as the hashing speeds up the process significantly: - useHashes = true; - } - patterns = updatedPatterns; - } - return checkCovered(selector.type, patterns); - } catch (CompletionFailure cf) { - chk.completionError(selector.pos(), cf); - return true; //error recovery - } - } - - private boolean checkCovered(Type seltype, Iterable patterns) { - for (Type seltypeComponent : components(seltype)) { - for (PatternDescription pd : patterns) { - if(isBpCovered(seltypeComponent, pd)) { - return true; - } - } - } - return false; - } - - private List components(Type seltype) { - return switch (seltype.getTag()) { - case CLASS -> { - if (seltype.isCompound()) { - if (seltype.isIntersection()) { - yield ((Type.IntersectionClassType) seltype).getComponents() - .stream() - .flatMap(t -> components(t).stream()) - .collect(List.collector()); - } - yield List.nil(); - } - yield List.of(types.erasure(seltype)); - } - case TYPEVAR -> components(((TypeVar) seltype).getUpperBound()); - default -> List.of(types.erasure(seltype)); - }; - } - - /* In a set of patterns, search for a sub-set of binding patterns that - * in combination exhaust their sealed supertype. If such a sub-set - * is found, it is removed, and replaced with a binding pattern - * for the sealed supertype. - */ - private Set reduceBindingPatterns(Type selectorType, Set patterns) { - Set existingBindings = patterns.stream() - .filter(pd -> pd instanceof BindingPattern) - .map(pd -> ((BindingPattern) pd).type.tsym) - .collect(Collectors.toSet()); - - for (PatternDescription pdOne : patterns) { - if (pdOne instanceof BindingPattern bpOne) { - Set toAdd = new HashSet<>(); - - for (Type sup : types.directSupertypes(bpOne.type)) { - ClassSymbol clazz = (ClassSymbol) types.erasure(sup).tsym; - - clazz.complete(); - - if (clazz.isSealed() && clazz.isAbstract() && - //if a binding pattern for clazz already exists, no need to analyze it again: - !existingBindings.contains(clazz)) { - ListBuffer bindings = new ListBuffer<>(); - //do not reduce to types unrelated to the selector type: - Type clazzErasure = types.erasure(clazz.type); - if (components(selectorType).stream() - .map(types::erasure) - .noneMatch(c -> types.isSubtype(clazzErasure, c))) { - continue; - } - - Set permitted = allPermittedSubTypes(clazz, csym -> { - Type instantiated; - if (csym.type.allparams().isEmpty()) { - instantiated = csym.type; - } else { - instantiated = infer.instantiatePatternType(selectorType, csym); - } - - return instantiated != null && types.isCastable(selectorType, instantiated); - }); - - for (PatternDescription pdOther : patterns) { - if (pdOther instanceof BindingPattern bpOther) { - Set currentPermittedSubTypes = - allPermittedSubTypes(bpOther.type.tsym, s -> true); - - PERMITTED: for (Iterator it = permitted.iterator(); it.hasNext();) { - Symbol perm = it.next(); - - for (Symbol currentPermitted : currentPermittedSubTypes) { - if (types.isSubtype(types.erasure(currentPermitted.type), - types.erasure(perm.type))) { - it.remove(); - continue PERMITTED; - } - } - if (types.isSubtype(types.erasure(perm.type), - types.erasure(bpOther.type))) { - it.remove(); - } - } - } - } - - if (permitted.isEmpty()) { - toAdd.add(new BindingPattern(clazz.type)); - } - } - } - - if (!toAdd.isEmpty()) { - Set newPatterns = new HashSet<>(patterns); - newPatterns.addAll(toAdd); - return newPatterns; - } - } - } - return patterns; - } - - private Set allPermittedSubTypes(TypeSymbol root, Predicate accept) { - Set permitted = new HashSet<>(); - List permittedSubtypesClosure = baseClasses(root); - - while (permittedSubtypesClosure.nonEmpty()) { - ClassSymbol current = permittedSubtypesClosure.head; - - permittedSubtypesClosure = permittedSubtypesClosure.tail; - - current.complete(); - - if (current.isSealed() && current.isAbstract()) { - for (Type t : current.getPermittedSubclasses()) { - ClassSymbol csym = (ClassSymbol) t.tsym; - - if (accept.test(csym)) { - permittedSubtypesClosure = permittedSubtypesClosure.prepend(csym); - permitted.add(csym); - } - } - } - } - - return permitted; - } - - private List baseClasses(TypeSymbol root) { - if (root instanceof ClassSymbol clazz) { - return List.of(clazz); - } else if (root instanceof TypeVariableSymbol tvar) { - ListBuffer result = new ListBuffer<>(); - for (Type bound : tvar.getBounds()) { - result.appendList(baseClasses(bound.tsym)); - } - return result.toList(); - } else { - return List.nil(); - } - } - - /* Among the set of patterns, find sub-set of patterns such: - * $record($prefix$, $nested, $suffix$) - * Where $record, $prefix$ and $suffix$ is the same for each pattern - * in the set, and the patterns only differ in one "column" in - * the $nested pattern. - * Then, the set of $nested patterns is taken, and passed recursively - * to reduceNestedPatterns and to reduceBindingPatterns, to - * simplify the pattern. If that succeeds, the original found sub-set - * of patterns is replaced with a new set of patterns of the form: - * $record($prefix$, $resultOfReduction, $suffix$) - * - * useHashes: when true, patterns will be subject to exact equivalence; - * when false, two binding patterns will be considered equivalent - * if one of them is more generic than the other one; - * when false, the processing will be significantly slower, - * as pattern hashes cannot be used to speed up the matching process - */ - private Set reduceNestedPatterns(Set patterns, - Map> replaces, - boolean useHashes) { - /* implementation note: - * finding a sub-set of patterns that only differ in a single - * column is time-consuming task, so this method speeds it up by: - * - group the patterns by their record class - * - for each column (nested pattern) do: - * -- group patterns by their hash - * -- in each such by-hash group, find sub-sets that only differ in - * the chosen column, and then call reduceBindingPatterns and reduceNestedPatterns - * on patterns in the chosen column, as described above - */ - var groupByRecordClass = - patterns.stream() - .filter(pd -> pd instanceof RecordPattern) - .map(pd -> (RecordPattern) pd) - .collect(groupingBy(pd -> (ClassSymbol) pd.recordType.tsym)); - - for (var e : groupByRecordClass.entrySet()) { - int nestedPatternsCount = e.getKey().getRecordComponents().size(); - Set current = new HashSet<>(e.getValue()); - - for (int mismatchingCandidate = 0; - mismatchingCandidate < nestedPatternsCount; - mismatchingCandidate++) { - int mismatchingCandidateFin = mismatchingCandidate; - var groupEquivalenceCandidates = - current - .stream() - //error recovery, ignore patterns with incorrect number of nested patterns: - .filter(pd -> pd.nested.length == nestedPatternsCount) - .collect(groupingBy(pd -> useHashes ? pd.hashCode(mismatchingCandidateFin) : 0)); - for (var candidates : groupEquivalenceCandidates.values()) { - var candidatesArr = candidates.toArray(RecordPattern[]::new); - - for (int firstCandidate = 0; - firstCandidate < candidatesArr.length; - firstCandidate++) { - RecordPattern rpOne = candidatesArr[firstCandidate]; - ListBuffer join = new ListBuffer<>(); - - join.append(rpOne); - - NEXT_PATTERN: for (int nextCandidate = 0; - nextCandidate < candidatesArr.length; - nextCandidate++) { - if (firstCandidate == nextCandidate) { - continue; - } - - RecordPattern rpOther = candidatesArr[nextCandidate]; - if (rpOne.recordType.tsym == rpOther.recordType.tsym) { - ACCEPT: for (int i = 0; i < rpOne.nested.length; i++) { - if (i != mismatchingCandidate) { - if (!rpOne.nested[i].equals(rpOther.nested[i])) { - if (useHashes) { - continue NEXT_PATTERN; - } - //when not using hashes, - //check if rpOne.nested[i] is - //a subtype of rpOther.nested[i]: - if (!(rpOther.nested[i] instanceof BindingPattern bpOther)) { - continue NEXT_PATTERN; - } - if (rpOne.nested[i] instanceof BindingPattern bpOne) { - if (!types.isSubtype(types.erasure(bpOne.type), types.erasure(bpOther.type))) { - continue NEXT_PATTERN; - } - } else if (rpOne.nested[i] instanceof RecordPattern nestedRPOne) { - boolean foundMatchingReplaced = false; - Set pendingReplacedPatterns = new HashSet<>(replaces.getOrDefault(rpOther.nested[i], Set.of())); - - while (!pendingReplacedPatterns.isEmpty()) { - PatternDescription currentReplaced = pendingReplacedPatterns.iterator().next(); - - pendingReplacedPatterns.remove(currentReplaced); - - if (nestedRPOne.equals(currentReplaced)) { - foundMatchingReplaced = true; - break; - } - - pendingReplacedPatterns.addAll(replaces.getOrDefault(currentReplaced, Set.of())); - } - if (!foundMatchingReplaced) { - continue NEXT_PATTERN; - } - } else { - continue NEXT_PATTERN; - } - } - } - } - join.append(rpOther); - } - } - - var nestedPatterns = join.stream().map(rp -> rp.nested[mismatchingCandidateFin]).collect(Collectors.toSet()); - var updatedPatterns = reduceNestedPatterns(nestedPatterns, replaces, useHashes); - - updatedPatterns = reduceRecordPatterns(updatedPatterns, replaces); - updatedPatterns = removeCoveredRecordPatterns(updatedPatterns); - updatedPatterns = reduceBindingPatterns(rpOne.fullComponentTypes()[mismatchingCandidateFin], updatedPatterns); - - if (!nestedPatterns.equals(updatedPatterns)) { - if (useHashes) { - current.removeAll(join); - } - - for (PatternDescription nested : updatedPatterns) { - PatternDescription[] newNested = - Arrays.copyOf(rpOne.nested, rpOne.nested.length); - newNested[mismatchingCandidateFin] = nested; - RecordPattern nue = new RecordPattern(rpOne.recordType(), - rpOne.fullComponentTypes(), - newNested); - current.add(nue); - replaces.put(nue, new HashSet<>(join)); - } - } - } - } - } - - if (!current.equals(new HashSet<>(e.getValue()))) { - Set result = new HashSet<>(patterns); - result.removeAll(e.getValue()); - result.addAll(current); - return result; - } - } - return patterns; - } - - /* In the set of patterns, find those for which, given: - * $record($nested1, $nested2, ...) - * all the $nestedX pattern cover the given record component, - * and replace those with a simple binding pattern over $record. - */ - private Set reduceRecordPatterns(Set patterns, Map> replaces) { - var newPatterns = new HashSet(); - boolean modified = false; - for (PatternDescription pd : patterns) { - if (pd instanceof RecordPattern rpOne) { - PatternDescription reducedPattern = reduceRecordPattern(rpOne, replaces); - if (reducedPattern != rpOne) { - newPatterns.add(reducedPattern); - modified = true; - continue; - } - } - newPatterns.add(pd); - } - return modified ? newPatterns : patterns; - } - - private PatternDescription reduceRecordPattern(PatternDescription pattern, Map> replaces) { - if (pattern instanceof RecordPattern rpOne) { - Type[] componentType = rpOne.fullComponentTypes(); - //error recovery, ignore patterns with incorrect number of nested patterns: - if (componentType.length != rpOne.nested.length) { - return pattern; - } - PatternDescription[] reducedNestedPatterns = null; - boolean covered = true; - for (int i = 0; i < componentType.length; i++) { - PatternDescription newNested = reduceRecordPattern(rpOne.nested[i], replaces); - if (newNested != rpOne.nested[i]) { - if (reducedNestedPatterns == null) { - reducedNestedPatterns = Arrays.copyOf(rpOne.nested, rpOne.nested.length); - } - reducedNestedPatterns[i] = newNested; - } - - covered &= checkCovered(componentType[i], List.of(newNested)); - } - if (covered) { - PatternDescription pd = new BindingPattern(rpOne.recordType); - replaces.put(pd, Set.of(pattern)); - return pd; - } else if (reducedNestedPatterns != null) { - PatternDescription pd = new RecordPattern(rpOne.recordType, rpOne.fullComponentTypes(), reducedNestedPatterns); - replaces.put(pd, Set.of(pattern)); - return pd; - } - } - return pattern; - } - - private Set removeCoveredRecordPatterns(Set patterns) { - Set existingBindings = patterns.stream() - .filter(pd -> pd instanceof BindingPattern) - .map(pd -> ((BindingPattern) pd).type.tsym) - .collect(Collectors.toSet()); - Set result = new HashSet<>(patterns); - - for (Iterator it = result.iterator(); it.hasNext();) { - PatternDescription pd = it.next(); - if (pd instanceof RecordPattern rp && existingBindings.contains(rp.recordType.tsym)) { - it.remove(); - } - } - - return result; - } - public void visitTry(JCTry tree) { ListBuffer prevPendingExits = pendingExits; pendingExits = new ListBuffer<>(); @@ -1362,18 +890,6 @@ public void analyzeTree(Env env, JCTree tree, TreeMaker make) { } } - private boolean isBpCovered(Type componentType, PatternDescription newNested) { - if (newNested instanceof BindingPattern bp) { - Type seltype = types.erasure(componentType); - Type pattype = types.erasure(bp.type); - - return seltype.isPrimitive() ? - types.isUnconditionallyExact(seltype, pattype) : - (bp.type.isPrimitive() && types.isUnconditionallyExact(types.unboxedType(seltype), bp.type)) || types.isSubtype(seltype, pattype); - } - return false; - } - /** * This pass implements the second step of the dataflow analysis, namely * the exception analysis. This is to ensure that every checked exception that is @@ -3509,93 +3025,4 @@ public static Liveness from(boolean value) { } } - sealed interface PatternDescription { } - public PatternDescription makePatternDescription(Type selectorType, JCPattern pattern) { - if (pattern instanceof JCBindingPattern binding) { - Type type = !selectorType.isPrimitive() && types.isSubtype(selectorType, binding.type) - ? selectorType : binding.type; - return new BindingPattern(type); - } else if (pattern instanceof JCRecordPattern record) { - Type[] componentTypes; - - if (!record.type.isErroneous()) { - componentTypes = ((ClassSymbol) record.type.tsym).getRecordComponents() - .map(r -> types.memberType(record.type, r)) - .toArray(s -> new Type[s]); - } - else { - componentTypes = record.nested.map(t -> types.createErrorType(t.type)).toArray(s -> new Type[s]);; - } - - PatternDescription[] nestedDescriptions = - new PatternDescription[record.nested.size()]; - int i = 0; - for (List it = record.nested; - it.nonEmpty(); - it = it.tail, i++) { - Type componentType = i < componentTypes.length ? componentTypes[i] - : syms.errType; - nestedDescriptions[i] = makePatternDescription(types.erasure(componentType), it.head); - } - return new RecordPattern(record.type, componentTypes, nestedDescriptions); - } else if (pattern instanceof JCAnyPattern) { - return new BindingPattern(selectorType); - } else { - throw Assert.error(); - } - } - record BindingPattern(Type type) implements PatternDescription { - @Override - public int hashCode() { - return type.tsym.hashCode(); - } - @Override - public boolean equals(Object o) { - return o instanceof BindingPattern other && - type.tsym == other.type.tsym; - } - @Override - public String toString() { - return type.tsym + " _"; - } - } - record RecordPattern(Type recordType, int _hashCode, Type[] fullComponentTypes, PatternDescription... nested) implements PatternDescription { - - public RecordPattern(Type recordType, Type[] fullComponentTypes, PatternDescription[] nested) { - this(recordType, hashCode(-1, recordType, nested), fullComponentTypes, nested); - } - - @Override - public int hashCode() { - return _hashCode; - } - - @Override - public boolean equals(Object o) { - return o instanceof RecordPattern other && - recordType.tsym == other.recordType.tsym && - Arrays.equals(nested, other.nested); - } - - public int hashCode(int excludeComponent) { - return hashCode(excludeComponent, recordType, nested); - } - - public static int hashCode(int excludeComponent, Type recordType, PatternDescription... nested) { - int hash = 5; - hash = 41 * hash + recordType.tsym.hashCode(); - for (int i = 0; i < nested.length; i++) { - if (i != excludeComponent) { - hash = 41 * hash + nested[i].hashCode(); - } - } - return hash; - } - @Override - public String toString() { - return recordType.tsym + "(" + Arrays.stream(nested) - .map(pd -> pd.toString()) - .collect(Collectors.joining(", ")) + ")"; - } - } } From 696383588ae2cb1f9aa5efa1a73250f46e81681d Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Fri, 12 Sep 2025 11:21:54 +0200 Subject: [PATCH 11/33] Adding doc comment. --- .../com/sun/tools/javac/comp/ExhaustivenessComputer.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java index 7683450ac2bdc..6eb87abb8bf26 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java @@ -47,7 +47,8 @@ import static java.util.stream.Collectors.groupingBy; -/** +/** A class to compute exhaustiveness of set of switch cases. + * *

This is NOT part of any supported API. * If you write code that depends on this, you do so at your own risk. * This code and its internal interfaces are subject to change or From 31d3990ca72185056c2ab3027352131255584b8f Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Fri, 12 Sep 2025 12:51:20 +0200 Subject: [PATCH 12/33] Cleanup, making timeout work. --- .../javac/comp/ExhaustivenessComputer.java | 94 +++++++++++++++---- .../com/sun/tools/javac/comp/Flow.java | 35 ++++--- .../ExhaustivenessConvenientErrors.java | 7 +- 3 files changed, 100 insertions(+), 36 deletions(-) diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java index 3801cec4cbf1d..b95cbf959afa9 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java @@ -60,13 +60,15 @@ * deletion without notice. */ public class ExhaustivenessComputer { + private static final long DEFAULT_TIMEOUT = 5000; //5s protected static final Context.Key exhaustivenessKey = new Context.Key<>(); private final Symtab syms; private final Types types; private final Check chk; private final Infer infer; - private final int missingExhaustivenessTimeout; + private final long missingExhaustivenessTimeout; + private long startTime = -1; public static ExhaustivenessComputer instance(Context context) { ExhaustivenessComputer instance = context.get(exhaustivenessKey); @@ -84,18 +86,19 @@ protected ExhaustivenessComputer(Context context) { infer = Infer.instance(context); Options options = Options.instance(context); String timeout = options.get("exhaustivityTimeout"); - int computedTimeout = 1000; + long computedTimeout = DEFAULT_TIMEOUT; if (timeout != null) { try { - computedTimeout = Integer.parseInt(timeout); + computedTimeout = Long.parseLong(timeout); } catch (NumberFormatException ex) { //TODO: notify? } } + missingExhaustivenessTimeout = computedTimeout; } - public boolean exhausts(JCExpression selector, List cases, Set pendingNotExhaustiveDetails) { + public ExhaustivenessResult exhausts(JCExpression selector, List cases) { Set patternSet = new HashSet<>(); Map> enum2Constants = new HashMap<>(); Set booleanLiterals = new HashSet<>(Set.of(0, 1)); @@ -129,7 +132,7 @@ public boolean exhausts(JCExpression selector, List cases, Set p } if (types.unboxedTypeOrType(selector.type).hasTag(TypeTag.BOOLEAN) && booleanLiterals.isEmpty()) { - return true; + return ExhaustivenessResult.ofExhaustive(); } for (Entry> e : enum2Constants.entrySet()) { @@ -140,26 +143,49 @@ public boolean exhausts(JCExpression selector, List cases, Set p try { CoverageResult coveredResult = computeCoverage(selector.type, patternSet, false); if (coveredResult.covered()) { - return true; - } - if (missingExhaustivenessTimeout == 0) { - return false; + return ExhaustivenessResult.ofExhaustive(); } - //TODO: should stop computation when time runs out: - PatternDescription defaultPattern = new BindingPattern(selector.type); - Set missingPatterns = expandMissingPatternDescriptions(selector.type, selector.type, defaultPattern, coveredResult.incompletePatterns(), Set.of(defaultPattern)); - for (PatternDescription missing : missingPatterns) { - pendingNotExhaustiveDetails.add(missing.toString()); - } - return false; + Set details = + this.computeMissingPatternDescriptions(selector.type, coveredResult.incompletePatterns()) + .stream() + .map(PatternDescription::toString) + .collect(Collectors.toSet()); + + return ExhaustivenessResult.ofDetails(details); } catch (CompletionFailure cf) { chk.completionError(selector.pos(), cf); - return true; //error recovery + return ExhaustivenessResult.ofExhaustive(); //error recovery + } + } + + protected Set computeMissingPatternDescriptions(Type selectorType, Set incompletePatterns) { + if (missingExhaustivenessTimeout == 0) { + return Set.of(); + } + try { + startTime = System.currentTimeMillis(); + PatternDescription defaultPattern = new BindingPattern(selectorType); + return expandMissingPatternDescriptions(selectorType, selectorType, defaultPattern, incompletePatterns, Set.of(defaultPattern)); + } catch (TimeoutException ex) { + return ex.missingPatterns != null ? ex.missingPatterns : Set.of(); + } finally { + startTime = -1; } } private Set expandMissingPatternDescriptions(Type selectorType, Type targetType, PatternDescription toExpand, Set basePatterns, Set inMissingPatterns) { + try { + return doExpandMissingPatternDescriptions(selectorType, targetType, toExpand, basePatterns, inMissingPatterns); + } catch (TimeoutException ex) { + if (ex.missingPatterns == null) { + ex = new TimeoutException(inMissingPatterns); + } + throw ex; + } + } + + private Set doExpandMissingPatternDescriptions(Type selectorType, Type targetType, PatternDescription toExpand, Set basePatterns, Set inMissingPatterns) { if (toExpand instanceof BindingPattern bp) { if (bp.type.tsym.isSealed()) { List permitted = ((ClassSymbol) bp.type.tsym).getPermittedSubclasses(); @@ -717,6 +743,8 @@ private Set reduceNestedPatterns(Set pat .filter(pd -> pd.nested.length == nestedPatternsCount) .collect(groupingBy(pd -> useHashes ? pd.hashCode(mismatchingCandidateFin) : 0)); for (var candidates : groupEquivalenceCandidates.values()) { + checkTimeout(); + var candidatesArr = candidates.toArray(RecordPattern[]::new); for (int firstCandidate = 0; @@ -897,6 +925,8 @@ private Set removeCoveredRecordPatterns(Set missingExhaustivenessTimeout) { + throw new TimeoutException(null); + } + } + + protected sealed interface PatternDescription { public Type type(); - } + } + public PatternDescription makePatternDescription(Type selectorType, JCPattern pattern) { if (pattern instanceof JCBindingPattern binding) { Type type = !selectorType.isPrimitive() && types.isSubtype(selectorType, binding.type) @@ -1009,4 +1046,23 @@ public Type type() { return recordType; } } + + public record ExhaustivenessResult(boolean exhaustive, Set notExhaustiveDetails) { + public static ExhaustivenessResult ofExhaustive() { + return new ExhaustivenessResult(true, null); + } + public static ExhaustivenessResult ofDetails(Set notExhaustiveDetails) { + return new ExhaustivenessResult(false, notExhaustiveDetails != null ? notExhaustiveDetails : Set.of()); + } + } + + protected static class TimeoutException extends RuntimeException { + private static final long serialVersionUID = 0L; + private transient final Set missingPatterns; + + public TimeoutException(Set missingPatterns) { + super(null, null, false, false); + this.missingPatterns = missingPatterns; + } + } } diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java index 222a0d518840c..dd3df7ca8a73d 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java @@ -53,6 +53,7 @@ import static com.sun.tools.javac.code.Kinds.Kind.*; import static com.sun.tools.javac.code.TypeTag.BOOLEAN; import static com.sun.tools.javac.code.TypeTag.VOID; +import com.sun.tools.javac.comp.ExhaustivenessComputer.ExhaustivenessResult; import com.sun.tools.javac.resources.CompilerProperties.Fragments; import static com.sun.tools.javac.tree.JCTree.Tag.*; import com.sun.tools.javac.util.JCDiagnostic.Fragment; @@ -699,13 +700,17 @@ public void visitSwitch(JCSwitch tree) { tree.isExhaustive = tree.hasUnconditionalPattern || TreeInfo.isErrorEnumSwitch(tree.selector, tree.cases); if (exhaustiveSwitch) { - Set pendingNotExhaustiveDetails = new TreeSet<>(); - tree.isExhaustive = tree.isExhaustive || exhaustiveness.exhausts(tree.selector, tree.cases, pendingNotExhaustiveDetails); if (!tree.isExhaustive) { - if (pendingNotExhaustiveDetails.isEmpty()) { - log.error(tree, Errors.NotExhaustiveStatement); - } else { - log.error(tree, Errors.NotExhaustiveStatementDetails(pendingNotExhaustiveDetails.stream().collect(Collectors.joining("\n")))); + ExhaustivenessResult exhaustivenessResult = exhaustiveness.exhausts(tree.selector, tree.cases); + + tree.isExhaustive = exhaustivenessResult.exhaustive(); + + if (!tree.isExhaustive) { + if (exhaustivenessResult.notExhaustiveDetails().isEmpty()) { + log.error(tree, Errors.NotExhaustiveStatement); + } else { + log.error(tree, Errors.NotExhaustiveStatementDetails(exhaustivenessResult.notExhaustiveDetails().stream().collect(Collectors.joining("\n")))); + } } } } @@ -739,21 +744,23 @@ public void visitSwitchExpression(JCSwitchExpression tree) { } } - Set pendingNotExhaustiveDetails = new TreeSet<>(); if (tree.hasUnconditionalPattern || TreeInfo.isErrorEnumSwitch(tree.selector, tree.cases)) { tree.isExhaustive = true; } else { - tree.isExhaustive = exhaustiveness.exhausts(tree.selector, tree.cases, pendingNotExhaustiveDetails); - } + ExhaustivenessResult exhaustivenessResult = exhaustiveness.exhausts(tree.selector, tree.cases); - if (!tree.isExhaustive) { - if (pendingNotExhaustiveDetails.isEmpty()) { - log.error(tree, Errors.NotExhaustive); - } else { - log.error(tree, Errors.NotExhaustiveDetails(pendingNotExhaustiveDetails.stream().collect(Collectors.joining("\n")))); + tree.isExhaustive = exhaustivenessResult.exhaustive(); + + if (!tree.isExhaustive) { + if (exhaustivenessResult.notExhaustiveDetails().isEmpty()) { + log.error(tree, Errors.NotExhaustive); + } else { + log.error(tree, Errors.NotExhaustiveDetails(exhaustivenessResult.notExhaustiveDetails().stream().collect(Collectors.joining("\n")))); + } } } + alive = prevAlive; alive = alive.or(resolveYields(tree, prevPendingExits)); } diff --git a/test/langtools/tools/javac/patterns/ExhaustivenessConvenientErrors.java b/test/langtools/tools/javac/patterns/ExhaustivenessConvenientErrors.java index 96e3f6f39a31a..df04df05046b0 100644 --- a/test/langtools/tools/javac/patterns/ExhaustivenessConvenientErrors.java +++ b/test/langtools/tools/javac/patterns/ExhaustivenessConvenientErrors.java @@ -23,8 +23,8 @@ /** * @test - * @bug 9999999 - * @summary Check exhaustiveness of switches over sealed types. + * @bug 8367530 + * @summary Check enhanced exhaustiveness errors * @library /tools/lib * @modules jdk.compiler/com.sun.tools.javac.api * jdk.compiler/com.sun.tools.javac.main @@ -445,7 +445,8 @@ private void doTest(Path base, String[] libraryCode, String testCode, String... "-Xlint:-preview", "--class-path", libClasses.toString(), "-XDshould-stop.at=FLOW", - "-XDshould-stop.ifNoError=FLOW") + "-XDshould-stop.ifNoError=FLOW", + "-XDexhaustivityTimeout=" + Long.MAX_VALUE) //never timeout .outdir(classes) .files(tb.findJavaFiles(src)) .diagnosticListener(d -> { From ce838030dbc7c313bb75c508b3297ab59abb7137 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Thu, 25 Sep 2025 17:32:51 +0200 Subject: [PATCH 13/33] Enabling disabled test. --- test/langtools/tools/javac/patterns/Exhaustiveness.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/langtools/tools/javac/patterns/Exhaustiveness.java b/test/langtools/tools/javac/patterns/Exhaustiveness.java index 5b60f46af502b..11a7fd840ac81 100644 --- a/test/langtools/tools/javac/patterns/Exhaustiveness.java +++ b/test/langtools/tools/javac/patterns/Exhaustiveness.java @@ -1633,7 +1633,7 @@ public void testDeeplyNestedExhaustive(Path base) throws Exception { true); } -// @Test + @Test public void testDeeplyNestedNotExhaustive(Path base) throws Exception { List variants = createDeeplyNestedVariants().stream().collect(Collectors.toCollection(ArrayList::new)); int removed = (int) (Math.random() * variants.size()); From 0ee608625d2b6829881fa8b7a695074f0cbb275e Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Tue, 30 Sep 2025 17:10:42 +0200 Subject: [PATCH 14/33] Simplifying the code as suggested. --- .../share/classes/com/sun/tools/javac/comp/Flow.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java index c2ad9deb57dcb..743ff9a924ff7 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java @@ -1080,7 +1080,6 @@ private Set reduceNestedPatterns(Set pat continue NEXT_PATTERN; } } else if (rpOne.nested[i] instanceof RecordPattern nestedRPOne) { - boolean foundMatchingReplaced = false; Set pendingReplacedPatterns = new HashSet<>(replaces.getOrDefault(rpOther.nested[i], Set.of())); while (!pendingReplacedPatterns.isEmpty()) { @@ -1089,15 +1088,12 @@ private Set reduceNestedPatterns(Set pat pendingReplacedPatterns.remove(currentReplaced); if (nestedRPOne.equals(currentReplaced)) { - foundMatchingReplaced = true; - break; + continue ACCEPT; } pendingReplacedPatterns.addAll(replaces.getOrDefault(currentReplaced, Set.of())); } - if (!foundMatchingReplaced) { - continue NEXT_PATTERN; - } + continue NEXT_PATTERN; } else { continue NEXT_PATTERN; } From c78f6964eb64cca0be917f04bbd242bfae406dcb Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Tue, 7 Oct 2025 19:01:42 +0200 Subject: [PATCH 15/33] Cleanup. --- .../javac/comp/ExhaustivenessComputer.java | 772 ++++++++++-------- .../ExhaustivenessConvenientErrors.java | 64 +- 2 files changed, 501 insertions(+), 335 deletions(-) diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java index b95cbf959afa9..a154929e4ae91 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java @@ -49,6 +49,7 @@ import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Collectors; +import java.util.stream.Stream; import static java.util.stream.Collectors.groupingBy; @@ -149,7 +150,14 @@ public ExhaustivenessResult exhausts(JCExpression selector, List cases) Set details = this.computeMissingPatternDescriptions(selector.type, coveredResult.incompletePatterns()) .stream() - .map(PatternDescription::toString) + .flatMap(pd -> { + if (pd instanceof BindingPattern bp && enum2Constants.containsKey(bp.type.tsym)) { + Symbol enumType = bp.type.tsym; + return enum2Constants.get(enumType).stream().map(c -> enumType.toString() + "." + c.name); + } else { + return Stream.of(pd.toString()); + } + }) .collect(Collectors.toSet()); return ExhaustivenessResult.ofDetails(details); @@ -159,318 +167,6 @@ public ExhaustivenessResult exhausts(JCExpression selector, List cases) } } - protected Set computeMissingPatternDescriptions(Type selectorType, Set incompletePatterns) { - if (missingExhaustivenessTimeout == 0) { - return Set.of(); - } - try { - startTime = System.currentTimeMillis(); - PatternDescription defaultPattern = new BindingPattern(selectorType); - return expandMissingPatternDescriptions(selectorType, selectorType, defaultPattern, incompletePatterns, Set.of(defaultPattern)); - } catch (TimeoutException ex) { - return ex.missingPatterns != null ? ex.missingPatterns : Set.of(); - } finally { - startTime = -1; - } - } - - private Set expandMissingPatternDescriptions(Type selectorType, Type targetType, PatternDescription toExpand, Set basePatterns, Set inMissingPatterns) { - try { - return doExpandMissingPatternDescriptions(selectorType, targetType, toExpand, basePatterns, inMissingPatterns); - } catch (TimeoutException ex) { - if (ex.missingPatterns == null) { - ex = new TimeoutException(inMissingPatterns); - } - throw ex; - } - } - - private Set doExpandMissingPatternDescriptions(Type selectorType, Type targetType, PatternDescription toExpand, Set basePatterns, Set inMissingPatterns) { - if (toExpand instanceof BindingPattern bp) { - if (bp.type.tsym.isSealed()) { - List permitted = ((ClassSymbol) bp.type.tsym).getPermittedSubclasses(); - Set viablePermittedPatterns = permitted.stream().map(type -> type.tsym).filter(csym -> { - Type instantiated; - if (csym.type.allparams().isEmpty()) { - instantiated = csym.type; - } else { - instantiated = infer.instantiatePatternType(targetType, csym); - } - - return instantiated != null && types.isCastable(targetType, instantiated); - }).map(csym -> new BindingPattern(types.erasure(csym.type))).collect(Collectors.toSet()); - - boolean reduced = false; - - for (Iterator it = viablePermittedPatterns.iterator(); it.hasNext(); ) { - BindingPattern current = it.next(); - Set reducedPermittedPatterns = new HashSet<>(viablePermittedPatterns); - - reducedPermittedPatterns.remove(current); - - Set replaced = replace(inMissingPatterns, toExpand, reducedPermittedPatterns); - - if (computeCoverage(selectorType, joinSets(basePatterns, replaced), true).covered()) { - it.remove(); - reduced = true; - } - } - - if (!reduced) { - return inMissingPatterns; - } - - Set currentMissingPatterns = replace(inMissingPatterns, toExpand, viablePermittedPatterns); - - //try to recursively work on each viable pattern: - for (PatternDescription viable : viablePermittedPatterns) { - currentMissingPatterns = expandMissingPatternDescriptions(selectorType, targetType, viable, basePatterns, currentMissingPatterns); - } - - return currentMissingPatterns; - } else if ((bp.type.tsym.flags_field & Flags.RECORD) != 0 && - basePatternsHaveRecordPatternOnThisSpot(basePatterns, findRootContaining(inMissingPatterns, toExpand), toExpand)) { //only expand record types into record patterns if there's a chance it may change the outcome - Type[] componentTypes = ((ClassSymbol) bp.type.tsym).getRecordComponents() - .map(r -> types.memberType(bp.type, r)) - .toArray(s -> new Type[s]); - List> combinatorialNestedTypes = List.of(List.nil()); - for (Type componentType : componentTypes) { - List variants; - if (componentType.tsym.isSealed()) { - variants = leafPermittedSubTypes(componentType.tsym, csym -> { - Type instantiated; - if (csym.type.allparams().isEmpty()) { - instantiated = csym.type; - } else { - instantiated = infer.instantiatePatternType(componentType, csym); - } - - return instantiated != null && types.isCastable(componentType, instantiated); - }).stream().map(csym -> csym.type).collect(List.collector()); //XXX: csym.type => instantiate - } else { - variants = List.of(componentType); - } - List> newCombinatorialNestedTypes = List.nil(); - for (List existing : combinatorialNestedTypes) { - for (Type nue : variants) { - newCombinatorialNestedTypes = newCombinatorialNestedTypes.prepend(existing.append(nue)); - } - } - combinatorialNestedTypes = newCombinatorialNestedTypes; - } - - Set combinatorialPatterns = combinatorialNestedTypes.stream().map(combination -> new RecordPattern(bp.type, componentTypes, combination.map(BindingPattern::new).toArray(PatternDescription[]::new))).collect(Collectors.toSet()); - - //remove unnecessary: - //preserve the most specific: - combinatorialPatterns = new LinkedHashSet<>(sortPattern(combinatorialPatterns, basePatterns, inMissingPatterns).reversed()); - - for (Iterator it = combinatorialPatterns.iterator(); it.hasNext(); ) { - PatternDescription current = it.next(); - Set reducedAdded = new HashSet<>(combinatorialPatterns); - - reducedAdded.remove(current); - - if (computeCoverage(selectorType, joinSets(basePatterns, replace(inMissingPatterns, bp, reducedAdded)), true).covered()) { - it.remove(); - } - } - - CoverageResult coverageResult = computeCoverage(targetType, combinatorialPatterns, true); - - if (!coverageResult.covered()) { - //nothing better can be done(?) - combinatorialPatterns = coverageResult.incompletePatterns(); - } - - //combine sealed subtypes into the supertype, if all is covered. - //but preserve more specific record patterns in positions where there are record patterns - //this is particularly for the case where the sealed supertype only has one permitted type, the record: - Set sortedCandidates = sortPattern(combinatorialPatterns, basePatterns, combinatorialPatterns); - - //remove unnecessary: - OUTER: for (Iterator it = sortedCandidates.iterator(); it.hasNext(); ) { - PatternDescription current = it.next(); - Set reducedAdded = new HashSet<>(sortedCandidates); - - reducedAdded.remove(current); - - if (computeCoverage(selectorType, joinSets(basePatterns, replace(inMissingPatterns, bp, reducedAdded)), true).covered()) { - it.remove(); - } - } - - Set currentMissingPatterns = replace(inMissingPatterns, toExpand, sortedCandidates); - - for (PatternDescription addedPattern : sortedCandidates) { - if (addedPattern instanceof RecordPattern addedRP) { - for (int c = 0; c < addedRP.nested.length; c++) { - currentMissingPatterns = expandMissingPatternDescriptions(selectorType, addedRP.fullComponentTypes[c], addedRP.nested[c], basePatterns, currentMissingPatterns); - } - } - } - - return currentMissingPatterns; - } - } - return inMissingPatterns; - } - - private PatternDescription findRootContaining(Set rootPatterns, PatternDescription added) { - for (PatternDescription pd : rootPatterns) { - if (isUnderRoot(pd, added)) { - return pd; - } - } - - //assert? - return null; - } - - private boolean basePatternsHaveRecordPatternOnThisSpot(Set basePatterns, PatternDescription rootPattern, PatternDescription added) { - if (rootPattern == added) { - return basePatterns.stream().anyMatch(pd -> pd instanceof RecordPattern); - } - if (!(rootPattern instanceof RecordPattern rootPatternRecord)) { - return false; - } - int index = -1; - for (int c = 0; c < rootPatternRecord.nested.length; c++) { - if (isUnderRoot(rootPatternRecord.nested[c], added)) { - index = c; - break; - } - } - Assert.check(index != (-1)); - //TODO: isSameType erasure? - //TODO: indexing into the nested array - error recovery(!) - int indexFin = index; - Set filteredBasePatterns = basePatterns.stream().filter(pd -> pd instanceof RecordPattern).map(rp -> (RecordPattern) rp).filter(rp -> types.isSameType(rp.recordType(), rootPatternRecord.recordType())).map(rp -> rp.nested[indexFin]).collect(Collectors.toSet()); - return basePatternsHaveRecordPatternOnThisSpot(filteredBasePatterns, rootPatternRecord.nested[index], added); - } - - private boolean isUnderRoot(PatternDescription root, PatternDescription searchFor) { - if (root == searchFor) { - return true; - } else if (root instanceof RecordPattern rp) { - for (int c = 0; c < rp.nested.length; c++) { - if (isUnderRoot(rp.nested[c], searchFor)) { - return true; - } - } - } - return false; - } - - private SequencedSet sortPattern(Set candidates, Set basePatterns, Set missingPatterns) { - SequencedSet sortedCandidates = new LinkedHashSet<>(); - - while (!candidates.isEmpty()) { - PatternDescription mostSpecific = null; - for (PatternDescription current : candidates) { - if (mostSpecific == null || isMoreImportant(current, mostSpecific, basePatterns, missingPatterns)) { - mostSpecific = current; - } - } - sortedCandidates.add(mostSpecific); - candidates.remove(mostSpecific); - } - return sortedCandidates; - } - - private Set replace(Iterable in, PatternDescription what, Collection to) { - Set result = new HashSet<>(); - - for (PatternDescription pd : in) { - Collection replaced = replace(pd, what, to); - if (replaced != null) { - result.addAll(replaced); - } else { - result.add(pd); - } - } - - return result; - } - - //null: no change - private Collection replace(PatternDescription in, PatternDescription what, Collection to) { - if (in == what) { - return to; - } else if (in instanceof RecordPattern rp) { - for (int c = 0; c < rp.nested.length; c++) { - Collection replaced = replace(rp.nested[c], what, to); - if (replaced != null) { - Set withReplaced = new HashSet<>(); - - generatePatternsWithReplacedNestedPattern(rp, c, replaced, withReplaced::add); - - return replace(withReplaced, what, to); - } - } - return null; - } else { - return null; //binding patterns have no children - } - } - - //true iff pd1 is more important than pd2 - //false otherwise - //TODO: there may be a better name for this method: - private boolean isMoreImportant(PatternDescription pd1, PatternDescription pd2, Set basePatterns, Set missingPatterns) { - if (pd1 instanceof RecordPattern rp1 && pd2 instanceof RecordPattern rp2) { - for (int c = 0; c < rp1.nested.length; c++) { - if (isMoreImportant((BindingPattern) rp1.nested[c], (BindingPattern) rp2.nested[c], basePatterns, missingPatterns)) { - return true; - } - } - } else if (pd1 instanceof BindingPattern bp1 && pd2 instanceof BindingPattern bp2) { - Type t1 = bp1.type(); - Type t2 = bp2.type(); - boolean t1IsImportantRecord = (t1.tsym.flags_field & RECORD) != 0 && hasMatchingRecordPattern(basePatterns, missingPatterns, bp1); - boolean t2IsImportantRecord = (t2.tsym.flags_field & RECORD) != 0 && hasMatchingRecordPattern(basePatterns, missingPatterns, bp2); - if (t1IsImportantRecord && !t2IsImportantRecord) { - return false; - } - if (!t1IsImportantRecord && t2IsImportantRecord) { - return true; - } - if (!types.isSameType(t1, t2) && types.isSubtype(t1, t2)) { - return true; - } - } - return false; - } - - private boolean hasMatchingRecordPattern(Set basePatterns, Set missingPatterns, PatternDescription query) { - PatternDescription root = findRootContaining(missingPatterns, query); - - if (root == null) { - return false; - } - return basePatternsHaveRecordPatternOnThisSpot(basePatterns, root, query); - } - - private Set joinSets(Collection s1, Collection s2) { - Set result = new HashSet<>(); - - result.addAll(s1); - result.addAll(s2); - return result; - } - - //TODO: unify with the similar code below: - private void generatePatternsWithReplacedNestedPattern(RecordPattern basePattern, int replaceComponent, Iterable updatedNestedPatterns, Consumer sink) { - for (PatternDescription nested : updatedNestedPatterns) { - PatternDescription[] newNested = - Arrays.copyOf(basePattern.nested, basePattern.nested.length); - newNested[replaceComponent] = nested; - sink.accept(new RecordPattern(basePattern.recordType(), - basePattern.fullComponentTypes(), - newNested)); - } - } - private CoverageResult computeCoverage(Type selectorType, Set patterns, boolean search) { Set updatedPatterns; Map> replaces = new IdentityHashMap<>(); @@ -573,16 +269,7 @@ private Set reduceBindingPatterns(Type selectorType, Set permitted = allPermittedSubTypes(clazz, csym -> { - Type instantiated; - if (csym.type.allparams().isEmpty()) { - instantiated = csym.type; - } else { - instantiated = infer.instantiatePatternType(selectorType, csym); - } - - return instantiated != null && types.isCastable(selectorType, instantiated); - }); + Set permitted = allPermittedSubTypes(clazz, isPossibleSubtypePredicate(selectorType)); int permittedSubtypes = permitted.size(); for (PatternDescription pdOther : patterns) { @@ -650,8 +337,24 @@ private Set allPermittedSubTypes(TypeSymbol root, Predicate return permitted; } - private Set leafPermittedSubTypes(TypeSymbol root, Predicate accept) { - Set permitted = new HashSet<>(); + private Predicate isPossibleSubtypePredicate(Type targetType) { + return csym -> { + Type instantiated = instantiatePatternType(targetType, csym); + + return instantiated != null && types.isCastable(targetType, instantiated); + }; + } + + private Type instantiatePatternType(Type targetType, TypeSymbol csym) { + if (csym.type.allparams().isEmpty()) { + return csym.type; + } else { + return infer.instantiatePatternType(targetType, csym); + } + } + + private Set leafPermittedSubTypes(TypeSymbol root, Predicate accept) { + Set permitted = new HashSet<>(); List permittedSubtypesClosure = baseClasses(root); while (permittedSubtypesClosure.nonEmpty()) { @@ -827,16 +530,13 @@ private Set reduceNestedPatterns(Set pat current.removeAll(join); } - for (PatternDescription nested : updatedPatterns) { - PatternDescription[] newNested = - Arrays.copyOf(rpOne.nested, rpOne.nested.length); - newNested[mismatchingCandidateFin] = nested; - RecordPattern nue = new RecordPattern(rpOne.recordType(), - rpOne.fullComponentTypes(), - newNested); + generatePatternsWithReplacedNestedPattern(rpOne, + mismatchingCandidateFin, + updatedPatterns, + nue -> { current.add(nue); replaces.put(nue, new HashSet<>(join)); - } + }); } } } @@ -987,7 +687,7 @@ record BindingPattern(Type type, int permittedSubtypes) implements PatternDescri public BindingPattern(Type type) { this(type, -1); } - + @Override public int hashCode() { return type.tsym.hashCode(); @@ -1056,6 +756,410 @@ public static ExhaustivenessResult ofDetails(Set notExhaustiveDetails) { } } + //computation of missing patterns: + protected Set computeMissingPatternDescriptions(Type selectorType, + Set incompletePatterns) { + if (missingExhaustivenessTimeout == 0) { + return Set.of(); + } + try { + startTime = System.currentTimeMillis(); + PatternDescription defaultPattern = new BindingPattern(selectorType); + return expandMissingPatternDescriptions(selectorType, + selectorType, + defaultPattern, + incompletePatterns, + Set.of(defaultPattern)); + } catch (TimeoutException ex) { + return ex.missingPatterns != null ? ex.missingPatterns : Set.of(); + } finally { + startTime = -1; + } + } + + private Set expandMissingPatternDescriptions(Type selectorType, + Type targetType, + PatternDescription toExpand, + Set basePatterns, + Set inMissingPatterns) { + try { + return doExpandMissingPatternDescriptions(selectorType, targetType, + toExpand, basePatterns, + inMissingPatterns); + } catch (TimeoutException ex) { + if (ex.missingPatterns == null) { + ex = new TimeoutException(inMissingPatterns); + } + throw ex; + } + } + + private Set doExpandMissingPatternDescriptions(Type selectorType, + Type targetType, + PatternDescription toExpand, + Set basePatterns, + Set inMissingPatterns) { + if (toExpand instanceof BindingPattern bp) { + if (bp.type.tsym.isSealed()) { + //try to replace binding patterns for sealed types with all their immediate permitted types: + List permitted = ((ClassSymbol) bp.type.tsym).getPermittedSubclasses(); + Set viablePermittedPatterns = + permitted.stream() + .map(type -> type.tsym) + .filter(isPossibleSubtypePredicate(targetType)) + .map(csym -> new BindingPattern(types.erasure(csym.type))) + .collect(Collectors.toCollection(HashSet::new)); + + //remove the permitted subtypes that are not needed to achieve exhaustivity + boolean reduced = false; + + for (Iterator it = viablePermittedPatterns.iterator(); it.hasNext(); ) { + BindingPattern current = it.next(); + Set reducedPermittedPatterns = new HashSet<>(viablePermittedPatterns); + + reducedPermittedPatterns.remove(current); + + Set replaced = + replace(inMissingPatterns, toExpand, reducedPermittedPatterns); + + if (computeCoverage(selectorType, joinSets(basePatterns, replaced), true).covered()) { + it.remove(); + reduced = true; + } + } + + if (!reduced) { + //if all immediate permitted subtypes are needed + //give up, and simply use the current pattern: + return inMissingPatterns; + } + + Set currentMissingPatterns = + replace(inMissingPatterns, toExpand, viablePermittedPatterns); + + //try to recursively expand on each viable pattern: + for (PatternDescription viable : viablePermittedPatterns) { + currentMissingPatterns = expandMissingPatternDescriptions(selectorType, targetType, + viable, basePatterns, + currentMissingPatterns); + } + + return currentMissingPatterns; + } else if ((bp.type.tsym.flags_field & Flags.RECORD) != 0 && + //only expand record types into record patterns if there's a chance it may change the outcome + //i.e. there is a record pattern in at the spot in the original base patterns: + hasMatchingRecordPattern(basePatterns, inMissingPatterns, toExpand)) { + //if there is a binding pattern at a place where the original based patterns + //have a record pattern, try to expand the binding pattern into a record pattern + //create all possible combinations of record pattern components: + Type[] componentTypes = ((ClassSymbol) bp.type.tsym).getRecordComponents() + .map(r -> types.memberType(bp.type, r)) + .toArray(s -> new Type[s]); + List> combinatorialNestedTypes = List.of(List.nil()); + + for (Type componentType : componentTypes) { + List variants; + + if (componentType.tsym.isSealed()) { + variants = leafPermittedSubTypes(componentType.tsym, + isPossibleSubtypePredicate(componentType)) + .stream() + .map(csym -> instantiatePatternType(componentType, csym)) + .collect(List.collector()); + } else { + variants = List.of(componentType); + } + + List> newCombinatorialNestedTypes = List.nil(); + + for (List existing : combinatorialNestedTypes) { + for (Type nue : variants) { + newCombinatorialNestedTypes = newCombinatorialNestedTypes.prepend(existing.append(nue)); + } + } + + combinatorialNestedTypes = newCombinatorialNestedTypes; + } + + Set combinatorialPatterns = + combinatorialNestedTypes.stream() + .map(combination -> new RecordPattern(bp.type, + componentTypes, + combination.map(BindingPattern::new) + .toArray(PatternDescription[]::new))) + .collect(Collectors.toCollection(HashSet::new)); + + //remove unnecessary: + for (Iterator it = combinatorialPatterns.iterator(); it.hasNext(); ) { + PatternDescription current = it.next(); + Set reducedAdded = new HashSet<>(combinatorialPatterns); + + reducedAdded.remove(current); + + Set combinedPatterns = + joinSets(basePatterns, replace(inMissingPatterns, bp, reducedAdded)); + + if (computeCoverage(selectorType, combinedPatterns, true).covered()) { + it.remove(); + } + } + + CoverageResult coverageResult = computeCoverage(targetType, combinatorialPatterns, true); + + if (!coverageResult.covered()) { + //use the partially merged/combined patterns: + combinatorialPatterns = coverageResult.incompletePatterns(); + } + + //combine sealed subtypes into the supertype, if all is covered. + //but preserve more specific record types in positions where there are record patterns in the original patterns + //this is particularly for the case where the sealed supertype only has one permitted type, the record + //the base type could be used instead of the record otherwise, which would produce less specific missing pattern: + Set sortedCandidates = + partialSortPattern(combinatorialPatterns, basePatterns, combinatorialPatterns); + + //remove unnecessary: + OUTER: for (Iterator it = sortedCandidates.iterator(); it.hasNext(); ) { + PatternDescription current = it.next(); + Set reducedAdded = new HashSet<>(sortedCandidates); + + reducedAdded.remove(current); + + Set combinedPatterns = + joinSets(basePatterns, replace(inMissingPatterns, bp, reducedAdded)); + + if (computeCoverage(selectorType, combinedPatterns, true).covered()) { + it.remove(); + } + } + + Set currentMissingPatterns = + replace(inMissingPatterns, toExpand, sortedCandidates); + + for (PatternDescription addedPattern : sortedCandidates) { + if (addedPattern instanceof RecordPattern addedRP) { + for (int c = 0; c < addedRP.nested.length; c++) { + currentMissingPatterns = expandMissingPatternDescriptions(selectorType, + addedRP.fullComponentTypes[c], + addedRP.nested[c], + basePatterns, + currentMissingPatterns); + } + } + } + + return currentMissingPatterns; + } + } + return inMissingPatterns; + } + + /* + * Inside any pattern in {@code in}, in any nesting depth, replace + * pattern {@code what} with patterns {@code to}. + */ + private Set replace(Iterable in, + PatternDescription what, + Collection to) { + Set result = new HashSet<>(); + + for (PatternDescription pd : in) { + Collection replaced = replace(pd, what, to); + if (replaced != null) { + result.addAll(replaced); + } else { + result.add(pd); + } + } + + return result; + } + //where: + //null: no change + private Collection replace(PatternDescription in, + PatternDescription what, + Collection to) { + if (in == what) { + return to; + } else if (in instanceof RecordPattern rp) { + for (int c = 0; c < rp.nested.length; c++) { + Collection replaced = replace(rp.nested[c], what, to); + if (replaced != null) { + Set withReplaced = new HashSet<>(); + + generatePatternsWithReplacedNestedPattern(rp, c, replaced, withReplaced::add); + + return replace(withReplaced, what, to); + } + } + return null; + } else { + return null; //binding patterns have no children + } + } + + /* + * Sort patterns so that those that those that are prefered for removal + * are in front of those that are preferred to remain (when there's a choice). + */ + private SequencedSet partialSortPattern(Set candidates, + Set basePatterns, + Set missingPatterns) { + SequencedSet sortedCandidates = new LinkedHashSet<>(); + + while (!candidates.isEmpty()) { + PatternDescription mostSpecific = null; + for (PatternDescription current : candidates) { + if (mostSpecific == null || + shouldAppearBefore(current, mostSpecific, basePatterns, missingPatterns)) { + mostSpecific = current; + } + } + sortedCandidates.add(mostSpecific); + candidates.remove(mostSpecific); + } + return sortedCandidates; + } + //where: + //true iff pd1 should appear before pd2 + //false otherwise + private boolean shouldAppearBefore(PatternDescription pd1, + PatternDescription pd2, + Set basePatterns, + Set missingPatterns) { + if (pd1 instanceof RecordPattern rp1 && pd2 instanceof RecordPattern rp2) { + for (int c = 0; c < rp1.nested.length; c++) { + if (shouldAppearBefore((BindingPattern) rp1.nested[c], + (BindingPattern) rp2.nested[c], + basePatterns, + missingPatterns)) { + return true; + } + } + } else if (pd1 instanceof BindingPattern bp1 && pd2 instanceof BindingPattern bp2) { + Type t1 = bp1.type(); + Type t2 = bp2.type(); + boolean t1IsImportantRecord = + (t1.tsym.flags_field & RECORD) != 0 && + hasMatchingRecordPattern(basePatterns, missingPatterns, bp1); + boolean t2IsImportantRecord = + (t2.tsym.flags_field & RECORD) != 0 && + hasMatchingRecordPattern(basePatterns, missingPatterns, bp2); + if (t1IsImportantRecord && !t2IsImportantRecord) { + return false; + } + if (!t1IsImportantRecord && t2IsImportantRecord) { + return true; + } + if (!types.isSameType(t1, t2) && types.isSubtype(t1, t2)) { + return true; + } + } + + return false; + } + + /* + * Do the {@code basePatterns} have a record pattern at a place that corresponds to + * position of pattern {@code query} inside {@code missingPatterns}? + */ + private boolean hasMatchingRecordPattern(Set basePatterns, + Set missingPatterns, + PatternDescription query) { + PatternDescription root = findRootContaining(missingPatterns, query); + + if (root == null) { + return false; + } + return basePatternsHaveRecordPatternOnThisSpot(basePatterns, root, query); + } + //where: + private PatternDescription findRootContaining(Set rootPatterns, + PatternDescription added) { + for (PatternDescription pd : rootPatterns) { + if (isUnderRoot(pd, added)) { + return pd; + } + } + + //assert? + return null; + } + + private boolean basePatternsHaveRecordPatternOnThisSpot(Set basePatterns, + PatternDescription rootPattern, + PatternDescription added) { + if (rootPattern == added) { + return basePatterns.stream().anyMatch(pd -> pd instanceof RecordPattern); + } + if (!(rootPattern instanceof RecordPattern rootPatternRecord)) { + return false; + } + int index = -1; + for (int c = 0; c < rootPatternRecord.nested.length; c++) { + if (isUnderRoot(rootPatternRecord.nested[c], added)) { + index = c; + break; + } + } + Assert.check(index != (-1)); + + //TODO: isSameType erasure? + int indexFin = index; + Set filteredBasePatterns = + basePatterns.stream() + .filter(pd -> pd instanceof RecordPattern) + .map(rp -> (RecordPattern) rp) + .filter(rp -> types.isSameType(rp.recordType(), rootPatternRecord.recordType())) + .map(rp -> rp.nested[indexFin]) + .collect(Collectors.toSet()); + + return basePatternsHaveRecordPatternOnThisSpot(filteredBasePatterns, rootPatternRecord.nested[index], added); + } + + private boolean isUnderRoot(PatternDescription root, PatternDescription searchFor) { + if (root == searchFor) { + return true; + } else if (root instanceof RecordPattern rp) { + for (int c = 0; c < rp.nested.length; c++) { + if (isUnderRoot(rp.nested[c], searchFor)) { + return true; + } + } + } + return false; + } + + private Set joinSets(Collection s1, + Collection s2) { + Set result = new HashSet<>(); + + result.addAll(s1); + result.addAll(s2); + + return result; + } + + /* + * Based on {@code basePattern} generate new {@code RecordPattern}s such that all + * components instead of {@code replaceComponent}th component, which is replaced + * with values from {@code updatedNestedPatterns}. Resulting {@code RecordPatterns}s + * are sent to {@code target}. + */ + private void generatePatternsWithReplacedNestedPattern(RecordPattern basePattern, + int replaceComponent, + Iterable updatedNestedPatterns, + Consumer target) { + for (PatternDescription nested : updatedNestedPatterns) { + PatternDescription[] newNested = + Arrays.copyOf(basePattern.nested, basePattern.nested.length); + newNested[replaceComponent] = nested; + target.accept(new RecordPattern(basePattern.recordType(), + basePattern.fullComponentTypes(), + newNested)); + } + } + protected static class TimeoutException extends RuntimeException { private static final long serialVersionUID = 0L; private transient final Set missingPatterns; diff --git a/test/langtools/tools/javac/patterns/ExhaustivenessConvenientErrors.java b/test/langtools/tools/javac/patterns/ExhaustivenessConvenientErrors.java index df04df05046b0..0eadf122ee31f 100644 --- a/test/langtools/tools/javac/patterns/ExhaustivenessConvenientErrors.java +++ b/test/langtools/tools/javac/patterns/ExhaustivenessConvenientErrors.java @@ -394,7 +394,7 @@ record NestedBaseC() implements NestedBase {} } @Test - public void testInfiniteRecursion(Path base) throws Exception { + public void testNoInfiniteRecursion(Path base) throws Exception { doTest(base, new String[0], """ @@ -412,6 +412,68 @@ public record R(R r1, R r2, R r3, Object o) {} "test.R(test.R _, test.R _, test.R(test.R _, test.R _, test.R _, java.lang.Object _), java.lang.Object _)"); } + @Test + public void testEnum(Path base) throws Exception { + doTest(base, + new String[0], + """ + package test; + public class Test { + private int test(I i) { + return switch (i) { + case E.A -> 0; + case C _ -> 1; + }; + } + sealed interface I {} + enum E implements I {A, B} + final class C implements I {} + } + public record R(R r1, R r2, R r3, Object o) {} + """, + "test.Test.E.B"); + doTest(base, + new String[0], + """ + package test; + public class Test { + private int test(I i) { + return switch (i) { + case C _ -> 1; + }; + } + sealed interface I {} + enum E implements I {A, B} + final class C implements I {} + } + public record R(R r1, R r2, R r3, Object o) {} + """, + "test.Test.E _"); + } + + @Test + public void testInstantiateComponentTypes(Path base) throws Exception { + doTest(base, + new String[0], + """ + package test; + public class Test { + private int test(Pair> p) { + return switch (p) { + case Pair(A(A(_)) -> 0; + case Pair(A(B(_)) -> 0; + case Pair(B(A(_)) -> 0; + }; + } + record Pair(T c) {} + sealed interface Base permits A, B {} + record A(T c) implements Base {} + record B(T c) implements Base {} + } + """, + "test.Test.Pair(test.Test.B(test.Test.B _))"); + } + private void doTest(Path base, String[] libraryCode, String testCode, String... expectedMissingPatterns) throws IOException { Path current = base.resolve("."); Path libClasses = current.resolve("libClasses"); From 9066c51353270a57229f1ec3761b2c0f041e2e72 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Tue, 7 Oct 2025 19:03:02 +0200 Subject: [PATCH 16/33] Cleanup. --- .../com/sun/tools/javac/comp/ExhaustivenessComputer.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java index a154929e4ae91..f5eeda60c491e 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java @@ -88,11 +88,12 @@ protected ExhaustivenessComputer(Context context) { Options options = Options.instance(context); String timeout = options.get("exhaustivityTimeout"); long computedTimeout = DEFAULT_TIMEOUT; + if (timeout != null) { try { computedTimeout = Long.parseLong(timeout); - } catch (NumberFormatException ex) { - //TODO: notify? + } catch (NumberFormatException _) { + //ignore invalid values and use the default timeout } } @@ -1104,7 +1105,6 @@ private boolean basePatternsHaveRecordPatternOnThisSpot(Set filteredBasePatterns = basePatterns.stream() From 789f09e7c604264d5cd5a2769a11368a348a6517 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Tue, 7 Oct 2025 19:13:43 +0200 Subject: [PATCH 17/33] Cleanup. --- .../com/sun/tools/javac/comp/ExhaustivenessComputer.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java index f5eeda60c491e..2785f623a26f5 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java @@ -640,7 +640,8 @@ private boolean isBpCovered(Type componentType, PatternDescription newNested) { } protected void checkTimeout() { - if (startTime != (-1) && (System.currentTimeMillis() - startTime) > missingExhaustivenessTimeout) { + if (startTime != (-1) && + (System.currentTimeMillis() - startTime) > missingExhaustivenessTimeout) { throw new TimeoutException(null); } } From 78af109a06432872e7e125910341a7e0b40d2fb5 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Wed, 8 Oct 2025 15:59:19 +0200 Subject: [PATCH 18/33] Fixing tests. --- .../javac/diags/examples/NotExhaustive.java | 3 +- .../diags/examples/NotExhaustiveDetails.java | 33 +++++++++++++++++++ .../examples/NotExhaustiveStatement.java | 3 +- .../NotExhaustiveStatementDetails.java | 33 +++++++++++++++++++ .../PrimitiveInstanceOfComboTest.java | 3 ++ .../PrimitivePatternsSwitchErrors.java | 2 +- .../tools/javac/patterns/SwitchErrors.java | 2 +- .../platform/NonExportedPermittedTypes.java | 6 ++-- .../ExpressionSwitchNotExhaustive.java | 2 +- 9 files changed, 79 insertions(+), 8 deletions(-) create mode 100644 test/langtools/tools/javac/diags/examples/NotExhaustiveDetails.java create mode 100644 test/langtools/tools/javac/diags/examples/NotExhaustiveStatementDetails.java diff --git a/test/langtools/tools/javac/diags/examples/NotExhaustive.java b/test/langtools/tools/javac/diags/examples/NotExhaustive.java index 8d36b0136772e..b5ba932ac6064 100644 --- a/test/langtools/tools/javac/diags/examples/NotExhaustive.java +++ b/test/langtools/tools/javac/diags/examples/NotExhaustive.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -22,6 +22,7 @@ */ // key: compiler.err.not.exhaustive +// options: -XDexhaustivityTimeout=0 class NotExhaustive { int t(int i) { diff --git a/test/langtools/tools/javac/diags/examples/NotExhaustiveDetails.java b/test/langtools/tools/javac/diags/examples/NotExhaustiveDetails.java new file mode 100644 index 0000000000000..463e5e6516b9e --- /dev/null +++ b/test/langtools/tools/javac/diags/examples/NotExhaustiveDetails.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +// key: compiler.err.not.exhaustive.details +// options: -XDexhaustivityTimeout=-1 + +class NotExhaustiveDetails { + int t(int i) { + return switch (i) { + case 0 -> -1; + }; + } +} diff --git a/test/langtools/tools/javac/diags/examples/NotExhaustiveStatement.java b/test/langtools/tools/javac/diags/examples/NotExhaustiveStatement.java index 65a11abb0e67d..9a3da048d7c09 100644 --- a/test/langtools/tools/javac/diags/examples/NotExhaustiveStatement.java +++ b/test/langtools/tools/javac/diags/examples/NotExhaustiveStatement.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -22,6 +22,7 @@ */ // key: compiler.err.not.exhaustive.statement +// options: -XDexhaustivityTimeout=0 class NotExhaustive { void t(Object o) { diff --git a/test/langtools/tools/javac/diags/examples/NotExhaustiveStatementDetails.java b/test/langtools/tools/javac/diags/examples/NotExhaustiveStatementDetails.java new file mode 100644 index 0000000000000..8d7253aa7d79a --- /dev/null +++ b/test/langtools/tools/javac/diags/examples/NotExhaustiveStatementDetails.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +// key: compiler.err.not.exhaustive.statement.details +// options: -XDexhaustivityTimeout=-1 + +class NotExhaustiveDetails { + void t(Object o) { + switch (o) { + case String s -> System.err.println("String of length: " + s.length()); + }; + } +} diff --git a/test/langtools/tools/javac/patterns/PrimitiveInstanceOfComboTest.java b/test/langtools/tools/javac/patterns/PrimitiveInstanceOfComboTest.java index 82064bd4baf26..a0fe66f1f2cd5 100644 --- a/test/langtools/tools/javac/patterns/PrimitiveInstanceOfComboTest.java +++ b/test/langtools/tools/javac/patterns/PrimitiveInstanceOfComboTest.java @@ -103,16 +103,19 @@ protected void doWork() throws Throwable { ComboTask task1 = newCompilationTask() .withSourceFromTemplate(test1.replace("#{TYPE1}", type1.code).replace("#{TYPE2}", type2.code)) .withOption("--enable-preview") + .withOption("-XDexhaustivityTimeout=0") .withOption("-source").withOption(JAVA_VERSION); ComboTask task2 = newCompilationTask() .withSourceFromTemplate(test2.replace("#{TYPE1}", type1.code).replace("#{TYPE2}", type2.code)) .withOption("--enable-preview") + .withOption("-XDexhaustivityTimeout=0") .withOption("-source").withOption(JAVA_VERSION); ComboTask task3 = newCompilationTask() .withSourceFromTemplate(test3.replace("#{TYPE1}", type1.code).replace("#{TYPE2}", type2.code)) .withOption("--enable-preview") + .withOption("-XDexhaustivityTimeout=0") .withOption("-source").withOption(JAVA_VERSION); task1.generate(result1 -> { diff --git a/test/langtools/tools/javac/patterns/PrimitivePatternsSwitchErrors.java b/test/langtools/tools/javac/patterns/PrimitivePatternsSwitchErrors.java index 3d47c4ac9bc72..68a6e845cba32 100644 --- a/test/langtools/tools/javac/patterns/PrimitivePatternsSwitchErrors.java +++ b/test/langtools/tools/javac/patterns/PrimitivePatternsSwitchErrors.java @@ -3,7 +3,7 @@ * @bug 8304487 8325653 8332463 * @summary Compiler Implementation for Primitive types in patterns, instanceof, and switch (Preview) * @enablePreview - * @compile/fail/ref=PrimitivePatternsSwitchErrors.out -XDrawDiagnostics -XDshould-stop.at=FLOW PrimitivePatternsSwitchErrors.java + * @compile/fail/ref=PrimitivePatternsSwitchErrors.out -XDrawDiagnostics -XDshould-stop.at=FLOW -XDexhaustivityTimeout=0 PrimitivePatternsSwitchErrors.java */ public class PrimitivePatternsSwitchErrors { record R_int(int x) {} diff --git a/test/langtools/tools/javac/patterns/SwitchErrors.java b/test/langtools/tools/javac/patterns/SwitchErrors.java index 607052be583ef..0594ab1d82594 100644 --- a/test/langtools/tools/javac/patterns/SwitchErrors.java +++ b/test/langtools/tools/javac/patterns/SwitchErrors.java @@ -2,7 +2,7 @@ * @test /nodynamiccopyright/ * @bug 8262891 8269146 8269113 8348928 * @summary Verify errors related to pattern switches. - * @compile/fail/ref=SwitchErrors.out -XDrawDiagnostics -XDshould-stop.at=FLOW SwitchErrors.java + * @compile/fail/ref=SwitchErrors.out -XDrawDiagnostics -XDshould-stop.at=FLOW -XDexhaustivityTimeout=0 SwitchErrors.java */ public class SwitchErrors { diff --git a/test/langtools/tools/javac/platform/NonExportedPermittedTypes.java b/test/langtools/tools/javac/platform/NonExportedPermittedTypes.java index bdafef7e39adb..0343490359b51 100644 --- a/test/langtools/tools/javac/platform/NonExportedPermittedTypes.java +++ b/test/langtools/tools/javac/platform/NonExportedPermittedTypes.java @@ -26,9 +26,9 @@ * @bug 8318913 * @summary Verify no error is when compiling a class whose permitted types are not exported * @modules jdk.compiler - * @compile/fail/ref=NonExportedPermittedTypes.out -XDrawDiagnostics NonExportedPermittedTypes.java - * @compile/fail/ref=NonExportedPermittedTypes.out --release 21 -XDrawDiagnostics NonExportedPermittedTypes.java - * @compile/fail/ref=NonExportedPermittedTypes.out --release ${jdk.version} -XDrawDiagnostics NonExportedPermittedTypes.java + * @compile/fail/ref=NonExportedPermittedTypes.out -XDrawDiagnostics -XDexhaustivityTimeout=0 NonExportedPermittedTypes.java + * @compile/fail/ref=NonExportedPermittedTypes.out --release 21 -XDrawDiagnostics -XDexhaustivityTimeout=0 NonExportedPermittedTypes.java + * @compile/fail/ref=NonExportedPermittedTypes.out --release ${jdk.version} -XDrawDiagnostics -XDexhaustivityTimeout=0 NonExportedPermittedTypes.java */ diff --git a/test/langtools/tools/javac/switchexpr/ExpressionSwitchNotExhaustive.java b/test/langtools/tools/javac/switchexpr/ExpressionSwitchNotExhaustive.java index 802e66570efba..a301f715a9038 100644 --- a/test/langtools/tools/javac/switchexpr/ExpressionSwitchNotExhaustive.java +++ b/test/langtools/tools/javac/switchexpr/ExpressionSwitchNotExhaustive.java @@ -2,7 +2,7 @@ * @test /nodynamiccopyright/ * @bug 8206986 * @summary Verify behavior of not exhaustive switch expressions. - * @compile/fail/ref=ExpressionSwitchNotExhaustive.out -XDrawDiagnostics ExpressionSwitchNotExhaustive.java + * @compile/fail/ref=ExpressionSwitchNotExhaustive.out -XDrawDiagnostics -XDexhaustivityTimeout=0 ExpressionSwitchNotExhaustive.java */ public class ExpressionSwitchNotExhaustive { From 38089d183fc2a5b82fadbd17e39dc6cc3eec62f1 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Thu, 9 Oct 2025 17:35:21 +0200 Subject: [PATCH 19/33] Better visualisation. --- .../com/sun/tools/javac/comp/Flow.java | 16 ++++++++++++-- .../tools/javac/resources/compiler.properties | 8 +++++-- .../ExhaustivenessConvenientErrors.java | 21 ++++++++++++------- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java index dd3df7ca8a73d..7a465ea59e685 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java @@ -709,7 +709,9 @@ public void visitSwitch(JCSwitch tree) { if (exhaustivenessResult.notExhaustiveDetails().isEmpty()) { log.error(tree, Errors.NotExhaustiveStatement); } else { - log.error(tree, Errors.NotExhaustiveStatementDetails(exhaustivenessResult.notExhaustiveDetails().stream().collect(Collectors.joining("\n")))); + List details = + convertNotExhaustiveDetails(exhaustivenessResult); + log.error(tree, Errors.NotExhaustiveStatementDetails(details)); } } } @@ -756,7 +758,9 @@ public void visitSwitchExpression(JCSwitchExpression tree) { if (exhaustivenessResult.notExhaustiveDetails().isEmpty()) { log.error(tree, Errors.NotExhaustive); } else { - log.error(tree, Errors.NotExhaustiveDetails(exhaustivenessResult.notExhaustiveDetails().stream().collect(Collectors.joining("\n")))); + List details = + convertNotExhaustiveDetails(exhaustivenessResult); + log.error(tree, Errors.NotExhaustiveDetails(details)); } } } @@ -765,6 +769,14 @@ public void visitSwitchExpression(JCSwitchExpression tree) { alive = alive.or(resolveYields(tree, prevPendingExits)); } + private List convertNotExhaustiveDetails(ExhaustivenessResult exhaustivenessResult) { + return exhaustivenessResult.notExhaustiveDetails() + .stream() + .sorted() + .map(detail -> diags.fragment(Fragments.NotExhaustiveDetail(detail))) + .collect(List.collector()); + } + public void visitTry(JCTry tree) { ListBuffer prevPendingExits = pendingExits; pendingExits = new ListBuffer<>(); diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/compiler.properties b/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/compiler.properties index 56f67bda300cc..64f77e4475683 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/compiler.properties +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/compiler.properties @@ -1473,16 +1473,20 @@ compiler.err.not.exhaustive=\ compiler.err.not.exhaustive.statement=\ the switch statement does not cover all possible input values -# 0: string +# 0: list of diagnostic compiler.err.not.exhaustive.details=\ the switch expression does not cover all possible input values\n\ missing patterns: {0} -# 0: string +# 0: list of diagnostic compiler.err.not.exhaustive.statement.details=\ the switch statement does not cover all possible input values\n\ missing patterns: {0} +# 0: string +compiler.misc.not.exhaustive.detail=\ + \n{0} + compiler.err.initializer.must.be.able.to.complete.normally=\ initializer must be able to complete normally diff --git a/test/langtools/tools/javac/patterns/ExhaustivenessConvenientErrors.java b/test/langtools/tools/javac/patterns/ExhaustivenessConvenientErrors.java index 0eadf122ee31f..81b1af2bfd293 100644 --- a/test/langtools/tools/javac/patterns/ExhaustivenessConvenientErrors.java +++ b/test/langtools/tools/javac/patterns/ExhaustivenessConvenientErrors.java @@ -35,10 +35,12 @@ import com.sun.tools.javac.api.ClientCodeWrapper.DiagnosticSourceUnwrapper; import com.sun.tools.javac.util.JCDiagnostic; +import com.sun.tools.javac.util.JCDiagnostic.Fragment; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -365,13 +367,13 @@ private int test(Triple p) { case Triple(B _, _, _) -> 0; case Triple(_, A _, _) -> 0; case Triple(_, _, A _) -> 0; -// case Triple(A p, C(Nested _, NestedBaseA _), _) -> 0; - case Triple(A p, C(Nested _, NestedBaseB _), C(Nested _, NestedBaseA _)) -> 0; - case Triple(A p, C(Nested _, NestedBaseB _), C(Nested _, NestedBaseB _)) -> 0; - case Triple(A p, C(Nested _, NestedBaseB _), C(Nested _, NestedBaseC _)) -> 0; - case Triple(A p, C(Nested _, NestedBaseC _), C(Nested _, NestedBaseA _)) -> 0; - case Triple(A p, C(Nested _, NestedBaseC _), C(Nested _, NestedBaseB _)) -> 0; -// case Path(A p, C(Nested _, NestedBaseC _), C(Nested _, NestedBaseC _)) -> 0; +// case Triple(A _, C(Nested _, NestedBaseA _), _) -> 0; + case Triple(A _, C(Nested _, NestedBaseB _), C(Nested _, NestedBaseA _)) -> 0; + case Triple(A _, C(Nested _, NestedBaseB _), C(Nested _, NestedBaseB _)) -> 0; + case Triple(A _, C(Nested _, NestedBaseB _), C(Nested _, NestedBaseC _)) -> 0; + case Triple(A _, C(Nested _, NestedBaseC _), C(Nested _, NestedBaseA _)) -> 0; + case Triple(A _, C(Nested _, NestedBaseC _), C(Nested _, NestedBaseB _)) -> 0; +// case Path(A _, C(Nested _, NestedBaseC _), C(Nested _, NestedBaseC _)) -> 0; }; } record Triple(Base c1, Base c2, Base c3) {} @@ -517,7 +519,10 @@ private void doTest(Path base, String[] libraryCode, String testCode, String... d = uw.d; } if (d instanceof JCDiagnostic diag) { - missingPatterns.addAll(List.of(((String) diag.getArgs()[0]).split("\n"))); + ((Collection) diag.getArgs()[0]) + .stream() + .map(fragment -> (String) fragment.getArgs()[0]) + .forEach(missingPatterns::add); } } }) From 026dc4c813bd0b26defe54989ea0929e6d9eb511 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Mon, 13 Oct 2025 13:56:09 +0200 Subject: [PATCH 20/33] Adding tests with generic records. --- .../tools/javac/patterns/Exhaustiveness.java | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/test/langtools/tools/javac/patterns/Exhaustiveness.java b/test/langtools/tools/javac/patterns/Exhaustiveness.java index 5a252c7569542..7c521c32e76c4 100644 --- a/test/langtools/tools/javac/patterns/Exhaustiveness.java +++ b/test/langtools/tools/javac/patterns/Exhaustiveness.java @@ -2219,6 +2219,40 @@ record R2(Base b1, Base b2) implements Base {} record Root(Base b1, Base b2, Base b3) {} } """); + doTest(base, + new String[0], + """ + package test; + public class Test { + private int test(Root r) { + return switch (r) { + case Root(R1 _, _, _) -> 0; + case Root(R2 _, R1 _, _) -> 0; + case Root(R2 _, R2 _, R1 _) -> 0; + case Root(R2 _, R2(R1 _, R1 _), R2(R1 _, R1 _)) -> 0; + case Root(R2 _, R2(R1 _, R1 _), R2(R1 _, R2 _)) -> 0; + case Root(R2 _, R2(R1 _, R1 _), R2(R2 _, R1 _)) -> 0; + case Root(R2 _, R2(R1 _, R1 _), R2(R2 _, R2 _)) -> 0; + case Root(R2 _, R2(R1 _, R2 _), R2(R1 _, R1 _)) -> 0; + case Root(R2 _, R2(R1 _, R2 _), R2(R1 _, R2 _)) -> 0; + case Root(R2 _, R2(R1 _, R2 _), R2(R2 _, R1 _)) -> 0; + case Root(R2 _, R2(R1 _, R2 _), R2(R2 _, R2 _)) -> 0; + case Root(R2 _, R2(R2 _, R1 _), R2(R1 _, R1 _)) -> 0; + case Root(R2 _, R2(R2 _, R1 _), R2(R1 _, R2 _)) -> 0; + case Root(R2 _, R2(R2 _, R1 _), R2(R2 _, R1 _)) -> 0; + case Root(R2 _, R2(R2 _, R1 _), R2(R2 _, R2 _)) -> 0; + case Root(R2 _, R2(R2 _, R2 _), R2(R1 _, R1 _)) -> 0; + case Root(R2 _, R2(R2 _, R2 _), R2(R1 _, R2 _)) -> 0; + case Root(R2 _, R2(R2 _, R2 _), R2(R2 _, R1 _)) -> 0; + case Root(R2 _, R2(R2 _, R2 _), R2 _) -> 0; //functionally equivalent to: Root(R2 _, R2(R2 _, R2 _), R2(R2 _, R2 _)) + }; + } + sealed interface Base {} + record R1() implements Base {} + record R2(Base b1, Base b2) implements Base {} + record Root(T b1, T b2, T b3) {} + } + """); } @Test //JDK-8364991 @@ -2250,6 +2284,33 @@ record R2(Base b1) implements Base {} record Root(R2 b2, R2 b3) {} } """); + doTest(base, + new String[0], + """ + package test; + public class Test { + private int test1(Root> r) { + return switch (r) { + case Root(R2(R1 _), R2(R1 _)) -> 0; + case Root(R2(R1 _), R2(R2 _)) -> 0; + case Root(R2(R2 _), R2(R1 _)) -> 0; + case Root(R2(R2 _), R2 _) -> 0; + }; + } + private int test2(Root> r) { + return switch (r) { + case Root(R2(R1 _), R2(R1 _)) -> 0; + case Root(R2(R2 _), R2(R1 _)) -> 0; + case Root(R2(R1 _), R2(R2 _)) -> 0; + case Root(R2 _, R2(R2 _)) -> 0; + }; + } + sealed interface Base {} + record R1() implements Base {} + record R2(T b1) implements Base {} + record Root(T b2, T b3) {} + } + """); } @Test //JDK-8364991 @@ -2274,6 +2335,26 @@ record Root(R2 b2, R2 b3) {} """, "Test.java:4:16: compiler.err.not.exhaustive", "1 error"); + doTest(base, + new String[0], + """ + package test; + public class Test { + private int test(Root> r) { + return switch (r) { + case Root(R2 _, R2(R1 _)) -> 0; + case Root(R2(R1 _), R2(R2 _)) -> 0; + case Root(R2(R2 _), R2 _) -> 0; + }; + } + sealed interface Base {} + record R1() implements Base {} + record R2(T b) implements Base {} + record Root(T b2, T b3) {} + } + """, + "Test.java:4:16: compiler.err.not.exhaustive", + "1 error"); } private void doTest(Path base, String[] libraryCode, String testCode, String... expectedErrors) throws IOException { From 9138ef6bbba63787796bb964b29d273a3d6c76b9 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Wed, 15 Oct 2025 12:23:42 +0200 Subject: [PATCH 21/33] Factoring out the 'substitutable' check, as suggested. --- .../com/sun/tools/javac/comp/Flow.java | 103 +++++++++++------- 1 file changed, 61 insertions(+), 42 deletions(-) diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java index 743ff9a924ff7..73fcca145e0aa 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java @@ -59,6 +59,7 @@ import com.sun.tools.javac.resources.CompilerProperties.Fragments; import static com.sun.tools.javac.tree.JCTree.Tag.*; import com.sun.tools.javac.util.JCDiagnostic.Fragment; +import java.util.ArrayList; import java.util.Arrays; import java.util.IdentityHashMap; import java.util.Iterator; @@ -894,7 +895,6 @@ private Set reduceBindingPatterns(Type selectorType, Set bindings = new ListBuffer<>(); //do not reduce to types unrelated to the selector type: Type clazzErasure = types.erasure(clazz.type); if (components(selectorType).stream() @@ -1054,52 +1054,15 @@ private Set reduceNestedPatterns(Set pat join.append(rpOne); - NEXT_PATTERN: for (int nextCandidate = 0; - nextCandidate < candidatesArr.length; - nextCandidate++) { + for (int nextCandidate = 0; nextCandidate < candidatesArr.length; nextCandidate++) { if (firstCandidate == nextCandidate) { continue; } RecordPattern rpOther = candidatesArr[nextCandidate]; - if (rpOne.recordType.tsym == rpOther.recordType.tsym) { - ACCEPT: for (int i = 0; i < rpOne.nested.length; i++) { - if (i != mismatchingCandidate) { - if (!rpOne.nested[i].equals(rpOther.nested[i])) { - if (useHashes) { - continue NEXT_PATTERN; - } - //when not using hashes, - //check if rpOne.nested[i] is - //a subtype of rpOther.nested[i]: - if (!(rpOther.nested[i] instanceof BindingPattern bpOther)) { - continue NEXT_PATTERN; - } - if (rpOne.nested[i] instanceof BindingPattern bpOne) { - if (!types.isSubtype(types.erasure(bpOne.type), types.erasure(bpOther.type))) { - continue NEXT_PATTERN; - } - } else if (rpOne.nested[i] instanceof RecordPattern nestedRPOne) { - Set pendingReplacedPatterns = new HashSet<>(replaces.getOrDefault(rpOther.nested[i], Set.of())); - - while (!pendingReplacedPatterns.isEmpty()) { - PatternDescription currentReplaced = pendingReplacedPatterns.iterator().next(); - - pendingReplacedPatterns.remove(currentReplaced); - - if (nestedRPOne.equals(currentReplaced)) { - continue ACCEPT; - } - - pendingReplacedPatterns.addAll(replaces.getOrDefault(currentReplaced, Set.of())); - } - continue NEXT_PATTERN; - } else { - continue NEXT_PATTERN; - } - } - } - } + + if (rpOne.recordType.tsym == rpOther.recordType.tsym && + nestedComponentsEquivalent(rpOne, rpOther, mismatchingCandidate, replaces, useHashes)) { join.append(rpOther); } } @@ -1141,6 +1104,62 @@ private Set reduceNestedPatterns(Set pat return patterns; } + /* Returns true if all nested components of existing and candidate are + * equivalent (if useHashes == true), or "substitutable" (if useHashes == false). + * A candidate pattern is "substitutable" if it is a binding pattern, and: + * - it's type is a supertype of the existing pattern's type + * - it was produced by a reduction from a record pattern that is equivalent to + * the existing pattern + */ + private boolean nestedComponentsEquivalent(RecordPattern existing, + RecordPattern candidate, + int mismatchingCandidate, + Map> replaces, + boolean useHashes) { + NEXT_NESTED: + for (int i = 0; i < existing.nested.length; i++) { + if (i != mismatchingCandidate) { + if (!existing.nested[i].equals(candidate.nested[i])) { + if (useHashes) { + return false; + } + //when not using hashes, + //check if rpOne.nested[i] is + //a subtype of rpOther.nested[i]: + if (!(candidate.nested[i] instanceof BindingPattern nestedCandidate)) { + return false; + } + if (existing.nested[i] instanceof BindingPattern nestedExisting) { + if (!types.isSubtype(types.erasure(nestedExisting.type), types.erasure(nestedCandidate.type))) { + return false; + } + } else if (existing.nested[i] instanceof RecordPattern nestedExisting) { + java.util.List pendingReplacedPatterns = + new ArrayList<>(replaces.getOrDefault(nestedCandidate, Set.of())); + + while (!pendingReplacedPatterns.isEmpty()) { + PatternDescription currentReplaced = pendingReplacedPatterns.removeLast(); + + if (nestedExisting.equals(currentReplaced)) { + //candidate.nested[i] is substitutable for existing.nested[i] + //continue with the next nested pattern: + continue NEXT_NESTED; + } + + pendingReplacedPatterns.addAll(replaces.getOrDefault(currentReplaced, Set.of())); + } + + return false; + } else { + return false; + } + } + } + } + + return true; + } + /* In the set of patterns, find those for which, given: * $record($nested1, $nested2, ...) * all the $nestedX pattern cover the given record component, From 51b7fc283e88ac5947cfebfce6609704c360e235 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Wed, 15 Oct 2025 12:37:09 +0200 Subject: [PATCH 22/33] Adding explanation to the replaces map. --- .../share/classes/com/sun/tools/javac/comp/Flow.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java index 73fcca145e0aa..ed532149dd5ee 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java @@ -803,6 +803,12 @@ private boolean exhausts(JCExpression selector, List cases) { } } Set patterns = patternSet; + //A backtracking record of the original patterns (in value) that + //were used to produce the pattern in the key. + //Note that there may be multiple equivalent patterns in the key + //that originate in different sets of different original patterns, + //hence using identity map, to get the exact source patterns + //on which the resulting pattern is based: Map> replaces = new IdentityHashMap<>(); Set> seenFallback = new HashSet<>(); boolean useHashes = true; From 11ee4dfaa7ddef03ecdb410df5cf0f39857b12c9 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Wed, 15 Oct 2025 12:50:42 +0200 Subject: [PATCH 23/33] Caching isSubtype, as suggested. --- .../com/sun/tools/javac/comp/Flow.java | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java index ed532149dd5ee..39f9b88a0b6d7 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java @@ -760,6 +760,8 @@ public void visitSwitchExpression(JCSwitchExpression tree) { alive = alive.or(resolveYields(tree, prevPendingExits)); } + private final Map, Boolean> isSubtypeCache = new HashMap<>(); + private boolean exhausts(JCExpression selector, List cases) { Set patternSet = new HashSet<>(); Map> enum2Constants = new HashMap<>(); @@ -845,6 +847,8 @@ private boolean exhausts(JCExpression selector, List cases) { } catch (CompletionFailure cf) { chk.completionError(selector.pos(), cf); return true; //error recovery + } finally { + isSubtypeCache.clear(); } } @@ -902,10 +906,9 @@ private Set reduceBindingPatterns(Type selectorType, Set types.isSubtype(clazzErasure, c))) { + .noneMatch(c -> isSubtypeErasure(clazzType, c))) { continue; } @@ -929,14 +932,14 @@ private Set reduceBindingPatterns(Type selectorType, Set key = Pair.of(t, s); + + return isSubtypeCache.computeIfAbsent(key, _ -> + types.isSubtype(types.erasure(t), types.erasure(s))); + } + /* In the set of patterns, find those for which, given: * $record($nested1, $nested2, ...) * all the $nestedX pattern cover the given record component, From 25d1b956ebd607170463c24a62ecb6710b4f66ce Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Fri, 17 Oct 2025 13:38:55 +0200 Subject: [PATCH 24/33] Avoiding the use of IdentityHashMap. --- .../com/sun/tools/javac/comp/Flow.java | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java index 39f9b88a0b6d7..26aa661c4582f 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java @@ -805,13 +805,6 @@ private boolean exhausts(JCExpression selector, List cases) { } } Set patterns = patternSet; - //A backtracking record of the original patterns (in value) that - //were used to produce the pattern in the key. - //Note that there may be multiple equivalent patterns in the key - //that originate in different sets of different original patterns, - //hence using identity map, to get the exact source patterns - //on which the resulting pattern is based: - Map> replaces = new IdentityHashMap<>(); Set> seenFallback = new HashSet<>(); boolean useHashes = true; try { @@ -819,8 +812,8 @@ private boolean exhausts(JCExpression selector, List cases) { while (repeat) { Set updatedPatterns; updatedPatterns = reduceBindingPatterns(selector.type, patterns); - updatedPatterns = reduceNestedPatterns(updatedPatterns, replaces, useHashes); - updatedPatterns = reduceRecordPatterns(updatedPatterns, replaces); + updatedPatterns = reduceNestedPatterns(updatedPatterns, useHashes); + updatedPatterns = reduceRecordPatterns(updatedPatterns); updatedPatterns = removeCoveredRecordPatterns(updatedPatterns); repeat = !updatedPatterns.equals(patterns); if (checkCovered(selector.type, patterns)) { @@ -1020,7 +1013,6 @@ private List baseClasses(TypeSymbol root) { * as pattern hashes cannot be used to speed up the matching process */ private Set reduceNestedPatterns(Set patterns, - Map> replaces, boolean useHashes) { /* implementation note: * finding a sub-set of patterns that only differ in a single @@ -1071,15 +1063,15 @@ private Set reduceNestedPatterns(Set pat RecordPattern rpOther = candidatesArr[nextCandidate]; if (rpOne.recordType.tsym == rpOther.recordType.tsym && - nestedComponentsEquivalent(rpOne, rpOther, mismatchingCandidate, replaces, useHashes)) { + nestedComponentsEquivalent(rpOne, rpOther, mismatchingCandidate, useHashes)) { join.append(rpOther); } } var nestedPatterns = join.stream().map(rp -> rp.nested[mismatchingCandidateFin]).collect(Collectors.toSet()); - var updatedPatterns = reduceNestedPatterns(nestedPatterns, replaces, useHashes); + var updatedPatterns = reduceNestedPatterns(nestedPatterns, useHashes); - updatedPatterns = reduceRecordPatterns(updatedPatterns, replaces); + updatedPatterns = reduceRecordPatterns(updatedPatterns); updatedPatterns = removeCoveredRecordPatterns(updatedPatterns); updatedPatterns = reduceBindingPatterns(rpOne.fullComponentTypes()[mismatchingCandidateFin], updatedPatterns); @@ -1094,9 +1086,9 @@ private Set reduceNestedPatterns(Set pat newNested[mismatchingCandidateFin] = nested; RecordPattern nue = new RecordPattern(rpOne.recordType(), rpOne.fullComponentTypes(), - newNested); + newNested, + new HashSet<>(join)); current.add(nue); - replaces.put(nue, new HashSet<>(join)); } } } @@ -1123,7 +1115,6 @@ private Set reduceNestedPatterns(Set pat private boolean nestedComponentsEquivalent(RecordPattern existing, RecordPattern candidate, int mismatchingCandidate, - Map> replaces, boolean useHashes) { NEXT_NESTED: for (int i = 0; i < existing.nested.length; i++) { @@ -1144,7 +1135,7 @@ private boolean nestedComponentsEquivalent(RecordPattern existing, } } else if (existing.nested[i] instanceof RecordPattern nestedExisting) { java.util.List pendingReplacedPatterns = - new ArrayList<>(replaces.getOrDefault(nestedCandidate, Set.of())); + new ArrayList<>(nestedCandidate.sourcePatterns()); while (!pendingReplacedPatterns.isEmpty()) { PatternDescription currentReplaced = pendingReplacedPatterns.removeLast(); @@ -1155,7 +1146,7 @@ private boolean nestedComponentsEquivalent(RecordPattern existing, continue NEXT_NESTED; } - pendingReplacedPatterns.addAll(replaces.getOrDefault(currentReplaced, Set.of())); + pendingReplacedPatterns.addAll(currentReplaced.sourcePatterns()); } return false; @@ -1183,12 +1174,12 @@ private boolean isSubtypeErasure(Type t, Type s) { * all the $nestedX pattern cover the given record component, * and replace those with a simple binding pattern over $record. */ - private Set reduceRecordPatterns(Set patterns, Map> replaces) { + private Set reduceRecordPatterns(Set patterns) { var newPatterns = new HashSet(); boolean modified = false; for (PatternDescription pd : patterns) { if (pd instanceof RecordPattern rpOne) { - PatternDescription reducedPattern = reduceRecordPattern(rpOne, replaces); + PatternDescription reducedPattern = reduceRecordPattern(rpOne); if (reducedPattern != rpOne) { newPatterns.add(reducedPattern); modified = true; @@ -1200,7 +1191,7 @@ private Set reduceRecordPatterns(Set pat return modified ? newPatterns : patterns; } - private PatternDescription reduceRecordPattern(PatternDescription pattern, Map> replaces) { + private PatternDescription reduceRecordPattern(PatternDescription pattern) { if (pattern instanceof RecordPattern rpOne) { Type[] componentType = rpOne.fullComponentTypes(); //error recovery, ignore patterns with incorrect number of nested patterns: @@ -1210,7 +1201,7 @@ private PatternDescription reduceRecordPattern(PatternDescription pattern, Map

sourcePatterns(); + } public PatternDescription makePatternDescription(Type selectorType, JCPattern pattern) { if (pattern instanceof JCBindingPattern binding) { Type type = !selectorType.isPrimitive() && types.isSubtype(selectorType, binding.type) @@ -3577,7 +3568,12 @@ public PatternDescription makePatternDescription(Type selectorType, JCPattern pa throw Assert.error(); } } - record BindingPattern(Type type) implements PatternDescription { + record BindingPattern(Type type, Set sourcePatterns) implements PatternDescription { + + public BindingPattern(Type type) { + this(type, Set.of()); + } + @Override public int hashCode() { return type.tsym.hashCode(); @@ -3592,10 +3588,14 @@ public String toString() { return type.tsym + " _"; } } - record RecordPattern(Type recordType, int _hashCode, Type[] fullComponentTypes, PatternDescription... nested) implements PatternDescription { + record RecordPattern(Type recordType, int _hashCode, Type[] fullComponentTypes, PatternDescription[] nested, Set sourcePatterns) implements PatternDescription { public RecordPattern(Type recordType, Type[] fullComponentTypes, PatternDescription[] nested) { - this(recordType, hashCode(-1, recordType, nested), fullComponentTypes, nested); + this(recordType, fullComponentTypes, nested, Set.of()); + } + + public RecordPattern(Type recordType, Type[] fullComponentTypes, PatternDescription[] nested, Set sourcePatterns) { + this(recordType, hashCode(-1, recordType, nested), fullComponentTypes, nested, sourcePatterns); } @Override From 998e08ccd6a661a4f0ef8e32543d8d435188c86d Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Fri, 17 Oct 2025 14:13:49 +0200 Subject: [PATCH 25/33] 8367499: Refactor exhaustiveness computation from Flow into a separate class --- .../javac/comp/ExhaustivenessComputer.java | 605 ++++++++++++++++++ .../com/sun/tools/javac/comp/Flow.java | 545 +--------------- 2 files changed, 609 insertions(+), 541 deletions(-) create mode 100644 src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java new file mode 100644 index 0000000000000..77adbcbb07348 --- /dev/null +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java @@ -0,0 +1,605 @@ +/* + * Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.sun.tools.javac.comp; + +import java.util.Map; +import java.util.Map.Entry; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; +import com.sun.tools.javac.code.*; +import com.sun.tools.javac.tree.*; +import com.sun.tools.javac.util.*; + +import com.sun.tools.javac.code.Symbol.*; +import com.sun.tools.javac.tree.JCTree.*; + +import com.sun.tools.javac.code.Kinds.Kind; +import com.sun.tools.javac.code.Type.TypeVar; +import java.util.Arrays; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static java.util.stream.Collectors.groupingBy; + +/** A class to compute exhaustiveness of set of switch cases. + * + *

This is NOT part of any supported API. + * If you write code that depends on this, you do so at your own risk. + * This code and its internal interfaces are subject to change or + * deletion without notice. + */ +public class ExhaustivenessComputer { + protected static final Context.Key exhaustivenessKey = new Context.Key<>(); + + private final Symtab syms; + private final Types types; + private final Check chk; + private final Infer infer; + + public static ExhaustivenessComputer instance(Context context) { + ExhaustivenessComputer instance = context.get(exhaustivenessKey); + if (instance == null) + instance = new ExhaustivenessComputer(context); + return instance; + } + + @SuppressWarnings("this-escape") + protected ExhaustivenessComputer(Context context) { + context.put(exhaustivenessKey, this); + syms = Symtab.instance(context); + types = Types.instance(context); + chk = Check.instance(context); + infer = Infer.instance(context); + } + + public boolean exhausts(JCExpression selector, List cases) { + Set patternSet = new HashSet<>(); + Map> enum2Constants = new HashMap<>(); + Set booleanLiterals = new HashSet<>(Set.of(0, 1)); + for (JCCase c : cases) { + if (!TreeInfo.unguardedCase(c)) + continue; + + for (var l : c.labels) { + if (l instanceof JCPatternCaseLabel patternLabel) { + for (Type component : components(selector.type)) { + patternSet.add(makePatternDescription(component, patternLabel.pat)); + } + } else if (l instanceof JCConstantCaseLabel constantLabel) { + if (types.unboxedTypeOrType(selector.type).hasTag(TypeTag.BOOLEAN)) { + Object value = ((JCLiteral) constantLabel.expr).value; + booleanLiterals.remove(value); + } else { + Symbol s = TreeInfo.symbol(constantLabel.expr); + if (s != null && s.isEnum()) { + enum2Constants.computeIfAbsent(s.owner, x -> { + Set result = new HashSet<>(); + s.owner.members() + .getSymbols(sym -> sym.kind == Kind.VAR && sym.isEnum()) + .forEach(result::add); + return result; + }).remove(s); + } + } + } + } + } + + if (types.unboxedTypeOrType(selector.type).hasTag(TypeTag.BOOLEAN) && booleanLiterals.isEmpty()) { + return true; + } + + for (Entry> e : enum2Constants.entrySet()) { + if (e.getValue().isEmpty()) { + patternSet.add(new BindingPattern(e.getKey().type)); + } + } + Set patterns = patternSet; + boolean useHashes = true; + try { + boolean repeat = true; + while (repeat) { + Set updatedPatterns; + updatedPatterns = reduceBindingPatterns(selector.type, patterns); + updatedPatterns = reduceNestedPatterns(updatedPatterns, useHashes); + updatedPatterns = reduceRecordPatterns(updatedPatterns); + updatedPatterns = removeCoveredRecordPatterns(updatedPatterns); + repeat = !updatedPatterns.equals(patterns); + if (checkCovered(selector.type, patterns)) { + return true; + } + if (!repeat) { + //there may be situation like: + //class B permits S1, S2 + //patterns: R(S1, B), R(S2, S2) + //this might be joined to R(B, S2), as B could be rewritten to S2 + //but hashing in reduceNestedPatterns will not allow that + //disable the use of hashing, and use subtyping in + //reduceNestedPatterns to handle situations like this: + repeat = useHashes; + useHashes = false; + } else { + //if a reduction happened, make sure hashing in reduceNestedPatterns + //is enabled, as the hashing speeds up the process significantly: + useHashes = true; + } + patterns = updatedPatterns; + } + return checkCovered(selector.type, patterns); + } catch (CompletionFailure cf) { + chk.completionError(selector.pos(), cf); + return true; //error recovery + } + } + + private boolean checkCovered(Type seltype, Iterable patterns) { + for (Type seltypeComponent : components(seltype)) { + for (PatternDescription pd : patterns) { + if(isBpCovered(seltypeComponent, pd)) { + return true; + } + } + } + return false; + } + + private List components(Type seltype) { + return switch (seltype.getTag()) { + case CLASS -> { + if (seltype.isCompound()) { + if (seltype.isIntersection()) { + yield ((Type.IntersectionClassType) seltype).getComponents() + .stream() + .flatMap(t -> components(t).stream()) + .collect(List.collector()); + } + yield List.nil(); + } + yield List.of(types.erasure(seltype)); + } + case TYPEVAR -> components(((TypeVar) seltype).getUpperBound()); + default -> List.of(types.erasure(seltype)); + }; + } + + /* In a set of patterns, search for a sub-set of binding patterns that + * in combination exhaust their sealed supertype. If such a sub-set + * is found, it is removed, and replaced with a binding pattern + * for the sealed supertype. + */ + private Set reduceBindingPatterns(Type selectorType, Set patterns) { + Set existingBindings = patterns.stream() + .filter(pd -> pd instanceof BindingPattern) + .map(pd -> ((BindingPattern) pd).type.tsym) + .collect(Collectors.toSet()); + + for (PatternDescription pdOne : patterns) { + if (pdOne instanceof BindingPattern bpOne) { + Set toAdd = new HashSet<>(); + + for (Type sup : types.directSupertypes(bpOne.type)) { + ClassSymbol clazz = (ClassSymbol) types.erasure(sup).tsym; + + clazz.complete(); + + if (clazz.isSealed() && clazz.isAbstract() && + //if a binding pattern for clazz already exists, no need to analyze it again: + !existingBindings.contains(clazz)) { + ListBuffer bindings = new ListBuffer<>(); + //do not reduce to types unrelated to the selector type: + Type clazzErasure = types.erasure(clazz.type); + if (components(selectorType).stream() + .map(types::erasure) + .noneMatch(c -> types.isSubtype(clazzErasure, c))) { + continue; + } + + Set permitted = allPermittedSubTypes(clazz, csym -> { + Type instantiated; + if (csym.type.allparams().isEmpty()) { + instantiated = csym.type; + } else { + instantiated = infer.instantiatePatternType(selectorType, csym); + } + + return instantiated != null && types.isCastable(selectorType, instantiated); + }); + + for (PatternDescription pdOther : patterns) { + if (pdOther instanceof BindingPattern bpOther) { + Set currentPermittedSubTypes = + allPermittedSubTypes(bpOther.type.tsym, s -> true); + + PERMITTED: for (Iterator it = permitted.iterator(); it.hasNext();) { + Symbol perm = it.next(); + + for (Symbol currentPermitted : currentPermittedSubTypes) { + if (types.isSubtype(types.erasure(currentPermitted.type), + types.erasure(perm.type))) { + it.remove(); + continue PERMITTED; + } + } + if (types.isSubtype(types.erasure(perm.type), + types.erasure(bpOther.type))) { + it.remove(); + } + } + } + } + + if (permitted.isEmpty()) { + toAdd.add(new BindingPattern(clazz.type)); + } + } + } + + if (!toAdd.isEmpty()) { + Set newPatterns = new HashSet<>(patterns); + newPatterns.addAll(toAdd); + return newPatterns; + } + } + } + return patterns; + } + + private Set allPermittedSubTypes(TypeSymbol root, Predicate accept) { + Set permitted = new HashSet<>(); + List permittedSubtypesClosure = baseClasses(root); + + while (permittedSubtypesClosure.nonEmpty()) { + ClassSymbol current = permittedSubtypesClosure.head; + + permittedSubtypesClosure = permittedSubtypesClosure.tail; + + current.complete(); + + if (current.isSealed() && current.isAbstract()) { + for (Type t : current.getPermittedSubclasses()) { + ClassSymbol csym = (ClassSymbol) t.tsym; + + if (accept.test(csym)) { + permittedSubtypesClosure = permittedSubtypesClosure.prepend(csym); + permitted.add(csym); + } + } + } + } + + return permitted; + } + + private List baseClasses(TypeSymbol root) { + if (root instanceof ClassSymbol clazz) { + return List.of(clazz); + } else if (root instanceof TypeVariableSymbol tvar) { + ListBuffer result = new ListBuffer<>(); + for (Type bound : tvar.getBounds()) { + result.appendList(baseClasses(bound.tsym)); + } + return result.toList(); + } else { + return List.nil(); + } + } + + /* Among the set of patterns, find sub-set of patterns such: + * $record($prefix$, $nested, $suffix$) + * Where $record, $prefix$ and $suffix$ is the same for each pattern + * in the set, and the patterns only differ in one "column" in + * the $nested pattern. + * Then, the set of $nested patterns is taken, and passed recursively + * to reduceNestedPatterns and to reduceBindingPatterns, to + * simplify the pattern. If that succeeds, the original found sub-set + * of patterns is replaced with a new set of patterns of the form: + * $record($prefix$, $resultOfReduction, $suffix$) + * + * useHashes: when true, patterns will be subject to exact equivalence; + * when false, two binding patterns will be considered equivalent + * if one of them is more generic than the other one; + * when false, the processing will be significantly slower, + * as pattern hashes cannot be used to speed up the matching process + */ + private Set reduceNestedPatterns(Set patterns, + boolean useHashes) { + /* implementation note: + * finding a sub-set of patterns that only differ in a single + * column is time-consuming task, so this method speeds it up by: + * - group the patterns by their record class + * - for each column (nested pattern) do: + * -- group patterns by their hash + * -- in each such by-hash group, find sub-sets that only differ in + * the chosen column, and then call reduceBindingPatterns and reduceNestedPatterns + * on patterns in the chosen column, as described above + */ + var groupByRecordClass = + patterns.stream() + .filter(pd -> pd instanceof RecordPattern) + .map(pd -> (RecordPattern) pd) + .collect(groupingBy(pd -> (ClassSymbol) pd.recordType.tsym)); + + for (var e : groupByRecordClass.entrySet()) { + int nestedPatternsCount = e.getKey().getRecordComponents().size(); + Set current = new HashSet<>(e.getValue()); + + for (int mismatchingCandidate = 0; + mismatchingCandidate < nestedPatternsCount; + mismatchingCandidate++) { + int mismatchingCandidateFin = mismatchingCandidate; + var groupEquivalenceCandidates = + current + .stream() + //error recovery, ignore patterns with incorrect number of nested patterns: + .filter(pd -> pd.nested.length == nestedPatternsCount) + .collect(groupingBy(pd -> useHashes ? pd.hashCode(mismatchingCandidateFin) : 0)); + for (var candidates : groupEquivalenceCandidates.values()) { + var candidatesArr = candidates.toArray(RecordPattern[]::new); + + for (int firstCandidate = 0; + firstCandidate < candidatesArr.length; + firstCandidate++) { + RecordPattern rpOne = candidatesArr[firstCandidate]; + ListBuffer join = new ListBuffer<>(); + + join.append(rpOne); + + NEXT_PATTERN: for (int nextCandidate = 0; + nextCandidate < candidatesArr.length; + nextCandidate++) { + if (firstCandidate == nextCandidate) { + continue; + } + + RecordPattern rpOther = candidatesArr[nextCandidate]; + if (rpOne.recordType.tsym == rpOther.recordType.tsym) { + for (int i = 0; i < rpOne.nested.length; i++) { + if (i != mismatchingCandidate) { + if (!rpOne.nested[i].equals(rpOther.nested[i])) { + if (useHashes || + //when not using hashes, + //check if rpOne.nested[i] is + //a subtype of rpOther.nested[i]: + !(rpOne.nested[i] instanceof BindingPattern bpOne) || + !(rpOther.nested[i] instanceof BindingPattern bpOther) || + !types.isSubtype(types.erasure(bpOne.type), types.erasure(bpOther.type))) { + continue NEXT_PATTERN; + } + } + } + } + join.append(rpOther); + } + } + + var nestedPatterns = join.stream().map(rp -> rp.nested[mismatchingCandidateFin]).collect(Collectors.toSet()); + var updatedPatterns = reduceNestedPatterns(nestedPatterns, useHashes); + + updatedPatterns = reduceRecordPatterns(updatedPatterns); + updatedPatterns = removeCoveredRecordPatterns(updatedPatterns); + updatedPatterns = reduceBindingPatterns(rpOne.fullComponentTypes()[mismatchingCandidateFin], updatedPatterns); + + if (!nestedPatterns.equals(updatedPatterns)) { + if (useHashes) { + current.removeAll(join); + } + + for (PatternDescription nested : updatedPatterns) { + PatternDescription[] newNested = + Arrays.copyOf(rpOne.nested, rpOne.nested.length); + newNested[mismatchingCandidateFin] = nested; + current.add(new RecordPattern(rpOne.recordType(), + rpOne.fullComponentTypes(), + newNested)); + } + } + } + } + } + + if (!current.equals(new HashSet<>(e.getValue()))) { + Set result = new HashSet<>(patterns); + result.removeAll(e.getValue()); + result.addAll(current); + return result; + } + } + return patterns; + } + + /* In the set of patterns, find those for which, given: + * $record($nested1, $nested2, ...) + * all the $nestedX pattern cover the given record component, + * and replace those with a simple binding pattern over $record. + */ + private Set reduceRecordPatterns(Set patterns) { + var newPatterns = new HashSet(); + boolean modified = false; + for (PatternDescription pd : patterns) { + if (pd instanceof RecordPattern rpOne) { + PatternDescription reducedPattern = reduceRecordPattern(rpOne); + if (reducedPattern != rpOne) { + newPatterns.add(reducedPattern); + modified = true; + continue; + } + } + newPatterns.add(pd); + } + return modified ? newPatterns : patterns; + } + + private PatternDescription reduceRecordPattern(PatternDescription pattern) { + if (pattern instanceof RecordPattern rpOne) { + Type[] componentType = rpOne.fullComponentTypes(); + //error recovery, ignore patterns with incorrect number of nested patterns: + if (componentType.length != rpOne.nested.length) { + return pattern; + } + PatternDescription[] reducedNestedPatterns = null; + boolean covered = true; + for (int i = 0; i < componentType.length; i++) { + PatternDescription newNested = reduceRecordPattern(rpOne.nested[i]); + if (newNested != rpOne.nested[i]) { + if (reducedNestedPatterns == null) { + reducedNestedPatterns = Arrays.copyOf(rpOne.nested, rpOne.nested.length); + } + reducedNestedPatterns[i] = newNested; + } + + covered &= checkCovered(componentType[i], List.of(newNested)); + } + if (covered) { + return new BindingPattern(rpOne.recordType); + } else if (reducedNestedPatterns != null) { + return new RecordPattern(rpOne.recordType, rpOne.fullComponentTypes(), reducedNestedPatterns); + } + } + return pattern; + } + + private Set removeCoveredRecordPatterns(Set patterns) { + Set existingBindings = patterns.stream() + .filter(pd -> pd instanceof BindingPattern) + .map(pd -> ((BindingPattern) pd).type.tsym) + .collect(Collectors.toSet()); + Set result = new HashSet<>(patterns); + + for (Iterator it = result.iterator(); it.hasNext();) { + PatternDescription pd = it.next(); + if (pd instanceof RecordPattern rp && existingBindings.contains(rp.recordType.tsym)) { + it.remove(); + } + } + + return result; + } + + private boolean isBpCovered(Type componentType, PatternDescription newNested) { + if (newNested instanceof BindingPattern bp) { + Type seltype = types.erasure(componentType); + Type pattype = types.erasure(bp.type); + + return seltype.isPrimitive() ? + types.isUnconditionallyExact(seltype, pattype) : + (bp.type.isPrimitive() && types.isUnconditionallyExact(types.unboxedType(seltype), bp.type)) || types.isSubtype(seltype, pattype); + } + return false; + } + + sealed interface PatternDescription { } + public PatternDescription makePatternDescription(Type selectorType, JCPattern pattern) { + if (pattern instanceof JCBindingPattern binding) { + Type type = !selectorType.isPrimitive() && types.isSubtype(selectorType, binding.type) + ? selectorType : binding.type; + return new BindingPattern(type); + } else if (pattern instanceof JCRecordPattern record) { + Type[] componentTypes; + + if (!record.type.isErroneous()) { + componentTypes = ((ClassSymbol) record.type.tsym).getRecordComponents() + .map(r -> types.memberType(record.type, r)) + .toArray(s -> new Type[s]); + } + else { + componentTypes = record.nested.map(t -> types.createErrorType(t.type)).toArray(s -> new Type[s]);; + } + + PatternDescription[] nestedDescriptions = + new PatternDescription[record.nested.size()]; + int i = 0; + for (List it = record.nested; + it.nonEmpty(); + it = it.tail, i++) { + Type componentType = i < componentTypes.length ? componentTypes[i] + : syms.errType; + nestedDescriptions[i] = makePatternDescription(types.erasure(componentType), it.head); + } + return new RecordPattern(record.type, componentTypes, nestedDescriptions); + } else if (pattern instanceof JCAnyPattern) { + return new BindingPattern(selectorType); + } else { + throw Assert.error(); + } + } + record BindingPattern(Type type) implements PatternDescription { + @Override + public int hashCode() { + return type.tsym.hashCode(); + } + @Override + public boolean equals(Object o) { + return o instanceof BindingPattern other && + type.tsym == other.type.tsym; + } + @Override + public String toString() { + return type.tsym + " _"; + } + } + record RecordPattern(Type recordType, int _hashCode, Type[] fullComponentTypes, PatternDescription... nested) implements PatternDescription { + + public RecordPattern(Type recordType, Type[] fullComponentTypes, PatternDescription[] nested) { + this(recordType, hashCode(-1, recordType, nested), fullComponentTypes, nested); + } + + @Override + public int hashCode() { + return _hashCode; + } + + @Override + public boolean equals(Object o) { + return o instanceof RecordPattern other && + recordType.tsym == other.recordType.tsym && + Arrays.equals(nested, other.nested); + } + + public int hashCode(int excludeComponent) { + return hashCode(excludeComponent, recordType, nested); + } + + public static int hashCode(int excludeComponent, Type recordType, PatternDescription... nested) { + int hash = 5; + hash = 41 * hash + recordType.tsym.hashCode(); + for (int i = 0; i < nested.length; i++) { + if (i != excludeComponent) { + hash = 41 * hash + nested[i].hashCode(); + } + } + return hash; + } + @Override + public String toString() { + return recordType.tsym + "(" + Arrays.stream(nested) + .map(pd -> pd.toString()) + .collect(Collectors.joining(", ")) + ")"; + } + } +} diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java index 3bbd007c66a65..e74aed6a35703 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java @@ -27,11 +27,7 @@ package com.sun.tools.javac.comp; -import java.util.Map; -import java.util.Map.Entry; import java.util.HashMap; -import java.util.HashSet; -import java.util.Set; import java.util.function.Consumer; import com.sun.source.tree.LambdaExpressionTree.BodyKind; @@ -51,20 +47,12 @@ import static com.sun.tools.javac.code.Flags.*; import static com.sun.tools.javac.code.Flags.BLOCK; -import com.sun.tools.javac.code.Kinds.Kind; import static com.sun.tools.javac.code.Kinds.Kind.*; -import com.sun.tools.javac.code.Type.TypeVar; import static com.sun.tools.javac.code.TypeTag.BOOLEAN; import static com.sun.tools.javac.code.TypeTag.VOID; import com.sun.tools.javac.resources.CompilerProperties.Fragments; import static com.sun.tools.javac.tree.JCTree.Tag.*; import com.sun.tools.javac.util.JCDiagnostic.Fragment; -import java.util.Arrays; -import java.util.Iterator; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -import static java.util.stream.Collectors.groupingBy; /** This pass implements dataflow analysis for Java programs though * different AST visitor steps. Liveness analysis (see AliveAnalyzer) checks that @@ -213,8 +201,8 @@ public class Flow { private TreeMaker make; private final Resolve rs; private final JCDiagnostic.Factory diags; + private final ExhaustivenessComputer exhaustiveness; private Env attrEnv; - private final Infer infer; public static Flow instance(Context context) { Flow instance = context.get(flowKey); @@ -336,10 +324,9 @@ protected Flow(Context context) { syms = Symtab.instance(context); types = Types.instance(context); chk = Check.instance(context); - infer = Infer.instance(context); rs = Resolve.instance(context); diags = JCDiagnostic.Factory.instance(context); - Source source = Source.instance(context); + exhaustiveness = ExhaustivenessComputer.instance(context); } /** @@ -709,7 +696,7 @@ public void visitSwitch(JCSwitch tree) { tree.isExhaustive = tree.hasUnconditionalPattern || TreeInfo.isErrorEnumSwitch(tree.selector, tree.cases); if (exhaustiveSwitch) { - tree.isExhaustive |= exhausts(tree.selector, tree.cases); + tree.isExhaustive |= exhaustiveness.exhausts(tree.selector, tree.cases); if (!tree.isExhaustive) { log.error(tree, Errors.NotExhaustiveStatement); } @@ -748,7 +735,7 @@ public void visitSwitchExpression(JCSwitchExpression tree) { TreeInfo.isErrorEnumSwitch(tree.selector, tree.cases)) { tree.isExhaustive = true; } else { - tree.isExhaustive = exhausts(tree.selector, tree.cases); + tree.isExhaustive = exhaustiveness.exhausts(tree.selector, tree.cases); } if (!tree.isExhaustive) { @@ -758,429 +745,6 @@ public void visitSwitchExpression(JCSwitchExpression tree) { alive = alive.or(resolveYields(tree, prevPendingExits)); } - private boolean exhausts(JCExpression selector, List cases) { - Set patternSet = new HashSet<>(); - Map> enum2Constants = new HashMap<>(); - Set booleanLiterals = new HashSet<>(Set.of(0, 1)); - for (JCCase c : cases) { - if (!TreeInfo.unguardedCase(c)) - continue; - - for (var l : c.labels) { - if (l instanceof JCPatternCaseLabel patternLabel) { - for (Type component : components(selector.type)) { - patternSet.add(makePatternDescription(component, patternLabel.pat)); - } - } else if (l instanceof JCConstantCaseLabel constantLabel) { - if (types.unboxedTypeOrType(selector.type).hasTag(TypeTag.BOOLEAN)) { - Object value = ((JCLiteral) constantLabel.expr).value; - booleanLiterals.remove(value); - } else { - Symbol s = TreeInfo.symbol(constantLabel.expr); - if (s != null && s.isEnum()) { - enum2Constants.computeIfAbsent(s.owner, x -> { - Set result = new HashSet<>(); - s.owner.members() - .getSymbols(sym -> sym.kind == Kind.VAR && sym.isEnum()) - .forEach(result::add); - return result; - }).remove(s); - } - } - } - } - } - - if (types.unboxedTypeOrType(selector.type).hasTag(TypeTag.BOOLEAN) && booleanLiterals.isEmpty()) { - return true; - } - - for (Entry> e : enum2Constants.entrySet()) { - if (e.getValue().isEmpty()) { - patternSet.add(new BindingPattern(e.getKey().type)); - } - } - Set patterns = patternSet; - boolean useHashes = true; - try { - boolean repeat = true; - while (repeat) { - Set updatedPatterns; - updatedPatterns = reduceBindingPatterns(selector.type, patterns); - updatedPatterns = reduceNestedPatterns(updatedPatterns, useHashes); - updatedPatterns = reduceRecordPatterns(updatedPatterns); - updatedPatterns = removeCoveredRecordPatterns(updatedPatterns); - repeat = !updatedPatterns.equals(patterns); - if (checkCovered(selector.type, patterns)) { - return true; - } - if (!repeat) { - //there may be situation like: - //class B permits S1, S2 - //patterns: R(S1, B), R(S2, S2) - //this might be joined to R(B, S2), as B could be rewritten to S2 - //but hashing in reduceNestedPatterns will not allow that - //disable the use of hashing, and use subtyping in - //reduceNestedPatterns to handle situations like this: - repeat = useHashes; - useHashes = false; - } else { - //if a reduction happened, make sure hashing in reduceNestedPatterns - //is enabled, as the hashing speeds up the process significantly: - useHashes = true; - } - patterns = updatedPatterns; - } - return checkCovered(selector.type, patterns); - } catch (CompletionFailure cf) { - chk.completionError(selector.pos(), cf); - return true; //error recovery - } - } - - private boolean checkCovered(Type seltype, Iterable patterns) { - for (Type seltypeComponent : components(seltype)) { - for (PatternDescription pd : patterns) { - if(isBpCovered(seltypeComponent, pd)) { - return true; - } - } - } - return false; - } - - private List components(Type seltype) { - return switch (seltype.getTag()) { - case CLASS -> { - if (seltype.isCompound()) { - if (seltype.isIntersection()) { - yield ((Type.IntersectionClassType) seltype).getComponents() - .stream() - .flatMap(t -> components(t).stream()) - .collect(List.collector()); - } - yield List.nil(); - } - yield List.of(types.erasure(seltype)); - } - case TYPEVAR -> components(((TypeVar) seltype).getUpperBound()); - default -> List.of(types.erasure(seltype)); - }; - } - - /* In a set of patterns, search for a sub-set of binding patterns that - * in combination exhaust their sealed supertype. If such a sub-set - * is found, it is removed, and replaced with a binding pattern - * for the sealed supertype. - */ - private Set reduceBindingPatterns(Type selectorType, Set patterns) { - Set existingBindings = patterns.stream() - .filter(pd -> pd instanceof BindingPattern) - .map(pd -> ((BindingPattern) pd).type.tsym) - .collect(Collectors.toSet()); - - for (PatternDescription pdOne : patterns) { - if (pdOne instanceof BindingPattern bpOne) { - Set toAdd = new HashSet<>(); - - for (Type sup : types.directSupertypes(bpOne.type)) { - ClassSymbol clazz = (ClassSymbol) types.erasure(sup).tsym; - - clazz.complete(); - - if (clazz.isSealed() && clazz.isAbstract() && - //if a binding pattern for clazz already exists, no need to analyze it again: - !existingBindings.contains(clazz)) { - ListBuffer bindings = new ListBuffer<>(); - //do not reduce to types unrelated to the selector type: - Type clazzErasure = types.erasure(clazz.type); - if (components(selectorType).stream() - .map(types::erasure) - .noneMatch(c -> types.isSubtype(clazzErasure, c))) { - continue; - } - - Set permitted = allPermittedSubTypes(clazz, csym -> { - Type instantiated; - if (csym.type.allparams().isEmpty()) { - instantiated = csym.type; - } else { - instantiated = infer.instantiatePatternType(selectorType, csym); - } - - return instantiated != null && types.isCastable(selectorType, instantiated); - }); - - for (PatternDescription pdOther : patterns) { - if (pdOther instanceof BindingPattern bpOther) { - Set currentPermittedSubTypes = - allPermittedSubTypes(bpOther.type.tsym, s -> true); - - PERMITTED: for (Iterator it = permitted.iterator(); it.hasNext();) { - Symbol perm = it.next(); - - for (Symbol currentPermitted : currentPermittedSubTypes) { - if (types.isSubtype(types.erasure(currentPermitted.type), - types.erasure(perm.type))) { - it.remove(); - continue PERMITTED; - } - } - if (types.isSubtype(types.erasure(perm.type), - types.erasure(bpOther.type))) { - it.remove(); - } - } - } - } - - if (permitted.isEmpty()) { - toAdd.add(new BindingPattern(clazz.type)); - } - } - } - - if (!toAdd.isEmpty()) { - Set newPatterns = new HashSet<>(patterns); - newPatterns.addAll(toAdd); - return newPatterns; - } - } - } - return patterns; - } - - private Set allPermittedSubTypes(TypeSymbol root, Predicate accept) { - Set permitted = new HashSet<>(); - List permittedSubtypesClosure = baseClasses(root); - - while (permittedSubtypesClosure.nonEmpty()) { - ClassSymbol current = permittedSubtypesClosure.head; - - permittedSubtypesClosure = permittedSubtypesClosure.tail; - - current.complete(); - - if (current.isSealed() && current.isAbstract()) { - for (Type t : current.getPermittedSubclasses()) { - ClassSymbol csym = (ClassSymbol) t.tsym; - - if (accept.test(csym)) { - permittedSubtypesClosure = permittedSubtypesClosure.prepend(csym); - permitted.add(csym); - } - } - } - } - - return permitted; - } - - private List baseClasses(TypeSymbol root) { - if (root instanceof ClassSymbol clazz) { - return List.of(clazz); - } else if (root instanceof TypeVariableSymbol tvar) { - ListBuffer result = new ListBuffer<>(); - for (Type bound : tvar.getBounds()) { - result.appendList(baseClasses(bound.tsym)); - } - return result.toList(); - } else { - return List.nil(); - } - } - - /* Among the set of patterns, find sub-set of patterns such: - * $record($prefix$, $nested, $suffix$) - * Where $record, $prefix$ and $suffix$ is the same for each pattern - * in the set, and the patterns only differ in one "column" in - * the $nested pattern. - * Then, the set of $nested patterns is taken, and passed recursively - * to reduceNestedPatterns and to reduceBindingPatterns, to - * simplify the pattern. If that succeeds, the original found sub-set - * of patterns is replaced with a new set of patterns of the form: - * $record($prefix$, $resultOfReduction, $suffix$) - * - * useHashes: when true, patterns will be subject to exact equivalence; - * when false, two binding patterns will be considered equivalent - * if one of them is more generic than the other one; - * when false, the processing will be significantly slower, - * as pattern hashes cannot be used to speed up the matching process - */ - private Set reduceNestedPatterns(Set patterns, - boolean useHashes) { - /* implementation note: - * finding a sub-set of patterns that only differ in a single - * column is time-consuming task, so this method speeds it up by: - * - group the patterns by their record class - * - for each column (nested pattern) do: - * -- group patterns by their hash - * -- in each such by-hash group, find sub-sets that only differ in - * the chosen column, and then call reduceBindingPatterns and reduceNestedPatterns - * on patterns in the chosen column, as described above - */ - var groupByRecordClass = - patterns.stream() - .filter(pd -> pd instanceof RecordPattern) - .map(pd -> (RecordPattern) pd) - .collect(groupingBy(pd -> (ClassSymbol) pd.recordType.tsym)); - - for (var e : groupByRecordClass.entrySet()) { - int nestedPatternsCount = e.getKey().getRecordComponents().size(); - Set current = new HashSet<>(e.getValue()); - - for (int mismatchingCandidate = 0; - mismatchingCandidate < nestedPatternsCount; - mismatchingCandidate++) { - int mismatchingCandidateFin = mismatchingCandidate; - var groupEquivalenceCandidates = - current - .stream() - //error recovery, ignore patterns with incorrect number of nested patterns: - .filter(pd -> pd.nested.length == nestedPatternsCount) - .collect(groupingBy(pd -> useHashes ? pd.hashCode(mismatchingCandidateFin) : 0)); - for (var candidates : groupEquivalenceCandidates.values()) { - var candidatesArr = candidates.toArray(RecordPattern[]::new); - - for (int firstCandidate = 0; - firstCandidate < candidatesArr.length; - firstCandidate++) { - RecordPattern rpOne = candidatesArr[firstCandidate]; - ListBuffer join = new ListBuffer<>(); - - join.append(rpOne); - - NEXT_PATTERN: for (int nextCandidate = 0; - nextCandidate < candidatesArr.length; - nextCandidate++) { - if (firstCandidate == nextCandidate) { - continue; - } - - RecordPattern rpOther = candidatesArr[nextCandidate]; - if (rpOne.recordType.tsym == rpOther.recordType.tsym) { - for (int i = 0; i < rpOne.nested.length; i++) { - if (i != mismatchingCandidate) { - if (!rpOne.nested[i].equals(rpOther.nested[i])) { - if (useHashes || - //when not using hashes, - //check if rpOne.nested[i] is - //a subtype of rpOther.nested[i]: - !(rpOne.nested[i] instanceof BindingPattern bpOne) || - !(rpOther.nested[i] instanceof BindingPattern bpOther) || - !types.isSubtype(types.erasure(bpOne.type), types.erasure(bpOther.type))) { - continue NEXT_PATTERN; - } - } - } - } - join.append(rpOther); - } - } - - var nestedPatterns = join.stream().map(rp -> rp.nested[mismatchingCandidateFin]).collect(Collectors.toSet()); - var updatedPatterns = reduceNestedPatterns(nestedPatterns, useHashes); - - updatedPatterns = reduceRecordPatterns(updatedPatterns); - updatedPatterns = removeCoveredRecordPatterns(updatedPatterns); - updatedPatterns = reduceBindingPatterns(rpOne.fullComponentTypes()[mismatchingCandidateFin], updatedPatterns); - - if (!nestedPatterns.equals(updatedPatterns)) { - if (useHashes) { - current.removeAll(join); - } - - for (PatternDescription nested : updatedPatterns) { - PatternDescription[] newNested = - Arrays.copyOf(rpOne.nested, rpOne.nested.length); - newNested[mismatchingCandidateFin] = nested; - current.add(new RecordPattern(rpOne.recordType(), - rpOne.fullComponentTypes(), - newNested)); - } - } - } - } - } - - if (!current.equals(new HashSet<>(e.getValue()))) { - Set result = new HashSet<>(patterns); - result.removeAll(e.getValue()); - result.addAll(current); - return result; - } - } - return patterns; - } - - /* In the set of patterns, find those for which, given: - * $record($nested1, $nested2, ...) - * all the $nestedX pattern cover the given record component, - * and replace those with a simple binding pattern over $record. - */ - private Set reduceRecordPatterns(Set patterns) { - var newPatterns = new HashSet(); - boolean modified = false; - for (PatternDescription pd : patterns) { - if (pd instanceof RecordPattern rpOne) { - PatternDescription reducedPattern = reduceRecordPattern(rpOne); - if (reducedPattern != rpOne) { - newPatterns.add(reducedPattern); - modified = true; - continue; - } - } - newPatterns.add(pd); - } - return modified ? newPatterns : patterns; - } - - private PatternDescription reduceRecordPattern(PatternDescription pattern) { - if (pattern instanceof RecordPattern rpOne) { - Type[] componentType = rpOne.fullComponentTypes(); - //error recovery, ignore patterns with incorrect number of nested patterns: - if (componentType.length != rpOne.nested.length) { - return pattern; - } - PatternDescription[] reducedNestedPatterns = null; - boolean covered = true; - for (int i = 0; i < componentType.length; i++) { - PatternDescription newNested = reduceRecordPattern(rpOne.nested[i]); - if (newNested != rpOne.nested[i]) { - if (reducedNestedPatterns == null) { - reducedNestedPatterns = Arrays.copyOf(rpOne.nested, rpOne.nested.length); - } - reducedNestedPatterns[i] = newNested; - } - - covered &= checkCovered(componentType[i], List.of(newNested)); - } - if (covered) { - return new BindingPattern(rpOne.recordType); - } else if (reducedNestedPatterns != null) { - return new RecordPattern(rpOne.recordType, rpOne.fullComponentTypes(), reducedNestedPatterns); - } - } - return pattern; - } - - private Set removeCoveredRecordPatterns(Set patterns) { - Set existingBindings = patterns.stream() - .filter(pd -> pd instanceof BindingPattern) - .map(pd -> ((BindingPattern) pd).type.tsym) - .collect(Collectors.toSet()); - Set result = new HashSet<>(patterns); - - for (Iterator it = result.iterator(); it.hasNext();) { - PatternDescription pd = it.next(); - if (pd instanceof RecordPattern rp && existingBindings.contains(rp.recordType.tsym)) { - it.remove(); - } - } - - return result; - } - public void visitTry(JCTry tree) { ListBuffer prevPendingExits = pendingExits; pendingExits = new ListBuffer<>(); @@ -1326,18 +890,6 @@ public void analyzeTree(Env env, JCTree tree, TreeMaker make) { } } - private boolean isBpCovered(Type componentType, PatternDescription newNested) { - if (newNested instanceof BindingPattern bp) { - Type seltype = types.erasure(componentType); - Type pattype = types.erasure(bp.type); - - return seltype.isPrimitive() ? - types.isUnconditionallyExact(seltype, pattype) : - (bp.type.isPrimitive() && types.isUnconditionallyExact(types.unboxedType(seltype), bp.type)) || types.isSubtype(seltype, pattype); - } - return false; - } - /** * This pass implements the second step of the dataflow analysis, namely * the exception analysis. This is to ensure that every checked exception that is @@ -3473,93 +3025,4 @@ public static Liveness from(boolean value) { } } - sealed interface PatternDescription { } - public PatternDescription makePatternDescription(Type selectorType, JCPattern pattern) { - if (pattern instanceof JCBindingPattern binding) { - Type type = !selectorType.isPrimitive() && types.isSubtype(selectorType, binding.type) - ? selectorType : binding.type; - return new BindingPattern(type); - } else if (pattern instanceof JCRecordPattern record) { - Type[] componentTypes; - - if (!record.type.isErroneous()) { - componentTypes = ((ClassSymbol) record.type.tsym).getRecordComponents() - .map(r -> types.memberType(record.type, r)) - .toArray(s -> new Type[s]); - } - else { - componentTypes = record.nested.map(t -> types.createErrorType(t.type)).toArray(s -> new Type[s]);; - } - - PatternDescription[] nestedDescriptions = - new PatternDescription[record.nested.size()]; - int i = 0; - for (List it = record.nested; - it.nonEmpty(); - it = it.tail, i++) { - Type componentType = i < componentTypes.length ? componentTypes[i] - : syms.errType; - nestedDescriptions[i] = makePatternDescription(types.erasure(componentType), it.head); - } - return new RecordPattern(record.type, componentTypes, nestedDescriptions); - } else if (pattern instanceof JCAnyPattern) { - return new BindingPattern(selectorType); - } else { - throw Assert.error(); - } - } - record BindingPattern(Type type) implements PatternDescription { - @Override - public int hashCode() { - return type.tsym.hashCode(); - } - @Override - public boolean equals(Object o) { - return o instanceof BindingPattern other && - type.tsym == other.type.tsym; - } - @Override - public String toString() { - return type.tsym + " _"; - } - } - record RecordPattern(Type recordType, int _hashCode, Type[] fullComponentTypes, PatternDescription... nested) implements PatternDescription { - - public RecordPattern(Type recordType, Type[] fullComponentTypes, PatternDescription[] nested) { - this(recordType, hashCode(-1, recordType, nested), fullComponentTypes, nested); - } - - @Override - public int hashCode() { - return _hashCode; - } - - @Override - public boolean equals(Object o) { - return o instanceof RecordPattern other && - recordType.tsym == other.recordType.tsym && - Arrays.equals(nested, other.nested); - } - - public int hashCode(int excludeComponent) { - return hashCode(excludeComponent, recordType, nested); - } - - public static int hashCode(int excludeComponent, Type recordType, PatternDescription... nested) { - int hash = 5; - hash = 41 * hash + recordType.tsym.hashCode(); - for (int i = 0; i < nested.length; i++) { - if (i != excludeComponent) { - hash = 41 * hash + nested[i].hashCode(); - } - } - return hash; - } - @Override - public String toString() { - return recordType.tsym + "(" + Arrays.stream(nested) - .map(pd -> pd.toString()) - .collect(Collectors.joining(", ")) + ")"; - } - } } From b79bce1f40cb75fbfa07ea647a3663c6a7ed8ee4 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Mon, 20 Oct 2025 07:38:00 +0200 Subject: [PATCH 26/33] Fixing tests --- test/langtools/tools/javac/diags/Example.java | 10 +++++++--- .../javac/diags/examples/NotExhaustiveDetails.java | 1 + .../diags/examples/NotExhaustiveStatementDetails.java | 1 + 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/test/langtools/tools/javac/diags/Example.java b/test/langtools/tools/javac/diags/Example.java index 91d3bace4d8cb..45aa6190b9aa2 100644 --- a/test/langtools/tools/javac/diags/Example.java +++ b/test/langtools/tools/javac/diags/Example.java @@ -602,9 +602,13 @@ boolean run(PrintWriter out, Set keys, boolean raw, List opts, L */ private static void scanForKeys(JCDiagnostic d, Set keys) { keys.add(d.getCode()); - for (Object o: d.getArgs()) { - if (o instanceof JCDiagnostic) { - scanForKeys((JCDiagnostic) o, keys); + List todoArgs = new ArrayList<>(Arrays.asList(d.getArgs())); + while (!todoArgs.isEmpty()) { + Object o = todoArgs.removeLast(); + if (o instanceof JCDiagnostic sd) { + scanForKeys(sd, keys); + } else if (o instanceof List l) { + todoArgs.addAll(l); } } for (JCDiagnostic sd: d.getSubdiagnostics()) diff --git a/test/langtools/tools/javac/diags/examples/NotExhaustiveDetails.java b/test/langtools/tools/javac/diags/examples/NotExhaustiveDetails.java index 463e5e6516b9e..1a7e82b12e7c0 100644 --- a/test/langtools/tools/javac/diags/examples/NotExhaustiveDetails.java +++ b/test/langtools/tools/javac/diags/examples/NotExhaustiveDetails.java @@ -22,6 +22,7 @@ */ // key: compiler.err.not.exhaustive.details +// key: compiler.misc.not.exhaustive.detail // options: -XDexhaustivityTimeout=-1 class NotExhaustiveDetails { diff --git a/test/langtools/tools/javac/diags/examples/NotExhaustiveStatementDetails.java b/test/langtools/tools/javac/diags/examples/NotExhaustiveStatementDetails.java index 8d7253aa7d79a..650a81a01af9e 100644 --- a/test/langtools/tools/javac/diags/examples/NotExhaustiveStatementDetails.java +++ b/test/langtools/tools/javac/diags/examples/NotExhaustiveStatementDetails.java @@ -22,6 +22,7 @@ */ // key: compiler.err.not.exhaustive.statement.details +// key: compiler.misc.not.exhaustive.detail // options: -XDexhaustivityTimeout=-1 class NotExhaustiveDetails { From 50ddab0f8702583da510b724870cf6edc8a82e56 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Fri, 7 Nov 2025 10:52:52 +0100 Subject: [PATCH 27/33] Reflecting review comments. --- .../javac/comp/ExhaustivenessComputer.java | 146 +++++++++--------- 1 file changed, 77 insertions(+), 69 deletions(-) diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java index 74524b70426bd..96cddd8e193e6 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java @@ -145,7 +145,7 @@ public ExhaustivenessResult exhausts(JCExpression selector, List cases) } } try { - CoverageResult coveredResult = computeCoverage(selector.type, patternSet, false); + CoverageResult coveredResult = computeCoverage(selector.type, patternSet, PatternEquivalence.STRICT); if (coveredResult.covered()) { return ExhaustivenessResult.ofExhaustive(); } @@ -170,14 +170,20 @@ public ExhaustivenessResult exhausts(JCExpression selector, List cases) } } - private CoverageResult computeCoverage(Type selectorType, Set patterns, boolean search) { + /* Given the set of patterns, runs the reductions of it as long as possible. + * If the (reduced) set of patterns covers the given selector type, returns + * covered == true, and incompletePatterns == null. + * If the (reduced) set of patterns does not cover the given selector type, + * returns covered == false, and incompletePatterns == the reduced set of patterns. + */ + private CoverageResult computeCoverage(Type selectorType, Set patterns, PatternEquivalence patternEquivalence) { Set updatedPatterns; Set> seenPatterns = new HashSet<>(); boolean useHashes = true; boolean repeat = true; do { updatedPatterns = reduceBindingPatterns(selectorType, patterns); - updatedPatterns = reduceNestedPatterns(updatedPatterns, useHashes, search); + updatedPatterns = reduceNestedPatterns(updatedPatterns, useHashes, patternEquivalence); updatedPatterns = reduceRecordPatterns(updatedPatterns); updatedPatterns = removeCoveredRecordPatterns(updatedPatterns); repeat = !updatedPatterns.equals(patterns); @@ -270,7 +276,7 @@ private Set reduceBindingPatterns(Type selectorType, Set permitted = allPermittedSubTypes(clazz, isPossibleSubtypePredicate(selectorType)); + Set permitted = allPermittedSubTypes(clazz, isApplicableSubtypePredicate(selectorType)); int permittedSubtypes = permitted.size(); //the set of pending permitted subtypes needed to cover clazz: @@ -344,7 +350,7 @@ private Set allPermittedSubTypes(TypeSymbol root, Predicate return permitted; } - private Predicate isPossibleSubtypePredicate(Type targetType) { + private Predicate isApplicableSubtypePredicate(Type targetType) { return csym -> { Type instantiated = instantiatePatternType(targetType, csym); @@ -420,7 +426,7 @@ private List baseClasses(TypeSymbol root) { */ private Set reduceNestedPatterns(Set patterns, boolean useHashes, - boolean search) { + PatternEquivalence patternEquivalence) { /* implementation note: * finding a sub-set of patterns that only differ in a single * column is time-consuming task, so this method speeds it up by: @@ -472,13 +478,13 @@ private Set reduceNestedPatterns(Set pat RecordPattern rpOther = candidatesArr[nextCandidate]; if (rpOne.recordType.tsym == rpOther.recordType.tsym && - nestedComponentsEquivalent(rpOne, rpOther, mismatchingCandidate, useHashes, search)) { + nestedComponentsEquivalent(rpOne, rpOther, mismatchingCandidate, useHashes, patternEquivalence)) { join.append(rpOther); } } var nestedPatterns = join.stream().map(rp -> rp.nested[mismatchingCandidateFin]).collect(Collectors.toSet()); - var updatedPatterns = reduceNestedPatterns(nestedPatterns, useHashes, search); + var updatedPatterns = reduceNestedPatterns(nestedPatterns, useHashes, patternEquivalence); updatedPatterns = reduceRecordPatterns(updatedPatterns); updatedPatterns = removeCoveredRecordPatterns(updatedPatterns); @@ -515,12 +521,16 @@ private Set reduceNestedPatterns(Set pat * - it's type is a supertype of the existing pattern's type * - it was produced by a reduction from a record pattern that is equivalent to * the existing pattern + * - only if PatternEquivalence is LOOSE and the type is the same of the type + * of an existing record pattern (the binding pattern may stand in place of + * a record pattern). This is only used to compute the missing patterns that + * would make the original pattern set exhaustive. */ private boolean nestedComponentsEquivalent(RecordPattern existing, RecordPattern candidate, int mismatchingCandidate, boolean useHashes, - boolean search) { + PatternEquivalence patternEquivalence) { NEXT_NESTED: for (int i = 0; i < existing.nested.length; i++) { if (i != mismatchingCandidate) { @@ -539,7 +549,7 @@ private boolean nestedComponentsEquivalent(RecordPattern existing, return false; } } else if (existing.nested[i] instanceof RecordPattern nestedExisting) { - if (search) { + if (patternEquivalence == PatternEquivalence.LOOSE) { if (!types.isSubtype(types.erasure(nestedExisting.recordType()), types.erasure(nestedCandidate.type))) { return false; } @@ -833,32 +843,18 @@ private Set doExpandMissingPatternDescriptions(Type selector Set inMissingPatterns) { if (toExpand instanceof BindingPattern bp) { if (bp.type.tsym.isSealed()) { - //try to replace binding patterns for sealed types with all their immediate permitted types: + //try to replace binding patterns for sealed types with all their immediate permitted applicable types: List permitted = ((ClassSymbol) bp.type.tsym).getPermittedSubclasses(); - Set viablePermittedPatterns = + Set applicableDirectPermittedPatterns = permitted.stream() .map(type -> type.tsym) - .filter(isPossibleSubtypePredicate(targetType)) + .filter(isApplicableSubtypePredicate(targetType)) .map(csym -> new BindingPattern(types.erasure(csym.type))) .collect(Collectors.toCollection(HashSet::new)); //remove the permitted subtypes that are not needed to achieve exhaustivity - boolean reduced = false; - - for (Iterator it = viablePermittedPatterns.iterator(); it.hasNext(); ) { - BindingPattern current = it.next(); - Set reducedPermittedPatterns = new HashSet<>(viablePermittedPatterns); - - reducedPermittedPatterns.remove(current); - - Set replaced = - replace(inMissingPatterns, toExpand, reducedPermittedPatterns); - - if (computeCoverage(selectorType, joinSets(basePatterns, replaced), true).covered()) { - it.remove(); - reduced = true; - } - } + boolean reduced = + removeUnnecessaryPatterns(selectorType, bp, basePatterns, inMissingPatterns, applicableDirectPermittedPatterns); if (!reduced) { //if all immediate permitted subtypes are needed @@ -867,10 +863,10 @@ private Set doExpandMissingPatternDescriptions(Type selector } Set currentMissingPatterns = - replace(inMissingPatterns, toExpand, viablePermittedPatterns); + replace(inMissingPatterns, toExpand, applicableDirectPermittedPatterns); //try to recursively expand on each viable pattern: - for (PatternDescription viable : viablePermittedPatterns) { + for (PatternDescription viable : applicableDirectPermittedPatterns) { currentMissingPatterns = expandMissingPatternDescriptions(selectorType, targetType, viable, basePatterns, currentMissingPatterns); @@ -890,22 +886,23 @@ private Set doExpandMissingPatternDescriptions(Type selector List> combinatorialNestedTypes = List.of(List.nil()); for (Type componentType : componentTypes) { - List variants; + List applicableLeafPermittedSubtypes; if (componentType.tsym.isSealed()) { - variants = leafPermittedSubTypes(componentType.tsym, - isPossibleSubtypePredicate(componentType)) - .stream() - .map(csym -> instantiatePatternType(componentType, csym)) - .collect(List.collector()); + applicableLeafPermittedSubtypes = + leafPermittedSubTypes(componentType.tsym, + isApplicableSubtypePredicate(componentType)) + .stream() + .map(csym -> instantiatePatternType(componentType, csym)) + .collect(List.collector()); } else { - variants = List.of(componentType); + applicableLeafPermittedSubtypes = List.of(componentType); } List> newCombinatorialNestedTypes = List.nil(); for (List existing : combinatorialNestedTypes) { - for (Type nue : variants) { + for (Type nue : applicableLeafPermittedSubtypes) { newCombinatorialNestedTypes = newCombinatorialNestedTypes.prepend(existing.append(nue)); } } @@ -921,22 +918,9 @@ private Set doExpandMissingPatternDescriptions(Type selector .toArray(PatternDescription[]::new))) .collect(Collectors.toCollection(HashSet::new)); - //remove unnecessary: - for (Iterator it = combinatorialPatterns.iterator(); it.hasNext(); ) { - PatternDescription current = it.next(); - Set reducedAdded = new HashSet<>(combinatorialPatterns); - - reducedAdded.remove(current); + removeUnnecessaryPatterns(selectorType, bp, basePatterns, inMissingPatterns, combinatorialPatterns); - Set combinedPatterns = - joinSets(basePatterns, replace(inMissingPatterns, bp, reducedAdded)); - - if (computeCoverage(selectorType, combinedPatterns, true).covered()) { - it.remove(); - } - } - - CoverageResult coverageResult = computeCoverage(targetType, combinatorialPatterns, true); + CoverageResult coverageResult = computeCoverage(targetType, combinatorialPatterns, PatternEquivalence.LOOSE); if (!coverageResult.covered()) { //use the partially merged/combined patterns: @@ -945,25 +929,12 @@ private Set doExpandMissingPatternDescriptions(Type selector //combine sealed subtypes into the supertype, if all is covered. //but preserve more specific record types in positions where there are record patterns in the original patterns - //this is particularly for the case where the sealed supertype only has one permitted type, the record + //this is particularly important for the case where the sealed supertype only has one permitted type, the record //the base type could be used instead of the record otherwise, which would produce less specific missing pattern: Set sortedCandidates = partialSortPattern(combinatorialPatterns, basePatterns, combinatorialPatterns); - //remove unnecessary: - OUTER: for (Iterator it = sortedCandidates.iterator(); it.hasNext(); ) { - PatternDescription current = it.next(); - Set reducedAdded = new HashSet<>(sortedCandidates); - - reducedAdded.remove(current); - - Set combinedPatterns = - joinSets(basePatterns, replace(inMissingPatterns, bp, reducedAdded)); - - if (computeCoverage(selectorType, combinedPatterns, true).covered()) { - it.remove(); - } - } + removeUnnecessaryPatterns(selectorType, bp, basePatterns, inMissingPatterns, sortedCandidates); Set currentMissingPatterns = replace(inMissingPatterns, toExpand, sortedCandidates); @@ -1030,6 +1001,35 @@ private Collection replace(PatternDescription in, } } + /* Out of "candidates" remove patterns that are not necessary to achieve exhaustiveness. + * Note that iteration order of "candidates" is important - if the set contains + * two pattern, out of which either, but not both, is needed to achieve exhaustiveness, + * the first one in the iteration order will be removed. + */ + private boolean removeUnnecessaryPatterns(Type selectorType, + PatternDescription toExpand, + Set basePatterns, + Set inMissingPatterns, + Set candidates) { + boolean reduced = false; + + for (Iterator it = candidates.iterator(); it.hasNext(); ) { + PatternDescription current = it.next(); + Set reducedAdded = new HashSet<>(candidates); + + reducedAdded.remove(current); + + Set combinedPatterns = + joinSets(basePatterns, replace(inMissingPatterns, toExpand, reducedAdded)); + + if (computeCoverage(selectorType, combinedPatterns, PatternEquivalence.LOOSE).covered()) { + it.remove(); + reduced = true; + } + } + + return reduced; + } /* * Sort patterns so that those that those that are prefered for removal * are in front of those that are preferred to remain (when there's a choice). @@ -1193,6 +1193,14 @@ private void generatePatternsWithReplacedNestedPattern(RecordPattern basePattern } } + /* The stricness of determining the equivalent of patterns, used in + * nestedComponentsEquivalent. + */ + private enum PatternEquivalence { + STRICT, + LOOSE; + } + protected static class TimeoutException extends RuntimeException { private static final long serialVersionUID = 0L; private transient final Set missingPatterns; From 75c3cb0b9e025f3e3370b42e763416196d4b34a8 Mon Sep 17 00:00:00 2001 From: Jan Lahoda <51319204+lahodaj@users.noreply.github.com> Date: Thu, 13 Nov 2025 18:05:25 +0100 Subject: [PATCH 28/33] Apply suggestions from code review Co-authored-by: Aggelos Biboudis --- .../tools/javac/comp/ExhaustivenessComputer.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java index 96cddd8e193e6..ff9451323f380 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java @@ -1019,8 +1019,10 @@ private boolean removeUnnecessaryPatterns(Type selectorType, reducedAdded.remove(current); - Set combinedPatterns = - joinSets(basePatterns, replace(inMissingPatterns, toExpand, reducedAdded)); + Set combinedPatterns = + Stream.concat(basePatterns.stream(), + replace(inMissingPatterns, toExpand, reducedAdded).stream()) + .collect(Collectors.toSet()); if (computeCoverage(selectorType, combinedPatterns, PatternEquivalence.LOOSE).covered()) { it.remove(); @@ -1031,8 +1033,8 @@ private boolean removeUnnecessaryPatterns(Type selectorType, return reduced; } /* - * Sort patterns so that those that those that are prefered for removal - * are in front of those that are preferred to remain (when there's a choice). + * Sort patterns so that those that are preferred for removal are in front + * of those that are preferred to remain (when there's a choice). */ private SequencedSet partialSortPattern(Set candidates, Set basePatterns, @@ -1134,7 +1136,8 @@ private boolean basePatternsHaveRecordPatternOnThisSpot(Set filteredBasePatterns = From 8c48cf8fc0168b58521e987953a8ab5bbd9ac017 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Thu, 13 Nov 2025 18:07:08 +0100 Subject: [PATCH 29/33] Cleanup, reflecting review feedback. --- .../javac/comp/ExhaustivenessComputer.java | 41 +++++++++++-------- .../ExhaustivenessConvenientErrors.java | 1 - 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java index ff9451323f380..907d162f14a96 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java @@ -525,6 +525,22 @@ private Set reduceNestedPatterns(Set pat * of an existing record pattern (the binding pattern may stand in place of * a record pattern). This is only used to compute the missing patterns that * would make the original pattern set exhaustive. + * + * For example, having (with mismatchingCandidate == 0): + * existing: R(A _, Box(var _)) {} + * cadidate: R(B _, Box(var _)) {} + * these are always equivalent; as all nested patterns except of + * component 0 are exactly equivalent + * + * existing: R(A _, SubtypeOfBox _) {} + * cadidate: R(A _, Box _) {} + * this is only equivalent when useHashes == false; Box _ could be replaced + * with a more specific SubtypeOfBox _ + * + * existing: R(A _, Box(var _)) {} + * cadidate: R(A _, Box _) {} + * this is only equivalent when useHashes == false and patternEquivalence == LOOSE; + * Box _ is accepted in place of the more specific record pattern */ private boolean nestedComponentsEquivalent(RecordPattern existing, RecordPattern candidate, @@ -550,7 +566,7 @@ private boolean nestedComponentsEquivalent(RecordPattern existing, } } else if (existing.nested[i] instanceof RecordPattern nestedExisting) { if (patternEquivalence == PatternEquivalence.LOOSE) { - if (!types.isSubtype(types.erasure(nestedExisting.recordType()), types.erasure(nestedCandidate.type))) { + if (!isSubtypeErasure(nestedExisting.recordType(), nestedCandidate.type)) { return false; } } else { @@ -1116,8 +1132,7 @@ private PatternDescription findRootContaining(Set } } - //assert? - return null; + throw Assert.error(); } private boolean basePatternsHaveRecordPatternOnThisSpot(Set basePatterns, @@ -1164,21 +1179,13 @@ private boolean isUnderRoot(PatternDescription root, PatternDescription searchFo return false; } - private Set joinSets(Collection s1, - Collection s2) { - Set result = new HashSet<>(); - - result.addAll(s1); - result.addAll(s2); - - return result; - } - /* - * Based on {@code basePattern} generate new {@code RecordPattern}s such that all - * components instead of {@code replaceComponent}th component, which is replaced - * with values from {@code updatedNestedPatterns}. Resulting {@code RecordPatterns}s - * are sent to {@code target}. + * Using {@code basePattern} as a starting point, generate new {@code + * RecordPattern}s, such that all corresponding components but one, are the + * same. The component described by the {@code replaceComponent} index is + * replaced with all {@code PatternDescription}s taken from {@code + * updatedNestedPatterns} and the resulting {@code RecordPatterns}s are sent + * to {@code target}. */ private void generatePatternsWithReplacedNestedPattern(RecordPattern basePattern, int replaceComponent, diff --git a/test/langtools/tools/javac/patterns/ExhaustivenessConvenientErrors.java b/test/langtools/tools/javac/patterns/ExhaustivenessConvenientErrors.java index 81b1af2bfd293..9157266d54f45 100644 --- a/test/langtools/tools/javac/patterns/ExhaustivenessConvenientErrors.java +++ b/test/langtools/tools/javac/patterns/ExhaustivenessConvenientErrors.java @@ -35,7 +35,6 @@ import com.sun.tools.javac.api.ClientCodeWrapper.DiagnosticSourceUnwrapper; import com.sun.tools.javac.util.JCDiagnostic; -import com.sun.tools.javac.util.JCDiagnostic.Fragment; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; From da1307178912ab7bbca3ab520b44c6599cdcc1c2 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Thu, 13 Nov 2025 19:51:17 +0100 Subject: [PATCH 30/33] Fixing trailing whitespaces. --- .../com/sun/tools/javac/comp/ExhaustivenessComputer.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java index 907d162f14a96..fc654718670da 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java @@ -1035,8 +1035,8 @@ private boolean removeUnnecessaryPatterns(Type selectorType, reducedAdded.remove(current); - Set combinedPatterns = - Stream.concat(basePatterns.stream(), + Set combinedPatterns = + Stream.concat(basePatterns.stream(), replace(inMissingPatterns, toExpand, reducedAdded).stream()) .collect(Collectors.toSet()); @@ -1152,7 +1152,7 @@ private boolean basePatternsHaveRecordPatternOnThisSpot(Set filteredBasePatterns = From 08fdb6d93ea721541a16b3e1456606dc13198136 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Wed, 19 Nov 2025 17:09:23 +0100 Subject: [PATCH 31/33] Fixing cases that are based on review feedback. --- .../javac/comp/ExhaustivenessComputer.java | 6 +- .../ExhaustivenessConvenientErrors.java | 70 +++++++++++++++++-- 2 files changed, 66 insertions(+), 10 deletions(-) diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java index fc654718670da..aa61705b36dde 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java @@ -872,7 +872,7 @@ private Set doExpandMissingPatternDescriptions(Type selector boolean reduced = removeUnnecessaryPatterns(selectorType, bp, basePatterns, inMissingPatterns, applicableDirectPermittedPatterns); - if (!reduced) { + if (!reduced && !hasMatchingRecordPattern(basePatterns, inMissingPatterns, toExpand)) { //if all immediate permitted subtypes are needed //give up, and simply use the current pattern: return inMissingPatterns; @@ -948,7 +948,7 @@ private Set doExpandMissingPatternDescriptions(Type selector //this is particularly important for the case where the sealed supertype only has one permitted type, the record //the base type could be used instead of the record otherwise, which would produce less specific missing pattern: Set sortedCandidates = - partialSortPattern(combinatorialPatterns, basePatterns, combinatorialPatterns); + partialSortPattern(combinatorialPatterns, basePatterns, replace(inMissingPatterns, toExpand, combinatorialPatterns)); removeUnnecessaryPatterns(selectorType, bp, basePatterns, inMissingPatterns, sortedCandidates); @@ -1132,7 +1132,7 @@ private PatternDescription findRootContaining(Set } } - throw Assert.error(); + return null; } private boolean basePatternsHaveRecordPatternOnThisSpot(Set basePatterns, diff --git a/test/langtools/tools/javac/patterns/ExhaustivenessConvenientErrors.java b/test/langtools/tools/javac/patterns/ExhaustivenessConvenientErrors.java index 9157266d54f45..b92eca2d4a951 100644 --- a/test/langtools/tools/javac/patterns/ExhaustivenessConvenientErrors.java +++ b/test/langtools/tools/javac/patterns/ExhaustivenessConvenientErrors.java @@ -287,12 +287,12 @@ private int test(Triple p) { case Triple(_, A _, _) -> 0; case Triple(_, _, A _) -> 0; case Triple(A p, C(Nested _, NestedBaseA _), _) -> 0; - case Triple(A p, C(Nested _, NestedBaseB _), C(Underneath _, NestedBaseA _)) -> 0; - case Triple(A p, C(Nested _, NestedBaseB _), C(Underneath _, NestedBaseB _)) -> 0; - case Triple(A p, C(Nested _, NestedBaseB _), C(Underneath _, NestedBaseC _)) -> 0; - case Triple(A p, C(Nested _, NestedBaseC _), C(Underneath _, NestedBaseA _)) -> 0; - case Triple(A p, C(Nested _, NestedBaseC _), C(Underneath _, NestedBaseB _)) -> 0; -// case Path(A p, C(Nested _, NestedBaseC _), C(Underneath _, NestedBaseC _)) -> 0; + case Triple(A p, C(Nested _, NestedBaseB _), C(Nested _, NestedBaseA _)) -> 0; + case Triple(A p, C(Nested _, NestedBaseB _), C(Nested _, NestedBaseB _)) -> 0; + case Triple(A p, C(Nested _, NestedBaseB _), C(Nested _, NestedBaseC _)) -> 0; + case Triple(A p, C(Nested _, NestedBaseC _), C(Nested _, NestedBaseA _)) -> 0; + case Triple(A p, C(Nested _, NestedBaseC _), C(Nested _, NestedBaseB _)) -> 0; +// case Path(A p, C(Nested _, NestedBaseC _), C(Nested _, NestedBaseC _)) -> 0; }; } record Triple(Base c1, Base c2, Base c3) {} @@ -475,6 +475,61 @@ record B(T c) implements Base {} "test.Test.Pair(test.Test.B(test.Test.B _))"); } + @Test + public void testNeedToExpandIfRecordExists(Path base) throws Exception { + doTest(base, + new String[0], + """ + package test; + class Test { + sealed interface A { } + record B() implements A { } + record C(A a) implements A { } + + void test(A a) { + switch (a) { + case C(B _) -> throw null; + } + } + } """, + "test.Test.B _", + "test.Test.C(test.Test.C _)"); + } + + @Test + public void testComplex6(Path base) throws Exception { + doTest(base, + new String[0], + """ + public class Test { + sealed interface Base {} + record NoOp() implements Base {} + record Const() implements Base {} + record Pair(Base n1, + Base b2) implements Base {} + + int t(Base b) { + return switch (b) { + case NoOp _ -> 0; + case Const _ -> 0; + case Pair(NoOp _, _) -> 0; + case Pair(Const _, _) -> 0; + case Pair(Pair _, NoOp _) -> 0; + case Pair(Pair _, Const _) -> 0; + case Pair(Pair _, Pair(NoOp _, _)) -> 0; + case Pair(Pair _, Pair(Const _, _)) -> 0; + case Pair(Pair _, Pair(Pair(NoOp _, _), _)) -> 0; + case Pair(Pair _, Pair(Pair(Const _, _), _)) -> 0; + case Pair(Pair(NoOp _, _), Pair(Pair(Pair _, _), _)) -> 0; + case Pair(Pair(Const _, _), Pair(Pair(Pair _, _), _)) -> 0; +// case Pair(Pair(Pair _, _), Pair(Pair(Pair _, _), _)) -> 0; + }; + } + } + """, + "Test.Pair(Test.Pair(Test.Pair _, Test.Base _), Test.Pair(Test.Pair(Test.Pair _, Test.Base _), Test.Base _))"); + } + private void doTest(Path base, String[] libraryCode, String testCode, String... expectedMissingPatterns) throws IOException { Path current = base.resolve("."); Path libClasses = current.resolve("libClasses"); @@ -513,7 +568,8 @@ private void doTest(Path base, String[] libraryCode, String testCode, String... .outdir(classes) .files(tb.findJavaFiles(src)) .diagnosticListener(d -> { - if ("compiler.err.not.exhaustive.details".equals(d.getCode())) { + if ("compiler.err.not.exhaustive.details".equals(d.getCode()) || + "compiler.err.not.exhaustive.statement.details".equals(d.getCode())) { if (d instanceof DiagnosticSourceUnwrapper uw) { d = uw.d; } From cee029cdab181986f6a279890e31d649c8526d01 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Tue, 25 Nov 2025 15:15:08 +0100 Subject: [PATCH 32/33] Reflecting review feedback: using multi-line diagnostics; passing PatternDescriptions through the diagnostics to format it at the end. --- .../propertiesparser/parser/MessageType.java | 1 + .../javac/comp/ExhaustivenessComputer.java | 32 ++++++++++++++----- .../com/sun/tools/javac/comp/Flow.java | 20 ++++++------ .../tools/javac/resources/compiler.properties | 11 +++---- .../util/AbstractDiagnosticFormatter.java | 16 ++++++++++ .../javac/util/RichDiagnosticFormatter.java | 16 ++++++++++ .../ExhaustivenessConvenientErrors.java | 6 ++-- 7 files changed, 76 insertions(+), 26 deletions(-) diff --git a/make/langtools/tools/propertiesparser/parser/MessageType.java b/make/langtools/tools/propertiesparser/parser/MessageType.java index 4b7064e387287..4937f0a52290c 100644 --- a/make/langtools/tools/propertiesparser/parser/MessageType.java +++ b/make/langtools/tools/propertiesparser/parser/MessageType.java @@ -83,6 +83,7 @@ public enum SimpleType implements MessageType { FILE("file", "File", "java.io"), FILE_OBJECT("file object", "JavaFileObject", "javax.tools"), PATH("path", "Path", "java.nio.file"), + PATTERN("pattern", "PatternDescription", "com.sun.tools.javac.comp.ExhaustivenessComputer"), NAME("name", "Name", "com.sun.tools.javac.util"), LONG("long", "long", null), NUMBER("number", "int", null), diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java index aa61705b36dde..dac756292688c 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java @@ -150,15 +150,15 @@ public ExhaustivenessResult exhausts(JCExpression selector, List cases) return ExhaustivenessResult.ofExhaustive(); } - Set details = + Set details = this.computeMissingPatternDescriptions(selector.type, coveredResult.incompletePatterns()) .stream() .flatMap(pd -> { if (pd instanceof BindingPattern bp && enum2Constants.containsKey(bp.type.tsym)) { Symbol enumType = bp.type.tsym; - return enum2Constants.get(enumType).stream().map(c -> enumType.toString() + "." + c.name); + return enum2Constants.get(enumType).stream().map(c -> new EnumConstantPattern(bp.type, c.name)); } else { - return Stream.of(pd.toString()); + return Stream.of(pd); } }) .collect(Collectors.toSet()); @@ -697,7 +697,7 @@ protected void checkTimeout() { } } - protected sealed interface PatternDescription { + public sealed interface PatternDescription { public Type type(); public Set sourcePatterns(); } @@ -736,7 +736,7 @@ public PatternDescription makePatternDescription(Type selectorType, JCPattern pa throw Assert.error(); } } - record BindingPattern(Type type, int permittedSubtypes, Set sourcePatterns) implements PatternDescription { + public record BindingPattern(Type type, int permittedSubtypes, Set sourcePatterns) implements PatternDescription { public BindingPattern(Type type) { this(type, -1, Set.of()); @@ -756,7 +756,7 @@ public String toString() { return type.tsym + " _"; } } - record RecordPattern(Type recordType, int _hashCode, Type[] fullComponentTypes, PatternDescription[] nested, Set sourcePatterns) implements PatternDescription { + public record RecordPattern(Type recordType, int _hashCode, Type[] fullComponentTypes, PatternDescription[] nested, Set sourcePatterns) implements PatternDescription { public RecordPattern(Type recordType, Type[] fullComponentTypes, PatternDescription[] nested) { this(recordType, fullComponentTypes, nested, Set.of()); @@ -805,11 +805,27 @@ public Type type() { } } - public record ExhaustivenessResult(boolean exhaustive, Set notExhaustiveDetails) { + public record EnumConstantPattern(Type enumType, Name enumConstant) implements PatternDescription { + + @Override + public Type type() { + return enumType(); + } + + @Override + public Set sourcePatterns() { + return Set.of(); + } + public String toString() { + return enumType() + "." + enumConstant(); + } + } + + public record ExhaustivenessResult(boolean exhaustive, Set notExhaustiveDetails) { public static ExhaustivenessResult ofExhaustive() { return new ExhaustivenessResult(true, null); } - public static ExhaustivenessResult ofDetails(Set notExhaustiveDetails) { + public static ExhaustivenessResult ofDetails(Set notExhaustiveDetails) { return new ExhaustivenessResult(false, notExhaustiveDetails != null ? notExhaustiveDetails : Set.of()); } } diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java index 0cc6bb4a22e2b..045a91d107e9d 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java @@ -706,9 +706,7 @@ public void visitSwitch(JCSwitch tree) { if (exhaustivenessResult.notExhaustiveDetails().isEmpty()) { log.error(tree, Errors.NotExhaustiveStatement); } else { - List details = - convertNotExhaustiveDetails(exhaustivenessResult); - log.error(tree, Errors.NotExhaustiveStatementDetails(details)); + logNotExhaustiveError(tree.pos(), exhaustivenessResult, Errors.NotExhaustiveStatementDetails); } } } @@ -755,9 +753,7 @@ public void visitSwitchExpression(JCSwitchExpression tree) { if (exhaustivenessResult.notExhaustiveDetails().isEmpty()) { log.error(tree, Errors.NotExhaustive); } else { - List details = - convertNotExhaustiveDetails(exhaustivenessResult); - log.error(tree, Errors.NotExhaustiveDetails(details)); + logNotExhaustiveError(tree.pos(), exhaustivenessResult, Errors.NotExhaustiveDetails); } } } @@ -766,12 +762,18 @@ public void visitSwitchExpression(JCSwitchExpression tree) { alive = alive.or(resolveYields(tree, prevPendingExits)); } - private List convertNotExhaustiveDetails(ExhaustivenessResult exhaustivenessResult) { - return exhaustivenessResult.notExhaustiveDetails() + private void logNotExhaustiveError(DiagnosticPosition pos, + ExhaustivenessResult exhaustivenessResult, + Error errorKey) { + List details = + exhaustivenessResult.notExhaustiveDetails() .stream() - .sorted() + .sorted((pd1, pd2) -> pd1.toString().compareTo(pd2.toString())) .map(detail -> diags.fragment(Fragments.NotExhaustiveDetail(detail))) .collect(List.collector()); + JCDiagnostic main = diags.error(null, log.currentSource(), pos, errorKey); + JCDiagnostic d = new JCDiagnostic.MultilineDiagnostic(main, details); + log.report(d); } public void visitTry(JCTry tree) { diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/compiler.properties b/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/compiler.properties index fdbed72220ba5..4650d7d5a8e3f 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/compiler.properties +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/compiler.properties @@ -40,6 +40,7 @@ # number an integer # option name the name of a command line option # path a path +# patter a pattern/pattern description # profile a profile name # source a source version number, such as 1.5, 1.6, 1.7, taken from a com.sun.tools.javac.code.Source # source version a source version number, such as 1.5, 1.6, 1.7, taken from a javax.lang.model.SourceVersion @@ -1476,19 +1477,17 @@ compiler.err.not.exhaustive=\ compiler.err.not.exhaustive.statement=\ the switch statement does not cover all possible input values -# 0: list of diagnostic compiler.err.not.exhaustive.details=\ the switch expression does not cover all possible input values\n\ - missing patterns: {0} + missing patterns: -# 0: list of diagnostic compiler.err.not.exhaustive.statement.details=\ the switch statement does not cover all possible input values\n\ - missing patterns: {0} + missing patterns: -# 0: string +# 0: pattern compiler.misc.not.exhaustive.detail=\ - \n{0} + {0} compiler.err.initializer.must.be.able.to.complete.normally=\ initializer must be able to complete normally diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/util/AbstractDiagnosticFormatter.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/util/AbstractDiagnosticFormatter.java index 7fa2889c7d297..de0949351dcae 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/util/AbstractDiagnosticFormatter.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/util/AbstractDiagnosticFormatter.java @@ -33,6 +33,7 @@ import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import javax.tools.JavaFileObject; @@ -47,6 +48,9 @@ import com.sun.tools.javac.code.Symbol; import com.sun.tools.javac.code.Type; import com.sun.tools.javac.code.Type.CapturedType; +import com.sun.tools.javac.comp.ExhaustivenessComputer.BindingPattern; +import com.sun.tools.javac.comp.ExhaustivenessComputer.EnumConstantPattern; +import com.sun.tools.javac.comp.ExhaustivenessComputer.RecordPattern; import com.sun.tools.javac.file.PathFileObject; import com.sun.tools.javac.jvm.Profile; import com.sun.tools.javac.jvm.Target; @@ -230,6 +234,18 @@ else if (arg instanceof Tag tag) { return messages.getLocalizedString(l, "compiler.misc.tree.tag." + StringUtils.toLowerCase(tag.name())); } + else if (arg instanceof BindingPattern bp) { + return formatArgument(d, bp.type(), l) + " _"; + } + else if (arg instanceof RecordPattern rp) { + return formatArgument(d, rp.type(), l) + + Arrays.stream(rp.nested()) + .map(pd -> formatArgument(d, pd, l)) + .collect(Collectors.joining(", ", "(", ")")); + } + else if (arg instanceof EnumConstantPattern ep) { + return formatArgument(d, ep.type(), l) + "." + ep.enumConstant(); + } else { return String.valueOf(arg); } diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/util/RichDiagnosticFormatter.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/util/RichDiagnosticFormatter.java index f2ae7d8b581c1..e44d99e7188be 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/util/RichDiagnosticFormatter.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/util/RichDiagnosticFormatter.java @@ -47,8 +47,12 @@ import static com.sun.tools.javac.code.TypeTag.*; import static com.sun.tools.javac.code.Kinds.*; import static com.sun.tools.javac.code.Kinds.Kind.*; +import com.sun.tools.javac.comp.ExhaustivenessComputer.BindingPattern; +import com.sun.tools.javac.comp.ExhaustivenessComputer.EnumConstantPattern; +import com.sun.tools.javac.comp.ExhaustivenessComputer.RecordPattern; import static com.sun.tools.javac.util.LayoutCharacters.*; import static com.sun.tools.javac.util.RichDiagnosticFormatter.RichConfiguration.*; +import java.util.Arrays; /** * A rich diagnostic formatter is a formatter that provides better integration @@ -208,6 +212,7 @@ protected void preprocessDiagnostic(JCDiagnostic diag) { * @param arg the argument to be translated */ protected void preprocessArgument(Object arg) { + //TODO: preprocess for patterns if (arg instanceof Type type) { preprocessType(type); } @@ -225,6 +230,17 @@ else if (arg instanceof Iterable iterable && !(arg instanceof Path)) { preprocessArgument(o); } } + else if (arg instanceof BindingPattern bp) { + preprocessArgument(bp.type()); + } + else if (arg instanceof RecordPattern rp) { + preprocessArgument(rp.type()); + Arrays.stream(rp.nested()) + .forEach(this::preprocessArgument); + } + else if (arg instanceof EnumConstantPattern ep) { + preprocessArgument(ep.type()); + } } /** diff --git a/test/langtools/tools/javac/patterns/ExhaustivenessConvenientErrors.java b/test/langtools/tools/javac/patterns/ExhaustivenessConvenientErrors.java index b92eca2d4a951..462179bb3977f 100644 --- a/test/langtools/tools/javac/patterns/ExhaustivenessConvenientErrors.java +++ b/test/langtools/tools/javac/patterns/ExhaustivenessConvenientErrors.java @@ -573,10 +573,10 @@ private void doTest(Path base, String[] libraryCode, String testCode, String... if (d instanceof DiagnosticSourceUnwrapper uw) { d = uw.d; } - if (d instanceof JCDiagnostic diag) { - ((Collection) diag.getArgs()[0]) + if (d instanceof JCDiagnostic.MultilineDiagnostic diag) { + diag.getSubdiagnostics() .stream() - .map(fragment -> (String) fragment.getArgs()[0]) + .map(fragment -> fragment.getArgs()[0].toString()) .forEach(missingPatterns::add); } } From 1e6a0183a99991aae077d8acffda5149e94cc7d9 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Tue, 25 Nov 2025 18:33:38 +0100 Subject: [PATCH 33/33] Fixing test. --- .../tools/javac/patterns/PrimitivePatternsSwitchConstants.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/langtools/tools/javac/patterns/PrimitivePatternsSwitchConstants.java b/test/langtools/tools/javac/patterns/PrimitivePatternsSwitchConstants.java index 2890b315e622a..1c5131e730cc1 100644 --- a/test/langtools/tools/javac/patterns/PrimitivePatternsSwitchConstants.java +++ b/test/langtools/tools/javac/patterns/PrimitivePatternsSwitchConstants.java @@ -2,7 +2,7 @@ * @test /nodynamiccopyright/ * @summary Retain exhaustiveness properties of switches with a constant selector * @enablePreview - * @compile/fail/ref=PrimitivePatternsSwitchConstants.out -XDrawDiagnostics -XDshould-stop.at=FLOW PrimitivePatternsSwitchConstants.java + * @compile/fail/ref=PrimitivePatternsSwitchConstants.out -XDrawDiagnostics -XDshould-stop.at=FLOW -XDexhaustivityTimeout=0 PrimitivePatternsSwitchConstants.java */ public class PrimitivePatternsSwitchConstants { void testConstExpressions() {