55import java .util .List ;
66import java .util .Optional ;
77import java .util .Set ;
8+ import java .util .TreeSet ;
89import java .util .function .Supplier ;
910import java .util .stream .Collectors ;
1011import java .util .stream .Stream ;
@@ -34,10 +35,17 @@ public final class StateHints {
3435
3536 private final StateMachine machine ;
3637 private final String receiver ;
38+ /** The states that allow the failing call, grouped by the state refinement that allows it */
39+ private final List <Set <State >> goals ;
40+ /** The state sets the failing call checks, as indexes into {@link State#names()} */
41+ private final Set <Integer > checked ;
3742
38- private StateHints (StateMachine machine , String receiver ) {
43+ private StateHints (StateMachine machine , String receiver , RefinedFunction call ) {
3944 this .machine = machine ;
4045 this .receiver = receiver ;
46+ this .goals = machine .statesAllowingByRefinement (call );
47+ this .checked = goals .stream ().flatMap (g -> machine .stateSetsNamed (g ).stream ())
48+ .collect (Collectors .toCollection (TreeSet ::new ));
4149 }
4250
4351 /**
@@ -49,23 +57,24 @@ private StateHints(StateMachine machine, String receiver) {
4957 * the receiver as written in the code
5058 * @param category
5159 * the error's category, or {@code null} if it has none
60+ * @param checked
61+ * whether the receiver's state was checked elsewhere, though not decided, e.g. by an earlier call whose
62+ * result says which state it is in
5263 */
5364 public static List <Hint > forCall (StateMachine machine , RefinedFunction call , Set <State > current , String receiver ,
54- ErrorCategory category ) {
65+ ErrorCategory category , boolean checked ) {
5566 if (!machine .models (call ) || current .isEmpty () || machine .allows (current , call ))
5667 return List .of (); // the machine cannot explain why the call failed
5768 Set <State > goal = machine .statesAllowing (call );
5869 if (goal .isEmpty ())
5970 return List .of ();
6071
61- StateHints hints = new StateHints (machine , receiver );
62- if (category == ErrorCategory .UNKNOWN )
63- return hints .anyState (current , call , goal , false );
64- if (category == null && current .size () == machine .states ().size ())
65- return hints .anyState (current , call , goal , true ); // something is known, e.g. an earlier check failed
72+ StateHints hints = new StateHints (machine , receiver , call );
73+ if (category == ErrorCategory .UNKNOWN || checked )
74+ return hints .anyState (current , call , checked );
6675 boolean pathDependent = category == ErrorCategory .PATH_DEPENDENT_STATE ;
67- List <Hint > sequences = hints .howToReach (current , call , hints .names (current ),
68- () -> machine .complete () ? List .of (hints .unreachable (current , call , goal , pathDependent )) : List .of ());
76+ List <Hint > sequences = hints .howToReach (current , call , hints .namesHere (current ),
77+ () -> machine .complete () ? List .of (hints .unreachable (current , call , pathDependent )) : List .of ());
6978 return Stream .concat (sequences .stream (), hints .allowedCalls (current ).stream ()).toList ();
7079 }
7180
@@ -86,13 +95,13 @@ public static Hint decidingArgument(DecidingArgument deciding, String receiver)
8695 * A call allowed in only some states is never suggested, since whether it is allowed is just as unknown. Calls that
8796 * only tell which state the object is in are not in the machine, so no hint suggests checking one first.
8897 *
89- * @param somethingKnown
90- * whether anything is known about the state, though not enough to rule out any state
98+ * @param checked
99+ * whether the state was checked , though the check did not decide it
91100 */
92- private List <Hint > anyState (Set <State > current , RefinedFunction call , Set < State > goal , boolean somethingKnown ) {
93- String known = somethingKnown ? "What is known here does not decide the state of " + receiver
101+ private List <Hint > anyState (Set <State > current , RefinedFunction call , boolean checked ) {
102+ String known = checked ? "What is known here does not decide the state of " + receiver
94103 : "The state of " + receiver + " is unknown here" ;
95- String message = known + ", and this call needs it to be " + names ( goal );
104+ String message = known + ", and this call needs it to be " + needed ( );
96105 message += newObjects (call ).map (c -> ". Objects created with " + c + " start in such a state" ).orElse ("" );
97106 List <Hint > hints = new ArrayList <>();
98107 hints .add (new Hint (Kind .NEEDED_STATE , message ));
@@ -130,14 +139,14 @@ private Hint callSequence(Set<State> current, List<RefinedFunction> sequence, St
130139 receiver , from , names (reached )));
131140 }
132141
133- private Hint unreachable (Set <State > current , RefinedFunction call , Set < State > goal , boolean pathDependent ) {
142+ private Hint unreachable (Set <State > current , RefinedFunction call , boolean pathDependent ) {
134143 if (pathDependent )
135144 return new Hint (Kind .PATH_DEPENDENT_STATE , String .format (
136145 "%s can be in %s here, depending on the path taken, and no sequence of calls moves all of them to %s. "
137146 + "Make %s reach this line in the same state on every path" ,
138- receiver , names (current ), names ( goal ), receiver ));
139- String message = String .format ("No sequence of calls moves %s from %s to %s" , receiver , names (current ),
140- names ( goal ));
147+ receiver , namesHere (current ), needed ( ), receiver ));
148+ String message = String .format ("No sequence of calls moves %s from %s to %s" , receiver , namesHere (current ),
149+ needed ( ));
141150 message += newObjects (call ).map (c -> ". Create a new object instead with " + c ).orElse ("" );
142151 return new Hint (Kind .UNREACHABLE_STATE , message );
143152 }
@@ -148,13 +157,13 @@ private List<Hint> allowedCalls(Set<State> current) {
148157 List <String > signatures = allowed .stream ().map (StateHints ::signature ).toList ();
149158 String others = machine .complete () ? "" : " (and possibly others)" ;
150159 return List .of (new Hint (Kind .AVAILABLE_CALLS , String .format ("In state %s, %s can call: %s%s" ,
151- names (current ), receiver , list (signatures ), others )));
160+ namesHere (current ), receiver , list (signatures ), others )));
152161 }
153162 boolean noCallsInAnyState = current .stream ().allMatch (s -> machine .allowedIn (Set .of (s )).isEmpty ());
154163 if (machine .complete () && noCallsInAnyState )
155164 return List .of (new Hint (Kind .FINAL_STATE ,
156165 String .format ("%s is in state %s, where none of its methods with state refinements can be called" ,
157- receiver , names (current ))));
166+ receiver , namesHere (current ))));
158167 return List .of ();
159168 }
160169
@@ -186,8 +195,27 @@ private static String parameters(RefinedFunction function) {
186195
187196 /** {@code states} named as briefly as possible, e.g. {@code open or loud} */
188197 private String names (Collection <State > states ) {
189- return machine .describe (states ).stream ().map (group -> String .join (" and " , group ))
190- .collect (Collectors .joining (" or " ));
198+ return names (machine .describe (states ));
199+ }
200+
201+ /** The states the receiver can be in, named in the state sets the call checks and any set they all agree on */
202+ private String namesHere (Collection <State > current ) {
203+ return names (machine .describe (current , checked ));
204+ }
205+
206+ /**
207+ * The states that allow the call. If every state does, each by a different state refinement, the states each
208+ * refinement allows are named on their own: {@code a or b}, not every state.
209+ */
210+ private String needed () {
211+ Set <State > goal = machine .ordered (goals .stream ().flatMap (Set ::stream ).toList ());
212+ if (goal .size () < machine .states ().size ())
213+ return names (goal );
214+ return goals .stream ().map (this ::names ).distinct ().collect (Collectors .joining (" or " ));
215+ }
216+
217+ private static String names (List <List <String >> groups ) {
218+ return groups .stream ().map (group -> String .join (" and " , group )).collect (Collectors .joining (" or " ));
191219 }
192220
193221 private static String list (List <String > items ) {
0 commit comments