Skip to content

Add Owicki-Gries annotations#783

Draft
maul-esel wants to merge 562 commits into
devfrom
wip/dk/empire2-owicki
Draft

Add Owicki-Gries annotations#783
maul-esel wants to merge 562 commits into
devfrom
wip/dk/empire2-owicki

Conversation

@maul-esel

@maul-esel maul-esel commented May 29, 2026

Copy link
Copy Markdown
Contributor

This PR adds Owicki-Gries annotations as proofs for concurrent programs, as well as algorithms to compute such annotations after verification.


🚧 This is work in progress 🚧


Specifically, we add:

  • basic classes for describing and validating Owicki-Gries annotations.

  • 2 algorithms for the construction of Owicki-Gries annotations:

    • a naive algorithm (which creates very large annotations, but can be useful as a baseline for comparison)
    • the empire automaton-based algorithm described in our POPL'26 paper

    We previously implemented some other algorithm variants, but as they have been superseeded by the POPL'26 algorithm, they are no longer part of this PR. With one exception:

  • a partial implementation of a refined algorithm based on directed empires: while not yet complete, this may be a promising base for future improvements to our Owicki-Gries computation.

  • changes in backtranslation and correctness witness (v2.1) generation that allow us to output correctness witnesses for concurrent programs from Owicki-Gries proofs.

maul-esel and others added 30 commits January 19, 2024 14:28
Also fix base name for ghost mirror variables.
# Conflicts:
#	trunk/source/Library-ModelCheckerUtils/src/de/uni_freiburg/informatik/ultimate/lib/modelcheckerutils/smt/predicates/PredicateUtils.java
#	trunk/source/TraceAbstraction/src/de/uni_freiburg/informatik/ultimate/plugins/generator/traceabstraction/HoareAnnotationComposer.java
#	trunk/source/TraceAbstraction/src/de/uni_freiburg/informatik/ultimate/plugins/generator/traceabstraction/TraceAbstractionStarter.java
Previously, each declaration overwrote the previous one.
- initialize to zero (constant expression)
- avoid weird symbols in the name
private final IPetriNet<L, IPredicate> mInitialNet;
private HashRelation<IPredicate, Transition<L, IPredicate>> mPossibleInterferences;

private static final boolean USE_ON_DEMAND_RESULT = true;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to be careful about this change, it affects verification performance.

  • Do our algorithms still rely on changing this flag? This was probably only relevant for the unfolding-based approaches (but let's check).
  • If we do rely on it, we either need to adapt our implementation, or at least only modify this flag if proof production is enabled.

return null;
}
return scope.getDeclarator().getName().toString();
}));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a hack to get witness production working. I vaguely recall that I either fixed the NPE that occurred here in another way, or at least realised it should be fixed in another way ;-) I think this was related to invariants / ghost updates at global variable initializations (which we probably want to avoid).

Let's check this, and if it's not strictly needed, re-assess whether this change is a good idea or not.

}
return result;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this duplicates some logic from annotateFork; see if they can be combined.

new DeclarationInformation(StorageClass.QUANTIFIED, null));
mAuxiliaryVariables.put(variable, id);
return id;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we here also try to preserve meaningful ghost variable names?

import de.uni_freiburg.informatik.ultimate.util.datastructures.ImmutableSet;
import de.uni_freiburg.informatik.ultimate.util.datastructures.relation.Pair;

// TODO Give this class a more descriptive name

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... and document the class

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can rename it to Empire if we call the interface IEmpire?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems like a less descriptive name to me :)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would at least resolve any confusion, whether this class implements the empire as presented in the paper. But maybe something more descriptive would be EmpireTransitionFunctionProvider or EmpireTransitionProvider?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am ok with getting rid of the Automaton part (though I do not feel strongly about it).

But in regard to making the name more descriptive, I meant more of an indication of which empire is computed. If I see an interface IEmpire and an implementation Empire, I would always ask why two types are needed for the same concept.

I now renamed the interface IEmpire, and this implementation SaturatedEmpire (as in the paper).

* one transition in enabled(territory(s)), for which there is no successor in the automaton. In this case, the
* successor law must be false.
*/
public boolean isFinal2(final State<L, P> state) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-examine the usages of final states in this class (and the corresponding interfaces). Delete redundant methods.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After re-examination I did not find any usage of the final method of the EmpireAutomaton class (there are some relevant calls to the isFinal method of the InterpolantAutomaton though).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However as @schuessf pointed out here, there is still implicit usage of the isFinal method for states where we pruned transitions to a state with law=false. This is required to find entry points for the legal focus computation! What would be a nice name for such a method?

import de.uni_freiburg.informatik.ultimate.automata.statefactory.IStateFactory;
import de.uni_freiburg.informatik.ultimate.lib.modelcheckerutils.smt.predicates.IPredicate;

// TODO Possibly rename to IEmpire

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... and document

* automaton. For each region, the assigned law must be weaker than the state's full law, and satisfy additional
* conditions.
*
* TODO Document additional conditions

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(check in the paper)

import de.uni_freiburg.informatik.ultimate.util.datastructures.relation.HashRelation;
import de.uni_freiburg.informatik.ultimate.util.datastructures.relation.Pair;

public class LegalFocus<S, L, P> implements ILegalFocusFunction<S, P> {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

document this class

mSymbolTable = new DefaultIcfgSymbolTable(symbolTable, procedures);

// TODO let callers pass predicate factory
mFactory = new BasicPredicateFactory(services, mManagedScript, mSymbolTable);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this TODO still relevant?

*/
@Override
@Deprecated
Collection<S> getFinalStates();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is marked as deprecated, but it is used in LegalFocus::computeLegalFocus. Is this reason for deprecation ("We should not abuse the final states for empires, they do not represent any meaningful language. Instead introduce a suitably-named new method.") still valid, or should we remove it?

@matthiaszumkeller matthiaszumkeller left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for preparing this merge. I added some additional comments.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After inspection of this class and as far as I can recall, this class was a nearly 1-to-1 copy from the original (SaturatedEmpire) class at the time, which was only added so that we a able to quickly check test the approach. However if we would also parametrize the empire and State record with the type of region, with some engeneering effort most of this class (maybe even the whole class) should be obsolete (besides the method extendAll ).

import de.uni_freiburg.informatik.ultimate.util.datastructures.DataStructureUtils;
import de.uni_freiburg.informatik.ultimate.util.datastructures.ImmutableSet;

public class DirectedEmpireProduct<L, P> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this class should also just extend the IEmpire interface? The structure is practically the same and the empire product should again be an empire (I don't think we need to change this right now, but maybe a note for the future).

return interRegions;
}

private INestedWordAutomaton<Transition<L, P>, ProductState<L, P>> constructProductAutomaton() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this approach gets resumed in the future, I think this could also be done on-the-fly.

final var comparator = getPreference();
return (r1, r2) -> comparator.compare(new Pair<>(r1, law), new Pair<>(r2, law));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add some documentation here? As far as I remember, the heuristics for choosing the right region for the focus also left a lot of space for future work.

@maul-esel

Copy link
Copy Markdown
Contributor Author

In another project building of this work (branch wip/dk/civlized-og) we've just discovered a bug (or multiple bugs) that should be investigated and fixed before this is merged:

  • With the attached file fork-join-01.bpl, the generated O/G proof seems to be insufficient: the invariant between fork and join does not contain any information about x, and thus the proof should be insufficient to show the assert.

    • In the project, we ask a deductive verifier to confirm the proof; and indeed it fails to do so because of this issue.
  • Yet, no asserts fail. Do we not internally validate the proof when -ea is passed? Or is our validator defect?

  • Also, inserting some blank lines in the program (fork-join-01.backup-bpl) seems to "fix" this problem: we get a stronger invariant, and the proof is confirmed by the external validator.

    Apparently, there is a nondeterminism bug (likely, inserting blank lines changes the line numbers of some actions, which changes their hashcode, and affects some hashset iteration order).

fork-join01.backup.bpl.txt
fork-join01.bpl.txt
settings.epf.txt
toolchain.xml

@schuessf

schuessf commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

With the attached file fork-join-01.bpl, the generated O/G proof seems to be insufficient: the invariant between fork and join does not contain any information about x, and thus the proof should be insufficient to show the assert.

Is it really necessary to have an invariant stronger than true between fork and join? In this example, we already have x == 1 as an invariant at the exit location of the thread, so I would expect this to be sufficient to show the assert in the main thread.

In the project, we ask a deductive verifier to confirm the proof; and indeed it fails to do so because of this issue.

Maybe the validator (or your modelling) has a different understanding of the join that allows more behavior?

Also, inserting some blank lines in the program (fork-join-01.backup-bpl) seems to "fix" this problem: we get a stronger invariant, and the proof is confirmed by the external validator.
Apparently, there is a nondeterminism bug (likely, inserting blank lines changes the line numbers of some actions, which changes their hashcode, and affects some hashset iteration order).

Still, we should investigate this nondeterministic behavior!

@matthiaszumkeller

matthiaszumkeller commented Jun 10, 2026

Copy link
Copy Markdown
  • With the attached file fork-join-01.bpl, the generated O/G proof seems to be insufficient: the invariant between fork and join does not contain any information about x, and thus the proof should be insufficient to show the assert.

    • In the project, we ask a deductive verifier to confirm the proof; and indeed it fails to do so because of this issue.
  • Yet, no asserts fail. Do we not internally validate the proof when -ea is passed? Or is our validator defect?

We do check validity of the OG-proof internally and at least to me, the resulting proof seems valid wrt. the Petri net representation of the program. It is true, that no information about variable x is contained in the invariant between fork and join . However, for the transition representing the join in the Petri net, there are three predecessor places: {6#L13true,4#threadEXITtrue,threadThread1of1ForFork0InUse}. Two of them do not contain any information about x in their invariants, they ensure that the ghostvariable is set to the value 3 associated with the exit of the thread. However the third place (threadThread1of1ForFork0InUse) does contain xin its invariant, more specifically if ghost==3, then it ensures that the formula x==1 holds. Therefore, the transition corresponding to the join seems to satisfy inductivity and the postcondition contains the invariant x==1. Am I missing something, or are there any other validity concerns that I did not consider?

  • Also, inserting some blank lines in the program (fork-join-01.backup-bpl) seems to "fix" this problem: we get a stronger invariant, and the proof is confirmed by the external validator.
    Apparently, there is a nondeterminism bug (likely, inserting blank lines changes the line numbers of some actions, which changes their hashcode, and affects some hashset iteration order).

I also think, that the non-deterministic behavior should be investigated.

Update:
I again looked at the output of the Ultimate toolchain for the program and it seems to be the case that the invariant of threadThread1of1ForFork0InUse is missing in the result after backtranslation. Maybe this could cause the issue?

@schuessf

Copy link
Copy Markdown
Contributor

However, for the transition representing the join in the Petri net, there are three predecessor places: {6#L13true,4#threadEXITtrue,threadThread1of1ForFork0InUse}. Two of them do not contain any information about x in their invariants, they ensure that the ghostvariable is set to the value 3 associated with the exit of the thread. However the third place (threadThread1of1ForFork0InUse) does contain xin its invariant, more specifically if ghost==3, then it ensures that the invariant x==1 holds. Therefore, the transition corresponding to the join seems to satisfy inductivity and the postcondition contains the invariant x==1. Am I missing something, or are there any other validity concerns?

Oh, I see. So I guess we do not output the invariant for threadThread1of1ForFork0InUse if we consider the Boogie-program, since this location is just an auxiliary-location that does not belong to any location in the Boogie program. As a result, the annotation is valid for the Petri program but not for the Boogie program. I suppose we somehow still need to consider invariants at auxiliary locations during backtranslation.

This also seems to be the reason for non-determinism, it depends whether we put the invariant at threadThread1of1ForFork0InUse or 6#L13true.

@matthiaszumkeller

Copy link
Copy Markdown

This also seems to be the reason for non-determinism, it depends whether we put the invariant at threadThread1of1ForFork0InUse or 6#L13true.

Nice catch! I think that I found the location where the non-determinism occurs. In the legal focus, method chooseBestRegion chooses the smallest region (currently our only heuristics) that contains predecessor places of the transition to be focused. In this example, it can choose between those two regions [threadThread1of1ForFork0InUse], [6#L13true] (the third one contains two elements) and the regions are given as a set. If it is very inconvenient to also include the auxilliary places in the resulting backtranslation, we could maybe also filter regions with auxilliary places (that are predecessors of the transition) / adjust the comparator to choose another region if possible.

@schuessf

Copy link
Copy Markdown
Contributor

Nice catch! I think that I found the location where the non-determinism occurs. In the legal focus, method chooseBestRegion chooses the smallest region (currently our only heuristics) that contains predecessor places of the transition to be focused. In this example, it can choose between those two regions [threadThread1of1ForFork0InUse], [6#L13true] (the third one contains two elements) and the regions are given as a set. If it is very inconvenient to also include the auxilliary places in the resulting backtranslation, we could maybe also filter regions with auxilliary places (that are predecessors of the transition) / adjust the comparator to choose another region if possible.

I guess we could check for the auxiliary places in the legal focus as a heuristics (even if it is not quite nice). But I am still wondering whether the loss of precision when omitting the invariants for auxiliary places a) only occurs in combination with the legal focus and b) could be always fixed with such a simple heuristics (which I am quite skeptical).

@maul-esel

Copy link
Copy Markdown
Contributor Author

Thanks to both of you for investigating this! This explains why we could get a seemingly invalid annotation after backtranslation but not trigger an error before.

Let's maybe continue the discussion how to fix this in another channel to not overload this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants