@@ -634,12 +634,15 @@ private boolean isUninformativeCondition(Predicate conditionRefinement, CtExpres
634634 * must be discarded at the join.
635635 *
636636 * <p>
637+ * A statement that a labelled {@code break} leaves completes normally, since execution goes on after it.
638+ *
639+ * <p>
637640 * Not currently handled (treated conservatively as completing normally): {@code switch} where every case exits,
638- * labeled {@code break}/{@code continue} targets, {@code try}/{@code catch}/{@code finally} flow, and infinite
639- * loops such as {@code while (true)}. Extending this list only tightens precision.
641+ * {@code try}/{@code catch}/{@code finally} flow, and infinite loops such as {@code while (true)}. Extending this
642+ * list only tightens precision.
640643 */
641644 boolean canCompleteNormally (CtStatement statement ) {
642- if (statement == null )
645+ if (statement == null || isLeftByLabelledBreak ( statement ) )
643646 return true ;
644647 if (statement instanceof CtReturn <?> || statement instanceof CtThrow || statement instanceof CtBreak
645648 || statement instanceof CtContinue )
@@ -658,6 +661,11 @@ boolean canCompleteNormally(CtStatement statement) {
658661 return true ;
659662 }
660663
664+ private static boolean isLeftByLabelledBreak (CtStatement statement ) {
665+ return statement .getLabel () != null && statement .getElements (new TypeFilter <>(CtBreak .class )).stream ()
666+ .anyMatch (jump -> jump .getTargetLabel () != null && jump .getLabelledStatement () == statement );
667+ }
668+
661669 @ Override
662670 public void visitCtTry (CtTry tryBlock ) {
663671 tryChecker .visitTry (tryBlock );
@@ -736,9 +744,10 @@ public void visitCtContinue(CtContinue continueStatement) {
736744 */
737745 private void visitLoop (CtLoop loop , Consumer <LoopTypestates > iteration ) {
738746 List <RefinedVariable > pathVariables = vcChecker .getPathVariables ();
739- // a field may also change in a method the loop calls, out of sight here, so fields are only havocked
740- Set <String > locals = changedIn (loop ).stream ().filter (name -> !isField (name ))
741- .collect (Collectors .toCollection (LinkedHashSet ::new ));
747+ // a field may also change in a method the loop calls, out of sight here, so fields are only havocked; and
748+ // typestates are only followed through jumps whose states are recorded, else they are havocked too
749+ Set <String > locals = followsJumps (loop ) ? changedIn (loop ).stream ().filter (name -> !isField (name ))
750+ .collect (Collectors .toCollection (LinkedHashSet ::new )) : Set .of ();
742751 LoopTypestates states = new LoopTypestates (this , loop , locals );
743752 havocChangedIn (loop , states .tracked ());
744753 loops .push (states );
@@ -753,24 +762,35 @@ private void visitLoop(CtLoop loop, Consumer<LoopTypestates> iteration) {
753762 }
754763
755764 /**
756- * Checks iterations of a loop from the typestates at its head until they stop changing. Only the last iteration
757- * checked keeps its diagnostics: an earlier one starts from fewer states, so an error it throws, which stops the
758- * check, is also an error from the final states.
765+ * Checks iterations of a loop from the typestates at its head until they stop changing. Every iteration starts from
766+ * the same facts: those before it, with what the loop changes havocked, and only the head typestates differ, since
767+ * what an iteration adds to the context is dropped after it (its variables and instances are scoped to it, and its
768+ * path conditions are restored). So an earlier iteration starts from fewer states than the last one, and an error
769+ * it throws, which stops the check, is also an error from the final states. Only the last iteration keeps the
770+ * errors recorded without stopping it (e.g. in a lambda).
759771 */
760772 private void checkUntilFixpoint (LoopTypestates states , Consumer <LoopTypestates > iteration ) {
761773 List <RefinedVariable > pathVariables = vcChecker .getPathVariables ();
762774 while (true ) {
763775 Set <LJError > errors = new LinkedHashSet <>(diagnostics .getErrors ());
764- states .startIteration ();
765- iteration .accept (states );
776+ context .enterContext ();
777+ try {
778+ states .startIteration ();
779+ iteration .accept (states );
780+ } finally {
781+ context .exitContext ();
782+ vcChecker .replacePathVariables (pathVariables );
783+ }
766784 if (states .reachedFixpoint ())
767785 return ;
768786 diagnostics .getErrors ().retainAll (errors );
769- vcChecker .replacePathVariables (pathVariables );
770787 }
771788 }
772789
773- /** The loop exits where its condition is false: never if it has none, or if it is {@code true} */
790+ /**
791+ * Records the states where the loop exits through its condition, which are those where the condition is false: none
792+ * if there is no condition or it is {@code true}
793+ */
774794 private void recordExit (LoopTypestates states , CtExpression <Boolean > condition ) {
775795 if (condition == null )
776796 return ;
@@ -792,14 +812,43 @@ private void scanBody(CtLoop loop, LoopTypestates states) {
792812 states .endBody (canCompleteNormally (loop .getBody ()));
793813 }
794814
795- /** The loop being checked that {@code jump} leaves, if any (an unlabeled break in a switch leaves the switch) */
815+ /** The loop being checked that {@code jump} leaves, if any */
796816 private Optional <LoopTypestates > loopLeftBy (CtLabelledFlowBreak jump ) {
797- CtElement target = jump .getTargetLabel () != null ? jump .getLabelledStatement ()
798- : jump .getParent (e -> e instanceof CtLoop || jump instanceof CtBreak
799- && (e instanceof CtSwitch <?> || e instanceof CtSwitchExpression <?, ?>));
817+ CtElement target = jumpTarget (jump );
800818 return loops .stream ().filter (states -> states .loop () == target ).findFirst ();
801819 }
802820
821+ /** The statement a break or continue leaves: its label's, else the innermost loop, or switch for a break */
822+ private static CtElement jumpTarget (CtLabelledFlowBreak jump ) {
823+ if (jump .getTargetLabel () != null )
824+ return jump .getLabelledStatement ();
825+ return jump .getParent (e -> e instanceof CtLoop || (jump instanceof CtBreak && isSwitch (e )));
826+ }
827+
828+ private static boolean isSwitch (CtElement element ) {
829+ return element instanceof CtSwitch <?> || element instanceof CtSwitchExpression <?, ?>;
830+ }
831+
832+ /**
833+ * Whether the states at every jump in the loop are recorded where it goes: each jump goes to a loop or leaves a
834+ * switch (not a labelled block, which joins no states), and runs no finally block or resource closing on its way,
835+ * which could change states after they are recorded
836+ */
837+ private static boolean followsJumps (CtLoop loop ) {
838+ return loop .getElements (new TypeFilter <>(CtLabelledFlowBreak .class )).stream ().allMatch (jump -> {
839+ CtElement target = jumpTarget (jump );
840+ return (target instanceof CtLoop || isSwitch (target )) && !leavesTryWithCleanup (jump , target );
841+ });
842+ }
843+
844+ private static boolean leavesTryWithCleanup (CtLabelledFlowBreak jump , CtElement target ) {
845+ for (CtTry left = jump .getParent (CtTry .class ); left != null
846+ && left .hasParent (target ); left = left .getParent (CtTry .class ))
847+ if (left .getFinalizer () != null || left instanceof CtTryWithResource )
848+ return true ;
849+ return false ;
850+ }
851+
803852 /** Havocs what the loop may change, except the {@code tracked} typestates */
804853 private void havocChangedIn (CtLoop loop , Set <String > tracked ) {
805854 for (String name : changedIn (loop ))
@@ -865,8 +914,8 @@ private static boolean isField(String name) {
865914 private boolean mayChangeState (CtInvocation <?> invocation ) {
866915 String name = invocation .getExecutable ().getSimpleName ();
867916 int arity = invocation .getArguments ().size ();
868- return context .getCtxFunctions ( ).stream ().anyMatch (f -> Utils . getSimpleName ( f . getName ()). equals ( name )
869- && f .getArguments ().size () == arity && f .getAllStates ().stream ().anyMatch (ObjectState ::hasTo ));
917+ return context .getFunctionsWithSimpleName ( name ).stream ().anyMatch (
918+ f -> f .getArguments ().size () == arity && f .getAllStates ().stream ().anyMatch (ObjectState ::hasTo ));
870919 }
871920
872921 /**
0 commit comments